Skip to content

Commit

Permalink
Merge pull request #33 from iWECon/enhance
Browse files Browse the repository at this point in the history
Improvement Combine, and Use UIScheduler instead safetyAccessUI
  • Loading branch information
iWECon authored Jul 21, 2023
2 parents 9cd7aec + 55ec1f5 commit 1a44b09
Show file tree
Hide file tree
Showing 4 changed files with 222 additions and 27 deletions.
1 change: 1 addition & 0 deletions Demo/Demo/Preview1ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<AnyCancellable> = []

deinit {
Expand Down
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
```

# 🤔
Expand Down
66 changes: 66 additions & 0 deletions Sources/StackKit/Enums.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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
}

Expand Down
166 changes: 143 additions & 23 deletions Sources/StackKit/UIView+StackKit/UIView+Combine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<UInt8>()
private static let dispatchSpecificValue = UInt8.max
private static var __once: () = {
DispatchQueue.main.setSpecific(key: UIScheduler.dispatchSpecificKey,
value: dispatchSpecificValue)
}()

private let queueLength: UnsafeMutablePointer<Int32> = {
let memory = UnsafeMutablePointer<Int32>.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 {

Expand All @@ -35,6 +87,21 @@ extension StackKitCompatible where Base: UIView {
}.store(in: &cancellables)
return self
}

@discardableResult
public func receive<Value>(
publisher: Published<Value>.Publisher,
cancellable: inout AnyCancellable?,
sink receiveValue: @escaping ((Base, Published<Value>.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(
Expand All @@ -43,26 +110,28 @@ extension StackKitCompatible where Base: UIView {
) -> Self
{
receive(publisher: publisher, storeIn: &cancellables) { view, output in
safetyAccessUI {
UIScheduler().schedule {
view.isHidden = output
}
}
return self
}

@discardableResult
public func receive(
isHidden publisher: Published<Bool>.Publisher,
cancellable: inout AnyCancellable?
) -> Self
{
receive(publisher: publisher, cancellable: &cancellable) { view, output in
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 {

Expand All @@ -73,7 +142,20 @@ extension StackKitCompatible where Base: UILabel {
) -> Self
{
receive(publisher: publisher, storeIn: &cancellables) { view, output in
safetyAccessUI {
UIScheduler().schedule {
view.text = output
}
}
}

@discardableResult
public func receive(
text publisher: Published<String>.Publisher,
cancellable: inout AnyCancellable?
) -> Self
{
receive(publisher: publisher, cancellable: &cancellable) { view, output in
UIScheduler().schedule {
view.text = output
}
}
Expand All @@ -87,7 +169,20 @@ extension StackKitCompatible where Base: UILabel {
) -> Self
{
receive(publisher: publisher, storeIn: &cancellables) { view, output in
safetyAccessUI {
UIScheduler().schedule {
view.text = output
}
}
}

@discardableResult
public func receive(
text publisher: Published<String?>.Publisher,
cancellable: inout AnyCancellable?
) -> Self
{
receive(publisher: publisher, cancellable: &cancellable) { view, output in
UIScheduler().schedule {
view.text = output
}
}
Expand All @@ -100,7 +195,20 @@ extension StackKitCompatible where Base: UILabel {
) -> Self
{
receive(publisher: publisher, storeIn: &cancellables) { view, output in
safetyAccessUI {
UIScheduler().schedule {
view.attributedText = output
}
}
}

@discardableResult
public func receive(
attributedText publisher: Published<NSAttributedString>.Publisher,
cancellable: inout AnyCancellable?
) -> Self
{
receive(publisher: publisher, cancellable: &cancellable) { view, output in
UIScheduler().schedule {
view.attributedText = output
}
}
Expand All @@ -113,10 +221,22 @@ extension StackKitCompatible where Base: UILabel {
) -> Self
{
receive(publisher: publisher, storeIn: &cancellables) { view, output in
safetyAccessUI {
UIScheduler().schedule {
view.attributedText = output
}
}
}

@discardableResult
public func receive(
attributedText publisher: Published<NSAttributedString?>.Publisher,
cancellable: inout AnyCancellable?
) -> Self
{
receive(publisher: publisher, cancellable: &cancellable) { view, output in
UIScheduler().schedule {
view.attributedText = output
}
}
}
}

0 comments on commit 1a44b09

Please sign in to comment.