8
8
import UIKit
9
9
import Combine
10
10
11
- fileprivate func safetyAccessUI( _ closure: @escaping ( ) -> Void ) {
12
- if Thread . isMainThread {
13
- closure ( )
14
- } else {
15
- DispatchQueue . main. async {
16
- closure ( )
11
+ /// Safety Access UI.
12
+ ///
13
+ /// A scheduler that performs all work on the main queue, as soon as possible.
14
+ ///
15
+ /// If the caller is already running on the main queue when an action is
16
+ /// scheduled, it may be run synchronously. However, ordering between actions
17
+ /// will always be preserved.
18
+ fileprivate final class UIScheduler {
19
+ private static let dispatchSpecificKey = DispatchSpecificKey < UInt8 > ( )
20
+ private static let dispatchSpecificValue = UInt8 . max
21
+ private static var __once : ( ) = {
22
+ DispatchQueue . main. setSpecific ( key: UIScheduler . dispatchSpecificKey,
23
+ value: dispatchSpecificValue)
24
+ } ( )
25
+
26
+ private let queueLength : UnsafeMutablePointer < Int32 > = {
27
+ let memory = UnsafeMutablePointer< Int32> . allocate( capacity: 1 )
28
+ memory. initialize ( to: 0 )
29
+ return memory
30
+ } ( )
31
+
32
+ deinit {
33
+ queueLength. deinitialize ( count: 1 )
34
+ queueLength. deallocate ( )
35
+ }
36
+
37
+ init ( ) {
38
+ /// This call is to ensure the main queue has been setup appropriately
39
+ /// for `UIScheduler`. It is only called once during the application
40
+ /// lifetime, since Swift has a `dispatch_once` like mechanism to
41
+ /// lazily initialize global variables and static variables.
42
+ _ = UIScheduler . __once
43
+ }
44
+
45
+ /// Queues an action to be performed on main queue. If the action is called
46
+ /// on the main thread and no work is queued, no scheduling takes place and
47
+ /// the action is called instantly.
48
+ func schedule( _ action: @escaping ( ) -> Void ) {
49
+ let positionInQueue = enqueue ( )
50
+
51
+ // If we're already running on the main queue, and there isn't work
52
+ // already enqueued, we can skip scheduling and just execute directly.
53
+ if positionInQueue == 1 , DispatchQueue . getSpecific ( key: UIScheduler . dispatchSpecificKey) == UIScheduler . dispatchSpecificValue {
54
+ action ( )
55
+ dequeue ( )
56
+ } else {
57
+ DispatchQueue . main. async {
58
+ defer { self . dequeue ( ) }
59
+ action ( )
60
+ }
17
61
}
18
62
}
63
+
64
+ private func dequeue( ) {
65
+ OSAtomicDecrement32 ( queueLength)
66
+ }
67
+ private func enqueue( ) -> Int32 {
68
+ OSAtomicIncrement32 ( queueLength)
69
+ }
19
70
}
20
71
72
+
21
73
@available ( iOS 13 . 0 , * )
22
74
extension StackKitCompatible where Base: UIView {
23
75
@@ -35,6 +87,21 @@ extension StackKitCompatible where Base: UIView {
35
87
} . store ( in: & cancellables)
36
88
return self
37
89
}
90
+
91
+ @discardableResult
92
+ public func receive< Value> (
93
+ publisher: Published < Value > . Publisher ,
94
+ cancellable: inout AnyCancellable ? ,
95
+ sink receiveValue: @escaping ( ( Base , Published < Value > . Publisher . Output ) -> Void )
96
+ ) -> Self
97
+ {
98
+ let v = self . view
99
+ cancellable = publisher. sink ( receiveValue: { [ weak v] output in
100
+ guard let v else { return }
101
+ receiveValue ( v, output)
102
+ } )
103
+ return self
104
+ }
38
105
39
106
@discardableResult
40
107
public func receive(
@@ -43,26 +110,28 @@ extension StackKitCompatible where Base: UIView {
43
110
) -> Self
44
111
{
45
112
receive ( publisher: publisher, storeIn: & cancellables) { view, output in
46
- safetyAccessUI {
113
+ UIScheduler ( ) . schedule {
114
+ view. isHidden = output
115
+ }
116
+ }
117
+ return self
118
+ }
119
+
120
+ @discardableResult
121
+ public func receive(
122
+ isHidden publisher: Published < Bool > . Publisher ,
123
+ cancellable: inout AnyCancellable ?
124
+ ) -> Self
125
+ {
126
+ receive ( publisher: publisher, cancellable: & cancellable) { view, output in
127
+ UIScheduler ( ) . schedule {
47
128
view. isHidden = output
48
129
}
49
130
}
50
131
return self
51
132
}
52
133
}
53
134
54
- /**
55
- 这里由于设计逻辑,会有个问题
56
-
57
- 系统提供的 receive(on: DispatchQueue.main) 虽然也可以
58
- ⚠️ 但是:DispatchQueue 是个调度器
59
- 任务添加后需要等到下一个 loop cycle 才会执行
60
- 这样就会导致一个问题:
61
- ❌ 在主线程中修改值,并触发 `container.setNeedsLayout()` 的时候,
62
- `setNeedsLayout` 会先执行,而 `publisher` 会将任务派发到下一个 loop cycle (也就是 setNeedsLayout 和 receive 先后执行的问题)
63
- 所以这里采用 `safetyAccessUI` 来处理线程问题
64
- */
65
-
66
135
@available ( iOS 13 . 0 , * )
67
136
extension StackKitCompatible where Base: UILabel {
68
137
@@ -73,7 +142,20 @@ extension StackKitCompatible where Base: UILabel {
73
142
) -> Self
74
143
{
75
144
receive ( publisher: publisher, storeIn: & cancellables) { view, output in
76
- safetyAccessUI {
145
+ UIScheduler ( ) . schedule {
146
+ view. text = output
147
+ }
148
+ }
149
+ }
150
+
151
+ @discardableResult
152
+ public func receive(
153
+ text publisher: Published < String > . Publisher ,
154
+ cancellable: inout AnyCancellable ?
155
+ ) -> Self
156
+ {
157
+ receive ( publisher: publisher, cancellable: & cancellable) { view, output in
158
+ UIScheduler ( ) . schedule {
77
159
view. text = output
78
160
}
79
161
}
@@ -87,7 +169,20 @@ extension StackKitCompatible where Base: UILabel {
87
169
) -> Self
88
170
{
89
171
receive ( publisher: publisher, storeIn: & cancellables) { view, output in
90
- safetyAccessUI {
172
+ UIScheduler ( ) . schedule {
173
+ view. text = output
174
+ }
175
+ }
176
+ }
177
+
178
+ @discardableResult
179
+ public func receive(
180
+ text publisher: Published < String ? > . Publisher ,
181
+ cancellable: inout AnyCancellable ?
182
+ ) -> Self
183
+ {
184
+ receive ( publisher: publisher, cancellable: & cancellable) { view, output in
185
+ UIScheduler ( ) . schedule {
91
186
view. text = output
92
187
}
93
188
}
@@ -100,7 +195,20 @@ extension StackKitCompatible where Base: UILabel {
100
195
) -> Self
101
196
{
102
197
receive ( publisher: publisher, storeIn: & cancellables) { view, output in
103
- safetyAccessUI {
198
+ UIScheduler ( ) . schedule {
199
+ view. attributedText = output
200
+ }
201
+ }
202
+ }
203
+
204
+ @discardableResult
205
+ public func receive(
206
+ attributedText publisher: Published < NSAttributedString > . Publisher ,
207
+ cancellable: inout AnyCancellable ?
208
+ ) -> Self
209
+ {
210
+ receive ( publisher: publisher, cancellable: & cancellable) { view, output in
211
+ UIScheduler ( ) . schedule {
104
212
view. attributedText = output
105
213
}
106
214
}
@@ -113,10 +221,22 @@ extension StackKitCompatible where Base: UILabel {
113
221
) -> Self
114
222
{
115
223
receive ( publisher: publisher, storeIn: & cancellables) { view, output in
116
- safetyAccessUI {
224
+ UIScheduler ( ) . schedule {
117
225
view. attributedText = output
118
226
}
119
227
}
120
228
}
121
229
230
+ @discardableResult
231
+ public func receive(
232
+ attributedText publisher: Published < NSAttributedString ? > . Publisher ,
233
+ cancellable: inout AnyCancellable ?
234
+ ) -> Self
235
+ {
236
+ receive ( publisher: publisher, cancellable: & cancellable) { view, output in
237
+ UIScheduler ( ) . schedule {
238
+ view. attributedText = output
239
+ }
240
+ }
241
+ }
122
242
}
0 commit comments