Code cleanup

This commit is contained in:
Michael Ong
2023-04-06 09:50:21 +08:00
parent a9f90b8402
commit 2db3488edc
15 changed files with 405 additions and 383 deletions

View File

@@ -7,14 +7,14 @@ import Foundation
public struct FieldValidator<Shape, Value> {
public let validate: (Shape, Value) throws -> Void
public let propagates: Bool
public init(propagates: Bool = true, validate: @escaping (Shape, Value) throws -> Void) {
self.validate = validate
self.propagates = propagates
}
public init(propagates: Bool = true, validate: @escaping (Value) throws -> Void) {
self.validate = { _, value throws in
try validate(value)

View File

@@ -1,7 +1,7 @@
// form-binding.swift
//
// Code Copyright Buslo Collective
// Created 2/3/23
// Created 2/23/23
import Foundation
@@ -22,21 +22,6 @@ public struct Binding<Source: AnyObject, Value> {
initialValue = source[keyPath: path]
}
#if swift(<5.8) // issue fixed in swift 5.8!
public init(
source: Source,
to path: ReferenceWritableKeyPath<Source, Value?>
) {
action = { [unowned source] value in
source[keyPath: path] = value
return value
}
initialValue = source[keyPath: path]
}
#endif
public init<Out, T: FieldTransformable>(
source: Source,
to path: ReferenceWritableKeyPath<Source, Out>,
@@ -148,6 +133,23 @@ public extension Binding {
}
}
public extension Binding {
#if swift(<5.8)
init(
source: Source,
to path: ReferenceWritableKeyPath<Source, Value?>
) {
action = { [unowned source] value in
source[keyPath: path] = value
return value
}
initialValue = source[keyPath: path]
}
#endif
}
public typealias FieldPartialValueKeyPath<Source, Complete, Value> = ReferenceWritableKeyPath<
Source, FieldPartialValue<Complete, Value>
>

View File

@@ -1,4 +1,4 @@
// form.swift
// form-handler.swift
//
// Code Copyright Buslo Collective
// Created 2/3/23
@@ -18,7 +18,7 @@ public class FormHandler<Shape> {
public var isSubmitFailed: Bool {
submitErrors != nil
}
public let submitErrors: Error?
public let validationResult: FormValidation<Shape>.Result
@@ -30,11 +30,11 @@ public class FormHandler<Shape> {
var submitInvoked: Bool
var submitErrors: Error?
var updateHandler: UpdatesHandler = { _, _ async in }
var validations: FormValidation<Shape>?
var observations: FormObserver<Shape>?
public init(initial: Shape, submitInvoked: Bool = false) {
@@ -57,7 +57,7 @@ public class FormHandler<Shape> {
} catch {
submitErrors = error
await pushUpdates()
throw error
}
}
@@ -74,7 +74,7 @@ public class FormHandler<Shape> {
validationResult: result
)
)
return result
}
}
@@ -97,11 +97,11 @@ public extension FormHandler {
return self
}
@discardableResult
func setObserverHandler(_ handler: FormObserver<Shape>?) -> Self {
observations = handler
return self
}
}
@@ -113,11 +113,11 @@ public extension FormHandler {
}
set {
current[keyPath: member] = newValue
if let observations, let action = observations.observationDict[member] {
action(newValue)
}
Task { @MainActor () in await pushUpdates() }
}
}

View File

@@ -1,20 +1,18 @@
// form-observer.swift
//
// File.swift
//
//
// Created by Michael Ong on 4/5/23.
//
// Code Copyright Buslo Collective
// Created 4/5/23
import Foundation
public struct FormObserver<Shape> {
let observationDict: [PartialKeyPath<Shape> : (Any) -> Void]
let observationDict: [PartialKeyPath<Shape>: (Any) -> Void]
public init() {
observationDict = [:]
}
init(observationDict: [PartialKeyPath<Shape> : (Any) -> Void]) {
init(observationDict: [PartialKeyPath<Shape>: (Any) -> Void]) {
self.observationDict = observationDict
}
@@ -23,7 +21,7 @@ public struct FormObserver<Shape> {
newDict.updateValue({ value in
observer(value as! Value)
}, forKey: path)
return .init(observationDict: newDict)
}
}

View File

@@ -29,23 +29,23 @@ public struct FormValidation<Shape> {
if validationsDict[path] != nil {
fatalError("Appending additional validations for path not allowed.")
}
var newDict = validationsDict
newDict[path] = { shape in
var fieldErrors = [Error]()
for field in fields {
do {
try field.validate(shape, shape[keyPath: path])
} catch {
fieldErrors.append(error)
if !field.propagates {
break
}
}
}
return fieldErrors
}
@@ -94,63 +94,63 @@ public struct FormValidation<Shape> {
}
}
extension FormValidation {
#if swift(<5.8)
public func validations<Value>(
for path: KeyPath<Shape, Value?>,
are fields: FieldValidator<Shape, Value>...
) -> Self {
var newDict = validationsDict
newDict[path] = { shape in
var fieldErrors = [Error]()
public extension FormValidation {
#if swift(<5.8)
func validations<Value>(
for path: KeyPath<Shape, Value?>,
are fields: FieldValidator<Shape, Value>...
) -> Self {
var newDict = validationsDict
newDict[path] = { shape in
var fieldErrors = [Error]()
fields.forEach { field in
do {
if let value = shape[keyPath: path] {
try field.validate(shape, value)
} else {
fatalError("Tried to validate a field that does not exist or is nil.")
}
} catch {
fieldErrors.append(error)
}
}
return fieldErrors
}
return .init(newDict)
}
public func validations<Value>(
for path: KeyPath<Shape, FieldPartialValue<Value, some Any>?>,
are fields: FieldValidator<Shape, Value>...
) -> Self {
var newDict = validationsDict
newDict[path] = { shape in
var fieldErrors = [Error]()
fields.forEach { field in
do {
if let value = shape[keyPath: path] {
switch value {
case let .complete(complete):
try field.validate(shape, complete)
case let .partial(_, error):
throw FieldError.incomplete(error)
fields.forEach { field in
do {
if let value = shape[keyPath: path] {
try field.validate(shape, value)
} else {
fatalError("Tried to validate a field that does not exist or is nil.")
}
} else {
fatalError("Tried to validate a field that does not exist or is nil.")
} catch {
fieldErrors.append(error)
}
} catch {
fieldErrors.append(error)
}
return fieldErrors
}
return fieldErrors
return .init(newDict)
}
return .init(newDict)
}
#endif
func validations<Value>(
for path: KeyPath<Shape, FieldPartialValue<Value, some Any>?>,
are fields: FieldValidator<Shape, Value>...
) -> Self {
var newDict = validationsDict
newDict[path] = { shape in
var fieldErrors = [Error]()
fields.forEach { field in
do {
if let value = shape[keyPath: path] {
switch value {
case let .complete(complete):
try field.validate(shape, complete)
case let .partial(_, error):
throw FieldError.incomplete(error)
}
} else {
fatalError("Tried to validate a field that does not exist or is nil.")
}
} catch {
fieldErrors.append(error)
}
}
return fieldErrors
}
return .init(newDict)
}
#endif
}

View File

@@ -1,7 +1,7 @@
// protocol-field-transformable.swift
//
// Code Copyright Buslo Collective
// Created 2/3/23
// Created 2/23/23
import Foundation

View File

@@ -10,24 +10,22 @@ public protocol CollectionDelegate: AnyObject {
associatedtype DelegateItemType: Hashable
@MainActor func collectionItemSelected(at indexPath: IndexPath, _ item: DelegateItemType) async
@MainActor func collectionContentRectChanged(_ rect: Templates.CollectionContentRect)
}
public extension CollectionDelegate {
func collectionContentRectChanged(_ rect: Templates.CollectionContentRect) {
}
func collectionContentRectChanged(_: Templates.CollectionContentRect) {}
}
public extension Templates {
struct CollectionContentRect {
public let offset: CGPoint
public let offsetAdjusted: CGPoint
public let size: CGSize
}
final class Collection<S: Hashable, E: Hashable>: UIView, HasHeader {
public typealias SectionLayout = (__shared DataSource, Int, S) -> NSCollectionLayoutSection
@@ -38,11 +36,14 @@ public extension Templates {
public private(set) weak var headerView: (UIView & ViewHeader)?
public private(set) weak var collectionView: UICollectionView!
public var showHairlineBorder = true {
didSet {
let invalidationContext = UICollectionViewLayoutInvalidationContext()
invalidationContext.invalidateSupplementaryElements(ofKind: CollectionHeaderElementType, at: [ .init(row: 0, section: 0) ])
invalidationContext.invalidateSupplementaryElements(
ofKind: CollectionHeaderElementType,
at: [.init(row: 0, section: 0)]
)
collectionView.collectionViewLayout.invalidateLayout(with: invalidationContext)
}
@@ -52,13 +53,13 @@ public extension Templates {
public init(
frame: CGRect,
header: __owned (UIView & ViewHeader)? = nil,
header: __owned(UIView & ViewHeader)? = nil,
footer: __owned UIView? = nil,
source: (__shared UICollectionView) -> DataSource,
layout: (_ dataSource: DataSource) -> UICollectionViewCompositionalLayout
) {
headerView = header
let headerRegistration = Header.Registration
let footerRegistration = Footer.Registration
@@ -96,10 +97,11 @@ public extension Templates {
}
}
addSubview(view: collectionView) { make in
make.edges
.equalToSuperview()
}
addSubview(collectionView
.defineConstraints { make in
make.edges
.equalToSuperview()
})
let collectionLayout = layout(source)
let configuration = collectionLayout.configuration
@@ -143,7 +145,7 @@ public extension Templates {
public convenience init(
frame: CGRect,
header: __owned (UIView & ViewHeader)? = nil,
header: __owned(UIView & ViewHeader)? = nil,
footer: __owned UIView? = nil,
source: (__shared UICollectionView) -> DataSource,
layout: @escaping SectionLayout
@@ -152,15 +154,16 @@ public extension Templates {
frame: frame,
header: header,
footer: footer,
source: source) { source in
UICollectionViewCompositionalLayout(sectionProvider: { [unowned source] section, _ in
guard let sectionKind = source.sectionIdentifier(for: section) else {
fatalError()
}
return layout(source, section, sectionKind)
})
}
source: source
) { source in
UICollectionViewCompositionalLayout(sectionProvider: { [unowned source] section, _ in
guard let sectionKind = source.sectionIdentifier(for: section) else {
fatalError()
}
return layout(source, section, sectionKind)
})
}
}
@available(*, unavailable)
@@ -248,39 +251,42 @@ extension Templates.Collection {
func setContainingView(_ content: UIView) -> Self {
addSubview(
view: UIVisualEffectView(
effect: UIBlurEffect(style: .regular)
)
.assign(to: &visualEffect)
.hidden()
) { make in
make.leading
.bottom
.trailing
.equalToSuperview()
make.top
.equalToSuperview()
.inset(-300)
}
addSubview(view: content) { make in
make.edges
.equalToSuperview()
}
UIVisualEffectView(effect: UIBlurEffect(style: .regular))
.assign(to: &visualEffect)
.hidden()
.defineConstraints { make in
make.leading
.bottom
.trailing
.equalToSuperview()
make.top
.equalToSuperview()
.inset(-300)
}
)
addSubview(
view: UIView()
content
.defineConstraints { make in
make.edges
.equalToSuperview()
}
)
addSubview(
UIView()
.backgroundColor(.separator)
.hidden()
.assign(to: &border)
) { make in
make.horizontalEdges
.bottom
.equalToSuperview()
.defineConstraints { make in
make.horizontalEdges
.bottom
.equalToSuperview()
make.height
.equalTo(1 / UIScreen.main.scale)
}
make.height
.equalTo(1 / UIScreen.main.scale)
}
)
traitCollectionDidChange(nil)
@@ -325,25 +331,27 @@ extension Templates.Collection {
func setContainingView(_ content: UIView) -> Self {
addSubview(
view: UIVisualEffectView(
effect: UIBlurEffect(style: .regular)
)
.assign(to: &visualEffect)
.hidden()
) { make in
make.leading
.top
.trailing
.equalToSuperview()
make.bottom
.equalToSuperview()
.inset(-300)
}
UIVisualEffectView(effect: UIBlurEffect(style: .regular))
.assign(to: &visualEffect)
.hidden()
.defineConstraints { make in
make.leading
.top
.trailing
.equalToSuperview()
make.bottom
.equalToSuperview()
.inset(-300)
}
)
addSubview(view: content) { make in
make.edges
.equalToSuperview()
}
addSubview(
content
.defineConstraints { make in
make.edges
.equalToSuperview()
}
)
traitCollectionDidChange(nil)
@@ -357,7 +365,7 @@ extension Templates.Collection {
typealias DataSource<Section: Hashable> = UICollectionViewDiffableDataSource<Section, E>
var itemSelected: (IndexPath) -> Void = { _ in }
var contentRectChanged: (Templates.CollectionContentRect) -> Void = { _ in }
func setupDelegate<D: CollectionDelegate>(
@@ -369,7 +377,7 @@ extension Templates.Collection {
await delegate.collectionItemSelected(at: ip, dataSource.itemIdentifier(for: ip)!)
}
}
contentRectChanged = { [unowned delegate] rect in
delegate.collectionContentRectChanged(rect)
}
@@ -391,7 +399,7 @@ extension Templates.Collection {
header.displayUpdate(ypos >= 1)
}
}
contentRectChanged(
.init(
offset: scrollView.contentOffset,
@@ -404,9 +412,12 @@ extension Templates.Collection {
}
public extension Templates.CollectionContentRect {
func interpolate(from input: ClosedRange<CGFloat>, mapsTo output: ClosedRange<CGFloat>) -> (_ value: CGFloat) -> CGFloat {
func interpolate(
from input: ClosedRange<CGFloat>,
mapsTo output: ClosedRange<CGFloat>
) -> (_ value: CGFloat) -> CGFloat {
let length = (input.upperBound - input.lowerBound)
return { value in
let ratio = (value - input.lowerBound) / length
return min(output.upperBound, max(output.lowerBound, output.lowerBound + output.upperBound * ratio))

View File

@@ -9,23 +9,23 @@ import UIKit
open class ControllerTemplated<Template: UIView, Hook>: Controller<Hook> {
public lazy var screenTemplate: Template = loadTemplate(frame: parent?.view.frame ?? .zero)
open override var title: String? {
override open var title: String? {
didSet {
guard isViewLoaded else {
return
}
updateHeader()
}
}
open var backAction: UIAction {
.init { [unowned self] _ in
guard let navigationController else {
return
}
if navigationController.viewControllers.count == 1 && navigationController.topViewController == self {
if navigationController.viewControllers.count == 1, navigationController.topViewController == self {
navigationController.dismiss(animated: true)
} else {
navigationController.popViewController(animated: true)
@@ -33,21 +33,21 @@ open class ControllerTemplated<Template: UIView, Hook>: Controller<Hook> {
}
}
open func loadTemplate(frame: CGRect) -> Template {
open func loadTemplate(frame _: CGRect) -> Template {
fatalError()
}
override open func loadView() {
view = screenTemplate
updateHeader()
}
private func updateHeader() {
if let template = screenTemplate as? HasHeader {
template.headerView?.setupHeaderAppearance(title: title ?? "", backAction: backAction)
}
screenTemplate.setNeedsLayout()
screenTemplate.layoutIfNeeded()
}

View File

@@ -16,8 +16,9 @@ extension UIView: ConstraintBuildable { }
public extension ConstraintBuildable where Self: UIView {
@discardableResult
func addSubview(_ viewWithConstraints: ConstructedViewWithConstraints<some UIView>) -> Self {
addSubview(view: viewWithConstraints.view, constraints: viewWithConstraints.constraint)
addSubview(viewWithConstraints.view)
viewWithConstraints.view.snp.makeConstraints(viewWithConstraints.constraint)
return self
}

View File

@@ -12,10 +12,13 @@ open class CollectionReusableView: UICollectionReusableView, ReusableRegisterabl
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(view: generateTemplate()) { make in
make.edges
.equalToSuperview()
}
addSubview(
generateTemplate()
.defineConstraints { make in
make.edges
.equalToSuperview()
}
)
}
@available(*, unavailable)

View File

@@ -11,10 +11,13 @@ open class CollectionViewCell<ItemType>: UICollectionViewCell, CellRegisterable
super.init(frame: frame)
contentView
.addSubview(view: loadView()) { make in
make.edges
.equalToSuperview()
}
.addSubview(
loadView()
.defineConstraints { make in
make.edges
.equalToSuperview()
}
)
}
@available(*, unavailable)

View File

@@ -0,0 +1,173 @@
// view+uistackview.swift
//
// Code Copyright Buslo Collective
// Created 4/6/23
import Foundation
import makataInteraction
import UIKit
public var DefaultStackSpacing = CGFloat(8)
public extension UIStackView {
static func horizontal(
spacing: CGFloat = DefaultStackSpacing,
alignment: UIStackView.Alignment = .leading,
distribution: UIStackView.Distribution = .fill,
@ComponentBuilder components: () -> ComponentBuilder.Component
) -> Self {
let stack = Self(frame: .zero)
stack.axis = .horizontal
stack.spacing = spacing
stack.alignment = alignment
stack.distribution = distribution
stack.insetsLayoutMarginsFromSafeArea = false
stack.setContentHuggingPriority(.required, for: .vertical)
switch components() {
case let .single(view, maker):
stack.addArrangedSubview(view)
view.snp.makeConstraints(maker)
case let .result(views):
for (view, maker) in views {
stack.addArrangedSubview(view)
view.snp.makeConstraints(maker)
}
}
return stack
}
static func vertical(
spacing: CGFloat = DefaultStackSpacing,
alignment: UIStackView.Alignment = .leading,
distribution: UIStackView.Distribution = .fill,
@ComponentBuilder components: () -> ComponentBuilder.Component
) -> Self {
let stack = Self(frame: .zero)
stack.axis = .vertical
stack.spacing = spacing
stack.alignment = alignment
stack.distribution = distribution
stack.insetsLayoutMarginsFromSafeArea = false
stack.setContentHuggingPriority(.required, for: .vertical)
switch components() {
case let .single(view, maker):
stack.addArrangedSubview(view)
view.snp.makeConstraints(maker)
case let .result(views):
for (view, maker) in views {
stack.addArrangedSubview(view)
view.snp.makeConstraints(maker)
}
}
return stack
}
@discardableResult
func margins(_ insets: UIEdgeInsets, relativeToSafeArea: Bool = false) -> Self {
isLayoutMarginsRelativeArrangement = true
insetsLayoutMarginsFromSafeArea = relativeToSafeArea
layoutMargins = insets
return self
}
}
public extension UIStackView {
static func renderHorizontal<Value>(
from observable: Observable<Value>.Projection,
_ lifetime: inout Lifetimeable?,
spacing: CGFloat = DefaultStackSpacing,
alignment: UIStackView.Alignment = .leading,
distribution: UIStackView.Distribution = .fill,
@ComponentBuilder components: @escaping @MainActor (Value) -> ComponentBuilder.Component
) -> Self {
let stack = Self(frame: .zero)
stack.axis = .horizontal
stack.spacing = spacing
stack.alignment = alignment
stack.distribution = distribution
stack.insetsLayoutMarginsFromSafeArea = false
stack.setContentHuggingPriority(.required, for: .vertical)
var lastReferences = [UIView]()
lifetime = observable.subscribe { [unowned stack] value in
DispatchQueue.main.async {
lastReferences.forEach {
$0.removeFromSuperview()
}
switch components(value) {
case let .single(view, maker):
stack.addArrangedSubview(view)
view.snp.makeConstraints(maker)
lastReferences = [view]
case let .result(views):
for (view, maker) in views {
stack.addArrangedSubview(view)
view.snp.makeConstraints(maker)
}
lastReferences = views.map(\.0)
}
}
}
return stack
}
static func renderVertical<Value>(
from observable: Observable<Value>.Projection,
_ lifetime: inout Lifetimeable?,
spacing: CGFloat = DefaultStackSpacing,
alignment: UIStackView.Alignment = .leading,
distribution: UIStackView.Distribution = .fill,
@ComponentBuilder components: @escaping @MainActor (Value) -> ComponentBuilder.Component
) -> Self {
let stack = Self(frame: .zero)
stack.axis = .vertical
stack.spacing = spacing
stack.alignment = alignment
stack.distribution = distribution
stack.insetsLayoutMarginsFromSafeArea = false
stack.setContentHuggingPriority(.required, for: .vertical)
var lastReferences = [UIView]()
lifetime = observable.subscribe { [unowned stack] value in
DispatchQueue.main.async {
lastReferences.forEach {
$0.removeFromSuperview()
}
switch components(value) {
case let .single(view, maker):
stack.addArrangedSubview(view)
view.snp.makeConstraints(maker)
lastReferences = [view]
case let .result(views):
for (view, maker) in views {
stack.addArrangedSubview(view)
view.snp.makeConstraints(maker)
}
lastReferences = views.map(\.0)
}
}
}
return stack
}
}

View File

@@ -1,17 +1,11 @@
// view-builder.swift
//
// Code Copyright Buslo Collective
// Created 2/3/23
// Created 3/12/23
//
// File.swift
//
//
// Created by Michael Ong on 2/3/23.
//
import Foundation
import UIKit
import SnapKit
import UIKit
@resultBuilder
public enum ComponentBuilder {
@@ -21,11 +15,9 @@ public enum ComponentBuilder {
}
public static func buildExpression(_ expression: some UIView) -> Component {
.single(expression) { _ in
}
.single(expression) { _ in }
}
public static func buildExpression(_ expression: ConstructedViewWithConstraints<some UIView>) -> Component {
.single(expression.view, expression.constraint)
}
@@ -48,31 +40,31 @@ public enum ComponentBuilder {
public static func buildArray(_ components: [ComponentBuilder.Component]) -> ComponentBuilder.Component {
var items = [(UIView, (ConstraintMaker) -> Void)]()
for component in components {
switch component {
case .single(let view, let maker):
case let .single(view, maker):
items.append((view, maker))
case .result(let existing):
case let .result(existing):
items.append(contentsOf: existing)
}
}
return .result(items)
}
public static func buildBlock(_ components: Component...) -> Component {
var items = [(UIView, (ConstraintMaker) -> Void)]()
for component in components {
switch component {
case .single(let view, let maker):
case let .single(view, maker):
items.append((view, maker))
case .result(let existing):
case let .result(existing):
items.append(contentsOf: existing)
}
}
return .result(items)
}
}

View File

@@ -8,8 +8,6 @@ import makataInteraction
import SnapKit
import UIKit
public var DefaultStackSpacing = CGFloat(8)
extension UIView: Assignable, Attributable, Withable {}
public extension UIView {
@@ -28,15 +26,6 @@ public extension UIView {
return view
}
convenience init(
containing: some UIView,
constraints: (ConstraintMaker) -> Void = { $0.edges.equalToSuperview() }
) {
self.init(frame: .zero)
addSubview(view: containing, constraints: constraints)
}
@discardableResult
func autoFocus() -> Self {
becomeFirstResponder()
@@ -87,16 +76,6 @@ public extension UIView {
return self
}
@discardableResult
func addSubview(view: some UIView, constraints: (ConstraintMaker) -> Void) -> Self {
view.translatesAutoresizingMaskIntoConstraints = false
addSubview(view)
view.snp.makeConstraints(constraints)
return self
}
@discardableResult
func renderSubviews<Value>(
from observer: Observable<Value>.Projection,
@@ -113,12 +92,12 @@ public extension UIView {
switch update(value) {
case let .single(view, maker):
addSubview(view: view, constraints: maker)
addSubview(view.defineConstraints(maker))
lastReferences = [view]
case let .result(views):
for (view, maker) in views {
addSubview(view: view, constraints: maker)
addSubview(view.defineConstraints(maker))
}
lastReferences = views.map { $0.0 }
@@ -130,165 +109,25 @@ public extension UIView {
}
}
public extension UIStackView {
static func horizontal(
spacing: CGFloat = DefaultStackSpacing,
alignment: UIStackView.Alignment = .leading,
distribution: UIStackView.Distribution = .fill,
@ComponentBuilder components: () -> ComponentBuilder.Component
) -> Self {
let stack = Self(frame: .zero)
stack.axis = .horizontal
stack.spacing = spacing
stack.alignment = alignment
stack.distribution = distribution
stack.insetsLayoutMarginsFromSafeArea = false
public extension UIView {
@available(*, deprecated, message: "Call addSubview with defineConstraints as the last method call in its builder chain instead.")
convenience init(
containing: some UIView,
constraints: (ConstraintMaker) -> Void = { $0.edges.equalToSuperview() }
) {
self.init(frame: .zero)
stack.setContentHuggingPriority(.required, for: .vertical)
switch components() {
case let .single(view, maker):
stack.addArrangedSubview(view)
view.snp.makeConstraints(maker)
case let .result(views):
for (view, maker) in views {
stack.addArrangedSubview(view)
view.snp.makeConstraints(maker)
}
}
return stack
}
static func vertical(
spacing: CGFloat = DefaultStackSpacing,
alignment: UIStackView.Alignment = .leading,
distribution: UIStackView.Distribution = .fill,
@ComponentBuilder components: () -> ComponentBuilder.Component
) -> Self {
let stack = Self(frame: .zero)
stack.axis = .vertical
stack.spacing = spacing
stack.alignment = alignment
stack.distribution = distribution
stack.insetsLayoutMarginsFromSafeArea = false
stack.setContentHuggingPriority(.required, for: .vertical)
switch components() {
case let .single(view, maker):
stack.addArrangedSubview(view)
view.snp.makeConstraints(maker)
case let .result(views):
for (view, maker) in views {
stack.addArrangedSubview(view)
view.snp.makeConstraints(maker)
}
}
return stack
addSubview(view: containing, constraints: constraints)
}
@available(*, deprecated, message: "Call addSubview with defineConstraints as the last method call in its builder chain instead.")
@discardableResult
func margins(_ insets: UIEdgeInsets, relativeToSafeArea: Bool = false) -> Self {
isLayoutMarginsRelativeArrangement = true
insetsLayoutMarginsFromSafeArea = relativeToSafeArea
layoutMargins = insets
func addSubview(view: some UIView, constraints: (ConstraintMaker) -> Void) -> Self {
view.translatesAutoresizingMaskIntoConstraints = false
addSubview(view)
view.snp.makeConstraints(constraints)
return self
}
}
public extension UIStackView {
static func renderHorizontal<Value>(
from observable: Observable<Value>.Projection,
_ lifetime: inout Lifetimeable?,
spacing: CGFloat = DefaultStackSpacing,
alignment: UIStackView.Alignment = .leading,
distribution: UIStackView.Distribution = .fill,
@ComponentBuilder components: @escaping @MainActor (Value) -> ComponentBuilder.Component
) -> Self {
let stack = Self(frame: .zero)
stack.axis = .horizontal
stack.spacing = spacing
stack.alignment = alignment
stack.distribution = distribution
stack.insetsLayoutMarginsFromSafeArea = false
stack.setContentHuggingPriority(.required, for: .vertical)
var lastReferences = [UIView]()
lifetime = observable.subscribe { [unowned stack] value in
DispatchQueue.main.async {
lastReferences.forEach {
$0.removeFromSuperview()
}
switch components(value) {
case let .single(view, maker):
stack.addArrangedSubview(view)
view.snp.makeConstraints(maker)
lastReferences = [ view ]
case let .result(views):
for (view, maker) in views {
stack.addArrangedSubview(view)
view.snp.makeConstraints(maker)
}
lastReferences = views.map { $0.0 }
}
}
}
return stack
}
static func renderVertical<Value>(
from observable: Observable<Value>.Projection,
_ lifetime: inout Lifetimeable?,
spacing: CGFloat = DefaultStackSpacing,
alignment: UIStackView.Alignment = .leading,
distribution: UIStackView.Distribution = .fill,
@ComponentBuilder components: @escaping @MainActor (Value) -> ComponentBuilder.Component
) -> Self {
let stack = Self(frame: .zero)
stack.axis = .vertical
stack.spacing = spacing
stack.alignment = alignment
stack.distribution = distribution
stack.insetsLayoutMarginsFromSafeArea = false
stack.setContentHuggingPriority(.required, for: .vertical)
var lastReferences = [UIView]()
lifetime = observable.subscribe { [unowned stack] value in
DispatchQueue.main.async {
lastReferences.forEach {
$0.removeFromSuperview()
}
switch components(value) {
case let .single(view, maker):
stack.addArrangedSubview(view)
view.snp.makeConstraints(maker)
lastReferences = [ view ]
case let .result(views):
for (view, maker) in views {
stack.addArrangedSubview(view)
view.snp.makeConstraints(maker)
}
lastReferences = views.map { $0.0 }
}
}
}
return stack
}
}