Skip to content

Commit ea25581

Browse files
authored
Add ObservedObject implementation (#521)
1 parent a723a34 commit ea25581

File tree

12 files changed

+444
-80
lines changed

12 files changed

+444
-80
lines changed

Package.resolved

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//
2+
// Binding+ObjectLocation.swift
3+
// OpenSwiftUICore
4+
//
5+
// Audited for 6.5.4
6+
// Status: Complete
7+
// ID: 7719FABF28E05207C06C2817640AD611 (SwiftUICore)
8+
9+
import Foundation
10+
11+
extension Binding {
12+
init<ObjectType: AnyObject>(
13+
_ root: ObjectType,
14+
keyPath: ReferenceWritableKeyPath<ObjectType, Value>,
15+
isolation: (any Actor)? = #isolation
16+
) {
17+
let location = ObjectLocation(base: root, keyPath: keyPath, isolation: isolation)
18+
let box = LocationBox(location)
19+
self.init(value: location.get(), location: box)
20+
}
21+
}
22+
23+
private struct ObjectLocation<Root, Value>: Location where Root: AnyObject {
24+
var base: Root
25+
26+
var keyPath: ReferenceWritableKeyPath<Root, Value>
27+
28+
var isolation: (any Actor)?
29+
30+
var wasRead: Bool {
31+
get { true }
32+
nonmutating set {}
33+
}
34+
35+
func get() -> Value {
36+
checkIsolation()
37+
return base[keyPath: keyPath]
38+
}
39+
40+
func set(_ value: Value, transaction: Transaction) {
41+
withTransaction(transaction) {
42+
checkIsolation()
43+
base[keyPath: keyPath] = value
44+
}
45+
}
46+
47+
static func == (_ lhs: ObjectLocation, _ rhs: ObjectLocation) -> Bool {
48+
lhs.base === rhs.base && lhs.keyPath == rhs.keyPath
49+
}
50+
51+
func checkIsolation() {
52+
guard let isolation, isolation === MainActor.shared, !Thread.isMainThread else {
53+
return
54+
}
55+
let description = String(describing: keyPath)
56+
Log.runtimeIssues(
57+
"%s is isolated to the main actor. Accessing it via Binding from a different actor will cause undefined behaviors, and potential data races; This warning will become a runtime crash in a future version of OpenSwiftUI.",
58+
[description]
59+
)
60+
}
61+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
//
2+
// AttributeInvalidatingSubscriber.swift
3+
// OpenSwiftUICore
4+
//
5+
// Audited for 6.5.4
6+
// Status: Complete
7+
8+
import Foundation
9+
import OpenAttributeGraphShims
10+
#if OPENSWIFTUI_OPENCOMBINE
11+
import OpenCombine
12+
#else
13+
import Combine
14+
#endif
15+
16+
// MARK: - AttributeInvalidatingSubscriber
17+
18+
class AttributeInvalidatingSubscriber<Upstream> where Upstream: Publisher {
19+
typealias Input = Upstream.Output
20+
21+
typealias Failure = Upstream.Failure
22+
23+
// MARK: - StateType
24+
25+
enum StateType {
26+
case subscribed(any Subscription)
27+
case unsubscribed
28+
case complete
29+
}
30+
31+
weak var host: GraphHost?
32+
33+
let attribute: WeakAttribute<()>
34+
35+
var state: StateType
36+
37+
init(host: GraphHost, attribute: WeakAttribute<()>) {
38+
self.host = host
39+
self.attribute = attribute
40+
self.state = .unsubscribed
41+
}
42+
43+
private func invalidateAttribute() {
44+
let style: GraphMutation.Style
45+
if !Thread.isMainThread {
46+
Log.runtimeIssues("Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.")
47+
style = .immediate
48+
} else if Update.threadIsUpdating, isLinkedOnOrAfter(.v4) {
49+
Log.runtimeIssues("Publishing changes from within view updates is not allowed, this will cause undefined behavior.")
50+
style = .deferred
51+
} else {
52+
style = .immediate
53+
}
54+
Update.perform {
55+
guard let host else { return }
56+
host.asyncTransaction(
57+
.current,
58+
invalidating: attribute,
59+
style: style
60+
)
61+
}
62+
}
63+
}
64+
65+
// MARK: - AttributeInvalidatingSubscriber + Subscriber
66+
67+
extension AttributeInvalidatingSubscriber: Subscriber {
68+
func receive(subscription: any Subscription) {
69+
guard case .unsubscribed = state else {
70+
subscription.cancel()
71+
return
72+
}
73+
state = .subscribed(subscription)
74+
subscription.request(.unlimited)
75+
}
76+
77+
func receive(_ input: Input) -> Subscribers.Demand {
78+
if case .subscribed = state {
79+
invalidateAttribute()
80+
}
81+
return .none
82+
}
83+
84+
func receive(completion: Subscribers.Completion<Failure>) {
85+
guard case .subscribed = state else {
86+
return
87+
}
88+
state = .complete
89+
invalidateAttribute()
90+
}
91+
}
92+
93+
// MARK: - AttributeInvalidatingSubscriber + Cancellable
94+
95+
extension AttributeInvalidatingSubscriber: Cancellable {
96+
func cancel() {
97+
if case let .subscribed(subscription) = state {
98+
subscription.cancel()
99+
}
100+
state = .unsubscribed
101+
}
102+
}
103+
104+
// MARK: - AttributeInvalidatingSubscriber + CustomCombineIdentifierConvertible
105+
106+
extension AttributeInvalidatingSubscriber: CustomCombineIdentifierConvertible {}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
//
2+
// SubscriptionLifetime.swift
3+
// OpenSwiftUICore
4+
//
5+
// Audited for 6.5.4
6+
// Status: Complete
7+
// ID: 6C59EBF8CD01332EB851D19EA2F31D6B (SwiftUICore)
8+
9+
import OpenAttributeGraphShims
10+
#if OPENSWIFTUI_OPENCOMBINE
11+
import OpenCombine
12+
#else
13+
import Combine
14+
#endif
15+
16+
// MARK: - SubscriptionLifetime
17+
18+
class SubscriptionLifetime<Upstream>: Cancellable where Upstream: Publisher {
19+
20+
// MARK: - Connection
21+
22+
private struct Connection<Downstream>: Subscriber, CustomCombineIdentifierConvertible
23+
where Downstream: Subscriber, Upstream.Failure == Downstream.Failure, Upstream.Output == Downstream.Input {
24+
typealias Input = Downstream.Input
25+
26+
typealias Failure = Downstream.Failure
27+
28+
var combineIdentifier: CombineIdentifier = .init()
29+
30+
weak var parent: SubscriptionLifetime?
31+
32+
let downstream: Downstream
33+
34+
let subscriptionID: Int
35+
36+
init(
37+
parent: SubscriptionLifetime,
38+
downstream: Downstream,
39+
subscriptionID: Int
40+
) {
41+
self.parent = parent
42+
self.downstream = downstream
43+
self.subscriptionID = subscriptionID
44+
}
45+
46+
func receive(subscription: any Subscription) {
47+
guard let parent,
48+
parent.shouldAcceptSubscription(subscription, for: subscriptionID) else {
49+
return
50+
}
51+
downstream.receive(subscription: subscription)
52+
subscription.request(.unlimited)
53+
}
54+
55+
func receive(_ input: Input) -> Subscribers.Demand {
56+
guard let parent,
57+
parent.shouldAcceptValue(for: subscriptionID) else {
58+
return .none
59+
}
60+
_ = downstream.receive(input)
61+
return .none
62+
}
63+
64+
func receive(completion: Subscribers.Completion<Failure>) {
65+
guard let parent,
66+
parent.shouldAcceptCompletion(for: subscriptionID) else {
67+
return
68+
}
69+
downstream.receive(completion: completion)
70+
}
71+
}
72+
73+
// MARK: - StateType
74+
75+
enum StateType {
76+
case requestedSubscription(to: Upstream, subscriber: AnyCancellable, subscriptionID: Int)
77+
case subscribed(to: Upstream, subscriber: AnyCancellable, subscription: Subscription, subscriptionID: Int)
78+
case uninitialized
79+
}
80+
81+
var subscriptionID: UniqueSeedGenerator = .init()
82+
83+
var state: StateType = .uninitialized
84+
85+
init() {
86+
_openSwiftUIEmptyStub()
87+
}
88+
89+
deinit {
90+
cancel()
91+
}
92+
93+
var isUninitialized: Bool {
94+
guard case .uninitialized = state else {
95+
return false
96+
}
97+
return true
98+
}
99+
100+
private func shouldAcceptSubscription(_ subscription: any Subscription, for subscriptionID: Int) -> Bool {
101+
guard case let .requestedSubscription(oldPublisher, oldSubscriber, oldSubscriptionID) = state,
102+
oldSubscriptionID == subscriptionID else {
103+
subscription.cancel()
104+
return false
105+
}
106+
state = .subscribed(
107+
to: oldPublisher,
108+
subscriber: oldSubscriber,
109+
subscription: subscription,
110+
subscriptionID: subscriptionID
111+
)
112+
return true
113+
}
114+
115+
private func shouldAcceptValue(for subscriptionID: Int) -> Bool {
116+
guard case .subscribed = state else {
117+
return false
118+
}
119+
return true
120+
}
121+
122+
private func shouldAcceptCompletion(for subscriptionID: Int) -> Bool {
123+
guard case let .subscribed(_, _, _, oldSubscriptionID) = state,
124+
subscriptionID == oldSubscriptionID else {
125+
return false
126+
}
127+
state = .uninitialized
128+
return true
129+
}
130+
131+
func cancel() {
132+
guard case let .subscribed(_, subscriber, subscription, _) = state else {
133+
return
134+
}
135+
subscriber.cancel()
136+
subscription.cancel()
137+
}
138+
139+
func subscribe<S>(
140+
subscriber: S,
141+
to upstream: Upstream
142+
) where S: Cancellable, S: Subscriber, Upstream.Failure == S.Failure, Upstream.Output == S.Input {
143+
let shouldRequest: Bool
144+
if case let .subscribed(oldUpstream, oldSubscriber, oldSubscription, _) = state {
145+
if compareValues(oldUpstream, upstream) {
146+
shouldRequest = false
147+
} else {
148+
oldSubscriber.cancel()
149+
oldSubscription.cancel()
150+
shouldRequest = true
151+
}
152+
} else {
153+
shouldRequest = true
154+
}
155+
guard shouldRequest else {
156+
return
157+
}
158+
let id = subscriptionID.generate()
159+
let connection = Connection(parent: self, downstream: subscriber, subscriptionID: id)
160+
state = .requestedSubscription(to: upstream, subscriber: .init(subscriber), subscriptionID: id)
161+
upstream.subscribe(connection)
162+
}
163+
}

Sources/OpenSwiftUICore/Data/DynamicProperty/DynamicProperty.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,11 @@ public protocol DynamicProperty {
4242

4343
package struct DynamicPropertyBehaviors: OptionSet {
4444
package let rawValue: UInt32
45+
4546
package static let allowsAsync = DynamicPropertyBehaviors(rawValue: 1 << 0)
47+
4648
package static let requiresMainThread = DynamicPropertyBehaviors(rawValue: 1 << 1)
47-
49+
4850
package init(rawValue: UInt32) {
4951
self.rawValue = rawValue
5052
}

Sources/OpenSwiftUICore/Data/Environment/Environment.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ private struct EnvironmentBox<Value>: DynamicPropertyBox {
270270
guard case let .keyPath(propertyKeyPath) = property.content else {
271271
return false
272272
}
273-
let (environment, environmentChanged) = _environment.changedValue(options: [])
273+
let (environment, environmentChanged) = _environment.changedValue()
274274
let keyPathChanged = (propertyKeyPath != keyPath)
275275
if keyPathChanged { keyPath = propertyKeyPath }
276276
let valueChanged: Bool

0 commit comments

Comments
 (0)