// // 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) } }