Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ struct RecordingDataAtom: StateAtom, Hashable {

struct RecordingElapsedTimeAtom: PublisherAtom, Hashable {
func publisher(context: Context) -> AnyPublisher<TimeInterval, Never> {
let isRecording = context.watch(IsRecordingAtom())
let isRecording = context.watch(IsRecordingAtom().changes)

guard isRecording else {
return Just(.zero).eraseToAnyPublisher()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ struct PlayingElapsedTimeAtom: PublisherAtom {
}

func publisher(context: Context) -> AnyPublisher<TimeInterval, Never> {
let isPlaying = context.watch(IsPlayingAtom(voiceMemo: voiceMemo))
let isPlaying = context.watch(IsPlayingAtom(voiceMemo: voiceMemo).changes)

guard isPlaying else {
return Just(.zero).eraseToAnyPublisher()
Expand Down
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,7 @@ struct ContactView: View {

Modifiers can be applied to an atom to produce a different versions of the original atom to make it more coding friendly or to reduce view re-computation for performance optimization.

#### [select(_:)](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atom/select(_:))
#### [select](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atom/select(_:))

<details><summary><code>📖 Click to expand example code</code></summary>

Expand Down Expand Up @@ -695,6 +695,36 @@ struct CountDisplayView: View {
|Compatible |All atoms types. The selected property must be `Equatable` compliant.|
|Use Case |Performance optimization, Property scope restriction|

#### [changes](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atom/changes)

<details><summary><code>📖 Click to expand example code</code></summary>

```swift
struct CountAtom: StateAtom, Hashable {
func defaultValue(context: Context) -> Int {
12345
}
}

struct CountDisplayView: View {
@Watch(CountAtom().changes)
var count // : Int

var body: some View {
Text(count.description)
}
}
```

</details>

| |Description|
|:--------------|:----------|
|Summary |Prevents the atom from updating its child views or atoms when its new value is the same as its old value.|
|Output |`T: Equatable`|
|Compatible |All atom types that produce `Equatable` compliant value.|
|Use Case |Performance optimization|

#### [phase](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atom/phase)

<details><summary><code>📖 Click to expand example code</code></summary>
Expand Down
2 changes: 2 additions & 0 deletions Sources/Atoms/Atoms.docc/Atoms.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ SwiftUI Atom Properties offers practical capabilities to manage the complexity o
### Modifiers

- ``Atom/select(_:)``
- ``Atom/changes``
- ``Atom/phase``

### Attributes
Expand Down Expand Up @@ -66,6 +67,7 @@ SwiftUI Atom Properties offers practical capabilities to manage the complexity o
- ``AtomStore``
- ``AtomModifier``
- ``SelectModifier``
- ``ChangesModifier``
- ``TaskPhaseModifier``
- ``AtomLoader``
- ``RefreshableAtomLoader``
Expand Down
66 changes: 66 additions & 0 deletions Sources/Atoms/Modifier/ChangesModifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
public extension Atom where Loader.Value: Equatable {
/// Prevents the atom from updating its child views or atoms when its new value is the
/// same as its old value.
///
/// ```swift
/// struct FlagAtom: StateAtom, Hashable {
/// func defaultValue(context: Context) -> Bool {
/// true
/// }
/// }
///
/// struct ExampleView: View {
/// @Watch(FlagAtom().changes)
/// var flag
///
/// var body: some View {
/// if flag {
/// Text("true")
/// }
/// else {
/// Text("false")
/// }
/// }
/// }
/// ```
///
var changes: ModifiedAtom<Self, ChangesModifier<Loader.Value>> {
modifier(ChangesModifier())
}
}

/// A modifier that prevents the atom from updating its child views or atoms when
/// its new value is the same as its old value.
///
/// Use ``Atom/changes`` instead of using this modifier directly.
public struct ChangesModifier<T: Equatable>: AtomModifier {
/// A type of base value to be modified.
public typealias BaseValue = T

/// A type of modified value to provide.
public typealias Value = T

/// A type representing the stable identity of this atom associated with an instance.
public struct Key: Hashable {}

/// A unique value used to identify the modifier internally.
public var key: Key {
Key()
}

/// Returns a new value for the corresponding atom.
public func modify(value: BaseValue, context: Context) -> Value {
value
}

/// Associates given value and handle updates and cancellations.
public func associateOverridden(value: Value, context: Context) -> Value {
value
}

/// Returns a boolean value that determines whether it should notify the value update to
/// watchers with comparing the given old value and the new value.
public func shouldUpdate(newValue: Value, oldValue: Value) -> Bool {
newValue != oldValue
}
}
4 changes: 1 addition & 3 deletions Sources/Atoms/Modifier/TaskPhaseModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ public extension Atom where Loader: AsyncAtomLoader {
/// }
/// ```
///
/// This modifier converts the `Task` that the original atom provides into ``AsyncPhase``
/// and notifies its changes to downstream atoms and views.
var phase: ModifiedAtom<Self, TaskPhaseModifier<Loader.Success, Loader.Failure>> {
modifier(TaskPhaseModifier())
}
Expand All @@ -38,7 +36,7 @@ public extension Atom where Loader: AsyncAtomLoader {
///
/// Use ``Atom/phase`` instead of using this modifier directly.
public struct TaskPhaseModifier<Success, Failure: Error>: AtomModifier {
/// A type of original value to be modified.
/// A type of base value to be modified.
public typealias BaseValue = Task<Success, Failure>

/// A type of modified value to provide.
Expand Down
63 changes: 63 additions & 0 deletions Tests/AtomsTests/Modifier/ChangesModifierTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import XCTest

@testable import Atoms

@MainActor
final class ChangesModifierTests: XCTestCase {
func testChanges() {
let atom = TestStateAtom(defaultValue: "")
let context = AtomTestContext()
var updatedCount = 0

context.onUpdate = {
updatedCount += 1
}

XCTAssertEqual(updatedCount, 0)
XCTAssertEqual(context.watch(atom.changes), "")

context[atom] = "modified"

XCTAssertEqual(updatedCount, 1)
XCTAssertEqual(context.watch(atom.changes), "modified")

context[atom] = "modified"

// Should not be updated with an equivalent value.
XCTAssertEqual(updatedCount, 1)
}

func testKey() {
let modifier = ChangesModifier<Int>()

XCTAssertEqual(modifier.key, modifier.key)
XCTAssertEqual(modifier.key.hashValue, modifier.key.hashValue)
}

func testShouldUpdate() {
let modifier = ChangesModifier<Int>()

XCTAssertFalse(modifier.shouldUpdate(newValue: 100, oldValue: 100))
XCTAssertTrue(modifier.shouldUpdate(newValue: 100, oldValue: 200))
}

func testModify() {
let atom = TestValueAtom(value: 0)
let modifier = ChangesModifier<Int>()
let transaction = Transaction(key: AtomKey(atom)) {}
let context = AtomModifierContext<Int>(transaction: transaction) { _ in }
let value = modifier.modify(value: 100, context: context)

XCTAssertEqual(value, 100)
}

func testAssociateOverridden() {
let atom = TestValueAtom(value: 0)
let modifier = ChangesModifier<Int>()
let transaction = Transaction(key: AtomKey(atom)) {}
let context = AtomModifierContext<Int>(transaction: transaction) { _ in }
let value = modifier.associateOverridden(value: 100, context: context)

XCTAssertEqual(value, 100)
}
}