From beafbdcbb229d7feb59e932fbfdc669f29ab3a08 Mon Sep 17 00:00:00 2001 From: cjnevin Date: Sun, 16 Oct 2022 08:20:04 +1000 Subject: [PATCH] Added stackview helpers --- Sources/AutoLayoutBuilder/StackView.swift | 184 ++++++++++++++++++ Sources/AutoLayoutBuilder/StackableView.swift | 75 +++++++ .../StackableViewBuilder.swift | 32 +++ 3 files changed, 291 insertions(+) create mode 100644 Sources/AutoLayoutBuilder/StackView.swift create mode 100644 Sources/AutoLayoutBuilder/StackableView.swift create mode 100644 Sources/AutoLayoutBuilder/StackableViewBuilder.swift diff --git a/Sources/AutoLayoutBuilder/StackView.swift b/Sources/AutoLayoutBuilder/StackView.swift new file mode 100644 index 0000000..eaea60f --- /dev/null +++ b/Sources/AutoLayoutBuilder/StackView.swift @@ -0,0 +1,184 @@ +#if canImport(UIKit) + +import UIKit + +public protocol StackViewProtocol: UIView { + @discardableResult func addStackedView(_ view: StackableView) -> Self + @discardableResult func insertStackedView(_ view: StackableView, at index: Int) -> Self + @discardableResult func insertStackedView(_ view: StackableView, after: StackableView) -> Self + @discardableResult func removeStackedView(_ view: StackableView) -> Self + @discardableResult func hideStackedView(_ view: StackableView) -> Self + @discardableResult func showStackedView(_ view: StackableView) -> Self + @discardableResult func replaceStackedViews(@StackableViewBuilder _ views: () -> [StackableView]) -> Self + @discardableResult func spacing(_ spacing: CGFloat) -> Self + @discardableResult func spacing(_ spacing: CGFloat, after: StackableView) -> Self +} + +public class StackView: UIView, StackViewProtocol { + private let stackView = UIStackView() + private var stackedViews: [StackableView] + + public init(axis: NSLayoutConstraint.Axis = .vertical, @StackableViewBuilder subviews: () -> [StackableView] = { [] }) { + stackedViews = subviews() + super.init(frame: .zero) + stackedViews + .lazy + .map(\.view) + .forEach(stackView.addArrangedSubview) + stackView.axis = axis + addSubview(stackView) { + $0.edges == Superview() + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @discardableResult public func addStackedView(_ view: StackableView) -> Self { + stackedViews.append(view) + stackView.addArrangedSubview(view.view) + return self + } + + @discardableResult public func insertStackedView(_ view: StackableView, at index: Int) -> Self { + stackedViews.insert(view, at: index + 1) + stackView.insertArrangedSubview(view.view, at: index + 1) + return self + } + + @discardableResult public func insertStackedView(_ view: StackableView, after: StackableView) -> Self { + stackedIndex(for: after.view).map { index in + insertStackedView(view, at: index) + } ?? self + } + + @discardableResult public func removeStackedView(_ view: StackableView) -> Self { + stackedIndex(for: view).map { index in + stackedViews[index].view.removeFromSuperview() + stackView.removeArrangedSubview(stackedViews[index].view) + stackedViews.remove(at: index) + } + return self + } + + @discardableResult public func hideStackedView(_ view: StackableView) -> Self { + stackedView(for: view)?.view.isHidden = true + return self + } + + @discardableResult public func showStackedView(_ view: StackableView) -> Self { + stackedView(for: view)?.view.isHidden = false + return self + } + + @discardableResult public func replaceStackedViews(@StackableViewBuilder _ views: () -> [StackableView]) -> Self { + stackedViews.forEach { + removeStackedView($0) + } + views().forEach { + addStackedView($0) + } + return self + } + + @discardableResult public func spacing(_ spacing: CGFloat) -> Self { + stackView.spacing = spacing + return self + } + + @discardableResult public func spacing(_ spacing: CGFloat, after: StackableView) -> Self { + (stackedView(for: after)?.view).map { + stackView.setCustomSpacing(spacing, after: $0) + } + return self + } + + private func stackedIndex(for view: StackableView) -> Int? { + stackedViews.firstIndex { stackableView in + stackableView.view == view.originalView + || stackableView.originalView == view.originalView + || stackableView.view == view.view + || stackableView.originalView == view.view + } + } + + private func stackedView(for view: StackableView) -> StackableView? { + stackedIndex(for: view).map { + stackedViews[$0] + } + } +} + +public class ScrollableStackView: UIView, StackViewProtocol { + private let stackView: StackView + private let scrollView: UIScrollView + + public init(axis: NSLayoutConstraint.Axis = .vertical, @StackableViewBuilder subviews: () -> [StackableView] = { [] }) { + scrollView = UIScrollView() + stackView = StackView(axis: axis, subviews: subviews) + super.init(frame: .zero) + addSubview(scrollView) { + $0.edges == Superview() + $0.addSubview(stackView) { + if axis == .vertical { + $0.width == self + } else { + $0.height == self + } + $0.edges == Superview() + } + } + } + + @discardableResult public func addStackedView(_ view: StackableView) -> Self { + stackView.addStackedView(view.view) + return self + } + + @discardableResult public func insertStackedView(_ view: StackableView, at index: Int) -> Self { + stackView.insertStackedView(view, at: index) + return self + } + + @discardableResult public func insertStackedView(_ view: StackableView, after: StackableView) -> Self { + stackView.insertStackedView(view, after: after) + return self + } + + @discardableResult public func removeStackedView(_ view: StackableView) -> Self { + stackView.removeStackedView(view) + return self + } + + @discardableResult public func hideStackedView(_ view: StackableView) -> Self { + stackView.hideStackedView(view) + return self + } + + @discardableResult public func showStackedView(_ view: StackableView) -> Self { + stackView.showStackedView(view) + return self + } + + @discardableResult public func replaceStackedViews(@StackableViewBuilder _ views: () -> [StackableView]) -> Self { + stackView.replaceStackedViews(views) + return self + } + + @discardableResult public func spacing(_ spacing: CGFloat) -> Self { + stackView.spacing(spacing) + return self + } + + @discardableResult public func spacing(_ spacing: CGFloat, after: StackableView) -> Self { + stackView.spacing(spacing, after: after) + return self + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +#endif diff --git a/Sources/AutoLayoutBuilder/StackableView.swift b/Sources/AutoLayoutBuilder/StackableView.swift new file mode 100644 index 0000000..87b2f9b --- /dev/null +++ b/Sources/AutoLayoutBuilder/StackableView.swift @@ -0,0 +1,75 @@ +#if canImport(UIKit) + +import UIKit + +public protocol StackableView { + var view: UIView { get } + var originalView: UIView { get } +} + +extension UIView: StackableView { + public var view: UIView { self } + public var originalView: UIView { self } +} + +extension StackableView { + public func top(_ padding: CGFloat = 0) -> StackableView { + StackedView(view, originalView: originalView) { + $0.horizontalEdges.top(padding) == Superview() + $0.bottom <= Superview() + } + } + + public func leading(_ padding: CGFloat = 0) -> StackableView { + StackedView(view, originalView: originalView) { + $0.verticalEdges.leading(padding) == Superview() + $0.trailing <= Superview() + } + } + + public func trailing(_ padding: CGFloat = 0) -> StackableView { + StackedView(view, originalView: originalView) { + $0.verticalEdges.trailing(-padding) == Superview() + $0.leading >= Superview() + } + } + + public func bottom(_ padding: CGFloat = 0) -> StackableView { + StackedView(view, originalView: originalView) { + $0.horizontalEdges.bottom(-padding) == Superview() + $0.top >= Superview() + } + } + + public func centered(_ padding: CGFloat = 0) -> StackableView { + StackedView(view, originalView: originalView) { + $0.centerX.centerY == Superview() + $0.top(padding).leading(padding) >= Superview() + $0.trailing(-padding).bottom(-padding) <= Superview() + } + } + + public func padding(_ padding: CGFloat) -> StackableView { + StackedView(view, originalView: originalView) { + $0.horizontalEdges(padding).verticalEdges == Superview() + } + } + + public func verticalPadding(_ padding: CGFloat) -> StackableView { + StackedView(view, originalView: originalView) { + $0.horizontalEdges.verticalEdges(padding) == Superview() + } + } +} + +private struct StackedView: StackableView { + let view: UIView = UIView() + let originalView: UIView + + init(_ targetView: View, originalView: UIView, @AutoLayoutBuilder constraints: (View) -> [Constrainable]) { + self.originalView = originalView + self.view.addSubview(targetView, with: constraints) + } +} + +#endif diff --git a/Sources/AutoLayoutBuilder/StackableViewBuilder.swift b/Sources/AutoLayoutBuilder/StackableViewBuilder.swift new file mode 100644 index 0000000..a4aa9e4 --- /dev/null +++ b/Sources/AutoLayoutBuilder/StackableViewBuilder.swift @@ -0,0 +1,32 @@ +#if canImport(UIKit) + +import UIKit + +@resultBuilder +public enum StackableViewBuilder { + public static func buildBlock(_ components: StackableView...) -> [StackableView] { + components + } + + public static func buildArray(_ components: [[StackableView]]) -> [StackableView] { + components.flatMap { $0 } + } + + public static func buildOptional(_ component: [StackableView]?) -> [StackableView] { + component ?? [] + } + + public static func buildEither(first component: [StackableView]) -> [StackableView] { + component + } + + public static func buildEither(second component: [StackableView]) -> [StackableView] { + component + } + + public static func buildLimitedAvailability(_ component: [StackableView]) -> [StackableView] { + component + } +} + +#endif