From ab19a00e4e1cba1f53ed56c91b469f0facac16b4 Mon Sep 17 00:00:00 2001 From: iWe Date: Wed, 23 Nov 2022 22:29:47 +0800 Subject: [PATCH 1/3] improvement Combine --- Demo/Demo/Preview1ViewController.swift | 1 + .../UIView+StackKit/UIView+Combine.swift | 81 +++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/Demo/Demo/Preview1ViewController.swift b/Demo/Demo/Preview1ViewController.swift index 5026ff2..20ea647 100644 --- a/Demo/Demo/Preview1ViewController.swift +++ b/Demo/Demo/Preview1ViewController.swift @@ -20,6 +20,7 @@ class Preview1ViewController: UIViewController { @Published var name = "iWECon/StackKit" @Published var brief = "The best way to use HStack and VStack in UIKit, and also supports Spacer and Divider." + var nameCancellable: AnyCancellable? var cancellables: Set = [] deinit { diff --git a/Sources/StackKit/UIView+StackKit/UIView+Combine.swift b/Sources/StackKit/UIView+StackKit/UIView+Combine.swift index 9365894..b078094 100644 --- a/Sources/StackKit/UIView+StackKit/UIView+Combine.swift +++ b/Sources/StackKit/UIView+StackKit/UIView+Combine.swift @@ -35,6 +35,21 @@ extension StackKitCompatible where Base: UIView { }.store(in: &cancellables) return self } + + @discardableResult + public func receive( + publisher: Published.Publisher, + cancellable: inout AnyCancellable?, + sink receiveValue: @escaping ((Base, Published.Publisher.Output) -> Void) + ) -> Self + { + let v = self.view + cancellable = publisher.sink(receiveValue: { [weak v] output in + guard let v else { return } + receiveValue(v, output) + }) + return self + } @discardableResult public func receive( @@ -49,6 +64,21 @@ extension StackKitCompatible where Base: UIView { } return self } + + @discardableResult + public func receive( + isHidden publisher: Published.Publisher, + cancellable: inout AnyCancellable? + ) -> Self + { + receive(publisher: publisher, cancellable: &cancellable) { view, output in + safetyAccessUI { + view.isHidden = output + } + } + return self + } + } /** @@ -79,6 +109,19 @@ extension StackKitCompatible where Base: UILabel { } } + @discardableResult + public func receive( + text publisher: Published.Publisher, + cancellable: inout AnyCancellable? + ) -> Self + { + receive(publisher: publisher, cancellable: &cancellable) { view, output in + safetyAccessUI { + view.text = output + } + } + } + @discardableResult public func receive( @@ -93,6 +136,19 @@ extension StackKitCompatible where Base: UILabel { } } + @discardableResult + public func receive( + text publisher: Published.Publisher, + cancellable: inout AnyCancellable? + ) -> Self + { + receive(publisher: publisher, cancellable: &cancellable) { view, output in + safetyAccessUI { + view.text = output + } + } + } + @discardableResult public func receive( attributedText publisher: Published.Publisher, @@ -106,6 +162,19 @@ extension StackKitCompatible where Base: UILabel { } } + @discardableResult + public func receive( + attributedText publisher: Published.Publisher, + cancellable: inout AnyCancellable? + ) -> Self + { + receive(publisher: publisher, cancellable: &cancellable) { view, output in + safetyAccessUI { + view.attributedText = output + } + } + } + @discardableResult public func receive( attributedText publisher: Published.Publisher, @@ -119,4 +188,16 @@ extension StackKitCompatible where Base: UILabel { } } + @discardableResult + public func receive( + attributedText publisher: Published.Publisher, + cancellable: inout AnyCancellable? + ) -> Self + { + receive(publisher: publisher, cancellable: &cancellable) { view, output in + safetyAccessUI { + view.attributedText = output + } + } + } } From 45d1ec9e6782e9672052096a24748318c95b650e Mon Sep 17 00:00:00 2001 From: iWw Date: Mon, 28 Nov 2022 10:02:22 +0800 Subject: [PATCH 2/3] Update README, fill documents --- README.md | 16 ++++++--- Sources/StackKit/Enums.swift | 66 ++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b4bfae8..5da7cf9 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,13 @@ HStackView(alignment: .center, distribution: .spacing(14), padding: UIEdgeInsets 💡 There are no more examples, you can help to complete/optimize. -### Padding +### HStackView / VStackView + +Alignment, Distribution, Padding and `resultBuilder` ```swift -HStackView(padding: UIEdgeInsets) -VStackView(padding: UIEdgeInsets) +HStackView(alignment: HStackAlignment, distribution: HStackDistribution, padding: UIEdgeInsets, content: @resultBuilder) +VStackView(alignment: VStackAlignment, distribution: VStackDistribution, padding: UIEdgeInsets, content: @resultBuilder) ``` ### Subview size is fixed @@ -63,7 +65,7 @@ briefLabel.stack.offset(CGPoint?) ### SizeToFit ```swift -briefLabel.stack.width(220).sizeToFit(.width) +briefLabel.stack.width(220).sizeToFit(.width) // see `UIView+FitSize.swift` ``` ### Spacer & Divider @@ -125,6 +127,12 @@ HStackView { self.name = "StackKit version 1.2.3" // update stackView stackView.setNeedsLayout() + +// remember cleanup cancellables in deinit +deinit { + // the effective same as `cancellables.forEach { $0.cancel() }` + cancellables.removeAll() +} ``` # 🤔 diff --git a/Sources/StackKit/Enums.swift b/Sources/StackKit/Enums.swift index 843a0fc..df65064 100644 --- a/Sources/StackKit/Enums.swift +++ b/Sources/StackKit/Enums.swift @@ -2,8 +2,38 @@ import UIKit // MARK: HStack public enum HStackAlignment { + + /// Items are alignment top. + /// + /// |----------------------------------| + /// | |-------| |------| | + /// | | | | view2| | + /// | | view1 | | | | + /// | | | |------| | + /// | |-------| | + /// |----------------------------------| case top + + /// Items are alignment center. `Default`. + /// + /// |----------------------------------| + /// | |-------| | + /// | | | |------| | + /// | | view1 | | view2| | + /// | | | |------| | + /// | |-------| | + /// |----------------------------------| case center + + /// Items are alignment bottom. + /// + /// |----------------------------------| + /// | |-------| | + /// | | | | + /// | | view1 | |------| | + /// | | | | view2| | + /// | |-------| |------| | + /// |----------------------------------| case bottom } @@ -25,8 +55,44 @@ public enum HStackDistribution { // MARK: - VStack public enum VStackAlignment { + + /// Items are alignment left. + /// + /// |----------------------| + /// | |----------------| | + /// | | view1 | | + /// | |----------------| | + /// | | + /// | |---------| | + /// | | view2 | | + /// | |---------| | + /// |----------------------| case left + + /// Items are alignment center. `Default`. + /// + /// |----------------------| + /// | |----------------| | + /// | | view1 | | + /// | |----------------| | + /// | | + /// | |----------| | + /// | | view2 | | + /// | |----------| | + /// |----------------------| case center + + /// Items are alignment right. + /// + /// |----------------------| + /// | |----------------| | + /// | | view1 | | + /// | |----------------| | + /// | | + /// | |----------| | + /// | | view2 | | + /// | |----------| | + /// |----------------------| case right } From 55ec1f5a65da6493bb7f6bae2de9c667afa46573 Mon Sep 17 00:00:00 2001 From: iWe Date: Sat, 3 Dec 2022 21:55:15 +0800 Subject: [PATCH 3/3] Use UIScheduler instead safetyAccessUI --- .../UIView+StackKit/UIView+Combine.swift | 97 +++++++++++++------ 1 file changed, 68 insertions(+), 29 deletions(-) diff --git a/Sources/StackKit/UIView+StackKit/UIView+Combine.swift b/Sources/StackKit/UIView+StackKit/UIView+Combine.swift index b078094..7a09652 100644 --- a/Sources/StackKit/UIView+StackKit/UIView+Combine.swift +++ b/Sources/StackKit/UIView+StackKit/UIView+Combine.swift @@ -8,16 +8,68 @@ import UIKit import Combine -fileprivate func safetyAccessUI(_ closure: @escaping () -> Void) { - if Thread.isMainThread { - closure() - } else { - DispatchQueue.main.async { - closure() +/// Safety Access UI. +/// +/// A scheduler that performs all work on the main queue, as soon as possible. +/// +/// If the caller is already running on the main queue when an action is +/// scheduled, it may be run synchronously. However, ordering between actions +/// will always be preserved. +fileprivate final class UIScheduler { + private static let dispatchSpecificKey = DispatchSpecificKey() + private static let dispatchSpecificValue = UInt8.max + private static var __once: () = { + DispatchQueue.main.setSpecific(key: UIScheduler.dispatchSpecificKey, + value: dispatchSpecificValue) + }() + + private let queueLength: UnsafeMutablePointer = { + let memory = UnsafeMutablePointer.allocate(capacity: 1) + memory.initialize(to: 0) + return memory + }() + + deinit { + queueLength.deinitialize(count: 1) + queueLength.deallocate() + } + + init() { + /// This call is to ensure the main queue has been setup appropriately + /// for `UIScheduler`. It is only called once during the application + /// lifetime, since Swift has a `dispatch_once` like mechanism to + /// lazily initialize global variables and static variables. + _ = UIScheduler.__once + } + + /// Queues an action to be performed on main queue. If the action is called + /// on the main thread and no work is queued, no scheduling takes place and + /// the action is called instantly. + func schedule(_ action: @escaping () -> Void) { + let positionInQueue = enqueue() + + // If we're already running on the main queue, and there isn't work + // already enqueued, we can skip scheduling and just execute directly. + if positionInQueue == 1, DispatchQueue.getSpecific(key: UIScheduler.dispatchSpecificKey) == UIScheduler.dispatchSpecificValue { + action() + dequeue() + } else { + DispatchQueue.main.async { + defer { self.dequeue() } + action() + } } } + + private func dequeue() { + OSAtomicDecrement32(queueLength) + } + private func enqueue() -> Int32 { + OSAtomicIncrement32(queueLength) + } } + @available(iOS 13.0, *) extension StackKitCompatible where Base: UIView { @@ -58,7 +110,7 @@ extension StackKitCompatible where Base: UIView { ) -> Self { receive(publisher: publisher, storeIn: &cancellables) { view, output in - safetyAccessUI { + UIScheduler().schedule { view.isHidden = output } } @@ -72,27 +124,14 @@ extension StackKitCompatible where Base: UIView { ) -> Self { receive(publisher: publisher, cancellable: &cancellable) { view, output in - safetyAccessUI { + UIScheduler().schedule { view.isHidden = output } } return self } - } -/** - 这里由于设计逻辑,会有个问题 - - 系统提供的 receive(on: DispatchQueue.main) 虽然也可以 - ⚠️ 但是:DispatchQueue 是个调度器 - 任务添加后需要等到下一个 loop cycle 才会执行 - 这样就会导致一个问题: - ❌ 在主线程中修改值,并触发 `container.setNeedsLayout()` 的时候, - `setNeedsLayout` 会先执行,而 `publisher` 会将任务派发到下一个 loop cycle (也就是 setNeedsLayout 和 receive 先后执行的问题) - 所以这里采用 `safetyAccessUI` 来处理线程问题 - */ - @available(iOS 13.0, *) extension StackKitCompatible where Base: UILabel { @@ -103,7 +142,7 @@ extension StackKitCompatible where Base: UILabel { ) -> Self { receive(publisher: publisher, storeIn: &cancellables) { view, output in - safetyAccessUI { + UIScheduler().schedule { view.text = output } } @@ -116,7 +155,7 @@ extension StackKitCompatible where Base: UILabel { ) -> Self { receive(publisher: publisher, cancellable: &cancellable) { view, output in - safetyAccessUI { + UIScheduler().schedule { view.text = output } } @@ -130,7 +169,7 @@ extension StackKitCompatible where Base: UILabel { ) -> Self { receive(publisher: publisher, storeIn: &cancellables) { view, output in - safetyAccessUI { + UIScheduler().schedule { view.text = output } } @@ -143,7 +182,7 @@ extension StackKitCompatible where Base: UILabel { ) -> Self { receive(publisher: publisher, cancellable: &cancellable) { view, output in - safetyAccessUI { + UIScheduler().schedule { view.text = output } } @@ -156,7 +195,7 @@ extension StackKitCompatible where Base: UILabel { ) -> Self { receive(publisher: publisher, storeIn: &cancellables) { view, output in - safetyAccessUI { + UIScheduler().schedule { view.attributedText = output } } @@ -169,7 +208,7 @@ extension StackKitCompatible where Base: UILabel { ) -> Self { receive(publisher: publisher, cancellable: &cancellable) { view, output in - safetyAccessUI { + UIScheduler().schedule { view.attributedText = output } } @@ -182,7 +221,7 @@ extension StackKitCompatible where Base: UILabel { ) -> Self { receive(publisher: publisher, storeIn: &cancellables) { view, output in - safetyAccessUI { + UIScheduler().schedule { view.attributedText = output } } @@ -195,7 +234,7 @@ extension StackKitCompatible where Base: UILabel { ) -> Self { receive(publisher: publisher, cancellable: &cancellable) { view, output in - safetyAccessUI { + UIScheduler().schedule { view.attributedText = output } }