diff --git a/Package.swift b/Package.swift index 4744b28..9661f36 100644 --- a/Package.swift +++ b/Package.swift @@ -2,18 +2,18 @@ import PackageDescription let package = Package( - name: "Spatial", - platforms: [.iOS(.v15), .macOS(.v12)], + name: "Spatial", // The name of the package + platforms: [.iOS(.v15), .macOS(.v12)], // The platforms the package supports products: [ .library( - name: "Spatial", - targets: ["Spatial"]) + name: "Spatial", // The name of the library product + targets: ["Spatial"]) // The targets that make up the product ], - dependencies: [], + dependencies: [], // The dependencies of the package targets: [ - .target(name: "Spatial", - dependencies: [] + .target(name: "Spatial", // The name of the target + dependencies: [] // The dependencies of the target ), - .testTarget(name: "SpatialTests", dependencies: ["Spatial"]) + .testTarget(name: "SpatialTests", dependencies: ["Spatial"]) // The name of the test target and its dependencies ] -) +) \ No newline at end of file diff --git a/README.md b/README.md index 398f845..6b6c79e 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,18 @@ Definition: **Spatial** | ˈspeɪʃ(ə)l | adjective | **describes how objects fit together in space** +## Table of Contents +- [What is it](#what-is-it) +- [How does it work](#how-does-it-work) +- [How do I get it](#how-do-i-get-it) +- [Gotchas](#gotchas) +- [Example](#example) +- [Todo](#todo) + ### What is it Hassle-free AutoLayout, tailored for interactivity and animation. Created for how our mental model thinks autolayout works. Not optimized for brevity. + ### How does it work - Spatial is just extensions and enums which enable you to write less boilerplate code - Spatial is interchangeable with Vanilla AutoLayout diff --git a/Sources/Spatial/view/View+Anchor.swift b/Sources/Spatial/view/View+Anchor.swift index e9bdf9f..aa6cd40 100644 --- a/Sources/Spatial/view/View+Anchor.swift +++ b/Sources/Spatial/view/View+Anchor.swift @@ -1,4 +1,4 @@ -import Foundation +import Foundation // needed? #if os(iOS) import UIKit #elseif os(macOS) @@ -60,20 +60,34 @@ public final class Constraint { ) } /** - * Vertical anchoring + * Creates a vertical anchor constraint between the view and the parent view. * - Parameters: - * - view: target view - * - to: parent view - * - align: object alignment - * - alignTo: parent alginment - * - offset: point offset - * - useMargin: This works, but when you use size constraints then you have to pin to sides, or use the sizeOffset - * - relation: Constraint relation: equal, lessThanOrEqual, greaterThanOrEqual + * - view: The view to create the constraint for. + * - to: The parent view to anchor the constraint to. + * - align: The vertical alignment of the view relative to the parent view. + * - alignTo: The vertical alignment of the parent view to anchor the constraint to. + * - offset: The offset of the view from the parent view. + * - useMargin: Whether to use the view's layout margins when creating the constraint. + * - relation: The relation of the constraint: equal, lessThanOrEqual, greaterThanOrEqual. + * - Returns: A vertical anchor constraint. + * ## Examples: + * label.anchor(to: self, align: .top, alignTo: .top, offset: 16, useMargin: true, relation: .lessThanOrEqual) */ public static func anchor(_ view: View, to: View, align: VerticalAlign, alignTo: VerticalAlign, offset: CGFloat = .zero, useMargin: Bool = false, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint { + // Get the layout attribute for the specified vertical alignment of the view. let attr: NSLayoutConstraint.Attribute = layoutAttr(align: align) + // Get the layout attribute for the specified vertical alignment of the parent view. let relatedByAttr: NSLayoutConstraint.Attribute = layoutAttr(align: alignTo, useMargin: useMargin) - return .init(item: view, attribute: attr, relatedBy: relation, toItem: to, attribute: relatedByAttr, multiplier: 1.0, constant: offset) + // Create a vertical anchor constraint between the view and the parent view. + let constraint = NSLayoutConstraint( + item: view, // The view to create the constraint for. + attribute: attr, // The vertical attribute of the view to create the constraint for. + relatedBy: relation, // The relation of the constraint: equal, lessThanOrEqual, greaterThanOrEqual. + toItem: to, // The parent view to anchor the constraint to. + attribute: relatedByAttr, // The vertical attribute of the parent view to anchor the constraint to. + multiplier: 1.0, // The multiplier of the constraint. + constant: offset // The offset of the view from the parent view. + ) } } /** @@ -81,55 +95,67 @@ public final class Constraint { */ extension Constraint { /** - * For aligning in the x-axis (internal) - * - Remark: Layout margin is only available for iOS and tvOS + * Returns the layout attribute for the specified horizontal alignment of the view. + * * - Parameters: - * - align: object alignment - * - useMargin: os margin + * - align: The horizontal alignment of the view. + * - useMargin: Whether to use the view's layout margins when creating the constraint. + * + * - Returns: The layout attribute for the specified horizontal alignment of the view. + * + * - Remark: Layout margin is only available for iOS and tvOS. + * + * ## Examples: + * ``` + * layoutAttr(align: .left, useMargin: true) + * ``` */ private static func layoutAttr(align: HorizontalAlign, useMargin: Bool = false) -> NSLayoutConstraint.Attribute { switch align { case .left: #if os(iOS) - if useMargin { return .leftMargin } + if useMargin { return .leftMargin } // Use the view's left margin when creating the constraint if `useMargin` is `true`. #endif - return .left + return .left // Use the view's left edge when creating the constraint if `useMargin` is `false` or if the platform is not iOS. case .right: #if os(iOS) - if useMargin { return .rightMargin } + if useMargin { return .rightMargin } // Use the view's right margin when creating the constraint if `useMargin` is `true`. #endif - return .right + return .right // Use the view's right edge when creating the constraint if `useMargin` is `false` or if the platform is not iOS. case .centerX: #if os(iOS) - if useMargin { return .centerXWithinMargins } + if useMargin { return .centerXWithinMargins } // Use the view's horizontal center within its margins when creating the constraint if `useMargin` is `true`. #endif - return .centerX + return .centerX // Use the view's horizontal center when creating the constraint if `useMargin` is `false` or if the platform is not iOS. } } /** - * For aligning in the y axis (internal) - * - Remark: Layout margin is o ly available for `iOS` and `tvOS + * Returns the layout attribute for the specified vertical alignment of the view. * - Parameters: - * - align: object alignment - * - useMargin: os alignment + * - align: The vertical alignment of the view. + * - useMargin: Whether to use the view's layout margins when creating the constraint. + * - Returns: The layout attribute for the specified vertical alignment of the view. + * - Remark: Layout margin is only available for iOS and tvOS. + * ## Examples: + * layoutAttr(align: .top, useMargin: true) */ private static func layoutAttr(align: VerticalAlign, useMargin: Bool = false) -> NSLayoutConstraint.Attribute { switch align { - case .top: - #if os(iOS) - if useMargin { return .topMargin } - #endif - return .top - case .bottom: - #if os(iOS) - if useMargin { return .bottomMargin } - #endif - return .bottom - case .centerY: - #if os(iOS) - if useMargin { return .centerYWithinMargins } - #endif - return .centerY - } + case .top: + #if os(iOS) + if useMargin { return .topMargin } // Use the view's top margin when creating the constraint if `useMargin` is `true`. + #endif + return .top // Use the view's top edge when creating the constraint if `useMargin` is `false` or if the platform is not iOS. + case .bottom: + #if os(iOS) + if useMargin { return .bottomMargin } // Use the view's bottom margin when creating the constraint if `useMargin` is `true`. + #endif + return .bottom // Use the view's bottom edge when creating the constraint if `useMargin` is `false` or if the platform is not iOS. + case .centerY: + #if os(iOS) + if useMargin { return .centerYWithinMargins } // Use the view's vertical center within its margins when creating the constraint if `useMargin` is `true`. + #endif + return .centerY // Use the view's vertical center when creating the constraint if `useMargin` is `false` or if the platform is not iOS. + } } } diff --git a/Sources/Spatial/view/View+Distribution.swift b/Sources/Spatial/view/View+Distribution.swift index 39e4d12..0d45a9b 100644 --- a/Sources/Spatial/view/View+Distribution.swift +++ b/Sources/Spatial/view/View+Distribution.swift @@ -9,51 +9,65 @@ import Cocoa */ extension Constraint { /** - * Horizontal distribution - * - Remark: Alternativly you can do: views.enumerated().map { Constraint.anchor($0.1, to: self, align: .topLeft, alignTo:.topLeft,offset:CGPoint(x:0,y:48 * $0.0)) } etc - * - Remark: Sets only anchors not sizes - * - Fixme: ⚠️️ Make it throw? - * - Fixme: ⚠️️ Add support for spacing - * - Fixme: ⚠️️ Add support for alignTo: (because you might want to set a different anchor for the views than for the view to align to) - * - Fixme: ⚠️️ parent is always superview, then we must use UIView as type, remember your returning constriants, not setting actual anchor or size, you do that in activeConstraint + * Distributes the specified views horizontally with the specified alignment and spacing. + * + * - Parameters: + * - views: The views to distribute in a row. + * - align: The corner at which the first view should align. + * - spacing: The spacing between the views. + * - offset: The offset of the first view from the alignment corner. + * - Returns: An array of horizontal anchor constraints. + * - Remark: This method only sets anchors, not sizes. + * - Remark: The parent view is always the superview of the views. + * - Remark: You can also use `views.enumerated().map { Constraint.anchor($0.1, to: self, align: .topLeft, alignTo:.topLeft,offset:CGPoint(x:0,y:48 * $0.0)) }` to distribute the views horizontally. + * - Fixme: ⚠️️ Make it throw? + * - Fixme: ⚠️️ Add support for spacing. + * - Fixme: ⚠️️ Add support for alignTo: (because you might want to set a different anchor for the views than for the view to align to). * ## Examples: * [label1, label2, label3].applyAnchorsAndSizes { views in - * let anchors = Constraint.distribute(vertically:views,align:.left) - * let sizes = views.map{ Constraint.size($0, toView: self.frame.width, height: 48)) } + * let anchors = Constraint.distribute(horizontally: views, align: .topLeft, spacing: 16, offset: CGPoint(x: 0, y: 48)) + * let sizes = views.map { Constraint.size($0, toView: self.frame.width, height: 48) } * return (anchors, sizes) * } - * - Parameters: - * - spacing: A void between items - * - offset: Offset the X in the begining - * - align: At which corner should the first item align to - * - views: The views to distribute in a row */ public static func distribute(horizontally views: [View], align: Alignment = .topLeft, spacing: CGFloat = .zero, offset: CGPoint = .zero) -> [AnchorConstraint] { + // Distribute the views horizontally with the specified alignment, spacing, and offset. let xConstraints: [NSLayoutConstraint] = distribute(views, axis: .hor, align: align, spacing: spacing, offset: offset.x) + // Anchor each view vertically to the parent view with the specified alignment and offset. let yConstraints: [NSLayoutConstraint] = views.map { view in - guard let superView = view.superview else { fatalError("View must have superview") } - return Constraint.anchor(view, to: superView, align: align.verAlign, alignTo: align.verAlign, offset: offset.y) + guard let superView = view.superview else { fatalError("View must have superview") } // Get the superview of the view. + // Anchor the view vertically to the superview with the specified alignment and offset. + return Constraint.anchor(view, to: superView, align: align.verAlign, alignTo: align.verAlign, offset: offset.y) // Return the vertical anchor constraint. } + // Combine the horizontal and vertical anchor constraints into an array of anchor constraints. let anchors: [AnchorConstraint] = Array(zip(xConstraints, yConstraints)) + // Return the array of anchor constraints. return anchors } /** - * Vertical distribution - * - Important: ⚠️️ Sets only anchors not sizes - * - Fixme: ⚠️️ Make it throw? yepp + * Vertically distribute views within their superview + * - Important: This method only sets anchors, not sizes + * - Throws: `fatalError` if a view does not have a superview * - Parameters: - * - views: views to align - * - align: align to canvas - * - spacing: space btween - * - offset: point offset + * - views: The views to distribute + * - align: The alignment of the views within their superview + * - spacing: The spacing between the views + * - offset: The offset from the top-left corner of the superview + * - Returns: An array of `AnchorConstraint` objects representing the constraints applied to each view */ public static func distribute(vertically views: [View], align: Alignment = .topLeft, spacing: CGFloat = .zero, offset: CGPoint = .zero) -> [AnchorConstraint] { + // Set horizontal constraints for each view let xConstraints: [NSLayoutConstraint] = views.map { view in + // Ensure that the view has a superview, otherwise throw a fatal error guard let superView = view.superview else { fatalError("View must have superview") } + // Anchor the view to its superview with the specified horizontal alignment and offset return Constraint.anchor(view, to: superView, align: align.horAlign, alignTo: align.horAlign, offset: offset.x) } + // Set vertical constraints for each view let yConstraints = distribute(views, axis: .ver, align: align, spacing: spacing, offset: offset.y) + // Combine the horizontal and vertical constraints into an array of AnchorConstraints let anchors: [AnchorConstraint] = Array(zip(xConstraints, yConstraints)) + // Return the array of AnchorConstraints return anchors } } @@ -63,76 +77,93 @@ extension Constraint { */ extension Constraint { /** - * Distributes vertically or horizontally - * - Fixme: ⚠️️ Remove fatal error? make it throw? + * Distributes views either vertically or horizontally based on the specified axis and alignment + * - Fixme: ⚠️️ Consider replacing the fatal error with a throwing function * - Parameters: - * - views: views to align - * - axis: ver or hor - * - align: canvas align - * - spacing: space between - * - offset: point offset + * - views: The views to distribute + * - axis: The axis along which to distribute the views + * - align: The alignment of the views within their superview + * - spacing: The spacing between the views + * - offset: The offset from the top-left corner of the superview + * - Returns: An array of `NSLayoutConstraint` objects representing the constraints applied to each view */ fileprivate static func distribute(_ views: [View], axis: Axis, align: Alignment, spacing: CGFloat = .zero, offset: CGFloat = .zero) -> [NSLayoutConstraint] { switch (align.horAlign, align.verAlign) { - case (.left, _), (_, .top): return distribute(fromStart: views, axis: axis, spacing: spacing, offset: offset) - case (.right, _), (_, .bottom): return distribute(fromEnd: views, axis: axis, spacing: spacing, offset: offset) - default: fatalError("Type not supported: h: \(align.horAlign) v: \(align.verAlign)") + // Distribute views from the start (left or top) of the axis + case (.left, _), (_, .top): + return distribute(fromStart: views, axis: axis, spacing: spacing, offset: offset) + // Distribute views from the end (right or bottom) of the axis + case (.right, _), (_, .bottom): + return distribute(fromEnd: views, axis: axis, spacing: spacing, offset: offset) + // Throw a fatal error if the alignment is not supported + default: + fatalError("Type not supported: h: \(align.horAlign) v: \(align.verAlign)") } } /** - * Distributes from start to end - * - Fixme: ⚠️️ Remove fatal error? make it throw? + * Distributes views from the start of the axis to the end of the axis + * - Fixme: ⚠️️ Consider replacing the fatal error with a throwing function * - Parameters: - * - views: views to align - * - align: align to canvas - * - spacing: space btween - * - offset: point offset + * - views: The views to distribute + * - axis: The axis along which to distribute the views + * - spacing: The spacing between the views + * - offset: The offset from the start of the axis + * - Returns: An array of `NSLayoutConstraint` objects representing the constraints applied to each view */ fileprivate static func distribute(fromStart views: [View], axis: Axis, spacing: CGFloat = .zero, offset: CGFloat = .zero) -> [NSLayoutConstraint] { var anchors: [NSLayoutConstraint] = [] var prevView: View? views.enumerated().forEach { _, view in + // Ensure that the view has a superview or a previous view, otherwise throw a fatal error guard let toView: View = prevView ?? view.superview else { fatalError("View must have superview") } let offset: CGFloat = prevView == nil ? offset : .zero // Only the first view gets offset - let spacing: CGFloat = prevView != nil ? spacing : 0 // All subsequent views gets spacing + let spacing: CGFloat = prevView != nil ? spacing : 0 // All subsequent views get spacing switch axis { case .hor: - let alignTo: HorizontalAlign = prevView == nil ? .left : .right // First align to left pf superView, then right of each subsequent item + let alignTo: HorizontalAlign = prevView == nil ? .left : .right // First align to left of superview, then right of each subsequent view + // Anchor the view to its superview or the previous view with the specified horizontal alignment, aligning to the left or right of the superview or previous view, and with the specified offset and spacing anchors.append(Constraint.anchor(view, to: toView, align: .left, alignTo: alignTo, offset: offset + spacing)) case .ver: - let alignTo: VerticalAlign = prevView == nil ? .top : .bottom // First align to top pf superView, then bottom of each subsequent item + let alignTo: VerticalAlign = prevView == nil ? .top : .bottom // First align to top of superview, then bottom of each subsequent view + // Anchor the view to its superview or the previous view with the specified vertical alignment, aligning to the top or bottom of the superview or previous view, and with the specified offset and spacing anchors.append(Constraint.anchor(view, to: toView, align: .top, alignTo: alignTo, offset: offset + spacing)) } prevView = view } + // Return the array of NSLayoutConstraint objects return anchors } /** - * Aligns from end to start + * Distributes views from the end of the axis to the start of the axis * - Fixme: ⚠️️ Remove fatal error? make it throw? * - Parameters: - * - views: views to align - * - align: align to canvas - * - spacing: space btween - * - offset: point offset + * - views: The views to distribute + * - axis: The axis along which to distribute the views + * - spacing: The spacing between the views + * - offset: The offset from the end of the axis + * - Returns: An array of `NSLayoutConstraint` objects representing the constraints applied to each view */ fileprivate static func distribute(fromEnd views: [View], axis: Axis, spacing: CGFloat = .zero, offset: CGFloat = .zero) -> [NSLayoutConstraint] { var anchors: [NSLayoutConstraint] = [] var prevView: View? for view in views.reversed() { // Move backwards + // Ensure that the view has a superview or a previous view, otherwise throw a fatal error guard let toView: View = prevView ?? view.superview else { fatalError("View must have superview") } - let offset: CGFloat = prevView == nil ? offset : .zero - let spacing: CGFloat = prevView != nil ? spacing : 0 // All subsequent views gets spacing + let offset: CGFloat = prevView == nil ? offset : .zero // Only the last view gets offset + let spacing: CGFloat = prevView != nil ? spacing : 0 // All previous views get spacing switch axis { case .hor: - let alignTo: HorizontalAlign = prevView == nil ? .right : .left // First align to right pf superView, then left of each subsequent item + let alignTo: HorizontalAlign = prevView == nil ? .right : .left // First align to right of superview, then left of each subsequent view + // Anchor the view to its superview or the previous view with the specified horizontal alignment, aligning to the right or left of the superview or previous view, and with the specified offset and spacing anchors.append(Constraint.anchor(view, to: toView, align: .right, alignTo: alignTo, offset: offset + spacing)) - case.ver: - let alignTo: VerticalAlign = prevView == nil ? .bottom : .top // First align to bottom pf superView, then top of each subsequent item + case .ver: + let alignTo: VerticalAlign = prevView == nil ? .bottom : .top // First align to bottom of superview, then top of each subsequent view + // Anchor the view to its superview or the previous view with the specified vertical alignment, aligning to the bottom or top of the superview or previous view, and with the specified offset and spacing anchors.append(Constraint.anchor(view, to: toView, align: .bottom, alignTo: alignTo, offset: offset + spacing)) } prevView = view } + // Return the array of NSLayoutConstraint objects return anchors } } diff --git a/Sources/Spatial/view/View+Size.swift b/Sources/Spatial/view/View+Size.swift index 8c2ee0b..93831f5 100644 --- a/Sources/Spatial/view/View+Size.swift +++ b/Sources/Spatial/view/View+Size.swift @@ -16,129 +16,159 @@ import Cocoa */ extension Constraint { /** - * Creates a dimensional constraint + * Creates a size constraint for the specified view relative to another view * - Important: ⚠️️ Multiplier needs to be 1, 1 to not have an effect * - Important: ⚠️️ Offset needs to be 0, 0 to not have an effect * ## Examples: * let sizeConstraint = Constraint.size(square, to: parent, offset: .zero, multiplier: .init(x: 1, y: 0.5)) * let widthConstraint = Constraint.size(square, to: parent).w * - Parameters: - * - view: target view - * - to: align to thi view - * - offset: size offset - * - multiplier: size multiplier + * - view: The view to set the size constraint for + * - to: The view to align the size constraint to + * - offset: The size offset from the aligned view + * - multiplier: The size multiplier relative to the aligned view + * - Returns: A tuple of `NSLayoutConstraint` objects representing the width and height constraints applied to the view */ public static func size(_ view: View, to: View, offset: CGSize = .zero, multiplier: CGPoint = .init(x: 1, y: 1)) -> SizeConstraint { - (Constraint.width(view, to: to, offset: offset.width, multiplier: multiplier.x), - Constraint.height(view, to: to, offset: offset.height, multiplier: multiplier.y)) + // Set the width constraint for the view relative to the aligned view, with the specified width offset and multiplier + let widthConstraint = Constraint.width(view, to: to, offset: offset.width, multiplier: multiplier.x) + // Set the height constraint for the view relative to the aligned view, with the specified height offset and multiplier + let heightConstraint = Constraint.height(view, to: to, offset: offset.height, multiplier: multiplier.y) + // Return a tuple of the width and height constraints + return (widthConstraint, heightConstraint) } /** - * Creates a size constraint + * Creates a size constraint for the specified view with the specified size and multiplier * - Fixme: ⚠️️ This doesn't have offset, maybe it should, for now I guess you can always inset the size, or add etc - * ## Examples: - * let sizeConstraint = Constraint.size(square, size: .init(width: 100, height: 100)) * - Parameters: - * - view: target view - * - size: target size - * - multiplier: size multiploer + * - view: The view to set the size constraint for + * - size: The target size for the view + * - multiplier: The size multiplier for the view + * - Returns: A tuple of `NSLayoutConstraint` objects representing the width and height constraints applied to the view */ public static func size(_ view: View, size: CGSize, multiplier: CGSize = .init(width: 1, height: 1)) -> SizeConstraint { - (Constraint.width(view, width: size.width, multiplier: multiplier.width), - Constraint.height(view, height: size.height, multiplier: multiplier.height)) + // Set the width constraint for the view with the specified width and multiplier + let widthConstraint = Constraint.width(view, width: size.width, multiplier: multiplier.width) + // Set the height constraint for the view with the specified height and multiplier + let heightConstraint = Constraint.height(view, height: size.height, multiplier: multiplier.height) + // Return a tuple of the width and height constraints + return (widthConstraint, heightConstraint) } /** - * Returns size tuple (based on parent and or width or height) - * - Fixme: ⚠️️ use named prop for view? + * Creates a size constraint for the specified view based on the specified width, height, or another view's size + * - Fixme: ⚠️️ Use named property for the view parameter * - Parameters: - * - view: The view to be sized by AutoLayout - * - to: The view to be sized to - * - width: Custom width, instead of relying on another view to size against - * - height: Custom height, instead of relying on another view to size against - * - offset: Add extra offset to view in x,y dir - * - multiplier: Scale the size constraint by this scalar (works with other view and custom size) - * ## Examples: + * - view: The view to set the size constraint for + * - to: The view to align the size constraint to + * - width: The custom width for the view, instead of relying on another view to size against + * - height: The custom height for the view, instead of relying on another view to size against + * - offset: The size offset from the aligned view + * - multiplier: The size multiplier for the view, scaling the size constraint by this scalar (works with other view and custom size) + * - Returns: A tuple of `NSLayoutConstraint` objects representing the width and height constraints applied to the view + * - Example: * let s = Constraint.size(view, to: parent, height: 48) */ public static func size(_ view: View, to: View, width: CGFloat? = nil, height: CGFloat? = nil, offset: CGSize = .zero, multiplier: CGSize = .init(width: 1, height: 1)) -> SizeConstraint { let width: NSLayoutConstraint = { - if let width: CGFloat = width { return Constraint.width(view, width: width, multiplier: multiplier.width) } - else { return Constraint.width(view, to: to, offset: offset.width, multiplier: multiplier.width) } + // If a custom width is specified, set the width constraint for the view with the custom width and multiplier + if let width: CGFloat = width { + return Constraint.width(view, width: width, multiplier: multiplier.width) + } + // Otherwise, set the width constraint for the view relative to the aligned view, with the specified width offset and multiplier + else { + return Constraint.width(view, to: to, offset: offset.width, multiplier: multiplier.width) + } }() let height: NSLayoutConstraint = { - if let height: CGFloat = height { return Constraint.height(view, height: height, multiplier: multiplier.height) } - else { return Constraint.height(view, to: to, offset: offset.height, multiplier: multiplier.height) } + // If a custom height is specified, set the height constraint for the view with the custom height and multiplier + if let height: CGFloat = height { + return Constraint.height(view, height: height, multiplier: multiplier.height) + } + // Otherwise, set the height constraint for the view relative to the aligned view, with the specified height offset and multiplier + else { + return Constraint.height(view, to: to, offset: offset.height, multiplier: multiplier.height) + } }() + // Return a tuple of the width and height constraints return (width, height) } /** - * Creates a width constraint (Based on a `CGFloat` width) + * Creates a width constraint for the specified view with the specified width and multiplier * - Remark: When AutoLayout doesn't relate to a view the multiplier doesn't take effect, so we apply the multiplier directly to the constant * - Parameters: - * - view: target view - * - width: target width - * - multiplier: width multiploer - * - relation: constraint relation equalTo greater... less... etc - * - Returns: - Fixme: ⚠️️ + * - view: The view to set the width constraint for + * - width: The target width for the view + * - multiplier: The width multiplier for the view + * - relation: The relation of the constraint (equalTo, greaterThanOrEqual, lessThanOrEqual) + * - Returns: An `NSLayoutConstraint` object representing the width constraint applied to the view */ public static func width(_ view: View, width: CGFloat, multiplier: CGFloat = 1, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint { + // Create and return an NSLayoutConstraint object representing the width constraint applied to the view, with the specified width, multiplier, and relation .init(item: view, attribute: .width, relatedBy: relation, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: width * multiplier) } /** - * Creates a height constraint (based on a CGFloat height) - * - Remark: When AutoLayout doesnt relate to a view the multiplier doesnt take effect, so we apply the multiplier directly to the constant + * Creates a height constraint for the specified view with the specified height and multiplier + * - Remark: When AutoLayout doesn't relate to a view the multiplier doesn't take effect, so we apply the multiplier directly to the constant * - Parameters: - * - view: target view - * - height: target height - * - multiplier: width multiploer - * - relation: constraint relation equalTo greater... less... etc + * - view: The view to set the height constraint for + * - height: The target height for the view + * - multiplier: The height multiplier for the view + * - relation: The relation of the constraint (equalTo, greaterThanOrEqual, lessThanOrEqual) + * - Returns: An `NSLayoutConstraint` object representing the height constraint applied to the view */ public static func height(_ view: View, height: CGFloat, multiplier: CGFloat = 1, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint { + // Create and return an NSLayoutConstraint object representing the height constraint applied to the view, with the specified height, multiplier, and relation .init(item: view, attribute: .height, relatedBy: relation, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: height * multiplier) } /** - * Creates a width constraint (based on another views width constraint) + * Creates a width constraint for the specified view based on another view's width constraint * - Parameters: - * - view: target view - * - to: align to this view - * - offset: point offset - * - multiplier: width multiplier - * - relation: constraint relation + * - view: The view to set the width constraint for + * - to: The view to align the width constraint to + * - offset: The width offset from the aligned view + * - multiplier: The width multiplier for the view + * - relation: The relation of the constraint (equalTo, greaterThanOrEqual, lessThanOrEqual) + * - Returns: An `NSLayoutConstraint` object representing the width constraint applied to the view */ public static func width(_ view: View, to: View, offset: CGFloat = .zero, multiplier: CGFloat = 1, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint { + // Create and return an NSLayoutConstraint object representing the width constraint applied to the view, with the specified width offset, multiplier, and relation .init(item: view, attribute: .width, relatedBy: relation, toItem: to, attribute: .width, multiplier: multiplier, constant: offset) } /** - * Creates a height constraint (based on another views width constraint) + * Creates a height constraint for the specified view based on another view's height constraint * - Parameters: - * - view: target view - * - to: align to this view - * - offset: point offset - * - multiplier: width multiplier - * - relation: constraint relation + * - view: The view to set the height constraint for + * - to: The view to align the height constraint to + * - offset: The height offset from the aligned view + * - multiplier: The height multiplier for the view + * - relation: The relation of the constraint (equalTo, greaterThanOrEqual, lessThanOrEqual) + * - Returns: An `NSLayoutConstraint` object representing the height constraint applied to the view */ public static func height(_ view: View, to: View, offset: CGFloat = .zero, multiplier: CGFloat = 1, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint { + // Create and return an NSLayoutConstraint object representing the height constraint applied to the view, with the specified height offset, multiplier, and relation .init(item: view, attribute: .height, relatedBy: relation, toItem: to, attribute: .height, multiplier: multiplier, constant: offset) } /** - * Represents a side, basically you can base one axis-length on another, so vertical length represents horixontal length etc - * - Important: ⚠️️ the offset that is set in `toAttr`, is not carried along to the new Constraint, so be sure to add the same offset if needed + * Creates a constraint for the specified view's width or height based on another view's width or height * - Remark: Useful if you want to set a width of an object to the height of another object - * - Remark: You can also use it on it's own view to copy width to height for instance - * - Fixme: ⚠️️ Consider renaming this to side or `axisLength`? - * - Fixme: ⚠️️ Consider making the distinction between between `viewAxis` and `toAxis more clear * - Parameters: - * - to: The view you relate to (Usually the parent) - * - view: The view to apply the constraint to - * - viewAxis: The attribute to set to - * - toAxis: The attribute to derive from - * - offset: X or Y - * - multiplier: Scalar value, default is 1 + * - view: The view to set the constraint for + * - to: The view to align the constraint to (usually the parent view) + * - viewAxis: The axis to set the constraint for (horizontal or vertical) + * - toAxis: The axis to derive the constraint from (horizontal or vertical) + * - offset: The offset from the derived constraint + * - multiplier: The scalar value to multiply the derived constraint by (default is 1) + * - relation: The relation of the constraint (equalTo, greaterThanOrEqual, lessThanOrEqual) + * - Returns: An `NSLayoutConstraint` object representing the width or height constraint applied to the view * ## Examples: * let widthConstraint = Constraint.length(square, viewAxis: .horizontal, axis: .vertical) */ public static func length(_ view: View, to: View, viewAxis: Axis, toAxis: Axis, offset: CGFloat = .zero, multiplier: CGFloat = 1, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint { + // Determine the attribute to set the constraint for based on the specified view axis (horizontal or vertical) let viewAttr: NSLayoutConstraint.Attribute = viewAxis == .hor ? .width : .height + // Determine the attribute to derive the constraint from based on the specified to axis (horizontal or vertical) let toAttr: NSLayoutConstraint.Attribute = toAxis == .hor ? .width : .height + // Create and return an NSLayoutConstraint object representing the width or height constraint applied to the view, with the specified view axis, to axis, offset, multiplier, and relation return .init(item: view, attribute: viewAttr, relatedBy: relation, toItem: to, attribute: toAttr, multiplier: multiplier, constant: offset) } } diff --git a/Sources/Spatial/view/View+Type.swift b/Sources/Spatial/view/View+Type.swift index 2bdbca3..249e158 100644 --- a/Sources/Spatial/view/View+Type.swift +++ b/Sources/Spatial/view/View+Type.swift @@ -5,32 +5,43 @@ import UIKit import Cocoa #endif // Single +// A tuple of `NSLayoutConstraint` objects representing the x and y anchor constraints applied to a view public typealias AnchorConstraint = (x: NSLayoutConstraint, y: NSLayoutConstraint) +// A tuple of `NSLayoutConstraint` objects representing the width and height constraints applied to a view public typealias SizeConstraint = (w: NSLayoutConstraint, h: NSLayoutConstraint) +// A tuple of anchor and size constraints applied to a view public typealias AnchorAndSize = (anchor: AnchorConstraint, size: SizeConstraint) // Bulk +// A tuple of arrays of anchor and size constraints applied to a view public typealias AnchorConstraintsAndSizeConstraints = (anchorConstraints: [AnchorConstraint], sizeConstraints: [SizeConstraint]) /** * Single */ extension View { - /** - * We keep `AnchorsAndSizes` in a tuple, because `applyConstraints wouldn't work with just an array - */ - public typealias AnchorsAndSizes = (anchors: [NSLayoutConstraint], sizes: [NSLayoutConstraint]) // Can this go to [UIView].AnchorsAndSizes ? + // We keep `AnchorsAndSizes` in a tuple, because `applyConstraints wouldn't work with just an array + // A tuple of arrays of anchor and size constraints applied to a view + public typealias AnchorsAndSizes = (anchors: [NSLayoutConstraint], sizes: [NSLayoutConstraint]) + // A closure that returns an array of constraints applied to a view public typealias ConstraintsClosure = (_ view: View) -> [NSLayoutConstraint] + // A closure that returns a single constraint applied to a view public typealias ConstraintClosure = (_ view: View) -> NSLayoutConstraint // Tuple + // A closure that returns a tuple of anchor and size constraints applied to a view public typealias AnchorAndSizeClosure = (_ view: View) -> AnchorAndSize // Single + // A closure that returns a single anchor constraint applied to a view public typealias AnchorClosure = (_ view: View) -> AnchorConstraint + // A closure that returns a single size constraint applied to a view public typealias SizeClosure = (_ view: View) -> SizeConstraint } /** * Bulk */ extension Array where Element: View { + // A closure that returns a tuple of anchor and size constraints applied to an array of views public typealias ConstraintsClosure = (_ views: [View]) -> AnchorConstraintsAndSizeConstraints + // A closure that returns an array of anchor constraints applied to an array of views public typealias AnchorConstraintsClosure = (_ views: [View]) -> [AnchorConstraint] + // A closure that returns an array of size constraints applied to an array of views public typealias SizeConstraintsClosure = (_ views: [View]) -> [SizeConstraint] } diff --git a/SpatialExample/AppDelegate.swift b/SpatialExample/AppDelegate.swift index 407d827..5a04359 100644 --- a/SpatialExample/AppDelegate.swift +++ b/SpatialExample/AppDelegate.swift @@ -3,9 +3,9 @@ import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { lazy var window: UIWindow? = { - let win = UIWindow(frame: UIScreen.main.bounds) - let vc = MainVC() - win.rootViewController = vc + let win = UIWindow(frame: UIScreen.main.bounds) // Create a new UIWindow object with the same frame as the main screen + let vc = MainVC() // Create a new instance of MainVC + win.rootViewController = vc // Set the root view controller of the window to the MainVC instance win.makeKeyAndVisible() // Important since we have no Main storyboard anymore return win }() diff --git a/SpatialExample/MainVC/AnimationTestView/AnimationTestView+Create.swift b/SpatialExample/MainVC/AnimationTestView/AnimationTestView+Create.swift index 03a3168..cb84300 100644 --- a/SpatialExample/MainVC/AnimationTestView/AnimationTestView+Create.swift +++ b/SpatialExample/MainVC/AnimationTestView/AnimationTestView+Create.swift @@ -24,7 +24,7 @@ extension AnimationTest { } @objc func buttonTouched(sender: UIButton) { Swift.print("It Works!!!") - // let to:CGFloat = 0//(UIScreen.main.bounds.height/2) + (button.frame.height/2) + // let to:CGFloat = 0//(UIScreen.main.bounds.height/2) + (button.frame.height/2) button.animate(to: .zero, align: .topLeft, alignTo: .topLeft) {} } } diff --git a/SpatialExample/common/Constants.swift b/SpatialExample/common/Constants.swift index 38a6bcf..846a0c7 100644 --- a/SpatialExample/common/Constants.swift +++ b/SpatialExample/common/Constants.swift @@ -6,7 +6,10 @@ import UIKit */ class Constants { enum Colors: String, CaseIterable { - case blue = "FB1B4D", yellow = "1DE3E6", red = "22FFA0", green = "FED845" + case blue = "FB1B4D" + case yellow = "1DE3E6" + case red = "22FFA0" + case green = "FED845" var uiColor: UIColor { .init(hex: self.rawValue) } diff --git a/SpatialExampleMac/AppDelegate.swift b/SpatialExampleMac/AppDelegate.swift index 6743687..b29488d 100644 --- a/SpatialExampleMac/AppDelegate.swift +++ b/SpatialExampleMac/AppDelegate.swift @@ -13,11 +13,11 @@ class AppDelegate: NSObject, NSApplicationDelegate { } extension AppDelegate { func createView() -> NSView { - let contentRect = window.contentRect(forFrameRect: window.frame)/*size of win sans titlebar*/ - let view: MainView = .init(frame: contentRect) - window.contentView = view - view.layer?.backgroundColor = NSColor.white.cgColor - return view + let contentRect = window.contentRect(forFrameRect: window.frame) // Get the size of the window without the title bar + let view: MainView = .init(frame: contentRect) // Create a new instance of MainView with the specified frame + window.contentView = view // Set the content view of the window to the MainView instance + view.layer?.backgroundColor = NSColor.white.cgColor // Set the background color of the MainView instance to white + return view // Return the MainView instance } } open class MainView: NSView {