From 5ebc7408f5d8c1a1ccaea49066e650321d5d61bd Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 5 Sep 2023 11:02:34 -0500 Subject: [PATCH] added container Signed-off-by: Matt Bruce --- VDS.xcodeproj/project.pbxproj | 4 + VDS/Classes/Container.swift | 636 ++++++++++++++++++++++++++++++++++ 2 files changed, 640 insertions(+) create mode 100644 VDS/Classes/Container.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 7495635c..7eec81c6 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ EA0B18022A9E236900F2D0CD /* SelectorGroupBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0B18012A9E236900F2D0CD /* SelectorGroupBase.swift */; }; EA0B18052A9E2D2D00F2D0CD /* SelectorBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0B18032A9E2D2D00F2D0CD /* SelectorBase.swift */; }; EA0B18062A9E2D2D00F2D0CD /* SelectorItemBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0B18042A9E2D2D00F2D0CD /* SelectorItemBase.swift */; }; + EA0B18082AA22A8500F2D0CD /* Container.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0B18072AA22A8500F2D0CD /* Container.swift */; }; EA0D1C372A681CCE00E5C127 /* ToggleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0D1C362A681CCE00E5C127 /* ToggleView.swift */; }; EA0D1C392A6AD4DF00E5C127 /* Typography+SpacingConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0D1C382A6AD4DF00E5C127 /* Typography+SpacingConfig.swift */; }; EA0D1C3B2A6AD51B00E5C127 /* Typogprahy+Styles.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0D1C3A2A6AD51B00E5C127 /* Typogprahy+Styles.swift */; }; @@ -160,6 +161,7 @@ EA0B18012A9E236900F2D0CD /* SelectorGroupBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectorGroupBase.swift; sourceTree = ""; }; EA0B18032A9E2D2D00F2D0CD /* SelectorBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectorBase.swift; sourceTree = ""; }; EA0B18042A9E2D2D00F2D0CD /* SelectorItemBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectorItemBase.swift; sourceTree = ""; }; + EA0B18072AA22A8500F2D0CD /* Container.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Container.swift; sourceTree = ""; }; EA0D1C362A681CCE00E5C127 /* ToggleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleView.swift; sourceTree = ""; }; EA0D1C382A6AD4DF00E5C127 /* Typography+SpacingConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Typography+SpacingConfig.swift"; sourceTree = ""; }; EA0D1C3A2A6AD51B00E5C127 /* Typogprahy+Styles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Typogprahy+Styles.swift"; sourceTree = ""; }; @@ -520,6 +522,7 @@ EA985C1C296CD13600F2FF2E /* BundleManager.swift */, EAF7F0B8289C139800B287F5 /* ColorConfiguration.swift */, EAB5FEF02927F4AA00998C17 /* SelfSizingCollectionView.swift */, + EA0B18072AA22A8500F2D0CD /* Container.swift */, ); path = Classes; sourceTree = ""; @@ -937,6 +940,7 @@ EAF7F11728A1475A00B287F5 /* RadioButtonItem.swift in Sources */, EA985BEE2968A92400F2FF2E /* TitleLockupSubTitleModel.swift in Sources */, EA985BF22968B5BB00F2FF2E /* TitleLockupTextStyle.swift in Sources */, + EA0B18082AA22A8500F2D0CD /* Container.swift in Sources */, EAB1D2CD28ABE76100DAE764 /* Withable.swift in Sources */, EAC846F3294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift in Sources */, EAF7F0952899861000B287F5 /* CheckboxItem.swift in Sources */, diff --git a/VDS/Classes/Container.swift b/VDS/Classes/Container.swift new file mode 100644 index 00000000..11a5d3a7 --- /dev/null +++ b/VDS/Classes/Container.swift @@ -0,0 +1,636 @@ +// +// Container.swift +// VDS +// +// Created by Matt Bruce on 9/1/23. +// + +import Foundation +import UIKit + +open class Container: View { + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + required public init() { + super.init(frame: .zero) + } + + public override init(frame: CGRect) { + super.init(frame: .zero) + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + private let constainerManager = ContainerManager() + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + public var view: UIView? { didSet { setNeedsUpdate() } } + public var containerModel: ContainerManager.ContainerModel? { didSet { setNeedsUpdate() } } + + //-------------------------------------------------- + // MARK: - Overrides + //-------------------------------------------------- + open override func setup() { + super.setup() + isAccessibilityElement = false + } + + override open func reset() { + super.reset() + (view as? ViewProtocol)?.reset() + } + + open override func updateView() { + super.updateView() + + if var view = view as? ViewProtocol { + view.isEnabled = isEnabled + view.surface = surface + } + } + + open override func updateAccessibility() { + super.updateAccessibility() + guard let view, let superView = view.superview else { return } + superView.isAccessibilityElement = false + if let elements = view.accessibilityElements { + superView.accessibilityElements = elements + } else { + superView.accessibilityElements = [view] + } + } +} + +open class ContainerManager { + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + public init() {} + + //-------------------------------------------------- + // MARK: - Constraints + //-------------------------------------------------- + private var topConstraint: NSLayoutConstraint? + private var leadingConstraint: NSLayoutConstraint? + private var bottomConstraint: NSLayoutConstraint? + private var trailingConstraint: NSLayoutConstraint? + + private var alignCenterHorizontalConstraint: NSLayoutConstraint? + private var alignCenterLeadingConstraint: NSLayoutConstraint? + private var alignCenterTrailingConstraint: NSLayoutConstraint? + + private var alignCenterVerticalConstraint: NSLayoutConstraint? + private var alignCenterTopConstraint: NSLayoutConstraint? + private var alignCenterBottomConstraint: NSLayoutConstraint? + + //-------------------------------------------------- + // MARK: - Public Methods + //-------------------------------------------------- + open func constrain(view: UIView, with model: ContainerModel) { + addConstraints(for: view, model: model) + + if let horizontalAlignment = model.horizontalAlignment { + alignHorizontal(horizontalAlignment.alignment) + } + + if let verticalAlignment = model.verticalAlignment { + alignVertical(verticalAlignment.alignment) + } + + } + + private func addConstraints(for view: UIView, model: ContainerModel) { + guard let margins = view.superview?.layoutMarginsGuide else { return } + + + if let horizontal = model.horizontalAlignment { + switch horizontal.alignment { + + case .fill: + leadingConstraint = view.leadingAnchor.constraint(equalTo: margins.leadingAnchor) + trailingConstraint = margins.trailingAnchor.constraint(equalTo: view.trailingAnchor) + + case .leading: + leadingConstraint = view.leadingAnchor.constraint(equalTo: margins.leadingAnchor) + trailingConstraint = margins.trailingAnchor.constraint(greaterThanOrEqualTo: view.trailingAnchor) + + case .trailing: + leadingConstraint = view.leadingAnchor.constraint(greaterThanOrEqualTo: margins.leadingAnchor) + trailingConstraint = margins.trailingAnchor.constraint(equalTo: view.trailingAnchor) + + default: + break + } + + leadingConstraint?.priority = horizontal.leading.priority + leadingConstraint?.constant = horizontal.leading.padding + + trailingConstraint?.priority = horizontal.leading.priority + trailingConstraint?.constant = horizontal.leading.padding + + } + + if let vertical = model.verticalAlignment { + switch vertical.alignment { + + case .fill: + topConstraint = view.topAnchor.constraint(equalTo: margins.topAnchor) + bottomConstraint = margins.bottomAnchor.constraint(equalTo: view.bottomAnchor) + + + case .top: + topConstraint = view.topAnchor.constraint(equalTo: margins.topAnchor) + bottomConstraint = margins.bottomAnchor.constraint(greaterThanOrEqualTo: view.bottomAnchor) + + case .bottom: + topConstraint = view.topAnchor.constraint(greaterThanOrEqualTo: margins.topAnchor) + bottomConstraint = margins.bottomAnchor.constraint(greaterThanOrEqualTo: view.bottomAnchor) + + default: + break + + } + + topConstraint?.priority = vertical.top.priority + topConstraint?.constant = vertical.top.padding + + bottomConstraint?.priority = vertical.bottom.priority + bottomConstraint?.constant = vertical.bottom.padding + + } + + alignCenterHorizontalConstraint = view.centerXAnchor.constraint(equalTo: margins.centerXAnchor) + alignCenterLeadingConstraint = view.leadingAnchor.constraint(greaterThanOrEqualTo: margins.leadingAnchor) + alignCenterTrailingConstraint = margins.trailingAnchor.constraint(greaterThanOrEqualTo: view.trailingAnchor) + + alignCenterVerticalConstraint = view.centerYAnchor.constraint(equalTo: margins.centerYAnchor) + alignCenterTopConstraint = view.topAnchor.constraint(greaterThanOrEqualTo: margins.topAnchor) + alignCenterBottomConstraint = margins.bottomAnchor.constraint(greaterThanOrEqualTo: view.bottomAnchor) + + } + + private func alignHorizontal(_ alignment: UIStackView.Alignment) { + switch alignment { + case .center: + alignCenterHorizontalConstraint?.isActive = true + alignCenterLeadingConstraint?.isActive = true + alignCenterTrailingConstraint?.isActive = true + leadingConstraint?.isActive = false + trailingConstraint?.isActive = false + case .leading: + alignCenterHorizontalConstraint?.isActive = false + alignCenterLeadingConstraint?.isActive = false + alignCenterTrailingConstraint?.isActive = true + leadingConstraint?.isActive = true + trailingConstraint?.isActive = false + case .trailing: + alignCenterHorizontalConstraint?.isActive = false + alignCenterLeadingConstraint?.isActive = true + alignCenterTrailingConstraint?.isActive = false + leadingConstraint?.isActive = false + trailingConstraint?.isActive = true + case .fill: + alignCenterHorizontalConstraint?.isActive = false + alignCenterLeadingConstraint?.isActive = false + alignCenterTrailingConstraint?.isActive = false + leadingConstraint?.isActive = true + trailingConstraint?.isActive = true + default: break + } + } + + private func alignVertical(_ alignment: UIStackView.Alignment) { + switch alignment { + case .center: + alignCenterVerticalConstraint?.isActive = true + alignCenterTopConstraint?.isActive = true + alignCenterBottomConstraint?.isActive = true + topConstraint?.isActive = false + bottomConstraint?.isActive = false + case .leading: + alignCenterVerticalConstraint?.isActive = false + alignCenterTopConstraint?.isActive = false + alignCenterBottomConstraint?.isActive = true + topConstraint?.isActive = true + bottomConstraint?.isActive = false + case .trailing: + alignCenterVerticalConstraint?.isActive = false + alignCenterTopConstraint?.isActive = true + alignCenterBottomConstraint?.isActive = false + topConstraint?.isActive = false + bottomConstraint?.isActive = true + case .fill: + alignCenterVerticalConstraint?.isActive = false + alignCenterTopConstraint?.isActive = false + alignCenterBottomConstraint?.isActive = false + topConstraint?.isActive = true + bottomConstraint?.isActive = true + default: break + } + } +} + +extension ContainerManager { + + public static func model(for horizontal: UIStackView.Alignment, vertical: UIStackView.Alignment, inset: UIEdgeInsets = .zero) -> ContainerModel { + .init(horizontal: .init(alignment: horizontal, leading: .init(padding: inset.left), trailing: .init(padding: inset.right)), + vertical: .init(alignment: vertical, top: .init(padding: inset.top), bottom: .init(padding: inset.bottom))) + } + + public struct Constraint { + public let padding: CGFloat + public let priority: UILayoutPriority + + public init(padding: CGFloat = 0, priority: UILayoutPriority = .required) { + self.padding = padding + self.priority = priority + } + } + + public struct HorizontalAlignment { + public let alignment: UIStackView.Alignment + public let leading: Constraint + public let trailing: Constraint + + public init(alignment: UIStackView.Alignment = .fill, + leading: Constraint = .init(), + trailing: Constraint = .init()) { + self.alignment = alignment + self.leading = leading + self.trailing = trailing + } + } + + public struct VerticalAlignment { + public let alignment: UIStackView.Alignment + public let top: Constraint + public let bottom: Constraint + + public init(alignment: UIStackView.Alignment = .fill, + top: Constraint = .init(), + bottom: Constraint = .init()) { + self.alignment = alignment + self.top = top + self.bottom = bottom + } + } + + public struct ContainerModel { + public var horizontalAlignment: HorizontalAlignment? + public var verticalAlignment: VerticalAlignment? + + public init(horizontal: HorizontalAlignment?, vertical: VerticalAlignment?) { + self.horizontalAlignment = horizontal + self.verticalAlignment = vertical + } + } +} + +import UIKit + +/// A protocol representing layout anchor providing objects like `UIView` and `UILayoutGuide`. +public protocol LayoutAnchorProviding { + var leadingAnchor: NSLayoutXAxisAnchor { get } + var trailingAnchor: NSLayoutXAxisAnchor { get } + var leftAnchor: NSLayoutXAxisAnchor { get } + var rightAnchor: NSLayoutXAxisAnchor { get } + var topAnchor: NSLayoutYAxisAnchor { get } + var bottomAnchor: NSLayoutYAxisAnchor { get } + var centerXAnchor: NSLayoutXAxisAnchor { get } + var centerYAnchor: NSLayoutYAxisAnchor { get } + var widthAnchor: NSLayoutDimension { get } + var heightAnchor: NSLayoutDimension { get } +} + +extension UIView: LayoutAnchorProviding {} +extension UILayoutGuide: LayoutAnchorProviding {} + +/// An enumeration representing different layout types for positioning. +public enum LayoutType { + case fill + case topLeft + case topCenter + case topRight + case left + case center + case right + case bottomLeft + case bottomCenter + case bottomRight +} + +/// An enumeration representing different anchor types. +public enum AnchorType: String { + case leading = "LayoutAnchorProvidingLeading" + case trailing = "LayoutAnchorProvidingTrailing" + case top = "LayoutAnchorProvidingTop" + case bottom = "LayoutAnchorProvidingBottom" + case centerX = "LayoutAnchorProvidingCenterX" + case centerY = "LayoutAnchorProvidingCenterY" +} + +/// An enumeration representing stack orientation. +public enum StackOrientation { + case horizontal + case vertical +} + +public struct LayoutAnchorModel { + public let layoutType: LayoutType + public let insets: UIEdgeInsets + public let priorities: [AnchorType: UILayoutPriority] + + public init (layoutType: LayoutType = .fill, insets: UIEdgeInsets = .zero, priorities: [AnchorType: UILayoutPriority] = [:]) { + self.layoutType = layoutType + self.insets = insets + self.priorities = priorities + } +} + +// Extension for UIView +public extension UIView { + + /// Pins the view to a `LayoutAnchorProviding` object based on the specified layout type, edge insets, and priorities. + /// - Parameters: + /// - layoutProvider: The `LayoutAnchorProviding` object to pin to. + /// - layoutType: The `LayoutType` specifying how to pin. + /// - edgeInsets: The `UIEdgeInsets` for the pinning. + /// - priorities: The `UILayoutPriority` dictionary for each anchor type. + func pin( + to layoutProvider: LayoutAnchorProviding, + with layoutType: LayoutType, + edgeInsets: UIEdgeInsets = .zero, + priorities: [AnchorType: UILayoutPriority] = [:] + ) { + translatesAutoresizingMaskIntoConstraints = false + var leadingConstraint: NSLayoutConstraint? + var trailingConstraint: NSLayoutConstraint? + var topConstraint: NSLayoutConstraint? + var bottomConstraint: NSLayoutConstraint? + var centerXConstraint: NSLayoutConstraint? + var centerYConstraint: NSLayoutConstraint? + + switch layoutType { + case .fill: + leadingConstraint = leadingAnchor.constraint(equalTo: layoutProvider.leadingAnchor, constant: edgeInsets.left) + trailingConstraint = trailingAnchor.constraint(equalTo: layoutProvider.trailingAnchor, constant: -edgeInsets.right) + topConstraint = topAnchor.constraint(equalTo: layoutProvider.topAnchor, constant: edgeInsets.top) + bottomConstraint = bottomAnchor.constraint(equalTo: layoutProvider.bottomAnchor, constant: -edgeInsets.bottom) + + case .topLeft: + leadingConstraint = leadingAnchor.constraint(equalTo: layoutProvider.leadingAnchor, constant: edgeInsets.left) + topConstraint = topAnchor.constraint(equalTo: layoutProvider.topAnchor, constant: edgeInsets.top) + trailingConstraint = trailingAnchor.constraint(lessThanOrEqualTo: layoutProvider.trailingAnchor, constant: -edgeInsets.right) + bottomConstraint = bottomAnchor.constraint(lessThanOrEqualTo: layoutProvider.bottomAnchor, constant: -edgeInsets.bottom) + + case .topCenter: + centerXConstraint = centerXAnchor.constraint(equalTo: layoutProvider.centerXAnchor) + topConstraint = topAnchor.constraint(equalTo: layoutProvider.topAnchor, constant: edgeInsets.top) + leadingConstraint = leadingAnchor.constraint(greaterThanOrEqualTo: layoutProvider.leadingAnchor, constant: edgeInsets.left) + trailingConstraint = trailingAnchor.constraint(lessThanOrEqualTo: layoutProvider.trailingAnchor, constant: -edgeInsets.right) + bottomConstraint = bottomAnchor.constraint(lessThanOrEqualTo: layoutProvider.bottomAnchor, constant: -edgeInsets.bottom) + + case .topRight: + trailingConstraint = trailingAnchor.constraint(equalTo: layoutProvider.trailingAnchor, constant: -edgeInsets.right) + topConstraint = topAnchor.constraint(equalTo: layoutProvider.topAnchor, constant: edgeInsets.top) + leadingConstraint = leadingAnchor.constraint(greaterThanOrEqualTo: layoutProvider.leadingAnchor, constant: edgeInsets.left) + bottomConstraint = bottomAnchor.constraint(lessThanOrEqualTo: layoutProvider.bottomAnchor, constant: -edgeInsets.bottom) + + case .left: + leadingConstraint = leadingAnchor.constraint(equalTo: layoutProvider.leadingAnchor, constant: edgeInsets.left) + centerYConstraint = centerYAnchor.constraint(equalTo: layoutProvider.centerYAnchor) + trailingConstraint = trailingAnchor.constraint(lessThanOrEqualTo: layoutProvider.trailingAnchor, constant: -edgeInsets.right) + topConstraint = topAnchor.constraint(equalTo: layoutProvider.topAnchor, constant: edgeInsets.top) + bottomConstraint = bottomAnchor.constraint(equalTo: layoutProvider.bottomAnchor, constant: -edgeInsets.bottom) + + case .center: + centerXConstraint = centerXAnchor.constraint(equalTo: layoutProvider.centerXAnchor) + centerYConstraint = centerYAnchor.constraint(equalTo: layoutProvider.centerYAnchor) + leadingConstraint = leadingAnchor.constraint(greaterThanOrEqualTo: layoutProvider.leadingAnchor, constant: edgeInsets.left) + trailingConstraint = trailingAnchor.constraint(lessThanOrEqualTo: layoutProvider.trailingAnchor, constant: -edgeInsets.right) + topConstraint = topAnchor.constraint(greaterThanOrEqualTo: layoutProvider.topAnchor, constant: edgeInsets.top) + bottomConstraint = bottomAnchor.constraint(lessThanOrEqualTo: layoutProvider.bottomAnchor, constant: -edgeInsets.bottom) + + case .right: + trailingConstraint = trailingAnchor.constraint(equalTo: layoutProvider.trailingAnchor, constant: -edgeInsets.right) + centerYConstraint = centerYAnchor.constraint(equalTo: layoutProvider.centerYAnchor) + leadingConstraint = leadingAnchor.constraint(greaterThanOrEqualTo: layoutProvider.leadingAnchor, constant: edgeInsets.left) + topConstraint = topAnchor.constraint(equalTo: layoutProvider.topAnchor, constant: edgeInsets.top) + bottomConstraint = bottomAnchor.constraint(equalTo: layoutProvider.bottomAnchor, constant: -edgeInsets.bottom) + + case .bottomLeft: + leadingConstraint = leadingAnchor.constraint(equalTo: layoutProvider.leadingAnchor, constant: edgeInsets.left) + bottomConstraint = bottomAnchor.constraint(equalTo: layoutProvider.bottomAnchor, constant: -edgeInsets.bottom) + trailingConstraint = trailingAnchor.constraint(lessThanOrEqualTo: layoutProvider.trailingAnchor, constant: -edgeInsets.right) + topConstraint = topAnchor.constraint(greaterThanOrEqualTo: layoutProvider.topAnchor, constant: edgeInsets.top) + + case .bottomCenter: + centerXConstraint = centerXAnchor.constraint(equalTo: layoutProvider.centerXAnchor) + bottomConstraint = bottomAnchor.constraint(equalTo: layoutProvider.bottomAnchor, constant: -edgeInsets.bottom) + leadingConstraint = leadingAnchor.constraint(greaterThanOrEqualTo: layoutProvider.leadingAnchor, constant: edgeInsets.left) + trailingConstraint = trailingAnchor.constraint(lessThanOrEqualTo: layoutProvider.trailingAnchor, constant: -edgeInsets.right) + topConstraint = topAnchor.constraint(greaterThanOrEqualTo: layoutProvider.topAnchor, constant: edgeInsets.top) + + case .bottomRight: + trailingConstraint = trailingAnchor.constraint(equalTo: layoutProvider.trailingAnchor, constant: -edgeInsets.right) + bottomConstraint = bottomAnchor.constraint(equalTo: layoutProvider.bottomAnchor, constant: -edgeInsets.bottom) + leadingConstraint = leadingAnchor.constraint(greaterThanOrEqualTo: layoutProvider.leadingAnchor, constant: edgeInsets.left) + topConstraint = topAnchor.constraint(greaterThanOrEqualTo: layoutProvider.topAnchor, constant: edgeInsets.top) + } + + // Set identifiers + leadingConstraint?.identifier = AnchorType.leading.rawValue + trailingConstraint?.identifier = AnchorType.trailing.rawValue + topConstraint?.identifier = AnchorType.top.rawValue + bottomConstraint?.identifier = AnchorType.bottom.rawValue + centerXConstraint?.identifier = AnchorType.centerX.rawValue + centerYConstraint?.identifier = AnchorType.centerY.rawValue + + // Set priorities + leadingConstraint?.priority = priorities[.leading] ?? .required + trailingConstraint?.priority = priorities[.trailing] ?? .required + topConstraint?.priority = priorities[.top] ?? .required + bottomConstraint?.priority = priorities[.bottom] ?? .required + centerXConstraint?.priority = priorities[.centerX] ?? .required + centerYConstraint?.priority = priorities[.centerY] ?? .required + + // Activate constraints + let constraints = [leadingConstraint, trailingConstraint, topConstraint, bottomConstraint, centerXConstraint, centerYConstraint].compactMap { $0 } + NSLayoutConstraint.activate(constraints) + } + + /// Pins the view to its superview based on the specified layout type, edge insets, and priorities. + /// - Parameters: + /// - layoutType: The `LayoutType` specifying how to pin. + /// - edgeInsets: The `UIEdgeInsets` for the pinning. + /// - priorities: The `UILayoutPriority` dictionary for each anchor type. + func pin( + to layoutProvider: LayoutAnchorProviding, + with model: LayoutAnchorModel + ) { + pin(to: layoutProvider, with: model.layoutType, edgeInsets: model.insets, priorities: model.priorities) + } + + /// Pins the view to its superview based on the specified layout type, edge insets, and priorities. + /// - Parameters: + /// - layoutType: The `LayoutType` specifying how to pin. + /// - edgeInsets: The `UIEdgeInsets` for the pinning. + /// - priorities: The `UILayoutPriority` dictionary for each anchor type. + func pinToSuperview( + with layoutType: LayoutType, + edgeInsets: UIEdgeInsets = .zero, + priorities: [AnchorType: UILayoutPriority] = [:] + ) { + guard let superview = superview else { + print("Superview is nil") + return + } + pin(to: superview, with: layoutType, edgeInsets: edgeInsets, priorities: priorities) + } + + /// Pins the view to its superview based on the specified layout type, edge insets, and priorities. + /// - Parameters: + /// - layoutType: The `LayoutType` specifying how to pin. + /// - edgeInsets: The `UIEdgeInsets` for the pinning. + /// - priorities: The `UILayoutPriority` dictionary for each anchor type. + func pinToSuperview( + with model: LayoutAnchorModel + ) { + guard let superview = superview else { + print("Superview is nil") + return + } + pin(to: superview, with: model) + } + + /// Sets the aspect ratio for the view. + /// - Parameters: + /// - aspectRatio: The aspect ratio as a `CGFloat`. + /// - priority: The `UILayoutPriority` for the constraint. Defaults to `.required`. + func setAspectRatio(_ aspectRatio: CGFloat, priority: UILayoutPriority = .required) { + let constraint = widthAnchor.constraint(equalTo: heightAnchor, multiplier: aspectRatio) + constraint.priority = priority + constraint.isActive = true + } + + /// Pins the view to the safe area of its superview. + /// - Parameters: + /// - layoutType: The `LayoutType` specifying how to pin. + /// - edgeInsets: The `UIEdgeInsets` for the pinning. + /// - priorities: The `UILayoutPriority` dictionary for each anchor type. + func pinToSafeArea( + with layoutType: LayoutType, + edgeInsets: UIEdgeInsets = .zero, + priorities: [AnchorType: UILayoutPriority] = [:] + ) { + guard let superview = superview else { + print("Superview is nil") + return + } + + let safeArea = superview.safeAreaLayoutGuide + pin(to: safeArea, with: layoutType, edgeInsets: edgeInsets, priorities: priorities) + } + + /// Stacks an array of `LayoutAnchorProviding` objects in a specified orientation. + /// - Parameters: + /// - views: The array of `LayoutAnchorProviding` objects. + /// - orientation: The `StackOrientation` specifying how to stack. + /// - spacing: The spacing between the objects. + func stack( + views: [LayoutAnchorProviding], + orientation: StackOrientation, + spacing: CGFloat + ) { + var previousView: LayoutAnchorProviding? + + for view in views { + if let previousView = previousView { + switch orientation { + case .horizontal: + let constraint = view.leadingAnchor.constraint(equalTo: previousView.trailingAnchor, constant: spacing) + constraint.isActive = true + case .vertical: + let constraint = view.topAnchor.constraint(equalTo: previousView.bottomAnchor, constant: spacing) + constraint.isActive = true + } + } + previousView = view + } + } + + /// Sets the minimum width for the view. + /// - Parameters: + /// - width: The minimum width as a `CGFloat`. + /// - priority: The `UILayoutPriority` for the constraint. Defaults to `.required`. + func setMinWidth(_ width: CGFloat, priority: UILayoutPriority = .required) { + let constraint = widthAnchor.constraint(greaterThanOrEqualToConstant: width) + constraint.priority = priority + constraint.isActive = true + } + + /// Sets the maximum width for the view. + /// - Parameters: + /// - width: The maximum width as a `CGFloat`. + /// - priority: The `UILayoutPriority` for the constraint. Defaults to `.required`. + func setMaxWidth(_ width: CGFloat, priority: UILayoutPriority = .required) { + let constraint = widthAnchor.constraint(lessThanOrEqualToConstant: width) + constraint.priority = priority + constraint.isActive = true + } + + /// Sets the minimum height for the view. + /// - Parameters: + /// - height: The minimum height as a `CGFloat`. + /// - priority: The `UILayoutPriority` for the constraint. Defaults to `.required`. + func setMinHeight(_ height: CGFloat, priority: UILayoutPriority = .required) { + let constraint = heightAnchor.constraint(greaterThanOrEqualToConstant: height) + constraint.priority = priority + constraint.isActive = true + } + + /// Sets the maximum height for the view. + /// - Parameters: + /// - height: The maximum height as a `CGFloat`. + /// - priority: The `UILayoutPriority` for the constraint. Defaults to `.required`. + func setMaxHeight(_ height: CGFloat, priority: UILayoutPriority = .required) { + let constraint = heightAnchor.constraint(lessThanOrEqualToConstant: height) + constraint.priority = priority + constraint.isActive = true + } + + /// Updates a constraint based on its identifier. + /// - Parameters: + /// - identifier: The identifier of the constraint to find. + /// - constant: The new constant value to set. Defaults to nil. + /// - priority: The new priority to set. Defaults to nil. + func updateConstraint(withIdentifier identifier: String, constant: CGFloat? = nil, priority: UILayoutPriority? = nil) { + if let constraint = constraints.first(where: { $0.identifier == identifier }) { + if let constant = constant { + constraint.constant = constant + } + if let priority = priority { + constraint.priority = priority + } + } else { + print("Constraint with identifier \(identifier) not found.") + } + } + + /// Updates a constraint based on its identifier. + /// - Parameters: + /// - identifier: The identifier of the constraint to find. + /// - constant: The new constant value to set. Defaults to nil. + /// - priority: The new priority to set. Defaults to nil. + func updateConstraint(withIdentifier identifier: AnchorType, constant: CGFloat? = nil, priority: UILayoutPriority? = nil) { + updateConstraint(withIdentifier: identifier.rawValue, constant: constant, priority: priority) + } +}