Skip to content

Commit 55ec1f5

Browse files
committed
Use UIScheduler instead safetyAccessUI
1 parent 45d1ec9 commit 55ec1f5

File tree

1 file changed

+68
-29
lines changed

1 file changed

+68
-29
lines changed

Sources/StackKit/UIView+StackKit/UIView+Combine.swift

Lines changed: 68 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,68 @@
88
import UIKit
99
import 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, *)
2274
extension 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, *)
97136
extension 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

Comments
 (0)