Skip to content

Commit 121b608

Browse files
authored
Bring back 'observe' for non-UIKit targets. (#3295)
1 parent 0c31f22 commit 121b608

File tree

3 files changed

+181
-3
lines changed

3 files changed

+181
-3
lines changed

ComposableArchitecture.xcworkspace/xcshareddata/swiftpm/Package.resolved

+2-2
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,8 @@
122122
"kind" : "remoteSourceControl",
123123
"location" : "https://github.com/pointfreeco/swift-navigation",
124124
"state" : {
125-
"revision" : "339ba3e305dc727b5baa6ddb476430a842167d4e",
126-
"version" : "2.0.5"
125+
"revision" : "70321c441d51b0b893c3abbf79687f550a027fde",
126+
"version" : "2.0.6"
127127
}
128128
},
129129
{

Package.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ let package = Package(
2727
.package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.3.5"),
2828
.package(url: "https://github.com/pointfreeco/swift-identified-collections", from: "1.1.0"),
2929
.package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.2.0"),
30-
.package(url: "https://github.com/pointfreeco/swift-navigation", from: "2.0.5"),
30+
.package(url: "https://github.com/pointfreeco/swift-navigation", from: "2.0.6"),
3131
.package(url: "https://github.com/pointfreeco/swift-perception", from: "1.3.4"),
3232
.package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.2.2"),
3333
.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0"),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
#if canImport(Perception) && canImport(ObjectiveC) && !canImport(UIKit)
2+
import Foundation
3+
import ObjectiveC
4+
import SwiftNavigation
5+
6+
extension NSObject {
7+
/// Observe access to properties of a `@Perceptible` or `@Observable` object.
8+
///
9+
/// This tool allows you to set up an observation loop so that you can access fields from an
10+
/// observable model in order to populate your view, and also automatically track changes to
11+
/// any accessed fields so that the view is always up-to-date.
12+
///
13+
/// It is most useful when dealing with non-SwiftUI views, such as UIKit views and controller.
14+
/// You can invoke the ``observe(_:)`` method a single time in the `viewDidLoad` and update all
15+
/// the view elements:
16+
///
17+
/// ```swift
18+
/// override func viewDidLoad() {
19+
/// super.viewDidLoad()
20+
///
21+
/// let countLabel = UILabel()
22+
/// let incrementButton = UIButton(primaryAction: .init { _ in
23+
/// store.send(.incrementButtonTapped)
24+
/// })
25+
///
26+
/// observe { [weak self] in
27+
/// guard let self
28+
/// else { return }
29+
///
30+
/// countLabel.text = "\(store.count)"
31+
/// }
32+
/// }
33+
/// ```
34+
///
35+
/// This closure is immediately called, allowing you to set the initial state of your UI
36+
/// components from the feature's state. And if the `count` property in the feature's state is
37+
/// ever mutated, this trailing closure will be called again, allowing us to update the view
38+
/// again.
39+
///
40+
/// Generally speaking you can usually have a single ``observe(_:)`` in the entry point of your
41+
/// view, such as `viewDidLoad` for `UIViewController`. This works even if you have many UI
42+
/// components to update:
43+
///
44+
/// ```swift
45+
/// override func viewDidLoad() {
46+
/// super.viewDidLoad()
47+
///
48+
/// observe { [weak self] in
49+
/// guard let self
50+
/// else { return }
51+
///
52+
/// countLabel.isHidden = store.isObservingCount
53+
/// if !countLabel.isHidden {
54+
/// countLabel.text = "\(store.count)"
55+
/// }
56+
/// factLabel.text = store.fact
57+
/// }
58+
/// }
59+
/// ```
60+
///
61+
/// This does mean that you may execute the line `factLabel.text = store.fact` even when something
62+
/// unrelated changes, such as `store.count`, but that is typically OK for simple properties of
63+
/// UI components. It is not a performance problem to repeatedly set the `text` of a label or
64+
/// the `isHidden` of a button.
65+
///
66+
/// However, if there is heavy work you need to perform when state changes, then it is best to
67+
/// put that in its own ``observe(_:)``. For example, if you needed to reload a table view or
68+
/// collection view when a collection changes:
69+
///
70+
/// ```swift
71+
/// override func viewDidLoad() {
72+
/// super.viewDidLoad()
73+
///
74+
/// observe { [weak self] in
75+
/// guard let self
76+
/// else { return }
77+
///
78+
/// self.dataSource = store.items
79+
/// self.tableView.reloadData()
80+
/// }
81+
/// }
82+
/// ```
83+
///
84+
/// ## Navigation
85+
///
86+
/// The ``observe(_:)`` method makes it easy to drive navigation from state. To do so you need
87+
/// a reference to the controller that you are presenting (held as an optional), and when state
88+
/// becomes non-`nil` you assign and present the controller, and when state becomes `nil` you
89+
/// dismiss the controller and `nil` out the reference.
90+
///
91+
/// For example, if your feature's state holds onto alert state, then an alert can be presented
92+
/// and dismissed with the following:
93+
///
94+
/// ```swift
95+
/// override func viewDidLoad() {
96+
/// super.viewDidLoad()
97+
///
98+
/// var alertController: UIAlertController?
99+
///
100+
/// observe { [weak self] in
101+
/// guard let self
102+
/// else { return }
103+
///
104+
/// if
105+
/// let store = store.scope(state: \.alert, action: \.alert),
106+
/// alertController == nil
107+
/// {
108+
/// alertController = UIAlertController(store: store)
109+
/// present(alertController!, animated: true, completion: nil)
110+
/// } else if store.alert == nil, alertController != nil {
111+
/// alertController?.dismiss(animated: true)
112+
/// alertController = nil
113+
/// }
114+
/// }
115+
/// }
116+
/// ```
117+
///
118+
/// Here we are using the ``Store/scope(state:action:)-36e72`` operator for optional state in
119+
/// order to detect when the `alert` state flips from `nil` to non-`nil` and vice-versa.
120+
///
121+
/// ## Cancellation
122+
///
123+
/// The method returns a ``ObserveToken`` that can be used to cancel observation. For example,
124+
/// if you only want to observe while a view controller is visible, you can start observation in
125+
/// the `viewWillAppear` and then cancel observation in the `viewWillDisappear`:
126+
///
127+
/// ```swift
128+
/// var observation: ObserveToken?
129+
///
130+
/// func viewWillAppear() {
131+
/// super.viewWillAppear()
132+
/// self.observation = observe { [weak self] in
133+
/// // ...
134+
/// }
135+
/// }
136+
/// func viewWillDisappear() {
137+
/// super.viewWillDisappear()
138+
/// self.observation?.cancel()
139+
/// }
140+
/// ```
141+
@discardableResult
142+
@_disfavoredOverload
143+
public func observe(_ apply: @escaping () -> Void) -> ObserveToken {
144+
let token = ObserveToken()
145+
self.tokens.insert(token)
146+
@Sendable func onChange() {
147+
guard !token.isCancelled
148+
else { return }
149+
150+
withPerceptionTracking(apply) {
151+
Task { @MainActor in
152+
guard !token.isCancelled
153+
else { return }
154+
onChange()
155+
}
156+
}
157+
}
158+
onChange()
159+
return token
160+
}
161+
162+
fileprivate var tokens: Set<ObserveToken> {
163+
get {
164+
(objc_getAssociatedObject(self, &NSObject.tokensHandle) as? Set<ObserveToken>) ?? []
165+
}
166+
set {
167+
objc_setAssociatedObject(
168+
self,
169+
&NSObject.tokensHandle,
170+
newValue,
171+
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
172+
)
173+
}
174+
}
175+
176+
private static var tokensHandle: UInt8 = 0
177+
}
178+
#endif

0 commit comments

Comments
 (0)