-
Notifications
You must be signed in to change notification settings - Fork 98
/
Observe.swift
173 lines (169 loc) · 6.02 KB
/
Observe.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
#if canImport(UIKit)
@_spi(Internals) import SwiftNavigation
import UIKit
extension NSObject {
/// Observe access to properties of an observable (or perceptible) object.
///
/// This tool allows you to set up an observation loop so that you can access fields from an
/// observable model in order to populate your view, and also automatically track changes to
/// any accessed fields so that the view is always up-to-date.
///
/// It is most useful when dealing with non-SwiftUI views, such as UIKit views and controller.
/// You can invoke the ``observe(_:)`` method a single time in the `viewDidLoad` and update all
/// the view elements:
///
/// ```swift
/// override func viewDidLoad() {
/// super.viewDidLoad()
///
/// let countLabel = UILabel()
/// let incrementButton = UIButton(primaryAction: UIAction { [weak self] _ in
/// self?.model.incrementButtonTapped()
/// })
///
/// observe { [weak self] in
/// guard let self
/// else { return }
///
/// countLabel.text = "\(model.count)"
/// }
/// }
/// ```
///
/// This closure is immediately called, allowing you to set the initial state of your UI
/// components from the feature's state. And if the `count` property in the feature's state is
/// ever mutated, this trailing closure will be called again, allowing us to update the view
/// again.
///
/// Generally speaking you can usually have a single ``observe(_:)`` in the entry point of your
/// view, such as `viewDidLoad` for `UIViewController`. This works even if you have many UI
/// components to update:
///
/// ```swift
/// override func viewDidLoad() {
/// super.viewDidLoad()
///
/// observe { [weak self] in
/// guard let self
/// else { return }
///
/// countLabel.isHidden = model.isObservingCount
/// if !countLabel.isHidden {
/// countLabel.text = "\(model.count)"
/// }
/// factLabel.text = model.fact
/// }
/// }
/// ```
///
/// This does mean that you may execute the line `factLabel.text = model.fact` even when
/// something unrelated changes, such as `store.model`, but that is typically OK for simple
/// properties of UI components. It is not a performance problem to repeatedly set the `text` of
/// a label or the `isHidden` of a button.
///
/// However, if there is heavy work you need to perform when state changes, then it is best to
/// put that in its own ``observe(_:)``. For example, if you needed to reload a table view or
/// collection view when a collection changes:
///
/// ```swift
/// override func viewDidLoad() {
/// super.viewDidLoad()
///
/// observe { [weak self] in
/// guard let self
/// else { return }
///
/// dataSource = model.items
/// tableView.reloadData()
/// }
/// }
/// ```
///
/// ## Cancellation
///
/// The method returns an ``ObservationToken`` that can be used to cancel observation. For
/// example, if you only want to observe while a view controller is visible, you can start
/// observation in the `viewWillAppear` and then cancel observation in the `viewWillDisappear`:
///
/// ```swift
/// var observation: ObservationToken?
///
/// func viewWillAppear() {
/// super.viewWillAppear()
/// observation = observe { [weak self] in
/// // ...
/// }
/// }
/// func viewWillDisappear() {
/// super.viewWillDisappear()
/// observation?.cancel()
/// }
/// ```
///
/// - Parameter apply: A closure that contains properties to track and is invoked when the value
/// of a property changes.
/// - Returns: A cancellation token.
@discardableResult
@MainActor
public func observe(_ apply: @escaping @MainActor @Sendable () -> Void) -> ObservationToken {
observe { _ in apply() }
}
/// Observe access to properties of an observable (or perceptible) object.
///
/// A version of ``observe(_:)`` that is passed the current transaction.
///
/// - Parameter apply: A closure that contains properties to track and is invoked when the value
/// of a property changes.
/// - Returns: A cancellation token.
@discardableResult
@MainActor
public func observe(
_ apply: @escaping @MainActor @Sendable (_ transaction: UITransaction) -> Void
) -> ObservationToken {
let token = SwiftNavigation.observe { transaction in
MainActor.assumeIsolated {
withUITransaction(transaction) {
if transaction.uiKit.disablesAnimations {
UIView.performWithoutAnimation { apply(transaction) }
for completion in transaction.uiKit.animationCompletions {
completion(true)
}
} else if let animation = transaction.uiKit.animation {
return animation.perform(
{ apply(transaction) },
completion: transaction.uiKit.animationCompletions.isEmpty
? nil
: {
for completion in transaction.uiKit.animationCompletions {
completion($0)
}
}
)
} else {
apply(transaction)
for completion in transaction.uiKit.animationCompletions {
completion(true)
}
}
}
}
} task: { transaction, work in
DispatchQueue.main.async {
withUITransaction(transaction, work)
}
}
tokens.append(token)
return token
}
fileprivate var tokens: [Any] {
get {
objc_getAssociatedObject(self, tokensKey) as? [Any] ?? []
}
set {
objc_setAssociatedObject(self, tokensKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
// TODO: Isolate?
private nonisolated(unsafe) let tokensKey = malloc(1)!
#endif