From b28f77a8ab66255af8b10bcf26f52f5db37c41b5 Mon Sep 17 00:00:00 2001 From: Selim Mustafaev Date: Sun, 30 Jul 2023 20:03:43 +0300 Subject: [PATCH] First vision of binding --- Sources/YadUI/Extensions/UILabel.swift | 16 ++++ .../YadUI/Extensions/UILayoutPriority.swift | 17 ++++ .../UIView/UIView+Constraints.swift | 21 ++-- .../Extensions/UIView/UIView+Fluent.swift | 27 ++++++ Sources/YadUI/Protocols/InitConvertible.swift | 15 +++ .../YadUI/Views/Stack/Stack+Directional.swift | 2 + Sources/YadUI/Views/Stack/Stack+Enums.swift | 4 + Sources/YadUI/Views/Stack/Stack.swift | 95 +++++++++++++------ .../Views/UIKit/YASegmentedControl.swift | 30 ++++++ YadUIDemo/YadUIDemo/ViewController.swift | 38 +++++--- 10 files changed, 213 insertions(+), 52 deletions(-) create mode 100644 Sources/YadUI/Extensions/UILabel.swift create mode 100644 Sources/YadUI/Extensions/UILayoutPriority.swift create mode 100644 Sources/YadUI/Protocols/InitConvertible.swift create mode 100644 Sources/YadUI/Views/UIKit/YASegmentedControl.swift diff --git a/Sources/YadUI/Extensions/UILabel.swift b/Sources/YadUI/Extensions/UILabel.swift new file mode 100644 index 0000000..b176b27 --- /dev/null +++ b/Sources/YadUI/Extensions/UILabel.swift @@ -0,0 +1,16 @@ +// +// UILabel.swift +// +// +// Created by Selim Mustafaev on 29.07.2023. +// + +import UIKit + +extension UILabel { + + public func text(_ text: String?) -> Self { + self.text = text + return self + } +} diff --git a/Sources/YadUI/Extensions/UILayoutPriority.swift b/Sources/YadUI/Extensions/UILayoutPriority.swift new file mode 100644 index 0000000..3fe4dbf --- /dev/null +++ b/Sources/YadUI/Extensions/UILayoutPriority.swift @@ -0,0 +1,17 @@ +// +// UILayoutPriority.swift +// +// +// Created by Selim Mustafaev on 29.07.2023. +// + +import UIKit + +extension UILayoutPriority { + + public static let defaultLowMinus: UILayoutPriority = .init(UILayoutPriority.defaultLow.rawValue - 1) + public static let defaultLowPlus: UILayoutPriority = .init(UILayoutPriority.defaultLow.rawValue + 1) + public static let defaultHighMinus: UILayoutPriority = .init(UILayoutPriority.defaultHigh.rawValue - 1) + public static let defaultHighPlus: UILayoutPriority = .init(UILayoutPriority.defaultHigh.rawValue + 1) + public static let almostRequired: UILayoutPriority = .init(UILayoutPriority.required.rawValue - 1) +} diff --git a/Sources/YadUI/Extensions/UIView/UIView+Constraints.swift b/Sources/YadUI/Extensions/UIView/UIView+Constraints.swift index 58af749..91afd54 100644 --- a/Sources/YadUI/Extensions/UIView/UIView+Constraints.swift +++ b/Sources/YadUI/Extensions/UIView/UIView+Constraints.swift @@ -27,18 +27,21 @@ extension UIView { ]) } - public func constraint(src: UIView, + @discardableResult + public func addConstraint(src: UIView, srcAttr: NSLayoutConstraint.Attribute, dst: UIView, dstAttr: NSLayoutConstraint.Attribute, - constant: CGFloat = 0) { + constant: CGFloat = 0) -> NSLayoutConstraint { - addConstraint(.init(item: src, - attribute: srcAttr, - relatedBy: .equal, - toItem: dst, - attribute: dstAttr, - multiplier: 1.0, - constant: constant)) + let constraint = NSLayoutConstraint(item: src, + attribute: srcAttr, + relatedBy: .equal, + toItem: dst, + attribute: dstAttr, + multiplier: 1.0, + constant: constant) + addConstraint(constraint) + return constraint } } diff --git a/Sources/YadUI/Extensions/UIView/UIView+Fluent.swift b/Sources/YadUI/Extensions/UIView/UIView+Fluent.swift index 54af469..43e23ea 100644 --- a/Sources/YadUI/Extensions/UIView/UIView+Fluent.swift +++ b/Sources/YadUI/Extensions/UIView/UIView+Fluent.swift @@ -28,4 +28,31 @@ extension UIView { backgroundColor = color return self } + + public func hugging(_ priority: UILayoutPriority, for axis: NSLayoutConstraint.Axis) -> Self { + setContentHuggingPriority(priority, for: axis) + return self + } + + public func hugging(_ priority: UILayoutPriority) -> Self { + setContentHuggingPriority(priority, for: .vertical) + setContentHuggingPriority(priority, for: .horizontal) + return self + } + + public func compressionResistance(_ priority: UILayoutPriority, for axis: NSLayoutConstraint.Axis) -> Self { + setContentCompressionResistancePriority(priority, for: axis) + return self + } + + public func compressionResistance(_ priority: UILayoutPriority) -> Self { + setContentCompressionResistancePriority(priority, for: .vertical) + setContentCompressionResistancePriority(priority, for: .horizontal) + return self + } + + public func tag(_ tag: Int) -> Self { + self.tag = tag + return self + } } diff --git a/Sources/YadUI/Protocols/InitConvertible.swift b/Sources/YadUI/Protocols/InitConvertible.swift new file mode 100644 index 0000000..92e8244 --- /dev/null +++ b/Sources/YadUI/Protocols/InitConvertible.swift @@ -0,0 +1,15 @@ +// +// InitConvertible.swift +// +// +// Created by Selim Mustafaev on 30.07.2023. +// + +import Foundation + +public protocol InitConvertible { + + associatedtype Param + + init(_ param: Param) +} diff --git a/Sources/YadUI/Views/Stack/Stack+Directional.swift b/Sources/YadUI/Views/Stack/Stack+Directional.swift index a49fab6..724addb 100644 --- a/Sources/YadUI/Views/Stack/Stack+Directional.swift +++ b/Sources/YadUI/Views/Stack/Stack+Directional.swift @@ -7,6 +7,7 @@ import UIKit +@available(iOS 13.0, *) public class VStack: Stack { public init(alignment: Alignment = .fill, @@ -26,6 +27,7 @@ public class VStack: Stack { } } +@available(iOS 13.0, *) public class HStack: Stack { public init(alignment: Alignment = .fill, diff --git a/Sources/YadUI/Views/Stack/Stack+Enums.swift b/Sources/YadUI/Views/Stack/Stack+Enums.swift index cddaef6..af3dac8 100644 --- a/Sources/YadUI/Views/Stack/Stack+Enums.swift +++ b/Sources/YadUI/Views/Stack/Stack+Enums.swift @@ -7,11 +7,15 @@ import UIKit +@available(iOS 13.0, *) extension Stack { public enum Alignment { case fill + case center + case start + case end } public enum Distribution { diff --git a/Sources/YadUI/Views/Stack/Stack.swift b/Sources/YadUI/Views/Stack/Stack.swift index 21ebac2..b0b52b5 100644 --- a/Sources/YadUI/Views/Stack/Stack.swift +++ b/Sources/YadUI/Views/Stack/Stack.swift @@ -6,22 +6,23 @@ // import UIKit +import Combine +@available(iOS 13.0, *) public class Stack: UIView { - private var arrangedSubviews: [UIView] - private var alignment: Alignment - private var distribution: Distribution - private var axis: Axis - private var spacing: CGFloat + public var arrangedSubviews: [UIView] + public var distribution: Distribution + public var axis: Axis + public var spacing: CGFloat - var aStart: NSLayoutConstraint.Attribute { - axis == .vertical ? .leading : .top + public var alignment: Alignment { + didSet { setNeedsUpdateConstraints() } } - var aEnd: NSLayoutConstraint.Attribute { - axis == .vertical ? .trailing : .bottom - } + private var allConstraints: [NSLayoutConstraint] = [] + + private var cancellables: [AnyCancellable] = [] var dStart: NSLayoutConstraint.Attribute { axis == .vertical ? .top : .leading @@ -31,6 +32,20 @@ public class Stack: UIView { axis == .vertical ? .bottom : .trailing } + var alignmentAttributes: [NSLayoutConstraint.Attribute] { + + switch alignment { + case .fill: + return axis == .vertical ? [.leading, .trailing] : [.top, .bottom] + case .center: + return axis == .vertical ? [.centerX] : [.centerY] + case .start: + return axis == .vertical ? [.leading] : [.top] + case .end: + return axis == .vertical ? [.trailing] : [.bottom] + } + } + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -49,15 +64,30 @@ public class Stack: UIView { super.init(frame: .zero) self.arrangedSubviews.forEach(addSubview) + setNeedsUpdateConstraints() + } + + public override func updateConstraints() { addAlignmentConstraints() addDistributionConstraints() + super.updateConstraints() + } + + func clearConstraints() { + NSLayoutConstraint.deactivate(allConstraints) + removeConstraints(allConstraints) + allConstraints.removeAll() } func addAlignmentConstraints() { - switch alignment { - case .fill: - addAlignmentFillConstraints() + clearConstraints() + + for view in arrangedSubviews { + alignmentAttributes.forEach { + let constraint = addConstraint(src: view, srcAttr: $0, dst: self, dstAttr: $0) + allConstraints.append(constraint) + } } } @@ -69,20 +99,6 @@ public class Stack: UIView { } } - func addAlignmentFillConstraints() { - guard let firstView = arrangedSubviews.first else { return } - - let otherViews = arrangedSubviews.dropFirst() - - constraint(src: firstView, srcAttr: aStart, dst: self, dstAttr: aStart) - constraint(src: firstView, srcAttr: aEnd, dst: self, dstAttr: aEnd) - - for view in otherViews { - constraint(src: view, srcAttr: aStart, dst: firstView, dstAttr: aStart) - constraint(src: view, srcAttr: aEnd, dst: firstView, dstAttr: aEnd) - } - } - func addDistributionFillConstraints() { guard let firstView = arrangedSubviews.first, let lastView = arrangedSubviews.last @@ -92,15 +108,32 @@ public class Stack: UIView { let middleViews = arrangedSubviews.dropFirst().dropLast() - constraint(src: firstView, srcAttr: dStart, dst: self, dstAttr: dStart) - constraint(src: lastView, srcAttr: dEnd, dst: self, dstAttr: dEnd) + addConstraint(src: firstView, srcAttr: dStart, dst: self, dstAttr: dStart) + addConstraint(src: lastView, srcAttr: dEnd, dst: self, dstAttr: dEnd) var currentView = firstView for view in middleViews { - constraint(src: view, srcAttr: dStart, dst: currentView, dstAttr: dEnd, constant: spacing) + addConstraint(src: view, srcAttr: dStart, dst: currentView, dstAttr: dEnd, constant: spacing) currentView = view } - constraint(src: lastView, srcAttr: dStart, dst: currentView, dstAttr: dEnd, constant: spacing) + addConstraint(src: lastView, srcAttr: dStart, dst: currentView, dstAttr: dEnd, constant: spacing) + } + + @available(iOS 16.0.0, *) + public func bind(_ prop: ReferenceWritableKeyPath, + to dstTag: Int, + dstProp: KeyPath.Publisher>) -> Self where Src: InitConvertible, Src.Param == Dst { + + // TODO: search relative to root view + guard let view = viewWithTag(dstTag) as? U else { + return self + } + + let pub = view[keyPath: dstProp].map { Src($0) } + pub.assign(to: prop, on: self) + .store(in: &cancellables) + + return self } } diff --git a/Sources/YadUI/Views/UIKit/YASegmentedControl.swift b/Sources/YadUI/Views/UIKit/YASegmentedControl.swift new file mode 100644 index 0000000..ada11ab --- /dev/null +++ b/Sources/YadUI/Views/UIKit/YASegmentedControl.swift @@ -0,0 +1,30 @@ +// +// YASegmentedControl.swift +// +// +// Created by Selim Mustafaev on 30.07.2023. +// + +import UIKit + +@available(iOS 13.0, *) +public class YASegmentedControl: UISegmentedControl { + + @Published + public var selectedIndex: Int = 0 + + public override init(items: [Any]?) { + super.init(items: items) + addTarget(self, action: #selector(segmentedValueChanged), for: .valueChanged) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc func segmentedValueChanged(_ sender: UISegmentedControl!) + { + print("Selected Segment Index is : \(sender.selectedSegmentIndex)") + selectedIndex = sender.selectedSegmentIndex + } +} diff --git a/YadUIDemo/YadUIDemo/ViewController.swift b/YadUIDemo/YadUIDemo/ViewController.swift index 3ae00e9..ecb48d8 100644 --- a/YadUIDemo/YadUIDemo/ViewController.swift +++ b/YadUIDemo/YadUIDemo/ViewController.swift @@ -8,6 +8,18 @@ import UIKit import YadUI +extension Stack.Alignment: InitConvertible { + + public init(_ value: Int) { + switch value { + case 0: self = .start + case 1: self = .center + case 2: self = .end + default: self = .fill + } + } +} + class ViewController: UIViewController { override func viewDidLoad() { @@ -15,23 +27,25 @@ class ViewController: UIViewController { let child = buildView() view.addSubview(child) - child.pin(toSafeArea: view) + child.pin(toSafeArea: view, insets: .init(all: 16)) } @RootViewBuilder func buildView() -> UIView { - VStack(spacing: 16) { - UIView() - .background(.red) - //.width(100) - .height(100) - UIView() - .background(.green) - //.width(100) - .height(100) - UIView() + VStack(alignment: .start, spacing: 16) { + UILabel() + .text("qwe") .background(.blue) - //.height(100) + UILabel() + .text("asdf") + .background(.blue) + UILabel() + .text("zxcv") + .background(.blue) + .hugging(.defaultLowMinus) + YASegmentedControl(items: ["Left", "Center", "Right"]) + .tag(100) } + .bind(\.alignment, to: 100, dstProp: \YASegmentedControl.$selectedIndex) } }