Skip to content

Commit 132b208

Browse files
authored
Merge pull request #29 from iWECon/combine
Support Combine
2 parents e48b54f + bde3c30 commit 132b208

3 files changed

Lines changed: 181 additions & 4 deletions

File tree

Demo/Demo/Preview1ViewController.swift

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import UIKit
99
import StackKit
10+
import Combine
1011

1112
class Preview1ViewController: UIViewController {
1213

@@ -15,6 +16,17 @@ class Preview1ViewController: UIViewController {
1516
let briefLabel = UILabel()
1617

1718
let container = HStackView(alignment: .center)
19+
20+
@Published var name = "iWECon/StackKit"
21+
@Published var brief = "The best way to use HStack and VStack in UIKit, and also supports Spacer and Divider."
22+
23+
var cancellables: Set<AnyCancellable> = []
24+
25+
deinit {
26+
// ⚠️ important
27+
// cleanup at deinit
28+
cancellables.forEach({ $0.cancel() })
29+
}
1830

1931
override func viewDidLoad() {
2032
super.viewDidLoad()
@@ -25,11 +37,11 @@ class Preview1ViewController: UIViewController {
2537
logoView.layer.cornerCurve = .continuous
2638
logoView.backgroundColor = .systemGroupedBackground
2739

28-
nameLabel.text = "iWECon/StackKit"
40+
nameLabel.text = name
2941
nameLabel.font = .systemFont(ofSize: 14, weight: .medium)
3042
nameLabel.textColor = .black
3143

32-
briefLabel.text = "The best way to use HStack and VStack in UIKit, and also supports Spacer and Divider."
44+
briefLabel.text = brief
3345
briefLabel.font = .systemFont(ofSize: 12, weight: .light)
3446
briefLabel.textColor = .black
3547
briefLabel.numberOfLines = 0
@@ -46,11 +58,23 @@ class Preview1ViewController: UIViewController {
4658
logoView.stack.size(80)
4759
VStackView(alignment: .left, distribution: .spacing(6)) {
4860
nameLabel
49-
briefLabel.stack.maxWidth(220)
61+
.stack
62+
.receive(text: $name, storeIn: &cancellables)
63+
64+
briefLabel
65+
.stack.maxWidth(220)
66+
.receive(text: $brief, storeIn: &cancellables)
5067
}
5168
}
5269

5370
self.view.addSubview(container)
71+
72+
// update combine value and refresh layout container
73+
DispatchQueue.main.asyncAfter(wallDeadline: .now() + 2) {
74+
self.name = "Designed by iWECon"
75+
self.brief = "Version: 1.2.3"
76+
self.container.setNeedsLayout()
77+
}
5478
}
5579

5680
override func viewDidLayoutSubviews() {

README.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
☝️ A layout library similar to SwiftUI.
44

5+
🔥 Support `Combine` (available iOS 13.0+)
6+
57
⚠️ If you have any questions or new ideas, you can bring me a PR at any time.
68

79

@@ -23,6 +25,8 @@ HStackView(alignment: .center, distribution: .spacing(14), padding: UIEdgeInsets
2325
![Demo](Demo/preview2.png)
2426

2527

28+
⚠️ If you have any question, please see the demo in `Demo/Demo.xcodeproj` or submit issue.
29+
2630
💡 There are no more examples, you can help to complete/optimize.
2731

2832

@@ -85,7 +89,6 @@ VStackView {
8589
}
8690
```
8791

88-
8992
### Change and update
9093

9194
```swift
@@ -96,6 +99,34 @@ briefLabel.text = "Bump version to 1.2.2"
9699
stackContainer.setNeedsLayout() // or .sizeToFit()
97100
```
98101

102+
### Combine
103+
104+
```swift
105+
106+
import Combine
107+
108+
// ...
109+
@Published var name: String = "StackKit"
110+
111+
var cancellables = Set<AnyCancellable>()
112+
113+
// ...
114+
HStackView {
115+
UILabel()
116+
.stack
117+
.then {
118+
$0.font = .systemFont(ofSize: 16)
119+
$0.textColor = .systemPink
120+
}
121+
.receive(text: $name, storeIn: &cancellables)
122+
}
123+
124+
// update name
125+
self.name = "StackKit version 1.2.3"
126+
// update stackView
127+
stackView.setNeedsLayout()
128+
```
129+
99130
# 🤔
100131

101132
I'm not very good at writing documents. If you have the advantages in this regard, please submit PR to me. Thank you very much.
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
//
2+
// File.swift
3+
//
4+
//
5+
// Created by bro on 2022/11/22.
6+
//
7+
8+
import UIKit
9+
import Combine
10+
11+
fileprivate func safetyAccessUI(_ closure: @escaping () -> Void) {
12+
if Thread.isMainThread {
13+
closure()
14+
} else {
15+
DispatchQueue.main.async {
16+
closure()
17+
}
18+
}
19+
}
20+
21+
@available(iOS 13.0, *)
22+
extension StackKitCompatible where Base: UIView {
23+
24+
@discardableResult
25+
public func receive<Value>(
26+
publisher: Published<Value>.Publisher,
27+
storeIn cancellables: inout Set<AnyCancellable>,
28+
sink receiveValue: @escaping ((Base, Published<Value>.Publisher.Output) -> Void)
29+
) -> Self
30+
{
31+
let v = self.view
32+
publisher.sink { [weak v] output in
33+
guard let v else { return }
34+
receiveValue(v, output)
35+
}.store(in: &cancellables)
36+
return self
37+
}
38+
39+
@discardableResult
40+
public func receive(
41+
isHidden publisher: Published<Bool>.Publisher,
42+
storeIn cancellables: inout Set<AnyCancellable>
43+
) -> Self
44+
{
45+
receive(publisher: publisher, storeIn: &cancellables) { view, output in
46+
safetyAccessUI {
47+
view.isHidden = output
48+
}
49+
}
50+
return self
51+
}
52+
}
53+
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+
@available(iOS 13.0, *)
67+
extension StackKitCompatible where Base: UILabel {
68+
69+
@discardableResult
70+
public func receive(
71+
text publisher: Published<String>.Publisher,
72+
storeIn cancellables: inout Set<AnyCancellable>
73+
) -> Self
74+
{
75+
receive(publisher: publisher, storeIn: &cancellables) { view, output in
76+
safetyAccessUI {
77+
view.text = output
78+
}
79+
}
80+
}
81+
82+
83+
@discardableResult
84+
public func receive(
85+
text publisher: Published<String?>.Publisher,
86+
storeIn cancellables: inout Set<AnyCancellable>
87+
) -> Self
88+
{
89+
receive(publisher: publisher, storeIn: &cancellables) { view, output in
90+
safetyAccessUI {
91+
view.text = output
92+
}
93+
}
94+
}
95+
96+
@discardableResult
97+
public func receive(
98+
attributedText publisher: Published<NSAttributedString>.Publisher,
99+
storeIn cancellables: inout Set<AnyCancellable>
100+
) -> Self
101+
{
102+
receive(publisher: publisher, storeIn: &cancellables) { view, output in
103+
safetyAccessUI {
104+
view.attributedText = output
105+
}
106+
}
107+
}
108+
109+
@discardableResult
110+
public func receive(
111+
attributedText publisher: Published<NSAttributedString?>.Publisher,
112+
storeIn cancellables: inout Set<AnyCancellable>
113+
) -> Self
114+
{
115+
receive(publisher: publisher, storeIn: &cancellables) { view, output in
116+
safetyAccessUI {
117+
view.attributedText = output
118+
}
119+
}
120+
}
121+
122+
}

0 commit comments

Comments
 (0)