Skip to content

Commit 1a44b09

Browse files
authored
Merge pull request #33 from iWECon/enhance
Improvement Combine, and Use UIScheduler instead safetyAccessUI
2 parents 9cd7aec + 55ec1f5 commit 1a44b09

File tree

4 files changed

+222
-27
lines changed

4 files changed

+222
-27
lines changed

Demo/Demo/Preview1ViewController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class Preview1ViewController: UIViewController {
2020
@Published var name = "iWECon/StackKit"
2121
@Published var brief = "The best way to use HStack and VStack in UIKit, and also supports Spacer and Divider."
2222

23+
var nameCancellable: AnyCancellable?
2324
var cancellables: Set<AnyCancellable> = []
2425

2526
deinit {

README.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,13 @@ HStackView(alignment: .center, distribution: .spacing(14), padding: UIEdgeInsets
3030
💡 There are no more examples, you can help to complete/optimize.
3131

3232

33-
### Padding
33+
### HStackView / VStackView
34+
35+
Alignment, Distribution, Padding and `resultBuilder`
3436

3537
```swift
36-
HStackView(padding: UIEdgeInsets)
37-
VStackView(padding: UIEdgeInsets)
38+
HStackView(alignment: HStackAlignment, distribution: HStackDistribution, padding: UIEdgeInsets, content: @resultBuilder)
39+
VStackView(alignment: VStackAlignment, distribution: VStackDistribution, padding: UIEdgeInsets, content: @resultBuilder)
3840
```
3941

4042
### Subview size is fixed
@@ -63,7 +65,7 @@ briefLabel.stack.offset(CGPoint?)
6365
### SizeToFit
6466

6567
```swift
66-
briefLabel.stack.width(220).sizeToFit(.width)
68+
briefLabel.stack.width(220).sizeToFit(.width) // see `UIView+FitSize.swift`
6769
```
6870

6971
### Spacer & Divider
@@ -125,6 +127,12 @@ HStackView {
125127
self.name = "StackKit version 1.2.3"
126128
// update stackView
127129
stackView.setNeedsLayout()
130+
131+
// remember cleanup cancellables in deinit
132+
deinit {
133+
// the effective same as `cancellables.forEach { $0.cancel() }`
134+
cancellables.removeAll()
135+
}
128136
```
129137

130138
# 🤔

Sources/StackKit/Enums.swift

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,38 @@ import UIKit
22

33
// MARK: HStack
44
public enum HStackAlignment {
5+
6+
/// Items are alignment top.
7+
///
8+
/// |----------------------------------|
9+
/// | |-------| |------| |
10+
/// | | | | view2| |
11+
/// | | view1 | | | |
12+
/// | | | |------| |
13+
/// | |-------| |
14+
/// |----------------------------------|
515
case top
16+
17+
/// Items are alignment center. `Default`.
18+
///
19+
/// |----------------------------------|
20+
/// | |-------| |
21+
/// | | | |------| |
22+
/// | | view1 | | view2| |
23+
/// | | | |------| |
24+
/// | |-------| |
25+
/// |----------------------------------|
626
case center
27+
28+
/// Items are alignment bottom.
29+
///
30+
/// |----------------------------------|
31+
/// | |-------| |
32+
/// | | | |
33+
/// | | view1 | |------| |
34+
/// | | | | view2| |
35+
/// | |-------| |------| |
36+
/// |----------------------------------|
737
case bottom
838
}
939

@@ -25,8 +55,44 @@ public enum HStackDistribution {
2555

2656
// MARK: - VStack
2757
public enum VStackAlignment {
58+
59+
/// Items are alignment left.
60+
///
61+
/// |----------------------|
62+
/// | |----------------| |
63+
/// | | view1 | |
64+
/// | |----------------| |
65+
/// | |
66+
/// | |---------| |
67+
/// | | view2 | |
68+
/// | |---------| |
69+
/// |----------------------|
2870
case left
71+
72+
/// Items are alignment center. `Default`.
73+
///
74+
/// |----------------------|
75+
/// | |----------------| |
76+
/// | | view1 | |
77+
/// | |----------------| |
78+
/// | |
79+
/// | |----------| |
80+
/// | | view2 | |
81+
/// | |----------| |
82+
/// |----------------------|
2983
case center
84+
85+
/// Items are alignment right.
86+
///
87+
/// |----------------------|
88+
/// | |----------------| |
89+
/// | | view1 | |
90+
/// | |----------------| |
91+
/// | |
92+
/// | |----------| |
93+
/// | | view2 | |
94+
/// | |----------| |
95+
/// |----------------------|
3096
case right
3197
}
3298

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

Lines changed: 143 additions & 23 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

@@ -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, *)
67136
extension 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

Comments
 (0)