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
@@ -58,7 +110,7 @@ extension StackKitCompatible where Base: UIView {
58110 ) -> Self
59111 {
60112 receive ( publisher: publisher, storeIn: & cancellables) { view, output in
61- safetyAccessUI {
113+ UIScheduler ( ) . schedule {
62114 view. isHidden = output
63115 }
64116 }
@@ -72,27 +124,14 @@ extension StackKitCompatible where Base: UIView {
72124 ) -> Self
73125 {
74126 receive ( publisher: publisher, cancellable: & cancellable) { view, output in
75- safetyAccessUI {
127+ UIScheduler ( ) . schedule {
76128 view. isHidden = output
77129 }
78130 }
79131 return self
80132 }
81-
82133}
83134
84- /**
85- 这里由于设计逻辑,会有个问题
86-
87- 系统提供的 receive(on: DispatchQueue.main) 虽然也可以
88- ⚠️ 但是:DispatchQueue 是个调度器
89- 任务添加后需要等到下一个 loop cycle 才会执行
90- 这样就会导致一个问题:
91- ❌ 在主线程中修改值,并触发 `container.setNeedsLayout()` 的时候,
92- `setNeedsLayout` 会先执行,而 `publisher` 会将任务派发到下一个 loop cycle (也就是 setNeedsLayout 和 receive 先后执行的问题)
93- 所以这里采用 `safetyAccessUI` 来处理线程问题
94- */
95-
96135@available ( iOS 13 . 0 , * )
97136extension StackKitCompatible where Base: UILabel {
98137
@@ -103,7 +142,7 @@ extension StackKitCompatible where Base: UILabel {
103142 ) -> Self
104143 {
105144 receive ( publisher: publisher, storeIn: & cancellables) { view, output in
106- safetyAccessUI {
145+ UIScheduler ( ) . schedule {
107146 view. text = output
108147 }
109148 }
@@ -116,7 +155,7 @@ extension StackKitCompatible where Base: UILabel {
116155 ) -> Self
117156 {
118157 receive ( publisher: publisher, cancellable: & cancellable) { view, output in
119- safetyAccessUI {
158+ UIScheduler ( ) . schedule {
120159 view. text = output
121160 }
122161 }
@@ -130,7 +169,7 @@ extension StackKitCompatible where Base: UILabel {
130169 ) -> Self
131170 {
132171 receive ( publisher: publisher, storeIn: & cancellables) { view, output in
133- safetyAccessUI {
172+ UIScheduler ( ) . schedule {
134173 view. text = output
135174 }
136175 }
@@ -143,7 +182,7 @@ extension StackKitCompatible where Base: UILabel {
143182 ) -> Self
144183 {
145184 receive ( publisher: publisher, cancellable: & cancellable) { view, output in
146- safetyAccessUI {
185+ UIScheduler ( ) . schedule {
147186 view. text = output
148187 }
149188 }
@@ -156,7 +195,7 @@ extension StackKitCompatible where Base: UILabel {
156195 ) -> Self
157196 {
158197 receive ( publisher: publisher, storeIn: & cancellables) { view, output in
159- safetyAccessUI {
198+ UIScheduler ( ) . schedule {
160199 view. attributedText = output
161200 }
162201 }
@@ -169,7 +208,7 @@ extension StackKitCompatible where Base: UILabel {
169208 ) -> Self
170209 {
171210 receive ( publisher: publisher, cancellable: & cancellable) { view, output in
172- safetyAccessUI {
211+ UIScheduler ( ) . schedule {
173212 view. attributedText = output
174213 }
175214 }
@@ -182,7 +221,7 @@ extension StackKitCompatible where Base: UILabel {
182221 ) -> Self
183222 {
184223 receive ( publisher: publisher, storeIn: & cancellables) { view, output in
185- safetyAccessUI {
224+ UIScheduler ( ) . schedule {
186225 view. attributedText = output
187226 }
188227 }
@@ -195,7 +234,7 @@ extension StackKitCompatible where Base: UILabel {
195234 ) -> Self
196235 {
197236 receive ( publisher: publisher, cancellable: & cancellable) { view, output in
198- safetyAccessUI {
237+ UIScheduler ( ) . schedule {
199238 view. attributedText = output
200239 }
201240 }
0 commit comments