Skip to content

Commit 6430adb

Browse files
committed
State: support Optional<ObservableObject>
1 parent fba2e45 commit 6430adb

File tree

3 files changed

+68
-9
lines changed

3 files changed

+68
-9
lines changed

Sources/SwiftCrossUI/State/ObservableObject.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,5 +60,20 @@ extension ObservableObject {
6060
}
6161
}
6262

63+
protocol OptionalObservableObject {
64+
var didChange: Publisher? { get }
65+
}
66+
67+
extension Optional: OptionalObservableObject where Wrapped: ObservableObject {
68+
var didChange: SwiftCrossUI.Publisher? {
69+
switch self {
70+
case .some(let object):
71+
object.didChange
72+
case .none:
73+
nil
74+
}
75+
}
76+
}
77+
6378
@available(*, deprecated, message: "Replace Observable with ObservableObject")
6479
public typealias Observable = ObservableObject

Sources/SwiftCrossUI/State/Publisher.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import Dispatch
22
import Foundation
33

4+
// TODO: Keep weak references to cancellables for downstream observations (instead
5+
// of the current strong references).
6+
47
/// A type that produces valueless observations.
58
public class Publisher {
69
/// The id for the next observation (ids are used to cancel observations).

Sources/SwiftCrossUI/State/State.swift

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import Foundation
22

3+
// TODO: Document State properly, this is an important type.
4+
// - It supports value types
5+
// - It supports ObservableObject
6+
// - It supports Optional<ObservableObject>
37
@propertyWrapper
48
public struct State<Value>: DynamicProperty, StateProperty {
59
class Storage {
@@ -9,18 +13,46 @@ public struct State<Value>: DynamicProperty, StateProperty {
913
// `update(with:previousValue:)` method. It's vital that the inner
1014
// box remains the same so that bindings can be stored across view
1115
// updates.
12-
var box: Box<Value>
13-
var didChange = Publisher()
16+
var box: InnerBox
17+
18+
class InnerBox {
19+
var value: Value
20+
var didChange = Publisher()
21+
var downstreamObservation: Cancellable?
22+
23+
init(value: Value) {
24+
self.value = value
25+
}
26+
27+
/// Call this to publish an observation to all observers after
28+
/// setting a new value. This isn't in a didSet property accessor
29+
/// because we want more granular control over when it does and
30+
/// doesn't trigger.
31+
func postSet() {
32+
// If the wrapped value is an Optional<some ObservableObject>
33+
// then we need to observe/unobserve whenever the optional
34+
// toggles between `.some` and `.none`.
35+
if let value = value as? OptionalObservableObject {
36+
if let innerDidChange = value.didChange, downstreamObservation == nil {
37+
downstreamObservation = didChange.link(toUpstream: innerDidChange)
38+
} else if value.didChange == nil, let observation = downstreamObservation {
39+
observation.cancel()
40+
downstreamObservation = nil
41+
}
42+
}
43+
didChange.send()
44+
}
45+
}
1446

1547
init(_ value: Value) {
16-
self.box = Box(value: value)
48+
self.box = InnerBox(value: value)
1749
}
1850
}
1951

2052
var storage: Storage
2153

2254
var didChange: Publisher {
23-
storage.didChange
55+
storage.box.didChange
2456
}
2557

2658
public var wrappedValue: Value {
@@ -29,7 +61,7 @@ public struct State<Value>: DynamicProperty, StateProperty {
2961
}
3062
nonmutating set {
3163
storage.box.value = newValue
32-
didChange.send()
64+
storage.box.postSet()
3365
}
3466
}
3567

@@ -43,23 +75,32 @@ public struct State<Value>: DynamicProperty, StateProperty {
4375
},
4476
set: { newValue in
4577
box.value = newValue
46-
didChange.send()
78+
box.postSet()
4779
}
4880
)
4981
}
5082

5183
public init(wrappedValue initialValue: Value) {
5284
storage = Storage(initialValue)
5385

54-
if let initialValue = initialValue as? ObservableObject {
55-
_ = didChange.link(toUpstream: initialValue.didChange)
86+
// Before casting the value we check the type, because casting an optional
87+
// to protocol Optional doesn't conform to can still succeed when the value
88+
// is `.some` and the wrapped type conforms to the protocol.
89+
if Value.self as? ObservableObject.Type != nil,
90+
let initialValue = initialValue as? ObservableObject {
91+
storage.box.downstreamObservation = didChange.link(toUpstream: initialValue.didChange)
92+
} else if let initialValue = initialValue as? OptionalObservableObject,
93+
let innerDidChange = initialValue.didChange
94+
{
95+
// If we have an Optional<some ObservableObject>.some, then observe its
96+
// inner value's publisher.
97+
storage.box.downstreamObservation = didChange.link(toUpstream: innerDidChange)
5698
}
5799
}
58100

59101
public func update(with environment: EnvironmentValues, previousValue: State<Value>?) {
60102
if let previousValue {
61103
storage.box = previousValue.storage.box
62-
storage.didChange = previousValue.storage.didChange
63104
}
64105
}
65106

0 commit comments

Comments
 (0)