88import UIKit
99import Combine
1010
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+ }
1761 }
1862 }
63+
64+ private func dequeue( ) {
65+ OSAtomicDecrement32 ( queueLength)
66+ }
67+ private func enqueue( ) -> Int32 {
68+ OSAtomicIncrement32 ( queueLength)
69+ }
1970}
2071
72+
2173@available ( iOS 13 . 0 , * )
2274extension StackKitCompatible where Base: UIView {
2375
@@ -35,6 +87,21 @@ extension StackKitCompatible where Base: UIView {
3587 } . store ( in: & cancellables)
3688 return self
3789 }
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+ }
38105
39106 @discardableResult
40107 public func receive(
@@ -43,26 +110,28 @@ extension StackKitCompatible where Base: UIView {
43110 ) -> Self
44111 {
45112 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 {
47128 view. isHidden = output
48129 }
49130 }
50131 return self
51132 }
52133}
53134
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-
66135@available ( iOS 13 . 0 , * )
67136extension StackKitCompatible where Base: UILabel {
68137
@@ -73,7 +142,20 @@ extension StackKitCompatible where Base: UILabel {
73142 ) -> Self
74143 {
75144 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 {
77159 view. text = output
78160 }
79161 }
@@ -87,7 +169,20 @@ extension StackKitCompatible where Base: UILabel {
87169 ) -> Self
88170 {
89171 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 {
91186 view. text = output
92187 }
93188 }
@@ -100,7 +195,20 @@ extension StackKitCompatible where Base: UILabel {
100195 ) -> Self
101196 {
102197 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 {
104212 view. attributedText = output
105213 }
106214 }
@@ -113,10 +221,22 @@ extension StackKitCompatible where Base: UILabel {
113221 ) -> Self
114222 {
115223 receive ( publisher: publisher, storeIn: & cancellables) { view, output in
116- safetyAccessUI {
224+ UIScheduler ( ) . schedule {
117225 view. attributedText = output
118226 }
119227 }
120228 }
121229
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+ }
122242}
0 commit comments