├── .github
└── workflows
│ └── Tests.yml
├── .gitignore
├── .swiftlint.yml
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── contents.xcworkspacedata
├── LICENSE
├── Package.swift
├── README.md
├── Sources
└── SpatialLib
│ ├── ConstraintKind
│ ├── ConstraintKind+Access.swift
│ ├── ConstraintKind+Apply.swift
│ ├── ConstraintKind+Bulk+Access.swift
│ ├── ConstraintKind+Bulk.swift
│ ├── ConstraintKind+SpaceAround.swift
│ ├── ConstraintKind+SpaceBetween.swift
│ ├── ConstraintKind+Type.swift
│ ├── ConstraintKind.swift
│ ├── ConstraintView
│ │ └── ConstraintView.swift
│ └── animation
│ │ ├── ConstraintKind+Animate.swift
│ │ └── ConstraintKind+Update.swift
│ ├── align
│ ├── Align.swift
│ ├── AlignType
│ │ ├── AlignType+Extension.swift
│ │ └── AlignType.swift
│ ├── alignment
│ │ ├── Alignment+Extension.swift
│ │ └── Alignment.swift
│ └── axis
│ │ ├── Axis.swift
│ │ ├── AxisType.swift
│ │ ├── HorizontalAlign.swift
│ │ └── VerticalAlign.swift
│ ├── common
│ ├── Hybrid.swift
│ └── View+Extension.swift
│ └── view
│ ├── View+Access+Bulk.swift
│ ├── View+Access.swift
│ ├── View+Activate+Bulk.swift
│ ├── View+Activate.swift
│ ├── View+Anchor.swift
│ ├── View+Distribution.swift
│ ├── View+Size.swift
│ └── View+Type.swift
├── Spatial.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ ├── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── xcuserdata
│ │ ├── andre.xcuserdatad
│ │ └── UserInterfaceState.xcuserstate
│ │ ├── andrejorgensen.xcuserdatad
│ │ └── UserInterfaceState.xcuserstate
│ │ └── eon.xcuserdatad
│ │ └── UserInterfaceState.xcuserstate
├── xcshareddata
│ └── xcschemes
│ │ └── SpatialExample.xcscheme
└── xcuserdata
│ ├── andre.xcuserdatad
│ └── xcschemes
│ │ └── xcschememanagement.plist
│ ├── andrejorgensen.xcuserdatad
│ └── xcschemes
│ │ └── xcschememanagement.plist
│ └── eon.xcuserdatad
│ ├── xcdebugger
│ └── Breakpoints_v2.xcbkptlist
│ └── xcschemes
│ └── xcschememanagement.plist
├── SpatialExample
├── AppDelegate.swift
├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── Contents.json
├── Base.lproj
│ └── LaunchScreen.storyboard
├── Info.plist
├── MainVC
│ ├── AnimationTestView
│ │ ├── AnimationTestView+Create.swift
│ │ └── AnimationTestView.swift
│ ├── MainVC+Create.swift
│ ├── MainVC.swift
│ └── MainView
│ │ ├── CardView
│ │ ├── BottomBar
│ │ │ ├── BottomBar+Constant.swift
│ │ │ └── BottomBar.swift
│ │ ├── CardView+Constant.swift
│ │ ├── CardView+Create.swift
│ │ ├── CardView.swift
│ │ ├── MiddleContent
│ │ │ ├── ItemView
│ │ │ │ ├── ItemView+Create.swift
│ │ │ │ └── ItemView.swift
│ │ │ ├── MiddleContent+Create.swift
│ │ │ └── MiddleContent.swift
│ │ └── TopBar
│ │ │ ├── TopBar+Constant.swift
│ │ │ └── TopBar.swift
│ │ ├── MainView+Create.swift
│ │ ├── MainView.swift
│ │ ├── MinMaxTestView
│ │ └── MinMaxTestView.swift
│ │ ├── SizeTestingView
│ │ └── SizeTestingView.swift
│ │ ├── SpacingTestView
│ │ ├── SpacingTestView+Create.swift
│ │ └── SpacingTestView.swift
│ │ └── TestView
│ │ ├── TestView+Create.swift
│ │ └── TestView.swift
└── common
│ ├── Constants.swift
│ └── Extension.swift
├── SpatialExampleMac
├── AppDelegate.swift
├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── Contents.json
├── Base.lproj
│ └── MainMenu.xib
├── Info.plist
└── SpatialExampleMac.entitlements
└── Tests
└── SpatialTests
└── SpatialTests.swift
/.github/workflows/Tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 | jobs:
9 | #SwiftLint: ## Adds swift-linting to GH actions
10 | #runs-on: ubuntu-latest
11 | #steps:
12 | #- uses: actions/checkout@v3
13 | #- name: GitHub Action for SwiftLint
14 | #uses: norio-nomura/action-swiftlint@3.2.1
15 | #with:
16 | #args: --config .swiftlint.yml
17 | build:
18 | runs-on: macos-latest
19 | steps:
20 | - uses: actions/checkout@v2
21 | ## - name: Build
22 | ## run: swift clean build -v
23 | ## - name: Run tests
24 | ## run: swift test -v
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore macOS .DS_Store files
2 | .DS_Store
3 |
4 | # Ignore Swift Package Manager build directory
5 | /.build
6 |
7 | # Ignore Xcode project files
8 | /*.xcodeproj
9 |
10 | # Ignore Xcode user data
11 | xcuserdata/
12 |
13 | # Ignore Swift Package Manager package directory
14 | /Packages
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | whitelist_rules:
2 | - anyobject_protocol
3 | - array_init
4 | #- attributes
5 | - block_based_kvo
6 | - class_delegate_protocol
7 | - closing_brace
8 | - closure_end_indentation
9 | - closure_parameter_position
10 | - closure_spacing
11 | - collection_alignment
12 | - colon
13 | - comma
14 | - compiler_protocol_init
15 | # - conditional_returns_on_newline
16 | - contains_over_first_not_nil
17 | - control_statement
18 | - deployment_target
19 | - discarded_notification_center_observer
20 | - discouraged_direct_init
21 | - discouraged_object_literal
22 | - discouraged_optional_boolean
23 | # - discouraged_optional_collection
24 | - duplicate_imports
25 | - dynamic_inline
26 | - empty_count
27 | - empty_enum_arguments
28 | - empty_parameters
29 | - empty_parentheses_with_trailing_closure
30 | - empty_string
31 | - empty_xctest_method
32 | - explicit_init
33 | - fallthrough
34 | - fatal_error_message
35 | - first_where
36 | - for_where
37 | - generic_type_name
38 | - identical_operands
39 | - identifier_name
40 | - implicit_getter
41 | - implicit_return
42 | - inert_defer
43 | - is_disjoint
44 | - joined_default_parameter
45 | - last_where
46 | - leading_whitespace
47 | - legacy_cggeometry_functions
48 | - legacy_constant
49 | - legacy_constructor
50 | - legacy_hashing
51 | - legacy_nsgeometry_functions
52 | - legacy_random
53 | - literal_expression_end_indentation
54 | - lower_acl_than_parent
55 | - mark
56 | - modifier_order
57 | - multiline_arguments
58 | # - multiline_function_chains
59 | - multiline_literal_brackets
60 | - multiline_parameters
61 | - multiline_parameters_brackets
62 | - multiple_closures_with_trailing_closure
63 | - nimble_operator
64 | - no_extension_access_modifier
65 | - no_fallthrough_only
66 | - notification_center_detachment
67 | - number_separator
68 | - object_literal
69 | - opening_brace
70 | - operator_usage_whitespace
71 | - operator_whitespace
72 | - overridden_super_call
73 | - pattern_matching_keywords
74 | - private_action
75 | # - private_outlet
76 | - private_unit_test
77 | - prohibited_super_call
78 | - protocol_property_accessors_order
79 | - redundant_discardable_let
80 | - redundant_nil_coalescing
81 | - redundant_objc_attribute
82 | - redundant_optional_initialization
83 | - redundant_set_access_control
84 | - redundant_string_enum_value
85 | - redundant_type_annotation
86 | - redundant_void_return
87 | - required_enum_case
88 | - return_arrow_whitespace
89 | - shorthand_operator
90 | - sorted_first_last
91 | # - statement_position
92 | - static_operator
93 | # - strong_iboutlet
94 | - superfluous_disable_command
95 | - switch_case_alignment
96 | # - switch_case_on_newline
97 | - syntactic_sugar
98 | - todo
99 | - toggle_bool
100 | - trailing_closure
101 | - trailing_comma
102 | - trailing_newline
103 | - trailing_semicolon
104 | - trailing_whitespace
105 | - type_name
106 | # - unavailable_function
107 | - unneeded_break_in_switch
108 | - unneeded_parentheses_in_closure_argument
109 | #- untyped_error_in_catch
110 | - unused_closure_parameter
111 | - unused_control_flow_label
112 | - unused_enumerated
113 | - unused_optional_binding
114 | - unused_setter_value
115 | - valid_ibinspectable
116 | - vertical_parameter_alignment
117 | - vertical_parameter_alignment_on_call
118 | - vertical_whitespace_closing_braces
119 | - vertical_whitespace_opening_braces
120 | - void_return
121 | - weak_computed_property
122 | - weak_delegate
123 | - xct_specific_matcher
124 | - xctfail_message
125 | - yoda_condition
126 | analyzer_rules:
127 | - unused_import
128 | - unused_private_declaration
129 | force_cast: warning
130 | force_unwrapping: warning
131 | number_separator:
132 | minimum_length: 5
133 | object_literal:
134 | image_literal: false
135 | discouraged_object_literal:
136 | color_literal: false
137 | identifier_name:
138 | max_length:
139 | warning: 100
140 | error: 100
141 | min_length:
142 | warning: 1
143 | error: 1
144 | validates_start_with_lowercase: false
145 | allowed_symbols:
146 | - '_'
147 | excluded:
148 | - 'x'
149 | - 'y'
150 | - 'a'
151 | - 'b'
152 | - 'x1'
153 | - 'x2'
154 | - 'y1'
155 | - 'y2'
156 | macOS_deployment_target: '10.12'
157 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 eonist
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.6
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "SpatialLib", // The name of the package
6 | platforms: [.iOS(.v15), .macOS(.v12)], // The platforms the package supports
7 | products: [
8 | .library(
9 | name: "SpatialLib", // The name of the library product
10 | targets: ["SpatialLib"]) // The targets that the product depends on
11 | ],
12 | dependencies: [
13 | ],
14 | targets: [
15 | .target(
16 | name: "SpatialLib", // The name of the target
17 | dependencies: [] // The dependencies of the target
18 | ),
19 | .testTarget(
20 | name: "SpatialTests", // The name of the test target
21 | dependencies: ["SpatialLib"]) // The dependencies of the test target
22 | ]
23 | )
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Spatial
2 | 
3 | 
4 | 
5 | [](https://github.com/apple/swift)
6 | 
7 | [](https://codebeat.co/projects/github-com-eonist-spatial-master)
8 |
9 |
10 |
11 | Definition: **Spatial** | ˈspeɪʃ(ə)l | adjective | **describes how objects fit together in space**
12 |
13 | > :warning: **Note:** Spatial has been renamed to SpatialLib due to a conflict with a recently introduced framework by Apple also named Spatial. SPM url is the same. Just use `import SpatialLib` instead of `import Spatial`
14 |
15 | ## Table of Contents
16 | - [What is it](#what-is-it)
17 | - [How does it work](#how-does-it-work)
18 | - [How do I get it](#how-do-i-get-it)
19 | - [Gotchas](#gotchas)
20 | - [Example](#example)
21 | - [Todo](#todo)
22 |
23 | ### What is it
24 | Hassle-free AutoLayout, tailored for interactivity and animation. Created for how our mental model thinks autolayout works. Not optimized for brevity.
25 |
26 |
27 | ### How does it work
28 | - Spatial is just extensions and enums which enable you to write less boilerplate code
29 | - Spatial is interchangeable with Vanilla AutoLayout
30 | - Spatial comes with examples how you can animate with AutoLayout
31 | - Spatial uses plain and simple math under the hood.
32 |
33 | ### How do I get it
34 | - SPM `"https://github.com/eonist/Spatial.git"` `branch: "master"`
35 | - Manual Open `Spatial.xcodeproj`
36 |
37 | ### Gotchas:
38 | - SnapKit and Carthography are too clever and caters to too many facets of autolayout. This library is just a simple extension that does basic autolayout while reducing the setup time in half.
39 |
40 | ### Example:
41 |
42 | ```swift
43 | // One-liner, single
44 | btn1.anchorAndSize(to: self, width: 96, height: 24)
45 | // Info regarding parameters and their meaning and effects
46 | btn1.anchorAndSize(to: button, // to what other AutoLayout element should self anchor and size to
47 | sizeTo: self, // inherit size of another AutoLayout element, overrides to param
48 | width: 100, // override sizeTo with constant
49 | height: 50, // override sizeTo with constant
50 | align: .topCenter, // decides where the pivot of self should be
51 | alignTo: .bottomCenter, // decides to where self should pivot to
52 | multiplier: .init(width: 1, height: 1), // multiply sizeTo, or constants
53 | offset: .init(x: 0, y: 20), // append constant to current position
54 | sizeOffset: .init(width: -20, height: 0), // append constant to current size
55 | useMargin: false) // adher to other autolayouts margin
56 |
57 | // Long-hand, single
58 | btn1.activateAnchorAndSize { view in
59 | let a = Constraint.anchor(view, to: self)
60 | let s = Constraint.size(view, width: 96, height: 24)
61 | return (a, s)
62 | }
63 | // or simpler:
64 | btn1.activateAnchorAndSize {
65 | Constraint.anchor($0, to: self),
66 | Constraint.size($0, width: 96, height: 24)
67 | }
68 | ```
69 |
70 | ```swift
71 | // Short-hand, bulk
72 | [btn1, btn2, btn3].distributeAndSize(dir: .vertical, width: 96, height: 24)
73 |
74 | // Long-hand, bulk
75 | [btn1,btn2,btn3].activateAnchorsAndSizes { views in
76 | let anchors = Constraint.distribute(vertically: views, align: .topLeft)
77 | let sizes = views.map { Constraint.size($0, size: .init(width: 96, height: 42)) }
78 | return (anchors, sizes)
79 | }
80 | ```
81 |
82 | ```swift
83 | // Pin something between something
84 | $0.activateConstraints { view in
85 | let tl = Constraint.anchor(view, to: self, align: .topLeft, alignTo: .topLeft)
86 | let br = Constraint.anchor(view, to: viewFinderView, align: .bottomRight, alignTo: .topRight)
87 | return [tl.x, tl.y, br.x, br.y] // pins a view to the TR of the parent and BL of another sibling-view
88 | }
89 | ```
90 |
91 | ```swift
92 | // Animation
93 | btn.animate(to: 100,align: left, alignTo: .left)
94 | ```
95 |
96 | ```swift
97 | // Distribute
98 | // |[--][--][--][--][--]|
99 | [label1, label2, label3].activateAnchorsAndSizes { views in // for anim: applyAnchorsAndSizes
100 | let anchors = Constraint.distribute(vertically: views, align: .left) // there is also: horizontally
101 | let sizes = views.map{ Constraint.size($0, toView: self.frame.width, height: 48)) }
102 | return (anchors, sizes)
103 | }
104 | ```
105 |
106 | ```swift
107 | // SpaceAround
108 | // |--[]--[]--[]--[]--[]--|
109 | let views: [ConstraintView] = [UIColor.purple, .orange, .red].map {
110 | let view: ConstraintView = .init(frame: .zero)
111 | self.addSubview(view)
112 | view.backgroundColor = $0
113 | return view
114 | }
115 | views.applySizes(width: 120, height: 48)
116 | views.applyAnchors(to: self, align: .top, alignTo: .top, offset: 20)
117 | views.spaceAround(dir: .hor, parent: self)
118 | ```
119 |
120 | ```swift
121 | // Space between
122 | // |[]--[]--[]--[]--[]|
123 | views.applySizes(width: 120, height: 48)
124 | views.applyAnchors(to: self, align: .top, alignTo: .top, offset: 20)
125 | views.spaceBetween(dir: .horizontal, parent: self, inset:x)
126 | ```
127 |
128 | ### Todo:
129 | - Complete the spaceAround and spaceBetween methods ✅
130 | - Add macOS support ✅
131 | - Document every param in every declaration (Since the API is more stable now) ✅
132 | - Make examples with AutoLayout margins not
133 | - Add methods for applyAnchor for horizontal and vertical types
134 | - Consider renaming anchor and size to pin and fit
135 | - Write problem / solution statment in readme?
136 |
--------------------------------------------------------------------------------
/Sources/SpatialLib/ConstraintKind/ConstraintKind+Access.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import QuartzCore
3 | /**
4 | * Update constraints (For items that are of type `ConstraintKind`)
5 | * - Remark: Adding a method called activateConstraints doesn't make any sense because you have only anchor and size or either
6 | */
7 | extension ConstraintKind where Self: View {
8 | /**
9 | * Applies anchor and size constraints to the view, with optional parameters for customization.
10 | * - Remark: This is a one-liner for `applyAnchorAndSize`, which is a more customizable method.
11 | * ## Examples:
12 | * view.applyAnchorAndSize(to: self, height: 100, align: .centerCenter, alignTo: .centerCenter)
13 | * - Parameters:
14 | * - to: The instance to apply the constraints to.
15 | * - sizeTo: The view to base the size on, if any.
16 | * - width: The width to apply to the view.
17 | * - height: The height to apply to the view.
18 | * - align: The alignment for the `to` view.
19 | * - alignTo: The alignment for the `sizeTo` view, if one was provided.
20 | * - multiplier: The multiplier for the size constraints.
21 | * - offset: The offset for the `to` parameter.
22 | * - sizeOffset: The offset for the `sizeTo` parameter (use negative values for inset).
23 | * - useMargin: Whether to align to AutoLayout margins or not.
24 | */
25 | public func applyAnchorAndSize(to: View, sizeTo: View? = nil, width: CGFloat? = nil, height: CGFloat? = nil, align: Alignment = .topLeft, alignTo: Alignment = .topLeft, multiplier: CGSize = .init(width: 1, height: 1), offset: CGPoint = .zero, sizeOffset: CGSize = .zero, useMargin: Bool = false) {
26 | // Call the more customizable `applyAnchorAndSize` method with a closure that defines the anchor and size constraints
27 | self.applyAnchorAndSize { (_: View) in
28 | let anchor: AnchorConstraint = Constraint.anchor(
29 | self, // The source view
30 | to: to, // The target view
31 | align: align, // The alignment to use
32 | alignTo: alignTo, // The alignment to align to
33 | offset: offset, // The offset to use
34 | useMargin: useMargin // Whether to use layout margins or not
35 | ) // Create an anchor constraint for the view
36 | let size: SizeConstraint = {
37 | if let width: CGFloat = width, let height: CGFloat = height { // Check if both width and height are defined
38 | // If both width and height are defined, create a size constraint with the specified width and height
39 | return Constraint.size(
40 | self, // The source view
41 | size: .init(width: width, height: height), // The size to use
42 | multiplier: multiplier // The multiplier to use
43 | )
44 | } else {
45 | // If either width or height is not defined, create a size constraint based on the size of another view or the view itself
46 | return Constraint.size(
47 | self, // The source view
48 | to: sizeTo ?? to, // The target view to use for size
49 | width: width, // The width to use
50 | height: height, // The height to use
51 | offset: sizeOffset, // The offset to use
52 | multiplier: multiplier // The multiplier to use
53 | )
54 | }
55 | }() // Create a size constraint for the view, based on the width, height, sizeTo, and sizeOffset parameters
56 | return (anchor, size) // Return a tuple containing the anchor and size constraints for the view
57 | }
58 | }
59 | /**
60 | * Applies an anchor constraint to the view, with optional parameters for customization.
61 | * ## Examples:
62 | * view.applyAnchor(to: self, align: .center, alignTo: .center)
63 | * - Remark: This is a one-liner for `applyAnchor`, which is a more customizable method.
64 | * - Parameters:
65 | * - to: The instance to apply the constraint to.
66 | * - align: The alignment for the `to` view.
67 | * - alignTo: The alignment for the `sizeTo` view, if one was provided.
68 | * - offset: The offset for the `to` parameter.
69 | * - useMargin: Whether to align to AutoLayout margins or not.
70 | */
71 | public func applyAnchor(to: View, align: Alignment = .topLeft, alignTo: Alignment = .topLeft, offset: CGPoint = .zero, useMargin: Bool = false) {
72 | // Call the more customizable `applyAnchor` method with a closure that defines the anchor constraint
73 | self.applyAnchor { (_: View) in // Call the `applyAnchor` method with a closure that defines the anchor constraint
74 | Constraint.anchor(
75 | self, // The source view
76 | to: to, // The target view
77 | align: align, // The alignment to use
78 | alignTo: alignTo, // The alignment to align to
79 | offset: offset, // The offset to use
80 | useMargin: useMargin // Whether to use layout margins or not
81 | ) // Create an anchor constraint for the view
82 | }
83 | }
84 | /**
85 | * Applies a size constraint to the view, with optional parameters for customization.
86 | * - Remark: This is a one-liner for `applySize`, which is a more customizable method.
87 | * ## Examples:
88 | * view.applySize(to:self) // multiplier,offset
89 | * - Parameters:
90 | * - to: The instance to apply the constraint to.
91 | * - width: The width to apply to the view.
92 | * - height: The height to apply to the view.
93 | * - multiplier: The multiplier for the size constraints.
94 | * - offset: The offset for the `to` parameter.
95 | */
96 | public func applySize(to: View, width: CGFloat? = nil, height: CGFloat? = nil, offset: CGSize = .zero, multiplier: CGSize = .init(width: 1, height: 1)) {
97 | // Call the more customizable `applySize` method with a closure that defines the size constraint
98 | self.applySize { (_: View) in // Call the `applySize` method with a closure that defines the size constraint
99 | Constraint.size(
100 | self, // The source view
101 | to: to, // The target view to use for size
102 | width: width, // The width to use
103 | height: height, // The height to use
104 | offset: offset, // The offset to use
105 | multiplier: multiplier // The multiplier to use
106 | ) // Create a size constraint for the view
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/Sources/SpatialLib/ConstraintKind/ConstraintKind+Apply.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | #if os(iOS)
3 | import UIKit
4 | #elseif os(macOS)
5 | import Cocoa
6 | #endif
7 | /**
8 | * Extension for updating constraints on items that are of type `ConstraintKind` and `View`.
9 | * - Remark: Adding a method called `activateConstraints` doesn't make sense because you can only have anchor and size constraints, or either.
10 | */
11 | extension ConstraintKind where Self: View {
12 | /**
13 | * Activates and sets the anchor and size constraints for a `ConstraintKind`.
14 | * - Important: ⚠️️ Remember to deactivate constraints before calling this method.
15 | * - Remark: This method is similar to `UIView().activateConstraint...`, but also sets the size and anchor constraints for animation purposes.
16 | * - Parameters:
17 | * - closure: A closure that returns the anchor and size constraints for the view.
18 | * ## Examples:
19 | * sliderBar.applyAnchorAndSize { view in
20 | * let anchor = Constraint.anchor(view, to: self, align: .topLeft, alignTo: .topLeft)
21 | * let size = Constraint.size(view, size: size)
22 | * return (anchor: anchor, size: size) // (anchor, size) also works
23 | * }
24 | */
25 | public func applyAnchorAndSize(closure: AnchorAndSizeClosure) {
26 | self.translatesAutoresizingMaskIntoConstraints = false // Disable the view's translation of autoresizing mask into constraints
27 | let constraints: AnchorAndSize = closure(self) // Call the closure to get the anchor and size constraints for the view
28 | setConstraint(anchor: constraints.anchor, size: constraints.size) // Set the anchor and size constraints for the view
29 | NSLayoutConstraint.activate([
30 | constraints.anchor.x, // The X-axis anchor constraint
31 | constraints.anchor.y, // The Y-axis anchor constraint
32 | constraints.size.w, // The width size constraint
33 | constraints.size.h // The height size constraint
34 | ]) // Activate the anchor and size constraints for the view
35 | }
36 | /**
37 | * Applies an anchor constraint to the view, with optional parameters for customization.
38 | * - Remark: This is a one-liner for `applyAnchorAndSize`, which also sets the size constraints for animation purposes.
39 | * - Parameters:
40 | * - closure: A closure that returns the anchor constraint for the view.
41 | * - Important: ⚠️️ Remember to deactivate constraints before calling this method.
42 | */
43 | public func applyAnchor(closure: AnchorClosure) {
44 | self.translatesAutoresizingMaskIntoConstraints = false // Disable the view's translation of autoresizing mask into constraints
45 | let anchorConstraint: AnchorConstraint = closure(self) // Call the closure to get the anchor constraint for the view
46 | let constraints: [NSLayoutConstraint] = [anchorConstraint.x, anchorConstraint.y] // Create an array of constraints for the view
47 | self.anchor = anchorConstraint // Set the anchor constraint for the view
48 | NSLayoutConstraint.activate(constraints) // Activate the anchor constraints for the view
49 | }
50 | /**
51 | * Applies a size constraint to the view, with optional parameters for customization.
52 | * - Important: ⚠️️ Remember to deactivate constraints before calling this method.
53 | * - Parameters:
54 | * - closure: A closure that returns the size constraint for the view.
55 | */
56 | public func applySize(closure: SizeClosure) {
57 | self.translatesAutoresizingMaskIntoConstraints = false // Disable the view's translation of autoresizing mask into constraints
58 | let sizeConstraint: SizeConstraint = closure(self) // Call the closure to get the size constraint for the view
59 | let constraints: [NSLayoutConstraint] = [sizeConstraint.w, sizeConstraint.h] // Create an array of constraints for the view
60 | self.size = sizeConstraint // Set the size constraint for the view
61 | NSLayoutConstraint.activate(constraints) // Activate the size constraints for the view
62 | }
63 | /**
64 | * Sets both anchor and size constraints for a `ConstraintKind`.
65 | * - Parameters:
66 | * - anchor: The anchor constraint to set.
67 | * - size: The size constraint to set.
68 | */
69 | public func setConstraint(anchor: AnchorConstraint, size: SizeConstraint) {
70 | self.anchorAndSize = (anchor, size) // Set the anchor and size constraints for the view
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Sources/SpatialLib/ConstraintKind/ConstraintKind+Bulk+Access.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import QuartzCore
3 | /**
4 | * Bulk
5 | */
6 | extension Array where Element: ConstraintKind.ViewConstraintKind {
7 | /**
8 | * Applies size constraints to an array of `UIViewConstraintKind`, with optional parameters for customization.
9 | * ## Examples:
10 | * [btn1, btn2, btn3].applySize(to: self, height: 24, offset: .init(width: -40, height: 0))
11 | * - Parameters:
12 | * - to: The target view to apply the size constraints to.
13 | * - width: The target width for the views.
14 | * - height: The target height for the views.
15 | * - offset: The size offset to add as margin to the views.
16 | * - multiplier: The scalar to multiply the size of the views by.
17 | * - Example:
18 | * ```
19 | * [btn1, btn2, btn3].applySizes(to: self, height: 24, offset: .init(width: -40, height: 0))
20 | * ```
21 | */
22 | public func applySizes(to: View, width: CGFloat? = nil, height: CGFloat? = nil, offset: CGSize = .zero, multiplier: CGSize = .init(width: 1, height: 1)) {
23 | self.applySizes { (views: [View]) in // Apply size constraints to the views in the array
24 | views.map { // Map each view to a size constraint
25 | Constraint.size(
26 | $0, // The source view
27 | to: to, // The target view to use for size
28 | width: width, // The width to use
29 | height: height, // The height to use
30 | offset: offset, // The offset to use
31 | multiplier: multiplier // The multiplier to use
32 | ) // Create a size constraint for the view
33 | }
34 | }
35 | }
36 | /**
37 | * Applies size constraints to an array of `UIViewConstraintKind`, with fixed width and height.
38 | * - Description: Same as `applySizes` but with fixed width and height.
39 | * - Parameters:
40 | * - width: The target width for the views.
41 | * - height: The target height for the views.
42 | * - multiplier: The scalar to multiply the size of the views by.
43 | */
44 | public func applySizes(width: CGFloat, height: CGFloat, multiplier: CGSize = .init(width: 1, height: 1)) {
45 | self.applySizes { (views: [View]) in // Apply size constraints to the views in the array
46 | views.map { // Map each view to a size constraint
47 | Constraint.size(
48 | $0, // The source view
49 | size: .init(width: width, height: height), // The size to use
50 | multiplier: multiplier // The multiplier to use
51 | ) // Create a size constraint for the view with fixed width and height
52 | }
53 | }
54 | }
55 | /**
56 | * Applies vertical anchor constraints to an array of `UIViewConstraintKind`.
57 | * - Description: Same as `applyAnchors` but just for vertical anchor.
58 | * - Parameters:
59 | * - to: The target view to apply the anchor constraints to.
60 | * - align: The object align point for the views.
61 | * - alignTo: The canvas align point for the views.
62 | * - offset: The point offset to add as margin to the views.
63 | * - useMargin: A Boolean value indicating whether to use the OS margin.
64 | * ## Examples:
65 | * view.applyAnchor(to: self, align: .top, alignTo: .top)
66 | */
67 | public func applyAnchors(to: View, align: VerticalAlign = .top, alignTo: VerticalAlign = .top, offset: CGFloat = .zero, useMargin: Bool = false) {
68 | self.applyAnchors(axis: .ver) { (views: [View]) in // Apply vertical anchor constraints to the views in the array
69 | views.map { // Map each view to an anchor constraint
70 | Constraint.anchor(
71 | $0, // The source view
72 | to: to, // The target view
73 | align: align, // The alignment to use
74 | alignTo: alignTo, // The alignment to align to
75 | offset: offset, // The offset to use
76 | useMargin: useMargin // Whether to use layout margins or not
77 | ) // Create a vertical anchor constraint for the view
78 | }
79 | }
80 | }
81 | /**
82 | * Applies horizontal anchor constraints to an array of `UIViewConstraintKind`.
83 | * - Description: Same as `applyAnchors` but just for horizontal anchor.
84 | * - Parameters:
85 | * - to: The target view to apply the anchor constraints to.
86 | * - align: The object align point for the views.
87 | * - alignTo: The canvas align point for the views.
88 | * - offset: The point offset to add as margin to the views.
89 | * - useMargin: A Boolean value indicating whether to use the OS margin.
90 | * ## Examples:
91 | * view.applyAnchor(to: self, align: .left, alignTo: .left)
92 | */
93 | public func applyAnchors(to: View, align: HorizontalAlign = .left, alignTo: HorizontalAlign = .left, offset: CGFloat = .zero, useMargin: Bool = false) {
94 | self.applyAnchors(axis: .hor) { (views: [View]) in // Apply horizontal anchor constraints to the views in the array
95 | views.map { // Map each view to an anchor constraint
96 | Constraint.anchor(
97 | $0, // The source view
98 | to: to, // The target view
99 | align: align, // The alignment to use
100 | alignTo: alignTo, // The alignment to align to
101 | offset: offset, // The offset to use
102 | useMargin: useMargin // Whether to use layout margins or not
103 | ) // Create a horizontal anchor constraint for the view
104 | }
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/Sources/SpatialLib/ConstraintKind/ConstraintKind+Bulk.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | #if os(iOS)
3 | import UIKit
4 | #elseif os(macOS)
5 | import Cocoa
6 | #endif
7 | /**
8 | * Update arrays of `UIViewConstraintKind`
9 | */
10 | extension Array where Element: ConstraintKind.ViewConstraintKind {
11 | /**
12 | * Applies anchor and size constraints to an array of `UIViewConstraintKind`.
13 | * - Remark: If you want to apply only anchors or only sizes then just pass an empty array for either.
14 | * - Parameters:
15 | * - closure: A closure that returns the anchor and size constraints for the views.
16 | * ## Examples:
17 | * [label1, label2, label3].applyAnchorsAndSizes { views in
18 | * let anchors = [] // Use Constraint.distribute
19 | * let sizes = [] // Use views.map { Constraint.size }
20 | * return (anchors, sizes)
21 | * }
22 | */
23 | public func applyAnchorsAndSizes(closure: AnchorAndSizeClosure) {
24 | self.forEach { $0.translatesAutoresizingMaskIntoConstraints = false } // Disable the translation of autoresizing mask into constraints for each view in the array
25 | let constraints: AnchorConstraintsAndSizeConstraints = closure(self) // Call the closure to get the anchor and size constraints for the views
26 | self.enumerated().forEach { // Loop through each view in the array, along with its index
27 | let anchor: AnchorConstraint = constraints.anchorConstraints[$0.offset] // Get the anchor constraint for the view at the current index
28 | let size: SizeConstraint = constraints.sizeConstraints[$0.offset] // Get the size constraint for the view at the current index
29 | $0.element.setConstraint(anchor: anchor, size: size) // Set the anchor and size constraints for the view at the current index
30 | }
31 | let layoutConstraints: [NSLayoutConstraint] = { // Create an array of layout constraints
32 | let anchors: [NSLayoutConstraint] = constraints.anchorConstraints.reduce([]) {
33 | $0 + [$1.x, $1.y]
34 | } // Create an array of anchor constraints
35 | let sizes: [NSLayoutConstraint] = constraints.sizeConstraints.reduce([]) {
36 | $0 + [$1.w, $1.h]
37 | } // Create an array of size constraints
38 | return anchors + sizes // Concatenate the arrays of anchor and size constraints
39 | }()
40 | NSLayoutConstraint.activate(layoutConstraints) // Activate the layout constraints for the views in the array
41 | }
42 | /**
43 | * Applies size constraints to an array of `UIViewConstraintKind`.
44 | * - Description: Same as `applyAnchorsAndSizes` but just for sizes.
45 | * - Parameters:
46 | * - closure: A closure that returns the size constraints for the views.
47 | */
48 | public func applySizes(closure: SizesClosure) {
49 | self.forEach { $0.translatesAutoresizingMaskIntoConstraints = false } // Disable the translation of autoresizing mask into constraints for each view in the array
50 | let constraints: [SizeConstraint] = closure(self) // Call the closure to get the size constraints for the views
51 | self.enumerated().forEach { // Loop through each view in the array, along with its index
52 | let size: SizeConstraint = constraints[$0.offset] // Get the size constraint for the view at the current index
53 | $0.element.size = size // Set the size constraint for the view at the current index
54 | }
55 | let layoutConstraints: [NSLayoutConstraint] = constraints.reduce([]) {
56 | $0 + [$1.w, $1.h]
57 | } // Create an array of layout constraints
58 | NSLayoutConstraint.activate(layoutConstraints) // Activate the layout constraints for the views in the array
59 | }
60 | /**
61 | * Applies anchor constraints to an array of `UIViewConstraintKind`.
62 | * - Description: Same as `applyAnchorsAndSizes` but just for anchors.
63 | * - Parameters:
64 | * - closure: A closure that returns the anchor constraints for the views.
65 | */
66 | public func applyAnchors(closure: AnchorClosure) {
67 | self.forEach { $0.translatesAutoresizingMaskIntoConstraints = false } // Disable the translation of autoresizing mask into constraints for each view in the array
68 | let constraints: [AnchorConstraint] = closure(self) // Call the closure to get the anchor constraints for the views
69 | self.enumerated().forEach { // Loop through each view in the array, along with its index
70 | let anchor: AnchorConstraint = constraints[$0.offset] // Get the anchor constraint for the view at the current index
71 | $0.element.anchor = anchor // Set the anchor constraint for the view at the current index
72 | }
73 | let layoutConstraints: [NSLayoutConstraint] = constraints.reduce([]) {
74 | $0 + [$1.x, $1.y]
75 | } // Create an array of layout constraints
76 | NSLayoutConstraint.activate(layoutConstraints) // Activate the layout constraints for the views in the array
77 | }
78 | /**
79 | * Applies horizontal or vertical anchor constraints to an array of `UIViewConstraintKind`.
80 | * - Description: Same as `applyAnchorsAndSizes` but just for horizontal or vertical anchor.
81 | * - Parameters:
82 | * - axis: The axis to apply the anchor constraints to (`hor` for horizontal, `ver` for vertical).
83 | * - closure: A closure that returns the anchor constraints for the views.
84 | */
85 | public func applyAnchors(axis: Axis, closure: AxisClosure) {
86 | self.forEach { $0.translatesAutoresizingMaskIntoConstraints = false } // Disable the translation of autoresizing mask into constraints for each view in the array
87 | let constraints: [NSLayoutConstraint] = closure(self) // Call the closure to get the anchor constraints for the views
88 | self.enumerated().forEach { // Loop through each view in the array, along with its index
89 | let anchor: NSLayoutConstraint = constraints[$0.offset] // Get the anchor constraint for the view at the current index
90 | switch axis {
91 | case .hor: $0.element.anchor?.x = anchor // Set the horizontal anchor constraint for the view at the current index
92 | case .ver: $0.element.anchor?.y = anchor // Set the vertical anchor constraint for the view at the current index
93 | }
94 | }
95 | let layoutConstraints: [NSLayoutConstraint] = constraints.reduce([]) {
96 | $0 + [$1]
97 | } // Create an array of layout constraints
98 | NSLayoutConstraint.activate(layoutConstraints) // Activate the layout constraints for the views in the array
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/Sources/SpatialLib/ConstraintKind/ConstraintKind+SpaceAround.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | #if os(iOS)
3 | import UIKit
4 | #elseif os(macOS)
5 | import Cocoa
6 | #endif
7 | /**
8 | * Extension for spaceAround method
9 | */
10 | extension Array where Element: ConstraintKind.ViewConstraintKind {
11 | /**
12 | * Adds equal spacing between views, including at the ends.
13 | * - Important: ⚠️️ Only works with `UIConstraintView` where size is available.
14 | * - Important: ⚠️️ Only works where the `parent.bound` are available.
15 | * - Description: Adds equal spacing between views, including at the ends, in either the vertical or horizontal direction.
16 | * - Note: |--[]--[]--[]--[]--[]--|
17 | * ## Examples:
18 | * let views: [ConstraintView] = [UIColor.purple, .orange,.red].map {
19 | * let view: ConstraintView = .init(frame: .zero)
20 | * self.addSubview(view)
21 | * view.backgroundColor = $0
22 | * return view
23 | * }
24 | * views.applySizes(width: 120, height: 48)
25 | * views.applyAnchors(to: self, align: .top, alignTo: .top, offset: 20)
26 | * views.spaceAround(dir: .hor, parent: self)
27 | * - Parameters:
28 | * - dir: The axis to add spacing to (`hor` for horizontal, `ver` for vertical).
29 | * - parent: The parent view of the views.
30 | * - inset: The amount to inset the parent bounds.
31 | */
32 | public func spaceAround(dir: Axis, parent: View, inset: EdgeInsets = .init()) {
33 | switch dir {
34 | case .hor:
35 | SpaceAroundUtil.spaceAround(
36 | horizontally: parent, // The parent view to align horizontally to
37 | views: self, // The views to align
38 | inset: inset // The inset to use
39 | ) // Add equal spacing between views horizontally
40 | case .ver:
41 | SpaceAroundUtil.spaceAround(
42 | vertically: parent, // The parent view to align vertically to
43 | views: self, // The views to align
44 | inset: inset // The inset to use
45 | ) // Add equal spacing between views vertically
46 | }
47 | }
48 | }
49 | /**
50 | * SpaceAround helper
51 | */
52 | fileprivate class SpaceAroundUtil {
53 | /**
54 | * spaceAround (Horizontal)
55 | * - Fixme: ⚠️️ use reduce on x
56 | * - Parameters:
57 | * - parent: parent view
58 | * - views: views to align
59 | * - inset: parent inset
60 | */
61 | static func spaceAround(horizontally parent: View, views: [ConstraintKind.ViewConstraintKind], inset: EdgeInsets) {
62 | let rect: CGRect = parent.bounds.inset(by: inset)
63 | let itemVoid: CGFloat = horizontalItemVoid(rect: rect, views: views)
64 | var x: CGFloat = rect.origin.x + itemVoid // Interim x
65 | views.forEach { (item: ConstraintKind.ViewConstraintKind) in
66 | item.activateConstraint { (_: View) in
67 | let constraint: NSLayoutConstraint = Constraint.anchor(
68 | item, // The item to anchor
69 | to: parent, // The parent view to anchor to
70 | align: .left, // The alignment to use for the item
71 | alignTo: .left, // The alignment to align to on the parent view
72 | offset: x // The offset to use
73 | )
74 | item.anchor?.x = constraint
75 | return constraint
76 | }
77 | x += (item.size?.w.constant ?? 0) + itemVoid
78 | }
79 | }
80 | /**
81 | * spaceAround (Vertical)
82 | * - Fixme: ⚠️️ write doc, and use reduce on y
83 | * - Parameters:
84 | * - parent: parent view
85 | * - views: views to align
86 | * - inset: parent inset
87 | */
88 | static func spaceAround(vertically parent: View, views: [ConstraintKind.ViewConstraintKind], inset: EdgeInsets) {
89 | let rect: CGRect = parent.bounds.inset(by: inset)
90 | let itemVoid: CGFloat = verticalItemVoid(rect: rect, views: views)
91 | var y: CGFloat = rect.origin.y + itemVoid // Interim y
92 | views.forEach { (item: ConstraintKind.ViewConstraintKind) in
93 | item.activateConstraint { _ in
94 | let constraint: NSLayoutConstraint = Constraint.anchor(
95 | item, // The item to anchor
96 | to: parent, // The parent view to anchor to
97 | align: .top, // The alignment to use for the item
98 | alignTo: .top, // The alignment to align to on the parent view
99 | offset: y // The offset to use
100 | )
101 | item.anchor?.y = constraint
102 | return constraint
103 | }
104 | y += (item.size?.h.constant ?? 0) + itemVoid
105 | }
106 | }
107 | }
108 | /**
109 | * Helpers
110 | */
111 | extension SpaceAroundUtil {
112 | /**
113 | * ItemVoid (horizontal)
114 | * - Parameters:
115 | * - rect: canvas
116 | * - views: views to align
117 | */
118 | private static func horizontalItemVoid(rect: CGRect, views: [ConstraintKind.ViewConstraintKind]) -> CGFloat {
119 | let totW: CGFloat = views.reduce(0) { $0 + ($1.size?.w.constant ?? 0) } // Find the totalW of all items
120 | let totVoid: CGFloat = rect.width - totW // Find totVoid by doing w - totw
121 | let numOfVoids: CGFloat = .init(views.count + 1) // Then divide this voidSpace with .count - 1 and
122 | return totVoid / numOfVoids // Iterate of each item and inserting itemVoid in + width
123 | }
124 | /**
125 | * ItemVoid (vertical)
126 | * - Parameters:
127 | * - rect: canvas
128 | * - views: views to align
129 | */
130 | private static func verticalItemVoid(rect: CGRect, views: [ConstraintKind.ViewConstraintKind]) -> CGFloat {
131 | let totH: CGFloat = views.reduce(0) { $0 + ($1.size?.h.constant ?? 0) } // Find the totalW of all items
132 | let totVoid: CGFloat = rect.height - totH // Find totVoid by doing w - totw
133 | let numOfVoids: CGFloat = .init(views.count + 1) // Then divide this voidSpace with .count - 1 and
134 | return totVoid / numOfVoids // Iterate of each item and inserting itemVoid in + width
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/Sources/SpatialLib/ConstraintKind/ConstraintKind+SpaceBetween.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | #if os(iOS)
3 | import UIKit
4 | #elseif os(macOS)
5 | import Cocoa
6 | #endif
7 | /**
8 | * Space items evenly to fill length
9 | */
10 | extension Array where Element: ConstraintKind.ViewConstraintKind {
11 | /**
12 | * Aligns all items horizontally from the absolute start to absolute end and adds equal spacing between them (only works on views that adher to ConstraintKind)
13 | * - Description: |[]--[]--[]--[]--[]|
14 | * - Important: ⚠️️ Views needs to have size constraint applied before calling this method
15 | * - Important: ⚠️️ This method from layoutSubViews, as you need the parent.bounds to be realized, and its only relaized from AutoLayout when layoutSubViews is called
16 | * - Important: ⚠️️ Only works with `UIConstraintView` classes (parent does not have to be UIViewConstraintKind)
17 | * - Parameters:
18 | * - dir: The direction in which to distribute the items.
19 | * - parent: The containing view that has the views as subviews.
20 | * - inset: Use this to inset where items should be set. If none is provided, parent bounds are used.
21 | * ## Examples:
22 | * views.spaceBetween(dir: .horizontal, parent: self, inset:x)
23 | */
24 | public func spaceBetween(dir: Axis, parent: View, inset: EdgeInsets = .init()) {
25 | switch dir {
26 | // If the direction is horizontal, call the spaceBetween method with the parent view, the views to distribute, and the inset value.
27 | case .hor:
28 | SpaceBetweenUtil.spaceBetween(
29 | horizontally: parent, // The parent view to align horizontally to
30 | views: self, // The views to align
31 | inset: inset // The inset to use
32 | )
33 | // If the direction is vertical, call the spaceBetween method with the parent view, the views to distribute, and the inset value.
34 | case .ver:
35 | SpaceBetweenUtil.spaceBetween(
36 | vertically: parent, // The parent view to align vertically to
37 | views: self, // The views to align
38 | inset: inset // The inset to use
39 | )
40 | }
41 | }
42 | }
43 | /**
44 | * SpaceBetween helper
45 | */
46 | private class SpaceBetweenUtil {
47 | /**
48 | * Distributes views horizontally with equal spacing between them (only works on views that adhere to ConstraintKind).
49 | * - Parameters:
50 | * - parent: The containing view that has the views as subviews.
51 | * - views: The views to distribute horizontally.
52 | * - inset: Use this to inset where items should be set. If none is provided, parent bounds are used.
53 | * ## Examples:
54 | * SpaceBetweenUtil.spaceBetween(horizontally: parent, views: self, inset: x)
55 | */
56 | static func spaceBetween(horizontally parent: View, views: [ConstraintKind.ViewConstraintKind], inset: EdgeInsets) {
57 | // Get the parent view's bounds and inset it by the provided inset value
58 | let rect: CGRect = parent.bounds.inset(by: inset)
59 | // Calculate the amount of space between each item
60 | let itemVoid: CGFloat = horizontalItemVoid(rect: rect, views: views)
61 | // Initialize the interim x value to the origin of the inset parent bounds
62 | var x: CGFloat = rect.origin.x // Interim x
63 | // Loop through each view and activate a constraint to align it to the left of the parent view
64 | views.forEach { (item: ConstraintKind.ViewConstraintKind) in
65 | item.activateConstraint { (_: View) in // Fixme: ⚠️️ Create applyAnchor for hor and ver
66 | let constraint: NSLayoutConstraint = Constraint.anchor(
67 | item, // The item to anchor
68 | to: parent, // The parent view to anchor to
69 | align: .left, // The alignment to use for the item
70 | alignTo: .left, // The alignment to align to on the parent view
71 | offset: x // The offset to use
72 | )
73 | item.anchor?.x = constraint
74 | return constraint
75 | }
76 | // Increment the interim x value by the width of the current view plus the itemVoid
77 | x += (item.size?.w.constant ?? 0) + itemVoid
78 | }
79 | }
80 | /**
81 | * Vertically aligns the given views within the parent view, with a fixed amount of space between each view.
82 | * - Parameters:
83 | * - parent: The parent view.
84 | * - views: The views to align.
85 | * - inset: The inset from the parent view's bounds to use for alignment.
86 | */
87 | static func spaceBetween(vertically parent: View, views: [ConstraintKind.ViewConstraintKind], inset: EdgeInsets) {
88 | let rect: CGRect = parent.bounds.inset(by: inset) // Get the parent view's bounds and inset them by the given amount
89 | let itemVoid: CGFloat = verticalItemVoid(rect: rect, views: views) // Calculate the vertical space between each view
90 | var y: CGFloat = rect.origin.y // Set the initial y position to the top of the parent view
91 | views.forEach { (item: ConstraintKind.ViewConstraintKind) in // Loop through each view to align
92 | item.activateConstraint { _ in // Activate the view's constraints
93 | let constraint: NSLayoutConstraint = Constraint.anchor(
94 | item, // The item to anchor
95 | to: parent, // The parent view to anchor to
96 | align: .top, // The alignment to use for the item
97 | alignTo: .top, // The alignment to align to on the parent view
98 | offset: y // The offset to use
99 | ) // Align the view to the top of the parent view with the given y offset
100 | item.anchor?.y = constraint // Store the constraint in the view's anchor property
101 | return constraint // Return the constraint
102 | }
103 | y += (item.size?.h.constant ?? 0) + itemVoid // Increment the y position by the height of the current view plus the vertical space between views
104 | }
105 | }
106 | }
107 | /**
108 | * Helpers
109 | */
110 | extension SpaceBetweenUtil {
111 | /**
112 | * Calculates the horizontal space between each view in order to evenly distribute them within the given canvas.
113 | * - Parameters:
114 | * - rect: The canvas to align the views within.
115 | * - views: The views to align.
116 | * - Returns: The amount of horizontal space to insert between each view.
117 | */
118 | private static func horizontalItemVoid(rect: CGRect, views: [ConstraintKind.ViewConstraintKind]) -> CGFloat {
119 | let totW: CGFloat = views.reduce(0) { $0 + ($1.size?.w.constant ?? 0) } // Calculate the total width of all views
120 | let totVoid: CGFloat = rect.width - totW // Calculate the total horizontal space available
121 | let numOfVoids: CGFloat = .init(views.count - 1) // Calculate the number of spaces between views
122 | return totVoid / numOfVoids // Calculate the amount of horizontal space to insert between each view
123 | }
124 | /**
125 | * Calculates the vertical space between each view in order to evenly distribute them within the given canvas.
126 | * - Parameters:
127 | * - rect: The canvas to align the views within.
128 | * - views: The views to align.
129 | * - Returns: The amount of vertical space to insert between each view.
130 | */
131 | private static func verticalItemVoid(rect: CGRect, views: [ConstraintKind.ViewConstraintKind]) -> CGFloat {
132 | let totH: CGFloat = views.reduce(0) { $0 + ($1.size?.h.constant ?? 0) } // Calculate the total height of all views
133 | let totVoid: CGFloat = rect.height - totH // Calculate the total vertical space available
134 | let numOfVoids: CGFloat = .init(views.count - 1) // Calculate the number of spaces between views
135 | return totVoid / numOfVoids // Calculate the amount of vertical space to insert between each view
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/Sources/SpatialLib/ConstraintKind/ConstraintKind+Type.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | #if os(iOS)
3 | import UIKit
4 | #elseif os(macOS)
5 | import Cocoa
6 | #endif
7 | /**
8 | * Single
9 | * Defines a type alias for a view that conforms to `ConstraintKind`.
10 | * - Remark: This is useful for creating combinational types and closure signatures.
11 | */
12 | extension ConstraintKind where Self: View {
13 | /**
14 | * Combinational types and closure signatures
15 | * - Remark: This could be useful in a global domain, for now just access it by: ConstraintKind.UIViewConstraintKind
16 | */
17 | public typealias ViewConstraintKind = View & ConstraintKind
18 | }
19 | /**
20 | * Bulk
21 | * Defines closures that operate on an array of `ViewConstraintKind` elements.
22 | */
23 | extension Array where Element: ConstraintKind.ViewConstraintKind {
24 | /**
25 | * A closure that returns anchor and size constraints for an array of views
26 | */
27 | public typealias AnchorAndSizeClosure = (_ views: [View]) -> AnchorConstraintsAndSizeConstraints
28 | /**
29 | * A closure that returns size constraints for an array of views
30 | */
31 | public typealias SizesClosure = (_ views: [View]) -> [SizeConstraint]
32 | /**
33 | * A closure that returns anchor constraints for an array of views
34 | */
35 | public typealias AnchorClosure = (_ views: [View]) -> [AnchorConstraint]
36 | /**
37 | * A closure that returns axis constraints for an array of views
38 | */
39 | public typealias AxisClosure = (_ views: [View]) -> [NSLayoutConstraint]
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/SpatialLib/ConstraintKind/ConstraintKind.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | /**
3 | * `UIView` and `NSView` classes that implement this protocol are able to store the anchor and size constraints.
4 | * - Remark: Use `anchorAndSize` as a variable, anchor and size are part of the legacy API and will be deprecated.
5 | * - Remark: Storing constraints is necessary if you want to change the constraints at a later point in time, for instance for animation.
6 | */
7 | public protocol ConstraintKind: AnyObject {
8 | // @available(*, deprecated, renamed: "anchorAndSize")
9 | var anchor: AnchorConstraint? { get set }
10 | // @available(*, deprecated, renamed: "anchorAndSize")
11 | var size: SizeConstraint? { get set }
12 | var anchorAndSize: AnchorAndSize? { get set }
13 | }
14 | // DEPRECATED ⚠️️
15 | extension ConstraintKind {
16 | // DEPRECATED ⚠️️
17 | // @available(*, deprecated, renamed: "anchorAndSize.anchor")
18 | public var anchor: AnchorConstraint? {
19 | get {
20 | self.anchorAndSize?.anchor
21 | } set {
22 | if let newValue: AnchorConstraint = newValue { anchorAndSize?.anchor = newValue }
23 | }
24 | }
25 | // DEPRECATED ⚠️️
26 | // @available(*, deprecated, renamed: "anchorAndSize.size")
27 | public var size: SizeConstraint? {
28 | get {
29 | self.anchorAndSize?.size
30 | } set {
31 | if let newValue: SizeConstraint = newValue { anchorAndSize?.size = newValue }
32 | }
33 | }
34 | /**
35 | * Default `anchorAndSize` value
36 | * - Important: ⚠️️ Will be deprecated, once anchor and size is deprecated
37 | */
38 | public var anchorAndSize: AnchorAndSize? {
39 | get {
40 | guard let anchor: AnchorConstraint = anchor,
41 | let size: SizeConstraint = size else { return nil } // If either anchor or size is nil, return nil
42 | return (anchor, size) // Return a tuple of anchor and size
43 | } set {
44 | anchor = newValue?.anchor // Set the anchor to the anchor value of the new tuple
45 | size = newValue?.size // Set the size to the size value of the new tuple
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Sources/SpatialLib/ConstraintKind/ConstraintView/ConstraintView.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | /**
3 | * A convenient `UIView` subclass that automatically adds anchor and size constraints.
4 | * - Note: To use this class, the view must implement the `ConstraintKind` protocol and set the `anchorAndSize` property.
5 | */
6 | open class ConstraintView: View, ConstraintKind {
7 | // The anchor and size constraints for the view
8 | public var anchorAndSize: AnchorAndSize?
9 | }
10 |
--------------------------------------------------------------------------------
/Sources/SpatialLib/ConstraintKind/animation/ConstraintKind+Animate.swift:
--------------------------------------------------------------------------------
1 | #if os(iOS)
2 | import UIKit
3 | /**
4 | * Provides examples of how to animate with Spatial and autolayout.
5 | * - Remark: The `UIViewConstraintKind` should be used instead of `ConstraintKind` for animating views on iOS. This extension is provided for reference only.
6 | * - Fixme: ⚠️️ Use `UIViewConstraintKind` instead of `ConstraintKind` for animating views on iOS.
7 | */
8 | extension ConstraintKind where Self: UIView {
9 | /**
10 | * Provides a method for animating a view's horizontal constraint.
11 | * - Parameters:
12 | * - to: The new value for the constraint.
13 | * - align: The alignment point of the view's constraint.
14 | * - alignTo: The alignment point of the canvas.
15 | * - onComplete: A closure that is called when the animation completes.
16 | * - Remark: The `animate` method updates the view's horizontal constraint by calling the `update` method with the specified offset, alignment, and canvas alignment. The `UIView.animate` method is used to animate the constraint change. When the animation completes, the `onComplete` closure is called. If no `onComplete` closure is provided, the default completion closure that prints a message to the console is used.
17 | * - Fixme: ⚠️️ Use `UIViewConstraintKind` instead of `ConstraintKind` for animating views on iOS.
18 | * - Example: `someView.animate(to: 100, align: .left, alignTo: .left)` animates the horizontal constraint of `someView` to a value of 100 points, aligned to the left edge of the canvas and the left edge of `someView`.
19 | */
20 | public func animate(to: CGFloat, align: HorizontalAlign, alignTo: HorizontalAlign, onComplete:@escaping AnimComplete = Self.defaultOnComplete) {
21 | // Call the `UIView.animate` method with a closure that updates the view's horizontal constraint by calling the `update` method with the specified offset, alignment, and canvas alignment
22 | UIView.animate({ // Start the animation block
23 | self.update(offset: to, align: align, alignTo: alignTo) // Update the offset and alignment
24 | }, onComplete: onComplete) // Call the completion handler when the animation is complete
25 | }
26 | /**
27 | * Provides a method for animating a view's vertical constraint.
28 | * - Parameters:
29 | * - to: The new value for the constraint.
30 | * - align: The alignment point of the view's constraint.
31 | * - alignTo: The alignment point of the canvas.
32 | * - onComplete: A closure that is called when the animation completes.
33 | * - Remark: The `animate` method updates the view's vertical constraint by calling the `update` method with the specified offset, alignment, and canvas alignment. The `UIView.animate` method is used to animate the constraint change. When the animation completes, the `onComplete` closure is called. If no `onComplete` closure is provided, the default completion closure that prints a message to the console is used.
34 | * - Example: `someView.animate(to: 100, align: .top, alignTo: .top)` animates the vertical constraint of `someView` to a value of 100 points, aligned to the top edge of the canvas and the top edge of `someView`.
35 | */
36 | public func animate(to: CGFloat, align: VerticalAlign, alignTo: VerticalAlign, onComplete:@escaping AnimComplete = Self.defaultOnComplete) {
37 | // Call the `UIView.animate` method with a closure that updates the view's vertical constraint by calling the `update` method with the specified offset, alignment, and canvas alignment
38 | UIView.animate({ // Start the animation block
39 | self.update(offset: to, align: align, alignTo: alignTo) // Update the offset and alignment
40 | }, onComplete: onComplete) // Call the completion handler when the animation is complete
41 | }
42 | /**
43 | * Provides a method for animating a view's vertical and horizontal constraints.
44 | * - Parameters:
45 | * - to: The new value for the constraints.
46 | * - align: The alignment point of the view's constraints.
47 | * - alignTo: The alignment point of the canvas.
48 | * - onComplete: A closure that is called when the animation completes.
49 | * - Remark: The `animate` method updates the view's vertical and horizontal constraints by calling the `update` method with the specified offset, alignment, and canvas alignment. The `UIView.animate` method is used to animate the constraint changes. When the animation completes, the `onComplete` closure is called. If no `onComplete` closure is provided, the default completion closure that prints a message to the console is used.
50 | * - Example: `someView.animate(to: CGPoint(x: 100, y: 100), align: .topLeft, alignTo: .topLeft)` animates the vertical and horizontal constraints of `someView` to a point of (100, 100), aligned to the top-left corner of the canvas and the top-left corner of `someView`.
51 | */
52 | public func animate(to: CGPoint, align: Alignment, alignTo: Alignment, onComplete:@escaping AnimComplete = Self.defaultOnComplete) {
53 | // Call the `UIView.animate` method with a closure that updates the view's constraints by calling the `update` method with the specified offset, alignment, and canvas alignment
54 | UIView.animate({
55 | self.update(
56 | offset: to, // The offset to update to
57 | align: align, // The alignment to use
58 | alignTo: alignTo // The alignment to align to
59 | )
60 | }, onComplete: onComplete) // The completion handler to call when the animation is complete
61 | }
62 | }
63 | #endif
64 |
--------------------------------------------------------------------------------
/Sources/SpatialLib/align/Align.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import QuartzCore
3 | /**
4 | * Align is a util class to help align views and graphics. The core principle is based around a stage-pivot and an object pivot. The align type you add to these two pivot deterimins the final position of the object
5 | * - Remark: This is a very usefull class to have when positioning things in cases where using `AutoLayout` is hard or for testing things out
6 | * - Remark: The align method only supports `NSView` / `UIView`. But use `alignmentPoint` instead if you have to work with different class types. As it does the same thing as the align method does
7 | */
8 | public final class Align {
9 | /**
10 | * Aligns a view to a specified alignment within a canvas of a given size, with an optional offset.
11 | * - Parameters:
12 | * - object: The view to be aligned.
13 | * - canvasSize: The size of the canvas on which the view will be aligned.
14 | * - canvasAlignment: The alignment of the view on the canvas.
15 | * - objectAlignment: The alignment of the view within its own canvas.
16 | * - offset: An optional offset to be applied to the view's position.
17 | * - Remark: The `object`'s frame will be updated to reflect the new position.
18 | * ## Examples:
19 | * Align.align(someCircle, canvasSize: .init(x: 400, y: 300), canvasAlign: .center, objectAlign: .topLeft) // Output: centers a circle within 400x300 rectangle
20 | * Align.align(someCircle, canvasSize: .init(x: 400, y: 300), canvasAlign: .centerRight, objectAlign: .centerRight) // Output: aligns the circle to the y axis center and to the right border of the rectangle but withinn the rectange
21 | */
22 | public static func align(object: View, canvasSize: CGSize, canvasAlignment: Alignment, objectAlignment: Alignment, offset: CGPoint = .zero) {
23 | // Calculate the alignment point for the object based on the given parameters
24 | let objSize: CGSize = .init(width: object.frame.width, height: object.frame.height)
25 | let alignmentPoint: CGPoint = Align.alignmentPoint(
26 | objectSize: objSize, // Size of the object being aligned
27 | canvasSize: canvasSize, // Size of the canvas
28 | canvasAlign: canvasAlignment, // Alignment of the canvas
29 | objectAlign: objectAlignment, // Alignment of the object
30 | offset: offset // Offset from the alignment point
31 | ) // Point to align the object to
32 | // Update the object's frame origin to the calculated alignment point
33 | object.frame.origin = alignmentPoint
34 | }
35 | /**
36 | Returns the point from where to align a target object of size `objectSize` within a canvas of size `canvasSize` at the specified `objectAlignment` and `canvasAlignment`, with an optional `offset`.
37 | - Parameters:
38 | - objectSize: The size of the object that is being aligned.
39 | - canvasSize: The size of the canvas the object is being aligned to.
40 | - canvasAlign: The point in the container to align to.
41 | - objectAlign: The object alignment point.
42 | - offset: An optional offset to be applied to the alignment point.
43 | - Remark: This function is useful when aligning two or more objects where you can add the size together and find the correct alignment point.
44 | - Returns: The calculated alignment point.
45 | - Example: `let alignmentPoint = Align.alignmentPoint(objectSize: CGSize(width: 50, height: 50), canvasSize: CGSize(width: 200, height: 200), canvasAlign: .center, objectAlign: .center, offset: CGPoint(x: 10, y: 10))` returns a `CGPoint` representing the alignment point for a 50x50 object centered within a 200x200 canvas, with a 10-point offset in both the x and y directions.
46 | */
47 | public static func alignmentPoint(objectSize: CGSize, canvasSize: CGSize, canvasAlign: Alignment, objectAlign: Alignment, offset: CGPoint = .zero) -> CGPoint {
48 | // Calculate the point in the canvas to align to based on the given canvas size and alignment
49 | let canvasP: CGPoint = Align.point(
50 | size: canvasSize, // size of the canvas
51 | align: canvasAlign // alignment of the canvas
52 | )
53 | // Calculate the point in the object to align from based on the given object size and alignment
54 | let objP: CGPoint = Align.point(
55 | size: objectSize, // size of the object
56 | align: objectAlign // alignment of the object
57 | )
58 | // Calculate the difference between the canvas point and the object point
59 | let p: CGPoint = .init(x: canvasP.x - objP.x, y: canvasP.y - objP.y)
60 | // Add the offset to the calculated point and return it as the final alignment point
61 | return .init(x: p.x + offset.x, y: p.y + offset.y)
62 | }
63 | /**
64 | * Returns the pivot point of an object according to its `pivotAlignment`.
65 | * - Parameters:
66 | * - size: The size of the object.
67 | * - align: The alignment point of the object.
68 | * - Returns: The calculated pivot point.
69 | * - Remark: The pivot point is the point around which the object rotates or scales.
70 | * - Example: `let pivotPoint = Align.point(size: CGSize(width: 50, height: 50), align: .centerCenter)` returns a `CGPoint` representing the pivot point for a 50x50 object with its center at the pivot point.
71 | */
72 | public static func point(size: CGSize, align: Alignment) -> CGPoint {
73 | switch align {
74 | case .topLeft: return .init() // Return the top-left corner of the object
75 | case .topRight: return .init(x: size.width, y: 0) // Return the top-right corner of the object
76 | case .centerCenter: return .init(x: round((size.width / 2)), y: round((size.height / 2))) // Return the center point of the object
77 | case .centerLeft: return .init(x: 0, y: round((size.height / 2))) // Return the center-left point of the object
78 | case .topCenter: return .init(x: round((size.width / 2)), y: 0) // Return the top-center point of the object
79 | case .centerRight: return .init(x: size.width, y: round((size.height / 2))) // Return the center-right point of the object
80 | case .bottomRight: return .init(x: size.width, y: size.height) // Return the bottom-right corner of the object
81 | case .bottomLeft: return .init(x: 0, y: size.height) // Return the bottom-left corner of the object
82 | case .bottomCenter: return .init(x: round((size.width / 2)), y: size.height) // Return the bottom-center point of the object
83 | }
84 | }
85 | }
86 | /**
87 | * Bulk
88 | */
89 | extension Align {
90 | /**
91 | * Aligns an array of view instances to a specified alignment within a canvas of a given size, with an optional offset.
92 | * - Parameters:
93 | * - objects: The array of views to be aligned.
94 | * - canvasSize: The size of the canvas on which the views will be aligned.
95 | * - canvasAlign: The alignment of the views on the canvas.
96 | * - objectAlign: The alignment of the views within their own canvas.
97 | * - offset: An optional offset to be applied to the views' positions.
98 | * - Remark: The `objects`' frames will be updated to reflect the new positions.
99 | * - Example: `Align.align([someView1, someView2], canvasSize: CGSize(width: 400, height: 300), canvasAlign: .center, objectAlign: .topLeft)` centers `someView1` and `someView2` within a 400x300 rectangle.
100 | * - Example: `Align.align([someView1, someView2], canvasSize: CGSize(width: 400, height: 300), canvasAlign: .centerRight, objectAlign: .centerRight)` aligns `someView1` and `someView2` to the y-axis center and to the right border of the rectangle, but within the rectangle.
101 | */
102 | public static func align(_ objects: [View], canvasSize: CGSize, canvasAlign: Alignment, objectAlign: Alignment, offset: CGPoint = .zero) {
103 | // Loop through each object in the array and align it to the specified canvas and object alignments
104 | objects.forEach { (object: View) in
105 | // Align the object to the specified canvas and object alignments with the given offset
106 | Align.align(
107 | object: object, // The object to align
108 | canvasSize: canvasSize, // The size of the canvas
109 | canvasAlignment: canvasAlign, // The alignment of the canvas
110 | objectAlignment: objectAlign, // The alignment of the object
111 | offset: offset // The offset to use
112 | )
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/Sources/SpatialLib/align/AlignType/AlignType+Extension.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | /**
3 | * Extension
4 | */
5 | extension AlignType {
6 | /**
7 | * Extension to `AlignType` enum to provide a computed property for the axis type.
8 | * Returns the axis type (horizontal or vertical) based on the alignment type.
9 | */
10 | public var axis: Axis {
11 | switch self {
12 | // If the alignment is top, bottom, or center vertical, return vertical alignment
13 | case .top, .bottom, .centerVer:
14 | return .ver
15 | // If the alignment is left, right, or center horizontal, return horizontal alignment
16 | case .left, .right, .centerHor:
17 | return .hor
18 | }
19 | }
20 | /**
21 | * Extension to `AlignType` enum to provide a computed property for the axis type.
22 | * Returns the axis type (start, middle, or end) based on the alignment type.
23 | */
24 | public var axisType: AxisType {
25 | switch self {
26 | // If the alignment is top or left, return start alignment
27 | case .top, .left:
28 | return .start
29 | // If the alignment is center horizontal or center vertical, return middle alignment
30 | case .centerHor, .centerVer:
31 | return .middle
32 | // If the alignment is bottom or right, return end alignment
33 | case .bottom, .right:
34 | return .end
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/SpatialLib/align/AlignType/AlignType.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | /**
3 | * Enum defining single alignment types.
4 | * Use `.rawValue` to get the string representation of the enum case.
5 | * Alternatively, you can use a no-type enum and print the case name directly.
6 | */
7 | public enum AlignType: String {
8 | case left // Aligns to the left
9 | case right // Aligns to the right
10 | case top // Aligns to the top
11 | case bottom // Aligns to the bottom
12 | case centerHor // Aligns to the horizontal center
13 | case centerVer // Aligns to the vertical center
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/SpatialLib/align/alignment/Alignment+Extension.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | /**
3 | * Extension to `Alignment` enum to provide computed properties for horizontal and vertical alignment types.
4 | */
5 | extension Alignment {
6 | /**
7 | * Returns the horizontal alignment type from the dual-axis alignment type `Alignment`.
8 | */
9 | public var horAlign: HorizontalAlign {
10 | switch self {
11 | // If the alignment is top left, center left, or bottom left, return left alignment
12 | case .topLeft, .centerLeft, .bottomLeft:
13 | return .left
14 | // If the alignment is top right, center right, or bottom right, return right alignment
15 | case .topRight, .bottomRight, .centerRight:
16 | return .right
17 | // If the alignment is top center, bottom center, or center center, return center X alignment
18 | case .topCenter, .bottomCenter, .centerCenter:
19 | return .centerX
20 | }
21 | }
22 | /**
23 | * Returns the vertical alignment type from the dual-axis alignment type `Alignment`.
24 | */
25 | public var verAlign: VerticalAlign {
26 | switch self {
27 | // If the alignment is top left, top center, or top right, return top alignment
28 | case .topLeft, .topCenter, .topRight:
29 | return .top
30 | // If the alignment is bottom left, bottom center, or bottom right, return bottom alignment
31 | case .bottomLeft, .bottomCenter, .bottomRight:
32 | return .bottom
33 | // If the alignment is center left, center right, or center center, return center Y alignment
34 | case .centerRight, .centerLeft, .centerCenter:
35 | return .centerY
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/SpatialLib/align/alignment/Alignment.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | /**
3 | * Enum defining alignment types for both horizontal and vertical axises.
4 | * Use `Alignment.topLeft.rawValue` to get the string "topLeft".
5 | */
6 | public enum Alignment: String {
7 | case topLeft // Aligns to the top left corner
8 | case topCenter // Aligns to the top center
9 | case topRight // Aligns to the top right corner
10 | case bottomLeft // Aligns to the bottom left corner
11 | case bottomCenter // Aligns to the bottom center
12 | case bottomRight // Aligns to the bottom right corner
13 | case centerLeft // Aligns to the center left
14 | case centerRight // Aligns to the center right
15 | case centerCenter // Aligns to the center of the container
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/SpatialLib/align/axis/Axis.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | /**
3 | * An enumeration representing the axis alignment of a view or layout constraint.
4 | * - Remark: Use `.rawValue` to get the string representation of the axis, e.g. "hor" or "ver".
5 | */
6 | public enum Axis: String {
7 | case hor // Horizontal axis
8 | case ver // Vertical axis
9 | }
10 |
--------------------------------------------------------------------------------
/Sources/SpatialLib/align/axis/AxisType.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | /**
3 | * An enumeration representing the type of axis alignment for a view or layout constraint.
4 | * - Remark: Use `.rawValue` to get the string representation of the axis type, e.g. "start", "middle", or "end".
5 | */
6 | public enum AxisType: String {
7 | case start // Represents the left or top edge of a view or layout constraint
8 | case middle // Represents the horizontal or vertical center of a view or layout constraint
9 | case end // Represents the right or bottom edge of a view or layout constraint
10 | }
11 |
--------------------------------------------------------------------------------
/Sources/SpatialLib/align/axis/HorizontalAlign.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | /**
3 | * An enumeration representing the horizontal alignment of a view or layout constraint.
4 | * - Remark: Use `.rawValue` to get the string representation of the horizontal alignment, e.g. "left", "right", or "centerX".
5 | */
6 | public enum HorizontalAlign: String {
7 | case left // Aligns the view or layout constraint to the left edge
8 | case right // Aligns the view or layout constraint to the right edge
9 | case centerX // Aligns the view or layout constraint to the horizontal center
10 | }
11 |
--------------------------------------------------------------------------------
/Sources/SpatialLib/align/axis/VerticalAlign.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | /**
3 | * An enumeration representing the vertical alignment of a view or layout constraint.
4 | * - Remark: Use `.rawValue` to get the string representation of the vertical alignment, e.g. "top", "bottom", or "centerY".
5 | */
6 | public enum VerticalAlign: String {
7 | case top // Aligns the view or layout constraint to the top edge
8 | case bottom // Aligns the view or layout constraint to the bottom edge
9 | case centerY // Aligns the view or layout constraint to the vertical center
10 | }
11 |
--------------------------------------------------------------------------------
/Sources/SpatialLib/common/Hybrid.swift:
--------------------------------------------------------------------------------
1 | #if os(iOS)
2 | import UIKit // Import the UIKit framework for iOS
3 | public typealias View = UIView // Define a type alias for UIView as View
4 | public typealias EdgeInsets = UIEdgeInsets // Define a type alias for UIEdgeInsets as EdgeInsets
5 | #elseif os(macOS)
6 | import Cocoa // Import the Cocoa framework for macOS
7 | public typealias View = NSView // Define a type alias for NSView as View
8 | public typealias EdgeInsets = NSEdgeInsets // Define a type alias for NSEdgeInsets as EdgeInsets
9 | #endif
10 | /**
11 | * - Description: This class makes Spatial work better for iOS and macOS
12 | */
13 | #if os(macOS)
14 | extension CGRect {
15 | /**
16 | * Returns a new rectangle that is inset from this rectangle by the specified edge insets.
17 | * - Parameters:
18 | * - inset: The edge insets to apply to the rectangle.
19 | * - Returns: A new rectangle that is inset from this rectangle by the specified edge insets.
20 | * - Remark: The `insetBy` method is uniform, meaning that it applies the same inset to all four edges of the rectangle. On iOS, this method is called `inset(by:)` and works exactly the same as the method below.
21 | * - Example: `NSRect.init(x: 0, y: 0, width: 100, height: 100).insetBy(dx: 10, dy: 10)` returns a new `CGRect` with an origin of (10.0, 10.0) and a size of (80.0, 80.0), which is the original rectangle inset by 10 points on all four edges.
22 | */
23 | public func inset(by inset: EdgeInsets) -> CGRect {
24 | // Calculate the new x-coordinate of the rectangle by adding the left inset to the original x-coordinate
25 | let x: CGFloat = self.origin.x + inset.left
26 | // Calculate the new y-coordinate of the rectangle by adding the top inset to the original y-coordinate
27 | let y: CGFloat = self.origin.y + inset.top
28 | // Calculate the new width of the rectangle by subtracting the left and right insets from the original width
29 | let width: CGFloat = self.size.width - inset.left - inset.right
30 | // Calculate the new height of the rectangle by subtracting the top and bottom insets from the original height
31 | let height: CGFloat = self.size.height - inset.top - inset.bottom
32 | // Return a new rectangle with the calculated x, y, width, and height values
33 | return .init(x: x, y: y, width: width, height: height)
34 | }
35 | }
36 | #endif
37 |
--------------------------------------------------------------------------------
/Sources/SpatialLib/common/View+Extension.swift:
--------------------------------------------------------------------------------
1 | /**
2 | * Defines type aliases and a default animation completion closure for views.
3 | * - Remark: The `AnimComplete` type alias represents a closure that is called when an animation completes. The `AnimUpdate` type alias represents a closure that is called during an animation update. The `defaultOnComplete` method is a default closure that prints a message to the console when an animation completes.
4 | * - Example: `someView.defaultOnComplete()` sets the default animation completion closure for `someView`.
5 | */
6 | extension View {
7 | // Define a type alias for an animation completion closure
8 | public typealias AnimComplete = () -> Void
9 | // Define a type alias for an animation update closure
10 | public typealias AnimUpdate = () -> Void
11 | // Define a default animation completion closure
12 | public static func defaultOnComplete() { Swift.print("default anim completed closure") }
13 | }
14 | #if os(iOS)
15 | import UIKit
16 | /**
17 | * Animation (Static & convenient)
18 | */
19 | extension View {
20 | /**
21 | * Provides a static method for animating a view's properties.
22 | * - Parameters:
23 | * - onUpdate: A closure that updates the view's properties on every frame of the animation.
24 | * - onComplete: A closure that is called when the animation completes.
25 | * - dur: The duration of the animation in seconds.
26 | * - easing: The easing curve to use for the animation.
27 | * - Remark: The `animate` method creates a `UIViewPropertyAnimator` object with the specified duration and easing curve, and uses it to animate the view's properties by calling the `onUpdate` closure on every frame of the animation. When the animation completes, the `onComplete` closure is called. If no `onComplete` closure is provided, the default completion closure that prints a message to the console is used.
28 | * ## Examples:
29 | * UIView.animate({ self.update(offset: to, align: align, alignTo: alignTo) }, onComplete: { Swift.print("🎉") })
30 | */
31 | public static func animate(_ onUpdate:@escaping AnimUpdate, onComplete:@escaping AnimComplete = View.defaultOnComplete, dur: Double = 0.3, easing: AnimationCurve = .easeOut) {
32 | // Create a new UIViewPropertyAnimator object with the specified duration and easing curve, and a closure that updates the view's properties on every frame of the animation
33 | let anim: UIViewPropertyAnimator = .init(duration: dur, curve: easing) {
34 | onUpdate()
35 | }
36 | // Add a completion closure to the animator that calls the specified closure when the animation completes
37 | anim.addCompletion { _ in onComplete() }
38 | // Start the animation
39 | anim.startAnimation()
40 | }
41 | }
42 | #elseif os(macOS)
43 | import Cocoa
44 | extension View {
45 | /**
46 | * Provides a static method for animating a view's properties on macOS.
47 | * - Parameters:
48 | * - onUpdate: A closure that updates the view's properties on every frame of the animation.
49 | * - onComplete: A closure that is called when the animation completes.
50 | * - dur: The duration of the animation in seconds.
51 | * - Remark: The `animate` method creates an `NSAnimationContext` object with the specified duration, and uses it to animate the view's properties by calling the `onUpdate` closure on every frame of the animation. When the animation completes, the `onComplete` closure is called. If no `onComplete` closure is provided, the default completion closure that prints a message to the console is used. The `allowsImplicitAnimation` property of the `NSAnimationContext` object must be set to `true` for constraints to be able to animate. Alternatively, the `.animator()` method can be used to animate constraints directly.
52 | * - Example: `NSView.animate({ self.update(offset: to, align: align, alignTo: alignTo) }, onComplete: { Swift.print("🎉") })` animates a view's properties by calling the `update` method on every frame of the animation, and prints a celebration message to the console when the animation completes.
53 | */
54 | public static func animate(_ onUpdate:@escaping AnimUpdate, onComplete:@escaping AnimComplete = View.defaultOnComplete, dur: Double = 0.3) {
55 | NSAnimationContext.runAnimationGroup({ (context: NSAnimationContext) -> Void in
56 | context.duration = dur // Set the length of the animation time in seconds
57 | context.allowsImplicitAnimation = true // Must be activated for constraints to be able to animate, or else must use .animator().constant etc
58 | onUpdate() // Call the onUpdate closure on every frame of the animation
59 | }, completionHandler: { () -> Void in
60 | onComplete() // Call the onComplete closure when the animation completes
61 | })
62 | }
63 | }
64 | #endif
65 |
--------------------------------------------------------------------------------
/Sources/SpatialLib/view/View+Access+Bulk.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import QuartzCore
3 | /**
4 | * One-liner for sizing and anchoring multiple views (Bulk)
5 | */
6 | extension Array where Element: View {
7 | /**
8 | * Activates anchors and sizes for multiple `UIView` instances, aligning and sizing them according to the specified parameters.
9 | * - Important: ⚠️️ This method is a work in progress and may not work as expected in all cases.
10 | * - Important: ⚠️️ This method requires the parent view to be used as a size reference, and cannot use a different view as a reference.
11 | * - Remark: This method works with regular `NSViews`.
12 | * - Fixme: ⚠️️ The align part of the method is not currently used and needs to be added to the code.
13 | * * ## Examples:
14 | * let views: [NSView] = [NSColor.blue, .green, .red].map { color in with (.init()) { $0.wantsLayer = true; $0.layer?.backgroundColor = color.cgColor; self.documentView?.addSubview($0) } // This example is for MacOS
15 | * views.distributeAndSize(dir: .hor, height: 42)
16 | * - Parameters:
17 | * - dir: The direction in which to distribute the views (`hor` for horizontal, `ver` for vertical).
18 | * - width: The target width for the views.
19 | * - height: The target height for the views.
20 | * - align: The alignment of the views.
21 | * - alignTo: The alignment of the canvas.
22 | * - spacing: The spacing between the views.
23 | * - multiplier: The size multiplier for the views.
24 | * - offset: The alignment offset for the views.
25 | * - sizeOffset: The size offset for the views.
26 | */
27 | public func distributeAndSize(dir: Axis, width: CGFloat? = nil, height: CGFloat? = nil, align: Alignment = .topLeft, alignTo: Alignment = .topLeft, spacing: CGFloat = .zero, multiplier: CGSize = .init(width: 1, height: 1), offset: CGPoint = .zero, sizeOffset: CGSize = .zero) {
28 | self.activateAnchorsAndSizes { (views: [View]) in // Activates anchors and sizes for multiple views, distributing and sizing them according to the specified parameters.
29 | // Distribute the views horizontally or vertically based on the specified direction
30 | let anchors: [AnchorConstraint] = {
31 | // Fixme: ⚠️️ this part is a duplicate of the single version of this method, so maybe reuse it somehow?
32 | switch dir {
33 | case .hor:
34 | // Distribute the views horizontally with the specified alignment, spacing, and offset
35 | return Constraint.distribute(
36 | horizontally: views, // The views to distribute horizontally
37 | align: alignTo, // The alignment to align to
38 | spacing: spacing, // The spacing to use
39 | offset: offset // The offset to use
40 | )
41 | case .ver:
42 | // Distribute the views vertically with the specified alignment, spacing, and offset
43 | return Constraint.distribute(
44 | vertically: views, // The views to distribute vertically
45 | align: alignTo, // The alignment to align to
46 | spacing: spacing, // The spacing to use
47 | offset: offset // The offset to use
48 | )
49 | }
50 | }()
51 | // Size the views based on the specified parameters
52 | let sizes: [SizeConstraint] = views.map { (view: View) in
53 | // Fixme: ⚠️️ this part is a duplicate of the single version of this method, so reuse it somehow
54 | let size: SizeConstraint = {
55 | if let width: CGFloat = width, let height: CGFloat = height {/*This method exists when you have size, but don't want to set size based on another view*/
56 | // If both width and height are specified, size the view based on those values
57 | return Constraint.size(
58 | view, // The source view
59 | size: .init(width: width, height: height), // The size to use
60 | multiplier: multiplier // The multiplier to use
61 | )
62 | } else {
63 | guard let superView: View = view.superview else {
64 | // If the view doesn't have a superview, throw a fatal error
65 | fatalError("View must have superview")
66 | }
67 | // Size the view based on the specified parameters and the superview's size
68 | return Constraint.size(
69 | view, // The source view
70 | to: superView, // The target view to use for size
71 | width: width, // The width to use
72 | height: height, // The height to use
73 | offset: sizeOffset, // The offset to use
74 | multiplier: multiplier // The multiplier to use
75 | )
76 | }
77 | }()
78 | return size
79 | }
80 | return (anchors, sizes)
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/Sources/SpatialLib/view/View+Access.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import QuartzCore
3 | /**
4 | * Convenient extension methods for UIView (One-liners)
5 | * - Description: Convenience the state of being able to proceed with something without difficulty
6 | */
7 | extension View {
8 | /**
9 | * Align and size a `UIView` instance with AutoLayout using a single call.
10 | * - Parameters:
11 | * - to: The view to align to.
12 | * - sizeTo: The view to size to. If not provided, `to` is used for sizing.
13 | * - width: The fixed width to use. If not provided, the view's intrinsic content size is used.
14 | * - height: The fixed height to use. If not provided, the view's intrinsic content size is used.
15 | * - align: The alignment for the `to` view.
16 | * - alignTo: The alignment for the `sizeTo` view, if one was provided.
17 | * - multiplier: The multiplier to apply to the size or `sizeTo`.
18 | * - offset: The offset for the `to` parameter.
19 | * - sizeOffset: The offset for the `sizeTo` parameter. Use negative values for inset.
20 | * - useMargin: Whether to align to auto-layout margins or not.
21 | * - Returns: A tuple containing the anchor and size constraints.
22 | * - Note: If `width` and `height` are provided, `sizeOffset` does not affect constraints. Set the offset to the width and height directly.
23 | * ## Examples:
24 | * view.anchorAndSize(to: self, height: 100, align: .centerCenter, alignTo: .centerCenter) // multiplier
25 | */
26 | public func anchorAndSize(to: View, sizeTo: View? = nil, width: CGFloat? = nil, height: CGFloat? = nil, align: Alignment = .topLeft, alignTo: Alignment = .topLeft, multiplier: CGSize = .init(width: 1, height: 1), offset: CGPoint = .zero, sizeOffset: CGSize = .zero, useMargin: Bool = false) {
27 | self.activateAnchorAndSize { (_: View) in // Activate the anchor and size constraints
28 | let anchor: AnchorConstraint = Constraint.anchor(self, to: to, align: align, alignTo: alignTo, offset: offset, useMargin: useMargin) // Create an anchor constraint
29 | let size: SizeConstraint = { // Create a size constraint
30 | if let width: CGFloat = width, let height: CGFloat = height { // If width and height are provided, create a size constraint with those values
31 | if sizeOffset != .zero { Swift.print("⚠️️ sizeOffset does not affect constraints when width and height are predfined, set offset to the width and height directly") } // If sizeOffset is not zero, print a warning message
32 | return Constraint.size(
33 | self, // The source view
34 | size: .init(width: width, height: height), // The size to use
35 | multiplier: multiplier // The multiplier to use
36 | )
37 | } else { // If width and height are not provided, create a size constraint based on sizeTo or to view
38 | return Constraint.size(
39 | self, // The source view
40 | to: sizeTo ?? to, // The target view to use for size
41 | width: width, // The width to use
42 | height: height, // The height to use
43 | offset: sizeOffset, // The offset to use
44 | multiplier: multiplier // The multiplier to use
45 | )
46 | }
47 | }()
48 | return (anchor, size) // Return the anchor and size constraints
49 | }
50 | }
51 | /**
52 | * Align a `UIView` instance with AutoLayout using a single call.
53 | * - Description: Position views with `AutoLayout` with one call.
54 | * - Parameters:
55 | * - to: The view to align to.
56 | * - align: The alignment for the `to` view.
57 | * - alignTo: The alignment for the parent view.
58 | * - offset: The offset for the `to` parameter.
59 | * - useMargin: Whether to align to auto-layout margins or not.
60 | * - Note: If `to` is not provided, the view is anchored to its superview.
61 | * - Fixme: ⚠️️ Maybe make `to` optional, and use superview.
62 | * ## Examples:
63 | * view.anchor(to: self, align: .center, alignTo: .center)
64 | */
65 | public func anchor(to: View, align: Alignment = .topLeft, alignTo: Alignment = .topLeft, offset: CGPoint = .zero, useMargin: Bool = false) {
66 | self.activateAnchor { (_: View) in // Activate the anchor constraint
67 | Constraint.anchor(
68 | self, // The source view
69 | to: to, // The target view
70 | align: align, // The alignment to use
71 | alignTo: alignTo, // The alignment to align to
72 | offset: offset, // The offset to use
73 | useMargin: useMargin // Whether to use layout margins or not
74 | ) // Create and return an anchor constraint
75 | }
76 | }
77 | /**
78 | * Horizontally align a `UIView` instance with AutoLayout using a single call.
79 | * - Description: Position views horizontally with `AutoLayout` with one call.
80 | * - Parameters:
81 | * - horTo: The view to align to.
82 | * - align: The horizontal alignment for the `to` view.
83 | * - alignTo: The horizontal alignment for the parent view.
84 | * - offset: The offset for the `to` parameter.
85 | * - useMargin: Whether to align to auto-layout margins or not.
86 | * - Note: If `horTo` is not provided, the view is anchored to its superview.
87 | * - Example: `view.anchor(horTo: self, align: .left, alignTo: .left)` with offset and useMargin.
88 | */
89 | public func anchor(horTo: View, align: HorizontalAlign = .left, alignTo: HorizontalAlign = .left, offset: CGFloat = .zero, useMargin: Bool = false) {
90 | self.activateConstraints { (view: View) in // Activate the constraints
91 | [ // Create an array of constraints
92 | Constraint.anchor(
93 | view, // The source view
94 | to: horTo, // The target view to anchor to horizontally
95 | align: align, // The alignment to use
96 | alignTo: alignTo, // The alignment to align to
97 | offset: offset, // The offset to use
98 | useMargin: useMargin // Whether to use layout margins or not
99 | ) // Create and return an anchor constraint
100 | ]
101 | }
102 | }
103 | /**
104 | * Vertically align a `UIView` instance with AutoLayout using a single call.
105 | * - Description: Position views vertically with `AutoLayout` with one call.
106 | * - Parameters:
107 | * - verTo: The view to align to.
108 | * - align: The vertical alignment for the `to` view.
109 | * - alignTo: The vertical alignment for the parent view.
110 | * - offset: The offset for the `to` parameter.
111 | * - useMargin: Whether to align to auto-layout margins or not.
112 | * - Note: If `verTo` is not provided, the view is anchored to its superview.
113 | * - Example: `view.anchor(verTo: self, align: .top, alignTo: .top)` with offset and useMargin.
114 | */
115 | public func anchor(verTo: View, align: VerticalAlign = .top, alignTo: VerticalAlign = .top, offset: CGFloat = .zero, useMargin: Bool = false) {
116 | self.activateConstraints { (view: View) in // Activate the constraints
117 | [ // Create an array of constraints
118 | Constraint.anchor(
119 | view, // The source view
120 | to: verTo, // The target view to anchor to vertically
121 | align: align, // The alignment to use
122 | alignTo: alignTo, // The alignment to align to
123 | offset: offset, // The offset to use
124 | useMargin: useMargin // Whether to use layout margins or not
125 | ) // Create and return an anchor constraint
126 | ]
127 | }
128 | }
129 | /**
130 | * Size a `UIView` instance with AutoLayout using a single call.
131 | * - Description: Size views with `AutoLayout` with one call.
132 | * - Parameters:
133 | * - to: The view to size to.
134 | * - sizeTo: The view to base the size on, if provided.
135 | * - width: The fixed width to use. If not provided, the view's intrinsic content size is used.
136 | * - height: The fixed height to use. If not provided, the view's intrinsic content size is used.
137 | * - multiplier: The multiplier to apply to the size or `sizeTo`.
138 | * - sizeOffset: The offset for the `sizeTo` parameter. Use negative values for inset.
139 | * - Note: If `width` and `height` are provided, `sizeOffset` does not affect constraints. Set the offset to the width and height directly.
140 | * - Example: `view.size(to: self, width: 100, height: 100)` with multiplier.
141 | */
142 | public func size(to: View, width: CGFloat? = nil, height: CGFloat? = nil, offset: CGSize = .zero, multiplier: CGSize = .init(width: 1, height: 1)) {
143 | self.activateSize { (_: View) in // Activate the size constraint
144 | Constraint.size(
145 | self, // The source view
146 | to: to, // The target view to use for size
147 | width: width, // The width to use
148 | height: height, // The height to use
149 | offset: offset, // The offset to use
150 | multiplier: multiplier // The multiplier to use
151 | ) // Create and return a size constraint
152 | }
153 | }
154 | /**
155 | * Size a UIView instance to a speccific metric size
156 | * - Fixme: ⚠️️ This doesn't have offset, maybe it should 🤔 for now I guess you can always inset the size
157 | * ## Examples:
158 | * view.size(width: 100, height: 100)
159 | * - Parameters:
160 | * - width: Provide this if you want to use a fixed width
161 | * - height: Provide this if you want to use a fixed height
162 | * - multiplier: Multiplies the `size` or `sizeTo`
163 | */
164 | public func size(width: CGFloat, height: CGFloat, multiplier: CGSize = .init(width: 1, height: 1)) {
165 | self.activateSize { (_: View) in // Activate the size constraint
166 | Constraint.size(
167 | self, // The source view
168 | size: .init(width: width, height: height), // The size to use
169 | multiplier: multiplier // The multiplier to use
170 | ) // Create and return a size constraint with the provided width, height, and multiplier
171 | }
172 | }
173 | /**
174 | * Size a `UIView` instance to a specific metric size.
175 | * - Description: Size views with `AutoLayout` with one call.
176 | * - Parameters:
177 | * - width: The fixed width to use.
178 | * - height: The fixed height to use.
179 | * - multiplier: The multiplier to apply to the size.
180 | * - Note: This method does not support offset. You can inset the size to achieve the desired offset.
181 | * - Example: `view.size(width: 100, height: 100)` with multiplier.
182 | */
183 | public func size(to: View, axis: Axis, toAxis: Axis, offset: CGFloat = 0, multiplier: CGFloat = 1) {
184 | self.activateConstraint { (view: View) in
185 | Constraint.length(
186 | view, // The source view
187 | to: to, // The target view to use for length
188 | viewAxis: axis, // The axis to use for the source view
189 | toAxis: toAxis, // The axis to use for the target view
190 | offset: offset, // The offset to use
191 | multiplier: multiplier // The multiplier to use
192 | )
193 | }
194 | }
195 | /**
196 | * Set the width of a `UIView` instance to a specific metric size.
197 | * - Description: Size views with `AutoLayout` with one call.
198 | * - Parameters:
199 | * - width: The fixed width to use.
200 | * - multiplier: The multiplier to apply to the width.
201 | * - Note: This method does not support offset. You can inset the size to achieve the desired offset.
202 | * - Example: `view.size(width: 100, multiplier: 0.5)`.
203 | */
204 | public func size(width: CGFloat, multiplier: CGFloat = 1) {
205 | self.activateConstraint { (view: View) in // Activate the constraint
206 | Constraint.width(
207 | view, // The source view
208 | width: width, // The width to use
209 | multiplier: multiplier // The multiplier to use
210 | ) // Create and return a width constraint with the provided width and multiplier
211 | }
212 | }
213 | /**
214 | * Set the height of a `UIView` instance to a specific metric size.
215 | * - Description: Size views with `AutoLayout` with one call.
216 | * - Parameters:
217 | * - height: The fixed height to use.
218 | * - multiplier: The multiplier to apply to the height.
219 | * - Note: This method does not support offset. You can inset the size to achieve the desired offset.
220 | * - Example: `view.size(height: 100, multiplier: 0.5)`.
221 | */
222 | public func size(height: CGFloat, multiplier: CGFloat = 1) {
223 | self.activateConstraint { (view: View) in // Activate the constraint
224 | Constraint.height(
225 | view, // The source view
226 | height: height, // The height to use
227 | multiplier: multiplier // The multiplier to use
228 | ) // Create and return a height constraint with the provided height and multiplier
229 | }
230 | }
231 | }
232 | /**
233 | * One-liner for anchoring multiple views (Bulk)
234 | */
235 | extension Array where Element: View {
236 | /**
237 | * Anchors an array of views and distributes them horizontally or vertically with the specified alignment, spacing, and offset.
238 | * ## Examples:
239 | * views.distribute(dir: .horizontal)
240 | * - Parameters:
241 | * - dir: The direction in which to distribute the views. Can be either `.hor` for horizontal or `.ver` for vertical.
242 | * - align: The alignment of the views within the canvas.
243 | * - spacing: The amount of space to leave between each view.
244 | * - offset: The offset from the top-left corner of the canvas.
245 | */
246 | public func distribute(dir: Axis, align: Alignment = .topLeft, spacing: CGFloat = .zero, offset: CGPoint = .zero) {
247 | // Activate the anchor constraints for the current view
248 | self.activateAnchors { (views: [View]) in
249 | switch dir {
250 | // If the direction is horizontal, distribute the views horizontally
251 | case .hor:
252 | return Constraint.distribute(
253 | horizontally: views, // The views to distribute horizontally
254 | align: align, // The alignment to use
255 | spacing: spacing, // The spacing to use
256 | offset: offset // The offset to use
257 | )
258 | // If the direction is vertical, distribute the views vertically
259 | case .ver:
260 | return Constraint.distribute(
261 | vertically: views, // The views to distribute vertically
262 | align: align, // The alignment to use
263 | spacing: spacing, // The spacing to use
264 | offset: offset // The offset to use
265 | )
266 | }
267 | }
268 | }
269 | }
270 | /**
271 | * One-liner for sizing multiple views (Bulk)
272 | */
273 | extension Array where Element: View {
274 | /**
275 | * Sizes an array of views to a target width and/or height, with an optional offset and multiplier.
276 | * ## Examples:
277 | * [btn1, btn2, btn3].size(to: self, height: 24, offset: .init(width: -40, height: 0))
278 | * - Parameters:
279 | * - to: The view to size the array of views to.
280 | * - width: The target width of the views.
281 | * - height: The target height of the views.
282 | * - offset: The offset to apply to the size of the views.
283 | * - multiplier: The multiplier to apply to the size of the views.
284 | */
285 | public func size(to: View, width: CGFloat? = nil, height: CGFloat? = nil, offset: CGSize = .zero, multiplier: CGSize = .init(width: 1, height: 1)) {
286 | // Activate the size constraints for the current view
287 | self.activateSizes { (views: [View]) in
288 | // For each view in the array, set its size to the target width and/or height
289 | views.map {
290 | // Set the size of the current view to the target width and/or height
291 | Constraint.size(
292 | $0, // The current view in the array
293 | to: to, // The view to size the array of views to
294 | width: width, // The target width of the views
295 | height: height, // The target height of the views
296 | offset: offset, // The offset to apply to the size of the views
297 | multiplier: multiplier // The multiplier to apply to the size of the views
298 | )
299 | }
300 | }
301 | }
302 | /**
303 | * Sizes an array of `UIView` instances to a specific metric size.
304 | * ## Examples:
305 | * [btn1, btn2, btn3].size(width: 96, height: 24)
306 | * - Parameters:
307 | * - width: The target width of the views.
308 | * - height: The target height of the views.
309 | * - multiplier: The multiplier to apply to the size of the views.
310 | */
311 | public func size(width: CGFloat, height: CGFloat, multiplier: CGSize = .init(width: 1, height: 1)) {
312 | // Activate the size constraints for the current view
313 | self.activateSizes { (views: [View]) in
314 | // For each view in the array, set its size to the target width and height
315 | views.map {
316 | // Set the size of the current view to the target width and height, with the specified multiplier
317 | Constraint.size(
318 | $0, // The current view in the array
319 | size: .init(width: width, height: height), // The target size of the view
320 | multiplier: multiplier // The multiplier to apply to the size of the view
321 | )
322 | }
323 | }
324 | }
325 | }
326 |
--------------------------------------------------------------------------------
/Sources/SpatialLib/view/View+Activate+Bulk.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | #if os(iOS)
3 | import UIKit
4 | #elseif os(macOS)
5 | import Cocoa
6 | #endif
7 | /**
8 | * Array
9 | */
10 | extension Array where Element: View {
11 | /**
12 | * Activates multiple layout constraints for an array of views using the specified closure that returns a tuple of anchor constraints and size constraints.
13 | * - Important: ⚠️️ This method should only be used for activating multiple constraints at once. For activating a single constraint, use the `activateConstraint` method instead.
14 | * - Parameter closure: A closure that takes an array of views as input and returns a tuple of anchor constraints and size constraints.
15 | * - Remark: This method sets the `translatesAutoresizingMaskIntoConstraints` property of each view to `false` before activating the constraints.
16 | * ## Examples:
17 | * [label1, label2, label3].activateAnchorsAndSizes { views in
18 | * let anchors = Constraint.distribute(vertically: views, align: .topLeft)
19 | * let sizes = views.map { Constraint.size(width: 96, height: 42) }
20 | * return (anchors, sizes)
21 | * }
22 | */
23 | public func activateAnchorsAndSizes(closure: ConstraintsClosure) {
24 | // Set the `translatesAutoresizingMaskIntoConstraints` property of each view to `false` to activate the constraints programmatically.
25 | self.forEach { $0.translatesAutoresizingMaskIntoConstraints = false }
26 | // Call the closure to get a tuple of anchor constraints and size constraints.
27 | let constraints: AnchorConstraintsAndSizeConstraints = closure(self)
28 | // Extract the anchor and size constraints from the tuple and activate them using the `NSLayoutConstraint.activate` method.
29 | let anchors: [NSLayoutConstraint] = constraints.anchorConstraints.reduce([]) {
30 | $0 + [$1.x, $1.y]
31 | }
32 | // Extract the width and height constraints from the size constraints and add them to the `sizes` array.
33 | let sizes: [NSLayoutConstraint] = constraints.sizeConstraints.reduce([]) {
34 | $0 + [$1.w, $1.h]
35 | }
36 | // Combine the anchor constraints and size constraints into a single array.
37 | let allConstraints: [NSLayoutConstraint] = anchors + sizes
38 | // Activate all the constraints using the `NSLayoutConstraint.activate` method.
39 | NSLayoutConstraint.activate(allConstraints)
40 | }
41 | /**
42 | * Activates multiple anchor constraints for an array of views using the specified closure that returns an array of anchor constraints.
43 | * - Important: ⚠️️ This method should only be used for activating anchor constraints. For activating size constraints, use the `activateSizes` method instead.
44 | * - Parameter closure: A closure that takes an array of views as input and returns an array of anchor constraints.
45 | * - Remark: This method sets the `translatesAutoresizingMaskIntoConstraints` property of each view to `false` before activating the constraints.
46 | * ## Examples:
47 | * [label1, label2, label3].activateAnchors {
48 | * return Constraint.distribute(vertically: views, align: .topCenter)
49 | * }
50 | */
51 | public func activateAnchors(closure: AnchorConstraintsClosure) {
52 | // Set the `translatesAutoresizingMaskIntoConstraints` property of each view to `false` to activate the constraints programmatically.
53 | self.forEach { $0.translatesAutoresizingMaskIntoConstraints = false }
54 | // Call the closure to get an array of anchor constraints.
55 | let anchorConstraints: [AnchorConstraint] = closure(self)
56 | // Extract the x and y anchor constraints from the anchor constraints and activate them using the `NSLayoutConstraint.activate` method.
57 | let constraints: [NSLayoutConstraint] = anchorConstraints.reduce([]) {
58 | $0 + [$1.x, $1.y]
59 | }
60 | NSLayoutConstraint.activate(constraints)
61 | }
62 | /**
63 | * Activates multiple size constraints for an array of views using the specified closure that returns an array of size constraints.
64 | * - Important: ⚠️️ This method should only be used for activating size constraints. For activating anchor constraints, use the `activateAnchors` method instead.
65 | * - Parameter closure: A closure that takes an array of views as input and returns an array of size constraints.
66 | * - Remark: This method sets the `translatesAutoresizingMaskIntoConstraints` property of each view to `false` before activating the constraints.
67 | * ## Examples:
68 | * [label1, label2, label3].activateSizes { views in
69 | * return views.map { Constraint.size(width: 96, height: 42) }
70 | * }
71 | */
72 | public func activateSizes(closure: SizeConstraintsClosure) {
73 | // Set the `translatesAutoresizingMaskIntoConstraints` property of each view to `false` to activate the constraints programmatically.
74 | self.forEach { $0.translatesAutoresizingMaskIntoConstraints = false }
75 | // Call the closure to get an array of size constraints.
76 | let sizeConstraints: [SizeConstraint] = closure(self)
77 | // Extract the width and height constraints from the size constraints and activate them using the `NSLayoutConstraint.activate` method.
78 | let constraints: [NSLayoutConstraint] = sizeConstraints.reduce([]) {
79 | $0 + [$1.w, $1.h]
80 | }
81 | NSLayoutConstraint.activate(constraints)
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/Sources/SpatialLib/view/View+Activate.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | #if os(iOS)
3 | import UIKit
4 | #elseif os(macOS)
5 | import Cocoa
6 | #endif
7 | /**
8 | * `AutoLayout` Sugar for `UIView`
9 | * - Remark: Method overloading doesn't work with closures so each method name needs to be unique
10 | */
11 | extension View {
12 | /**
13 | * Activates multiple layout constraints for the view using the specified closure.
14 | * - Important: ⚠️️ This method should only be used for activating multiple constraints at once. For activating a single constraint, use the `activate` method instead.
15 | * - Parameter closure: A closure that takes the view as input and returns an array of layout constraints.
16 | * - Remark: This method sets the `translatesAutoresizingMaskIntoConstraints` property of the view to `false` before activating the constraints.
17 | * ## Examples:
18 | * button.activateConstraints { view in
19 | * let anchor = Constraint.anchor(view, to: self, align: .topLeft, alignTo: .topLeft)
20 | * let size = Constraint.size(view, size: CGSize.init(width: UIScreen.main.bounds.width, height: TopBar.topBarHeight))
21 | * return [anchor.x, anchor.y, size.w, size.h]
22 | * }
23 | */
24 | public func activateConstraints(closure: ConstraintsClosure) {
25 | // Set the `translatesAutoresizingMaskIntoConstraints` property of the view to `false` to activate the constraints programmatically.
26 | self.translatesAutoresizingMaskIntoConstraints = false
27 | // Call the closure to get an array of layout constraints.
28 | let constraints: [NSLayoutConstraint] = closure(self)
29 | // Activate the constraints using the `NSLayoutConstraint.activate` method.
30 | NSLayoutConstraint.activate(constraints)
31 | }
32 | /**
33 | * Activates a single layout constraint for the view using the specified closure.
34 | * - Important: ⚠️️ This method should only be used for activating a single constraint at once. For activating multiple constraints, use the `activateConstraints` method instead.
35 | * - Parameter closure: A closure that takes the view as input and returns a layout constraint.
36 | * - Remark: This method sets the `translatesAutoresizingMaskIntoConstraints` property of the view to `false` before activating the constraint.
37 | * ## Examples:
38 | * button.activateConstraint { view in
39 | * let anchor = Constraint.anchor(view, to: self, align: .topLeft, alignTo: .topLeft)
40 | * return anchor.x
41 | * }
42 | */
43 | public func activateConstraint(closure: ConstraintClosure) {
44 | // Set the `translatesAutoresizingMaskIntoConstraints` property of the view to `false` to activate the constraint programmatically.
45 | self.translatesAutoresizingMaskIntoConstraints = false
46 | // Call the closure to get a layout constraint.
47 | let constraint: NSLayoutConstraint = closure(self)
48 | // Activate the constraint using the `NSLayoutConstraint.activate` method.
49 | NSLayoutConstraint.activate([constraint])
50 | }
51 | /**
52 | * Activates multiple layout constraints for the view using the specified closure that returns a tuple of anchor and size constraints.
53 | * - Important: ⚠️️ This method should only be used for activating multiple constraints at once. For activating a single constraint, use the `activateConstraint` method instead.
54 | * - Parameter closure: A closure that takes the view as input and returns a tuple of anchor and size constraints.
55 | * - Remark: This method sets the `translatesAutoresizingMaskIntoConstraints` property of the view to `false` before activating the constraints.
56 | * ## Examples:
57 | * label.activateAnchorAndSize { view in
58 | * let a: AnchorConstraint = Constraint.anchor(view, to: self, align: .topLeft, alignTo: .topLeft)
59 | * let s: SizeConstraint = Constraint.size(view, to: self)
60 | * return (a, s)
61 | * }
62 | */
63 | public func activateAnchorAndSize(closure: AnchorAndSizeClosure) {
64 | // Set the `translatesAutoresizingMaskIntoConstraints` property of the view to `false` to activate the constraints programmatically.
65 | self.translatesAutoresizingMaskIntoConstraints = false
66 | // Call the closure to get a tuple of anchor and size constraints.
67 | let anchorAndSize: AnchorAndSize = closure(self)
68 | // Extract the anchor and size constraints from the tuple and activate them using the `NSLayoutConstraint.activate` method.
69 | let constraints: [NSLayoutConstraint] = [
70 | anchorAndSize.anchor.x, // The x anchor constraint
71 | anchorAndSize.anchor.y, // The y anchor constraint
72 | anchorAndSize.size.w, // The width size constraint
73 | anchorAndSize.size.h // The height size constraint
74 | ]
75 | // Activate the constraints using the `NSLayoutConstraint.activate` method.
76 | NSLayoutConstraint.activate(constraints)
77 | }
78 | /**
79 | * Activates anchor constraints for the view using the specified closure that returns an anchor constraint.
80 | * - Important: ⚠️️ This method should only be used for activating anchor constraints. For activating size constraints, use the `activateSize` method instead.
81 | * - Parameter closure: A closure that takes the view as input and returns an anchor constraint.
82 | * - Remark: This method sets the `translatesAutoresizingMaskIntoConstraints` property of the view to `false` before activating the constraints.
83 | * ## Examples:
84 | * label.activateAnchor { view in
85 | * return Constraint.anchor(view, to: self, align: .topLeft, alignTo: .topLeft)
86 | * }
87 | */
88 | public func activateAnchor(closure: AnchorClosure) {
89 | // Set the `translatesAutoresizingMaskIntoConstraints` property of the view to `false` to activate the constraints programmatically.
90 | self.translatesAutoresizingMaskIntoConstraints = false
91 | // Call the closure to get an anchor constraint.
92 | let anchorConstraint: AnchorConstraint = closure(self)
93 | // Extract the x and y anchor constraints from the anchor constraint and activate them using the `NSLayoutConstraint.activate` method.
94 | let constraints: [NSLayoutConstraint] = [anchorConstraint.x, anchorConstraint.y]
95 | // Activate the constraints using the `NSLayoutConstraint.activate` method.
96 | NSLayoutConstraint.activate(constraints)
97 | }
98 | /**
99 | * Activates size constraints for the view using the specified closure that returns a size constraint.
100 | * - Important: ⚠️️ This method should only be used for activating size constraints. For activating anchor constraints, use the `activateAnchor` method instead.
101 | * - Parameter closure: A closure that takes the view as input and returns a size constraint.
102 | * - Remark: This method sets the `translatesAutoresizingMaskIntoConstraints` property of the view to `false` before activating the constraints.
103 | * ## Examples:
104 | * label.activateSize { view in
105 | * return Constraint.size(view, to: self)
106 | * }
107 | */
108 | public func activateSize(closure: SizeClosure) {
109 | // Set the `translatesAutoresizingMaskIntoConstraints` property of the view to `false` to activate the constraints programmatically.
110 | self.translatesAutoresizingMaskIntoConstraints = false
111 | // Call the closure to get a size constraint.
112 | let sizeConstraint: SizeConstraint = closure(self)
113 | // Extract the width and height constraints from the size constraint and activate them using the `NSLayoutConstraint.activate` method.
114 | let constraints: [NSLayoutConstraint] = [sizeConstraint.w, sizeConstraint.h]
115 | // Activate the constraints using the `NSLayoutConstraint.activate` method.
116 | NSLayoutConstraint.activate(constraints)
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/Sources/SpatialLib/view/View+Anchor.swift:
--------------------------------------------------------------------------------
1 | import Foundation // needed?
2 | #if os(iOS)
3 | import UIKit
4 | #elseif os(macOS)
5 | import Cocoa
6 | #endif
7 | /**
8 | * Positional constraints (Aligning relative to another view (x,y))
9 | * - Remark: Anchor is a more appropriate name than, pin, pos, pt, edge, put, magnet, align, corner (anchor can represent both corner, edge and center)
10 | * - Fixme: ⚠️️⚠️️ Move These into an extension of `NSLayoutConstraint` so that you can do dot notation etc
11 | * - Fixme: ⚠️️ Not really an anchor, consider renaming to `ConstraintAttribute` or pin, point, origin, position? or?
12 | */
13 | public final class Constraint {
14 | /**
15 | * Creates a positional constraint between two views.
16 | * - Important: ⚠️️ This method should only be used for creating positional constraints. For creating size constraints, use the `size` method instead.
17 | * - Parameters:
18 | * - view: The view to create the constraint for.
19 | * - to: The parent view to anchor the constraint to.
20 | * - align: The alignment of the view relative to the parent view.
21 | * - alignTo: The alignment of the parent view to anchor the constraint to.
22 | * - offset: The offset of the view from the parent view.
23 | * - useMargin: Whether to use the view's layout margins when creating the constraint.
24 | * - Returns: A tuple of horizontal and vertical anchor constraints.
25 | * ## Examples:
26 | * label.anchor(to: self, align: .topLeft, alignTo: .topLeft, offset: CGPoint(x: 16, y: 16))
27 | */
28 | public static func anchor(_ view: View, to: View, align: Alignment = .topLeft, alignTo: Alignment = .topLeft, offset: CGPoint = .zero, useMargin: Bool = false) -> AnchorConstraint {
29 | // Create a horizontal anchor constraint between the view and the parent view.
30 | let hor: NSLayoutConstraint = anchor(
31 | view, // The source view
32 | to: to, // The target view
33 | align: align.horAlign, // The horizontal alignment to use
34 | alignTo: alignTo.horAlign, // The horizontal alignment to align to
35 | offset: offset.x, // The offset to use for the x axis
36 | useMargin: useMargin // Whether to use layout margins or not
37 | )
38 | // Create a vertical anchor constraint between the view and the parent view.
39 | let ver: NSLayoutConstraint = anchor(
40 | view, // The source view
41 | to: to, // The target view
42 | align: align.verAlign, // The vertical alignment to use
43 | alignTo: alignTo.verAlign, // The vertical alignment to align to
44 | offset: offset.y, // The offset to use for the y axis
45 | useMargin: useMargin // Whether to use layout margins or not
46 | )
47 | // Return a tuple of the horizontal and vertical anchor constraints.
48 | return (hor, ver)
49 | }
50 | /**
51 | * Creates a horizontal anchor constraint between the view and the parent view.
52 | * - Parameters:
53 | * - view: The view to create the constraint for.
54 | * - to: The parent view to anchor the constraint to.
55 | * - align: The horizontal alignment of the view relative to the parent view.
56 | * - alignTo: The horizontal alignment of the parent view to anchor the constraint to.
57 | * - offset: The offset of the view from the parent view.
58 | * - useMargin: Whether to use the view's layout margins when creating the constraint.
59 | * - relation: The relation of the constraint: equal, lessThanOrEqual, greaterThanOrEqual.
60 | * - Returns: A horizontal anchor constraint.
61 | * ## Examples:
62 | * label.anchor(to: self, align: .left, alignTo: .left, offset: 16, useMargin: true, relation: .lessThanOrEqual)
63 | */
64 | public static func anchor(_ view: View, to: View, align: HorizontalAlign, alignTo: HorizontalAlign, offset: CGFloat = .zero, useMargin: Bool = false, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint {
65 | // Create a horizontal anchor constraint between the view and the parent view.
66 | let constraint: NSLayoutConstraint = .init(
67 | item: view, // The view to create the constraint for.
68 | attribute: layoutAttr(align: align), // The horizontal attribute of the view to create the constraint for.
69 | relatedBy: relation, // The relation of the constraint: equal, lessThanOrEqual, greaterThanOrEqual.
70 | toItem: to, // The parent view to anchor the constraint to.
71 | attribute: layoutAttr(align: alignTo, useMargin: useMargin), // The horizontal attribute of the parent view to anchor the constraint to.
72 | multiplier: 1.0, // The multiplier of the constraint.
73 | constant: offset // The offset of the view from the parent view.
74 | )
75 | return constraint
76 | }
77 | /**
78 | * Creates a vertical anchor constraint between the view and the parent view.
79 | * - Parameters:
80 | * - view: The view to create the constraint for.
81 | * - to: The parent view to anchor the constraint to.
82 | * - align: The vertical alignment of the view relative to the parent view.
83 | * - alignTo: The vertical alignment of the parent view to anchor the constraint to.
84 | * - offset: The offset of the view from the parent view.
85 | * - useMargin: Whether to use the view's layout margins when creating the constraint.
86 | * - relation: The relation of the constraint: equal, lessThanOrEqual, greaterThanOrEqual.
87 | * - Returns: A vertical anchor constraint.
88 | * ## Examples:
89 | * label.anchor(to: self, align: .top, alignTo: .top, offset: 16, useMargin: true, relation: .lessThanOrEqual)
90 | */
91 | public static func anchor(_ view: View, to: View, align: VerticalAlign, alignTo: VerticalAlign, offset: CGFloat = .zero, useMargin: Bool = false, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint {
92 | // Get the layout attribute for the specified vertical alignment of the view.
93 | let attr: NSLayoutConstraint.Attribute = layoutAttr(align: align)
94 | // Get the layout attribute for the specified vertical alignment of the parent view.
95 | let relatedByAttr: NSLayoutConstraint.Attribute = layoutAttr(
96 | align: alignTo, // The alignment to use
97 | useMargin: useMargin // Whether to use layout margins or not
98 | )
99 | // Create a vertical anchor constraint between the view and the parent view.
100 | let constraint: NSLayoutConstraint = .init(
101 | item: view, // The view to create the constraint for.
102 | attribute: attr, // The vertical attribute of the view to create the constraint for.
103 | relatedBy: relation, // The relation of the constraint: equal, lessThanOrEqual, greaterThanOrEqual.
104 | toItem: to, // The parent view to anchor the constraint to.
105 | attribute: relatedByAttr, // The vertical attribute of the parent view to anchor the constraint to.
106 | multiplier: 1.0, // The multiplier of the constraint.
107 | constant: offset // The offset of the view from the parent view.
108 | )
109 | return constraint
110 | }
111 | }
112 | /**
113 | * Internal helper methods
114 | */
115 | extension Constraint {
116 | /**
117 | * Returns the layout attribute for the specified horizontal alignment of the view.
118 | *
119 | * - Parameters:
120 | * - align: The horizontal alignment of the view.
121 | * - useMargin: Whether to use the view's layout margins when creating the constraint.
122 | *
123 | * - Returns: The layout attribute for the specified horizontal alignment of the view.
124 | *
125 | * - Remark: Layout margin is only available for iOS and tvOS.
126 | *
127 | * ## Examples:
128 | * ```
129 | * layoutAttr(align: .left, useMargin: true)
130 | * ```
131 | */
132 | private static func layoutAttr(align: HorizontalAlign, useMargin: Bool = false) -> NSLayoutConstraint.Attribute {
133 | switch align {
134 | case .left:
135 | #if os(iOS)
136 | if useMargin { return .leftMargin } // Use the view's left margin when creating the constraint if `useMargin` is `true`.
137 | #endif
138 | return .left // Use the view's left edge when creating the constraint if `useMargin` is `false` or if the platform is not iOS.
139 | case .right:
140 | #if os(iOS)
141 | if useMargin { return .rightMargin } // Use the view's right margin when creating the constraint if `useMargin` is `true`.
142 | #endif
143 | return .right // Use the view's right edge when creating the constraint if `useMargin` is `false` or if the platform is not iOS.
144 | case .centerX:
145 | #if os(iOS)
146 | if useMargin { return .centerXWithinMargins } // Use the view's horizontal center within its margins when creating the constraint if `useMargin` is `true`.
147 | #endif
148 | return .centerX // Use the view's horizontal center when creating the constraint if `useMargin` is `false` or if the platform is not iOS.
149 | }
150 | }
151 | /**
152 | * Returns the layout attribute for the specified vertical alignment of the view.
153 | * - Parameters:
154 | * - align: The vertical alignment of the view.
155 | * - useMargin: Whether to use the view's layout margins when creating the constraint.
156 | * - Returns: The layout attribute for the specified vertical alignment of the view.
157 | * - Remark: Layout margin is only available for iOS and tvOS.
158 | * ## Examples:
159 | * layoutAttr(align: .top, useMargin: true)
160 | */
161 | private static func layoutAttr(align: VerticalAlign, useMargin: Bool = false) -> NSLayoutConstraint.Attribute {
162 | switch align {
163 | case .top:
164 | #if os(iOS)
165 | if useMargin { return .topMargin } // Use the view's top margin when creating the constraint if `useMargin` is `true`.
166 | #endif
167 | return .top // Use the view's top edge when creating the constraint if `useMargin` is `false` or if the platform is not iOS.
168 | case .bottom:
169 | #if os(iOS)
170 | if useMargin { return .bottomMargin } // Use the view's bottom margin when creating the constraint if `useMargin` is `true`.
171 | #endif
172 | return .bottom // Use the view's bottom edge when creating the constraint if `useMargin` is `false` or if the platform is not iOS.
173 | case .centerY:
174 | #if os(iOS)
175 | if useMargin { return .centerYWithinMargins } // Use the view's vertical center within its margins when creating the constraint if `useMargin` is `true`.
176 | #endif
177 | return .centerY // Use the view's vertical center when creating the constraint if `useMargin` is `false` or if the platform is not iOS.
178 | }
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/Sources/SpatialLib/view/View+Distribution.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | #if os(iOS)
3 | import UIKit
4 | #elseif os(macOS)
5 | import Cocoa
6 | #endif
7 | /**
8 | * Distribute items horizontally or vertically
9 | */
10 | extension Constraint {
11 | /**
12 | * Distributes the specified views horizontally with the specified alignment and spacing.
13 | *
14 | * - Parameters:
15 | * - views: The views to distribute in a row.
16 | * - align: The corner at which the first view should align.
17 | * - spacing: The spacing between the views.
18 | * - offset: The offset of the first view from the alignment corner.
19 | * - Returns: An array of horizontal anchor constraints.
20 | * - Remark: This method only sets anchors, not sizes.
21 | * - Remark: The parent view is always the superview of the views.
22 | * - 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.
23 | * - Fixme: ⚠️️ Make it throw?
24 | * - Fixme: ⚠️️ Add support for spacing.
25 | * - Fixme: ⚠️️ Add support for alignTo: (because you might want to set a different anchor for the views than for the view to align to).
26 | * ## Examples:
27 | * [label1, label2, label3].applyAnchorsAndSizes { views in
28 | * let anchors = Constraint.distribute(horizontally: views, align: .topLeft, spacing: 16, offset: CGPoint(x: 0, y: 48))
29 | * let sizes = views.map { Constraint.size($0, toView: self.frame.width, height: 48) }
30 | * return (anchors, sizes)
31 | * }
32 | */
33 | public static func distribute(horizontally views: [View], align: Alignment = .topLeft, spacing: CGFloat = .zero, offset: CGPoint = .zero) -> [AnchorConstraint] {
34 | // Distribute the views horizontally with the specified alignment, spacing, and offset.
35 | let xConstraints: [NSLayoutConstraint] = distribute(
36 | views, // The views to distribute horizontally
37 | axis: .hor, // The axis to use for distribution
38 | align: align, // The alignment to use
39 | spacing: spacing, // The spacing to use
40 | offset: offset.x // The offset to use for the x axis
41 | )
42 | // Anchor each view vertically to the parent view with the specified alignment and offset.
43 | let yConstraints: [NSLayoutConstraint] = views.map { view in
44 | guard let superView: NSView = view.superview else { fatalError("View must have superview") } // Get the superview of the view.
45 | // Anchor the view vertically to the superview with the specified alignment and offset.
46 | return Constraint.anchor(
47 | view, // The source view
48 | to: superView, // The target view to anchor to vertically
49 | align: align.verAlign, // The vertical alignment to use
50 | alignTo: align.verAlign, // The vertical alignment to align to
51 | offset: offset.y // The offset to use for the y axis
52 | ) // Return the vertical anchor constraint.
53 | }
54 | // Combine the horizontal and vertical anchor constraints into an array of anchor constraints.
55 | let anchors: [AnchorConstraint] = Array(zip(xConstraints, yConstraints))
56 | // Return the array of anchor constraints.
57 | return anchors
58 | }
59 | /**
60 | * Vertically distribute views within their superview
61 | * - Important: This method only sets anchors, not sizes
62 | * - Throws: `fatalError` if a view does not have a superview
63 | * - Parameters:
64 | * - views: The views to distribute
65 | * - align: The alignment of the views within their superview
66 | * - spacing: The spacing between the views
67 | * - offset: The offset from the top-left corner of the superview
68 | * - Returns: An array of `AnchorConstraint` objects representing the constraints applied to each view
69 | */
70 | public static func distribute(vertically views: [View], align: Alignment = .topLeft, spacing: CGFloat = .zero, offset: CGPoint = .zero) -> [AnchorConstraint] {
71 | // Set horizontal constraints for each view
72 | let xConstraints: [NSLayoutConstraint] = views.map { view in
73 | // Ensure that the view has a superview, otherwise throw a fatal error
74 | guard let superView: NSView = view.superview else { fatalError("View must have superview") }
75 | // Anchor the view to its superview with the specified horizontal alignment and offset
76 | return Constraint.anchor(
77 | view, // The source view
78 | to: superView, // The target view to anchor to horizontally
79 | align: align.horAlign, // The horizontal alignment to use
80 | alignTo: align.horAlign, // The horizontal alignment to align to
81 | offset: offset.x // The offset to use for the x axis
82 | )
83 | }
84 | // Set vertical constraints for each view
85 | let yConstraints: [NSLayoutConstraint] = distribute(
86 | views, // the views to distribute
87 | axis: .ver, // the axis to distribute along
88 | align: align, // the alignment of the views
89 | spacing: spacing, // the spacing between the views
90 | offset: offset.y // the offset of the constraints
91 | )
92 | // Combine the horizontal and vertical constraints into an array of AnchorConstraints
93 | let anchors: [AnchorConstraint] = Array(zip(xConstraints, yConstraints))
94 | // Return the array of AnchorConstraints
95 | return anchors
96 | }
97 | }
98 | /**
99 | * Internal helper methods
100 | * - Remark: Consider moving to fileprivate Util class
101 | */
102 | extension Constraint {
103 | /**
104 | * Distributes views either vertically or horizontally based on the specified axis and alignment
105 | * - Fixme: ⚠️️ Consider replacing the fatal error with a throwing function
106 | * - Parameters:
107 | * - views: The views to distribute
108 | * - axis: The axis along which to distribute the views
109 | * - align: The alignment of the views within their superview
110 | * - spacing: The spacing between the views
111 | * - offset: The offset from the top-left corner of the superview
112 | * - Returns: An array of `NSLayoutConstraint` objects representing the constraints applied to each view
113 | */
114 | fileprivate static func distribute(_ views: [View], axis: Axis, align: Alignment, spacing: CGFloat = .zero, offset: CGFloat = .zero) -> [NSLayoutConstraint] {
115 | switch (align.horAlign, align.verAlign) {
116 | // Distribute views from the start (left or top) of the axis
117 | case (.left, _), (_, .top):
118 | return distribute(
119 | fromStart: views, // The starting view for distribution
120 | axis: axis, // The axis to use for distribution
121 | spacing: spacing, // The spacing to use
122 | offset: offset // The offset to use
123 | )
124 | // Distribute views from the end (right or bottom) of the axis
125 | case (.right, _), (_, .bottom):
126 | return distribute(
127 | fromEnd: views, // The ending view for distribution
128 | axis: axis, // The axis to use for distribution
129 | spacing: spacing, // The spacing to use
130 | offset: offset // The offset to use
131 | )
132 | // Throw a fatal error if the alignment is not supported
133 | default:
134 | fatalError("Type not supported: h: \(align.horAlign) v: \(align.verAlign)")
135 | }
136 | }
137 | /**
138 | * Distributes views from the start of the axis to the end of the axis
139 | * - Fixme: ⚠️️ Consider replacing the fatal error with a throwing function
140 | * - Parameters:
141 | * - views: The views to distribute
142 | * - axis: The axis along which to distribute the views
143 | * - spacing: The spacing between the views
144 | * - offset: The offset from the start of the axis
145 | * - Returns: An array of `NSLayoutConstraint` objects representing the constraints applied to each view
146 | */
147 | fileprivate static func distribute(fromStart views: [View], axis: Axis, spacing: CGFloat = .zero, offset: CGFloat = .zero) -> [NSLayoutConstraint] {
148 | var anchors: [NSLayoutConstraint] = []
149 | var prevView: View?
150 | views.enumerated().forEach { (_: Int, view: View) in
151 | // Ensure that the view has a superview or a previous view, otherwise throw a fatal error
152 | guard let toView: View = prevView ?? view.superview else { fatalError("View must have superview") }
153 | let offset: CGFloat = prevView == nil ? offset : .zero // Only the first view gets offset
154 | let spacing: CGFloat = prevView != nil ? spacing : 0 // All subsequent views get spacing
155 | switch axis {
156 | case .hor:
157 | let alignTo: HorizontalAlign = prevView == nil ? .left : .right // First align to left of superview, then right of each subsequent view
158 | // 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
159 | let anchor: NSLayoutConstraint = Constraint.anchor(
160 | view, // The source view
161 | to: toView, // The target view to anchor to horizontally
162 | align: .left, // The horizontal alignment to use
163 | alignTo: alignTo, // The horizontal alignment to align to
164 | offset: offset + spacing // The offset to use for the x axis
165 | )
166 | anchors.append(anchor)
167 | case .ver:
168 | let alignTo: VerticalAlign = prevView == nil ? .top : .bottom // First align to top of superview, then bottom of each subsequent view
169 | // 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
170 | let anchor: NSLayoutConstraint = Constraint.anchor(
171 | view, // The source view
172 | to: toView, // The target view to anchor to vertically
173 | align: .top, // The vertical alignment to use
174 | alignTo: alignTo, // The vertical alignment to align to
175 | offset: offset + spacing // The offset to use for the y axis
176 | )
177 | anchors.append(anchor)
178 | }
179 | prevView = view
180 | }
181 | // Return the array of NSLayoutConstraint objects
182 | return anchors
183 | }
184 | /**
185 | * Distributes views from the end of the axis to the start of the axis
186 | * - Fixme: ⚠️️ Remove fatal error? make it throw?
187 | * - Parameters:
188 | * - views: The views to distribute
189 | * - axis: The axis along which to distribute the views
190 | * - spacing: The spacing between the views
191 | * - offset: The offset from the end of the axis
192 | * - Returns: An array of `NSLayoutConstraint` objects representing the constraints applied to each view
193 | */
194 | fileprivate static func distribute(fromEnd views: [View], axis: Axis, spacing: CGFloat = .zero, offset: CGFloat = .zero) -> [NSLayoutConstraint] {
195 | var anchors: [NSLayoutConstraint] = []
196 | var prevView: View?
197 | for view: View in views.reversed() { // Move backwards
198 | // Ensure that the view has a superview or a previous view, otherwise throw a fatal error
199 | guard let toView: View = prevView ?? view.superview else { fatalError("View must have superview") }
200 | let offset: CGFloat = prevView == nil ? offset : .zero // Only the last view gets offset
201 | let spacing: CGFloat = prevView != nil ? spacing : 0 // All previous views get spacing
202 | switch axis {
203 | case .hor:
204 | let alignTo: HorizontalAlign = prevView == nil ? .right : .left // First align to right of superview, then left of each subsequent view
205 | // 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
206 | let anchor: NSLayoutConstraint = Constraint.anchor(
207 | view, // The source view
208 | to: toView, // The target view to anchor to horizontally
209 | align: .right, // The horizontal alignment to use
210 | alignTo: alignTo, // The horizontal alignment to align to
211 | offset: offset + spacing // The offset to use for the x axis
212 | )
213 | anchors.append(anchor)
214 | case .ver:
215 | let alignTo: VerticalAlign = prevView == nil ? .bottom : .top // First align to bottom of superview, then top of each subsequent view
216 | // 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
217 | let anchor: NSLayoutConstraint = Constraint.anchor(
218 | view, // The source view
219 | to: toView, // The target view to anchor to vertically
220 | align: .bottom, // The vertical alignment to use
221 | alignTo: alignTo, // The vertical alignment to align to
222 | offset: offset + spacing // The offset to use for the y axis
223 | )
224 | anchors.append(anchor)
225 | }
226 | prevView = view
227 | }
228 | // Return the array of NSLayoutConstraint objects
229 | return anchors
230 | }
231 | }
232 |
--------------------------------------------------------------------------------
/Sources/SpatialLib/view/View+Size.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | #if os(iOS)
3 | import UIKit
4 | #elseif os(macOS)
5 | import Cocoa
6 | #endif
7 | /**
8 | * Size constraints
9 | * - Note: Has a lot of `NSConstraint` and `NSAnchor` info: https://stackoverflow.com/a/26181982/5389500
10 | * ## Examples:
11 | * square.translatesAutoresizingMaskIntoConstraints = false (this enables you to set your own constraints)
12 | * contentView.layoutMargins = UIEdgeInsetsMake(12, 12, 12, 12) // adds margin to the containing view
13 | * let pos = Constraint.anchor(square, to: canvas, targetAlign: .topleft, toAlign: .topleft)
14 | * let size = Constraint.size(square, to: canvas)
15 | * NSLayoutConstraint.activate([anchor.x, anchor.y, size.w, size.h])
16 | */
17 | extension Constraint {
18 | /**
19 | * Creates a size constraint for the specified view relative to another view
20 | * - Important: ⚠️️ Multiplier needs to be 1, 1 to not have an effect
21 | * - Important: ⚠️️ Offset needs to be 0, 0 to not have an effect
22 | * ## Examples:
23 | * let sizeConstraint = Constraint.size(square, to: parent, offset: .zero, multiplier: .init(x: 1, y: 0.5))
24 | * let widthConstraint = Constraint.size(square, to: parent).w
25 | * - Parameters:
26 | * - view: The view to set the size constraint for
27 | * - to: The view to align the size constraint to
28 | * - offset: The size offset from the aligned view
29 | * - multiplier: The size multiplier relative to the aligned view
30 | * - Returns: A tuple of `NSLayoutConstraint` objects representing the width and height constraints applied to the view
31 | */
32 | public static func size(_ view: View, to: View, offset: CGSize = .zero, multiplier: CGPoint = .init(x: 1, y: 1)) -> SizeConstraint {
33 | // Set the width constraint for the view relative to the aligned view, with the specified width offset and multiplier
34 | let widthConstraint: NSLayoutConstraint = Constraint.width(
35 | view, // The source view
36 | to: to, // The target view to use for width
37 | offset: offset.width, // The offset to use for the width
38 | multiplier: multiplier.x // The multiplier to use for the width
39 | )
40 | // Set the height constraint for the view relative to the aligned view, with the specified height offset and multiplier
41 | let heightConstraint: NSLayoutConstraint = Constraint.height(
42 | view, // The source view
43 | to: to, // The target view to use for height
44 | offset: offset.height, // The offset to use for the height
45 | multiplier: multiplier.y // The multiplier to use for the height
46 | )
47 | // Return a tuple of the width and height constraints
48 | return (widthConstraint, heightConstraint)
49 | }
50 | /**
51 | * Creates a size constraint for the specified view with the specified size and multiplier
52 | * - Fixme: ⚠️️ This doesn't have offset, maybe it should, for now I guess you can always inset the size, or add etc
53 | * - Parameters:
54 | * - view: The view to set the size constraint for
55 | * - size: The target size for the view
56 | * - multiplier: The size multiplier for the view
57 | * - Returns: A tuple of `NSLayoutConstraint` objects representing the width and height constraints applied to the view
58 | */
59 | public static func size(_ view: View, size: CGSize, multiplier: CGSize = .init(width: 1, height: 1)) -> SizeConstraint {
60 | // Set the width constraint for the view with the specified width and multiplier
61 | let widthConstraint: NSLayoutConstraint = Constraint.width(
62 | view, // The source view
63 | width: size.width, // The width to use
64 | multiplier: multiplier.width // The multiplier to use
65 | )
66 | // Set the height constraint for the view with the specified height and multiplier
67 | let heightConstraint: NSLayoutConstraint = Constraint.height(
68 | view, // The source view
69 | height: size.height, // The height to use
70 | multiplier: multiplier.height // The multiplier to use
71 | )
72 | // Return a tuple of the width and height constraints
73 | return (widthConstraint, heightConstraint)
74 | }
75 | /**
76 | * Creates a size constraint for the specified view based on the specified width, height, or another view's size
77 | * - Fixme: ⚠️️ Use named property for the view parameter
78 | * - Parameters:
79 | * - view: The view to set the size constraint for
80 | * - to: The view to align the size constraint to
81 | * - width: The custom width for the view, instead of relying on another view to size against
82 | * - height: The custom height for the view, instead of relying on another view to size against
83 | * - offset: The size offset from the aligned view
84 | * - multiplier: The size multiplier for the view, scaling the size constraint by this scalar (works with other view and custom size)
85 | * - Returns: A tuple of `NSLayoutConstraint` objects representing the width and height constraints applied to the view
86 | * - Example:
87 | * let s = Constraint.size(view, to: parent, height: 48)
88 | */
89 | 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 {
90 | let width: NSLayoutConstraint = {
91 | // If a custom width is specified, set the width constraint for the view with the custom width and multiplier
92 | if let width: CGFloat = width {
93 | return Constraint.width(
94 | view, // The source view
95 | width: width, // The width to use
96 | multiplier: multiplier.width // The multiplier to use
97 | )
98 | }
99 | // Otherwise, set the width constraint for the view relative to the aligned view, with the specified width offset and multiplier
100 | else {
101 | return Constraint.width(
102 | view, // The source view
103 | to: to, // The target view to use for width
104 | offset: offset.width, // The offset to use for the width
105 | multiplier: multiplier.width // The multiplier to use for the width
106 | )
107 | }
108 | }()
109 | let height: NSLayoutConstraint = {
110 | // If a custom height is specified, set the height constraint for the view with the custom height and multiplier
111 | if let height: CGFloat = height {
112 | return Constraint.height(
113 | view, // The source view
114 | height: height, // The height to use
115 | multiplier: multiplier.height // The multiplier to use
116 | )
117 | }
118 | // Otherwise, set the height constraint for the view relative to the aligned view, with the specified height offset and multiplier
119 | else {
120 | return Constraint.height(
121 | view, // The source view
122 | to: to, // The target view to use for height
123 | offset: offset.height, // The offset to use for the height
124 | multiplier: multiplier.height // The multiplier to use for the height
125 | )
126 | }
127 | }()
128 | // Return a tuple of the width and height constraints
129 | return (width, height)
130 | }
131 | /**
132 | * Creates a width constraint for the specified view with the specified width and multiplier
133 | * - Remark: When AutoLayout doesn't relate to a view the multiplier doesn't take effect, so we apply the multiplier directly to the constant
134 | * - Parameters:
135 | * - view: The view to set the width constraint for
136 | * - width: The target width for the view
137 | * - multiplier: The width multiplier for the view
138 | * - relation: The relation of the constraint (equalTo, greaterThanOrEqual, lessThanOrEqual)
139 | * - Returns: An `NSLayoutConstraint` object representing the width constraint applied to the view
140 | */
141 | public static func width(_ view: View, width: CGFloat, multiplier: CGFloat = 1, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint {
142 | // Create and return an NSLayoutConstraint object representing the width constraint applied to the view, with the specified width, multiplier, and relation
143 | .init(
144 | item: view, // The source view
145 | attribute: .width, // The attribute to use for width
146 | relatedBy: relation, // The relation to use
147 | toItem: nil, // The target view to use for width
148 | attribute: .notAnAttribute, // The attribute to use for the target view
149 | multiplier: 1, // The multiplier to use for width
150 | constant: width * multiplier // The constant to use for width
151 | )
152 | }
153 | /**
154 | * Creates a height constraint for the specified view with the specified height and multiplier
155 | * - Remark: When AutoLayout doesn't relate to a view the multiplier doesn't take effect, so we apply the multiplier directly to the constant
156 | * - Parameters:
157 | * - view: The view to set the height constraint for
158 | * - height: The target height for the view
159 | * - multiplier: The height multiplier for the view
160 | * - relation: The relation of the constraint (equalTo, greaterThanOrEqual, lessThanOrEqual)
161 | * - Returns: An `NSLayoutConstraint` object representing the height constraint applied to the view
162 | */
163 | public static func height(_ view: View, height: CGFloat, multiplier: CGFloat = 1, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint {
164 | // Create and return an NSLayoutConstraint object representing the height constraint applied to the view, with the specified height, multiplier, and relation
165 | .init(
166 | item: view, // The source view
167 | attribute: .height, // The attribute to use for height
168 | relatedBy: relation, // The relation to use
169 | toItem: nil, // The target view to use for height
170 | attribute: .notAnAttribute, // The attribute to use for the target view
171 | multiplier: 1, // The multiplier to use for height
172 | constant: height * multiplier // The constant to use for height
173 | )
174 | }
175 | /**
176 | * Creates a width constraint for the specified view based on another view's width constraint
177 | * - Parameters:
178 | * - view: The view to set the width constraint for
179 | * - to: The view to align the width constraint to
180 | * - offset: The width offset from the aligned view
181 | * - multiplier: The width multiplier for the view
182 | * - relation: The relation of the constraint (equalTo, greaterThanOrEqual, lessThanOrEqual)
183 | * - Returns: An `NSLayoutConstraint` object representing the width constraint applied to the view
184 | */
185 | public static func width(_ view: View, to: View, offset: CGFloat = .zero, multiplier: CGFloat = 1, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint {
186 | // Create and return an NSLayoutConstraint object representing the width constraint applied to the view, with the specified width offset, multiplier, and relation
187 | .init(
188 | item: view, // The source view
189 | attribute: .width, // The attribute to use for width
190 | relatedBy: relation, // The relation to use
191 | toItem: to, // The target view to use for width
192 | attribute: .width, // The attribute to use for the target view
193 | multiplier: multiplier, // The multiplier to use for width
194 | constant: offset // The constant to use for width
195 | )
196 | }
197 | /**
198 | * Creates a height constraint for the specified view based on another view's height constraint
199 | * - Parameters:
200 | * - view: The view to set the height constraint for
201 | * - to: The view to align the height constraint to
202 | * - offset: The height offset from the aligned view
203 | * - multiplier: The height multiplier for the view
204 | * - relation: The relation of the constraint (equalTo, greaterThanOrEqual, lessThanOrEqual)
205 | * - Returns: An `NSLayoutConstraint` object representing the height constraint applied to the view
206 | */
207 | public static func height(_ view: View, to: View, offset: CGFloat = .zero, multiplier: CGFloat = 1, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint {
208 | // Create and return an NSLayoutConstraint object representing the height constraint applied to the view, with the specified height offset, multiplier, and relation
209 | .init(
210 | item: view, // The source view
211 | attribute: .height, // The attribute to use for height
212 | relatedBy: relation, // The relation to use
213 | toItem: to, // The target view to use for height
214 | attribute: .height, // The attribute to use for the target view
215 | multiplier: multiplier, // The multiplier to use for height
216 | constant: offset // The constant to use for height
217 | )
218 | }
219 | /**
220 | * Creates a constraint for the specified view's width or height based on another view's width or height
221 | * - Remark: Useful if you want to set a width of an object to the height of another object
222 | * - Parameters:
223 | * - view: The view to set the constraint for
224 | * - to: The view to align the constraint to (usually the parent view)
225 | * - viewAxis: The axis to set the constraint for (horizontal or vertical)
226 | * - toAxis: The axis to derive the constraint from (horizontal or vertical)
227 | * - offset: The offset from the derived constraint
228 | * - multiplier: The scalar value to multiply the derived constraint by (default is 1)
229 | * - relation: The relation of the constraint (equalTo, greaterThanOrEqual, lessThanOrEqual)
230 | * - Returns: An `NSLayoutConstraint` object representing the width or height constraint applied to the view
231 | * ## Examples:
232 | * let widthConstraint = Constraint.length(square, viewAxis: .horizontal, axis: .vertical)
233 | */
234 | public static func length(_ view: View, to: View, viewAxis: Axis, toAxis: Axis, offset: CGFloat = .zero, multiplier: CGFloat = 1, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint {
235 | // Determine the attribute to set the constraint for based on the specified view axis (horizontal or vertical)
236 | let viewAttr: NSLayoutConstraint.Attribute = viewAxis == .hor ? .width : .height
237 | // Determine the attribute to derive the constraint from based on the specified to axis (horizontal or vertical)
238 | let toAttr: NSLayoutConstraint.Attribute = toAxis == .hor ? .width : .height
239 | // 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
240 | return .init(
241 | item: view, // The source view
242 | attribute: viewAttr, // The attribute to use for the source view
243 | relatedBy: relation, // The relation to use
244 | toItem: to, // The target view to use
245 | attribute: toAttr, // The attribute to use for the target view
246 | multiplier: multiplier, // The multiplier to use
247 | constant: offset // The constant to use
248 | )
249 | }
250 | }
251 |
--------------------------------------------------------------------------------
/Sources/SpatialLib/view/View+Type.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | #if os(iOS)
3 | import UIKit
4 | #elseif os(macOS)
5 | import Cocoa
6 | #endif
7 | // Fix: add param doc etc
8 | // Single
9 | // A tuple of `NSLayoutConstraint` objects representing the x and y anchor constraints applied to a view
10 | public typealias AnchorConstraint = (x: NSLayoutConstraint, y: NSLayoutConstraint)
11 | // A tuple of `NSLayoutConstraint` objects representing the width and height constraints applied to a view
12 | public typealias SizeConstraint = (w: NSLayoutConstraint, h: NSLayoutConstraint)
13 | // A tuple of anchor and size constraints applied to a view
14 | public typealias AnchorAndSize = (anchor: AnchorConstraint, size: SizeConstraint)
15 | // Bulk
16 | // A tuple of arrays of anchor and size constraints applied to a view
17 | public typealias AnchorConstraintsAndSizeConstraints = (anchorConstraints: [AnchorConstraint], sizeConstraints: [SizeConstraint])
18 | /**
19 | * Single
20 | */
21 | extension View {
22 | // We keep `AnchorsAndSizes` in a tuple, because `applyConstraints wouldn't work with just an array
23 | // A tuple of arrays of anchor and size constraints applied to a view
24 | public typealias AnchorsAndSizes = (anchors: [NSLayoutConstraint], sizes: [NSLayoutConstraint])
25 | // A closure that returns an array of constraints applied to a view
26 | public typealias ConstraintsClosure = (_ view: View) -> [NSLayoutConstraint]
27 | // A closure that returns a single constraint applied to a view
28 | public typealias ConstraintClosure = (_ view: View) -> NSLayoutConstraint
29 | // Tuple
30 | // A closure that returns a tuple of anchor and size constraints applied to a view
31 | public typealias AnchorAndSizeClosure = (_ view: View) -> AnchorAndSize
32 | // Single
33 | // A closure that returns a single anchor constraint applied to a view
34 | public typealias AnchorClosure = (_ view: View) -> AnchorConstraint
35 | // A closure that returns a single size constraint applied to a view
36 | public typealias SizeClosure = (_ view: View) -> SizeConstraint
37 | }
38 | /**
39 | * Bulk
40 | */
41 | extension Array where Element: View {
42 | // A closure that returns a tuple of anchor and size constraints applied to an array of views
43 | public typealias ConstraintsClosure = (_ views: [View]) -> AnchorConstraintsAndSizeConstraints
44 | // A closure that returns an array of anchor constraints applied to an array of views
45 | public typealias AnchorConstraintsClosure = (_ views: [View]) -> [AnchorConstraint]
46 | // A closure that returns an array of size constraints applied to an array of views
47 | public typealias SizeConstraintsClosure = (_ views: [View]) -> [SizeConstraint]
48 | }
49 |
--------------------------------------------------------------------------------
/Spatial.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Spatial.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Spatial.xcodeproj/project.xcworkspace/xcuserdata/andre.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eonist/Spatial/61d272954c05cbc41294528f1f71006226ec367b/Spatial.xcodeproj/project.xcworkspace/xcuserdata/andre.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/Spatial.xcodeproj/project.xcworkspace/xcuserdata/andrejorgensen.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eonist/Spatial/61d272954c05cbc41294528f1f71006226ec367b/Spatial.xcodeproj/project.xcworkspace/xcuserdata/andrejorgensen.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/Spatial.xcodeproj/project.xcworkspace/xcuserdata/eon.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eonist/Spatial/61d272954c05cbc41294528f1f71006226ec367b/Spatial.xcodeproj/project.xcworkspace/xcuserdata/eon.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/Spatial.xcodeproj/xcshareddata/xcschemes/SpatialExample.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
42 |
48 |
49 |
50 |
51 |
52 |
62 |
64 |
70 |
71 |
72 |
73 |
79 |
81 |
87 |
88 |
89 |
90 |
92 |
93 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/Spatial.xcodeproj/xcuserdata/andre.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | Spatial.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 | SpatialExample.xcscheme_^#shared#^_
13 |
14 | orderHint
15 | 1
16 |
17 |
18 | SuppressBuildableAutocreation
19 |
20 | F1D369E52171FDAE00F498FA
21 |
22 | primary
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/Spatial.xcodeproj/xcuserdata/andrejorgensen.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | Spatial-macOS.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 1
11 |
12 | Spatial.xcscheme_^#shared#^_
13 |
14 | orderHint
15 | 0
16 |
17 | SpatialExample.xcscheme_^#shared#^_
18 |
19 | orderHint
20 | 2
21 |
22 | SpatialExampleMac.xcscheme_^#shared#^_
23 |
24 | orderHint
25 | 3
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/Spatial.xcodeproj/xcuserdata/eon.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/Spatial.xcodeproj/xcuserdata/eon.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | SpatialExample.xcscheme
8 |
9 | orderHint
10 | 1
11 |
12 | SpatialExample.xcscheme_^#shared#^_
13 |
14 | orderHint
15 | 1
16 |
17 | SpatialExampleMac.xcscheme_^#shared#^_
18 |
19 | orderHint
20 | 15
21 |
22 |
23 | SuppressBuildableAutocreation
24 |
25 | F185A3A522319E8E00AE66B2
26 |
27 | primary
28 |
29 |
30 | F1D369D62171FCDC00F498FA
31 |
32 | primary
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/SpatialExample/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | @UIApplicationMain
4 | class AppDelegate: UIResponder, UIApplicationDelegate {
5 | lazy var window: UIWindow? = {
6 | let win = UIWindow(frame: UIScreen.main.bounds) // Create a new UIWindow object with the same frame as the main screen
7 | let vc = MainVC() // Create a new instance of MainVC
8 | win.rootViewController = vc // Set the root view controller of the window to the MainVC instance
9 | win.makeKeyAndVisible() // Important since we have no Main storyboard anymore
10 | return win
11 | }()
12 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
13 | _ = window
14 | return true
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/SpatialExample/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "size" : "60x60",
36 | "scale" : "2x"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "size" : "60x60",
41 | "scale" : "3x"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "size" : "20x20",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "size" : "20x20",
51 | "scale" : "2x"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "size" : "29x29",
56 | "scale" : "1x"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "size" : "29x29",
61 | "scale" : "2x"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "size" : "40x40",
66 | "scale" : "1x"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "size" : "40x40",
71 | "scale" : "2x"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "size" : "76x76",
76 | "scale" : "1x"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "size" : "76x76",
81 | "scale" : "2x"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "size" : "83.5x83.5",
86 | "scale" : "2x"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "size" : "1024x1024",
91 | "scale" : "1x"
92 | }
93 | ],
94 | "info" : {
95 | "version" : 1,
96 | "author" : "xcode"
97 | }
98 | }
--------------------------------------------------------------------------------
/SpatialExample/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/SpatialExample/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/SpatialExample/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UILaunchStoryboardName
24 | LaunchScreen
25 | UIMainStoryboardFile
26 |
27 | UIRequiredDeviceCapabilities
28 |
29 | armv7
30 |
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 | UIInterfaceOrientationLandscapeLeft
35 | UIInterfaceOrientationLandscapeRight
36 |
37 | UISupportedInterfaceOrientations~ipad
38 |
39 | UIInterfaceOrientationPortrait
40 | UIInterfaceOrientationPortraitUpsideDown
41 | UIInterfaceOrientationLandscapeLeft
42 | UIInterfaceOrientationLandscapeRight
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/SpatialExample/MainVC/AnimationTestView/AnimationTestView+Create.swift:
--------------------------------------------------------------------------------
1 | #if os(iOS)
2 | import UIKit
3 |
4 | extension AnimationTest {
5 | /**
6 | * Button
7 | */
8 | func createButton() -> Button {
9 | // - Fixme: ⚠️️ use with here since it's not part of source
10 | let btn: Button = .init (type: .system)
11 | btn.backgroundColor = .gray
12 | btn.setTitle("Button", for: .normal)
13 | btn.setTitleColor(.black, for: .normal)
14 | btn.titleLabel?.textAlignment = .center
15 | btn.titleLabel?.font = .systemFont(ofSize: 12)
16 | // btn.frame = CGRect(x:100, y:50, width:100, height:50)
17 | btn.addTarget(self, action: #selector(buttonTouched), for: .touchUpInside)
18 | self.addSubview(btn)
19 | btn.applyAnchorAndSize { view in
20 | let anchor = Constraint.anchor(
21 | view, // The source view
22 | to: self, // The target view to anchor to
23 | align: .centerCenter, // The alignment to use for the source view
24 | alignTo: .centerCenter // The alignment to use for the target view
25 | )
26 | let size = Constraint.size(
27 | view, // The source view
28 | size: .init(width: 100, height: 48) // The size to use
29 | )
30 | return (anchor, size)
31 | }
32 | return btn
33 | }
34 | @objc func buttonTouched(sender: UIButton) {
35 | Swift.print("It Works!!!")
36 | // let to:CGFloat = 0//(UIScreen.main.bounds.height/2) + (button.frame.height/2)
37 | button.animate(to: .zero, align: .topLeft, alignTo: .topLeft) {}
38 | }
39 | }
40 | /**
41 | * Button that has ConstraintKind applied
42 | */
43 | class Button: UIButton, ConstraintKind {
44 | var anchorAndSize: AnchorAndSize?
45 | // var anchor: AnchorConstraint?
46 | // var size: SizeConstraint?
47 | }
48 |
49 | #endif
50 |
--------------------------------------------------------------------------------
/SpatialExample/MainVC/AnimationTestView/AnimationTestView.swift:
--------------------------------------------------------------------------------
1 | #if os(iOS)
2 | import UIKit
3 |
4 | class AnimationTest: UIView {
5 | lazy var button: Button = createButton()
6 | override init(frame: CGRect) {
7 | super.init(frame: frame)
8 | self.backgroundColor = .green
9 | _ = button
10 | }
11 | /**
12 | * Boilerplate
13 | */
14 | @available(*, unavailable)
15 | required init?(coder aDecoder: NSCoder) {
16 | fatalError("init(coder:) has not been implemented")
17 | }
18 | }
19 | #endif
20 |
--------------------------------------------------------------------------------
/SpatialExample/MainVC/MainVC+Create.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | /**
3 | * Create
4 | */
5 | extension MainVC {
6 | /**
7 | * Creates main view
8 | */
9 | func createMainView() -> MainView {
10 | let view: MainView = .init()
11 | self.view.addSubview(view)
12 | view.anchorAndSize(to: self.view)
13 | return view
14 | }
15 | /**
16 | * Creates animation test view
17 | */
18 | func createAnimTestView() -> AnimationTest {
19 | let view: AnimationTest = .init(frame: .init(origin: .zero, size: .zero))
20 | self.view.addSubview(view)
21 | view.anchorAndSize(to: self.view)
22 | return view
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/SpatialExample/MainVC/MainVC.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | class MainVC: UIViewController {
4 | lazy var mainView: MainView = createMainView()
5 | lazy var animTestView: AnimationTest = createAnimTestView()
6 | override func viewDidLoad() {
7 | super.viewDidLoad()
8 | _ = mainView
9 | //_ = animTestView
10 | self.view.backgroundColor = .lightGray
11 | }
12 | override var prefersStatusBarHidden: Bool { true } // hides statusbar
13 | }
14 |
--------------------------------------------------------------------------------
/SpatialExample/MainVC/MainView/CardView/BottomBar/BottomBar+Constant.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension BottomBar {
4 | static let bottomBarHeight: CGFloat = UIScreen.main.bounds.width / 4 + UIApplication.shared.statusBarFrame.height
5 | }
6 |
--------------------------------------------------------------------------------
/SpatialExample/MainVC/MainView/CardView/BottomBar/BottomBar.swift:
--------------------------------------------------------------------------------
1 | #if os(iOS)
2 | import UIKit
3 |
4 | class BottomBar: UIView {
5 | override init(frame: CGRect) {
6 | super.init(frame: frame)
7 | backgroundColor = .blue
8 | }
9 | /**
10 | * Boilerplate
11 | */
12 | @available(*, unavailable)
13 | required init?(coder aDecoder: NSCoder) {
14 | fatalError("init(coder:) has not been implemented")
15 | }
16 | }
17 |
18 | #endif
19 |
--------------------------------------------------------------------------------
/SpatialExample/MainVC/MainView/CardView/CardView+Constant.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension CardView {
4 | static let margin: UIEdgeInsets = .init(top: 24, left: 12, bottom: 24, right: 12)
5 | }
6 |
--------------------------------------------------------------------------------
/SpatialExample/MainVC/MainView/CardView/CardView+Create.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import With
3 | /**
4 | * UI elements
5 | */
6 | extension CardView {
7 | /**
8 | * Creates topBar
9 | */
10 | func createTopBar() -> TopBar {
11 | with(.init()) {
12 | self.addSubview($0)
13 | $0.anchorAndSize(to: self, height: TopBar.topBarHeight)
14 | }
15 | }
16 | /**
17 | * Creates the middle card content view
18 | */
19 | func createMiddleContent() -> MiddleContent {
20 | with(.init()) {
21 | self.addSubview($0)
22 | let sizeOffset: CGSize = .init(width: 0, height: -(TopBar.topBarHeight + BottomBar.bottomBarHeight))
23 | $0.anchorAndSize(to: topBar, sizeTo: self, alignTo: .bottomLeft, sizeOffset: sizeOffset)
24 | }
25 | }
26 | /**
27 | * Creates bottomBar
28 | */
29 | func createBottomBar() -> BottomBar {
30 | with(.init()) {
31 | self.addSubview($0)
32 | $0.anchorAndSize(to: cardContent, sizeTo: self, height: BottomBar.bottomBarHeight, alignTo: .bottomLeft)
33 | }
34 | }
35 | }
36 | /**
37 | * Background & layer
38 | */
39 | extension CardView {
40 | /**
41 | * Creates bg layer
42 | */
43 | func createBackgroundLayer() -> CALayer {
44 | let bgLayer: CALayer = {
45 | let layer = CALayer()
46 | layer.backgroundColor = UIColor.green.cgColor
47 | layer.frame = self.bounds//CGRect.init(x: CardView.margin.left, y: CardView.margin.top, width: self.frame.width - (CardView.margin.left + CardView.margin.right) , height: self.frame.height - (CardView.margin.top + CardView.margin.bottom))
48 | return layer
49 | }()
50 | self.layer.insertSublayer(bgLayer, at: 0)/*⚠️️ We need to insert bg at 0, or else it is put above all views*/
51 | return bgLayer
52 | }
53 | /**
54 | * Creates and applies mask to view
55 | */
56 | func createMaskLayer() -> CAShapeLayer {
57 | let maskLayer = CAShapeLayer()
58 | let path: UIBezierPath = .init(roundedRect: self.bounds, cornerRadius: 24)
59 | maskLayer.path = path.cgPath
60 | self.layer.mask = maskLayer/*Applies the mask to the view*/
61 | return maskLayer
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/SpatialExample/MainVC/MainView/CardView/CardView.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | class CardView: UIView {
4 | /*Graphics*/
5 | lazy var backgroundLayer: CALayer = createBackgroundLayer()
6 | lazy var maskLayer: CAShapeLayer = createMaskLayer()
7 | /*UI*/
8 | lazy var topBar: TopBar = createTopBar()
9 | lazy var cardContent: MiddleContent = createMiddleContent()
10 | lazy var bottomBar: BottomBar = createBottomBar()
11 | override init(frame: CGRect) {
12 | super.init(frame: frame)
13 | /*UI*/
14 | _ = topBar
15 | _ = cardContent
16 | _ = bottomBar
17 | }
18 | /**
19 | * Boilerplate
20 | */
21 | @available(*, unavailable)
22 | required init?(coder aDecoder: NSCoder) {
23 | fatalError("init(coder:) has not been implemented")
24 | }
25 | /**
26 | * We create graphics elements in the layoutSubViews method since we need to access the .frame in these UI elements
27 | */
28 | override func layoutSubviews() {
29 | super.layoutSubviews()
30 | self.backgroundColor = .gray
31 | /*Graphics*/
32 | _ = backgroundLayer
33 | _ = maskLayer
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/SpatialExample/MainVC/MainView/CardView/MiddleContent/ItemView/ItemView+Create.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension ItemView {
4 | /**
5 | * Creates horizontal items
6 | */
7 | func createHorizontalItems() -> [UIView] {
8 | let size: CGSize = .init(width: 48, height: 48)
9 | let views: [UIView] = [UIColor.purple, .orange, .red, .blue].map {
10 | let view: UIView = .init(frame: .zero)
11 | self.addSubview(view)
12 | view.backgroundColor = $0
13 | return view
14 | }
15 | views.activateAnchorsAndSizes { views in
16 | let anchors = Constraint.distribute(horizontally: views, align: .centerLeft)
17 | let sizes = views.map { Constraint.size($0, size: size) }
18 | return (anchors, sizes)
19 | }
20 | return views
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/SpatialExample/MainVC/MainView/CardView/MiddleContent/ItemView/ItemView.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | class ItemView: UIView {
4 | lazy var horizontalItems: [UIView] = createHorizontalItems()
5 | override init(frame: CGRect) {
6 | super.init(frame: frame)
7 | backgroundColor = .green
8 | _ = horizontalItems
9 | }
10 | /**
11 | * Boilerplate
12 | */
13 | @available(*, unavailable)
14 | required init?(coder aDecoder: NSCoder) {
15 | fatalError("init(coder:) has not been implemented")
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/SpatialExample/MainVC/MainView/CardView/MiddleContent/MiddleContent+Create.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension MiddleContent {
4 | /**
5 | * Create items
6 | */
7 | func createItemViews() -> [ItemView] {
8 | let itemViews: [ItemView] = (0..<5).map {_ in
9 | let itemView: ItemView = .init(frame: .zero)
10 | self.addSubview(itemView)
11 | return itemView
12 | }
13 | itemViews.distributeAndSize(dir: .ver, width: self.frame.width, height: 48, spacing: 12, offset: .init(x: 24, y: 24))
14 | return itemViews
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/SpatialExample/MainVC/MainView/CardView/MiddleContent/MiddleContent.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | class MiddleContent: UIView {
4 | lazy var itemViews: [ItemView] = createItemViews()
5 | override init(frame: CGRect) {
6 | super.init(frame: frame)
7 | self.backgroundColor = .yellow
8 | _ = itemViews
9 | }
10 | /**
11 | * Boilerplate
12 | */
13 | @available(*, unavailable)
14 | required init?(coder aDecoder: NSCoder) {
15 | fatalError("init(coder:) has not been implemented")
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/SpatialExample/MainVC/MainView/CardView/TopBar/TopBar+Constant.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension TopBar {
4 | static let topBarHeight: CGFloat = UIScreen.main.bounds.width / 4 + UIApplication.shared.statusBarFrame.height
5 | }
6 |
7 |
8 | //_ = buttons
9 | //applyButtonConstraints()
10 |
11 | //extension TopBar{
12 | // /**
13 | // * Creates Buttons
14 | // */
15 | // func createButtons() -> [UIButton] {
16 | // Swift.print("UIApplication.shared.statusBarFrame.height): \(UIApplication.shared.statusBarFrame.height))")
17 | // return (0..<4).indices.map { i in
18 | // let btn = UIButton.init(frame: .zero)
19 | // btn.backgroundColor = Constants.Colors.allCases[i].uiColor
20 | // btn.titleLabel?.font = .boldSystemFont(ofSize: 16)//.systemFont(ofSize: 12)
21 | // btn.setTitleColor(.black, for: .normal)
22 | // btn.titleLabel?.textAlignment = .center
23 | // btn.setTitle("\(i)", for: .normal)
24 | // addSubview(btn)
25 | // return btn
26 | // }
27 | // }
28 | // func applyButtonConstraints(){
29 | // /*Align first btn*/
30 | // let btn0 = buttons[0]
31 | // btn0.activateConstraint { view in
32 | // let anchor = Constraint.anchor(view, to: self, align: .topLeft, alignTo: .topLeft, offset:CGPoint(x:0,y:UIApplication.shared.statusBarFrame.height))
33 | // let size = Constraint.size(view, size: CGSize.init(width: UIScreen.main.bounds.width/4, height: UIScreen.main.bounds.width/4))
34 | // return [anchor.x,anchor.y,size.w,size.h]
35 | // }
36 | // /*Align other btns*/
37 | // (1..<4).indices.forEach{ i in
38 | // let btn = buttons[i]
39 | // btn.activateConstraint { view in
40 | // let prevBtn = buttons[i-1]
41 | // let anchor = Constraint.anchor(view, to: prevBtn, align: .topLeft, alignTo: .topRight)
42 | // let size = Constraint.size(view, size: CGSize.init(width: UIScreen.main.bounds.width/4, height: UIScreen.main.bounds.width/4))
43 | // return [anchor.x,anchor.y,size.w,size.h]
44 | // }
45 | // }
46 | // }
47 | //}
48 |
--------------------------------------------------------------------------------
/SpatialExample/MainVC/MainView/CardView/TopBar/TopBar.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | class TopBar: UIView {
4 | // lazy var buttons:[UIButton] = createButtons()
5 | override init(frame: CGRect) {
6 | super.init(frame: frame)
7 | self.backgroundColor = .orange
8 | }
9 | /**
10 | * Boilerplate
11 | */
12 | @available(*, unavailable)
13 | required init?(coder aDecoder: NSCoder) {
14 | fatalError("init(coder:) has not been implemented")
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/SpatialExample/MainVC/MainView/MainView+Create.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import With
3 |
4 | extension MainView {
5 | /**
6 | * Create the FlowView
7 | * - Abstract: tests rounded corners, distribution of items etc
8 | */
9 | func createCardView() -> CardView {
10 | let view: CardView = .init()//.init(frame: .init(origin: .zero, size: screenSize))
11 | self.addSubview(view)
12 | let offset: CGPoint = .init(x: CardView.margin.left, y: CardView.margin.top)
13 | let sizeOffset: CGSize = .init(width: -(CardView.margin.left + CardView.margin.right), height: -(CardView.margin.top + CardView.margin.bottom))
14 | view.anchorAndSize(to: self, offset: offset, sizeOffset: sizeOffset)
15 | return view
16 | }
17 | /**
18 | * Create spacing test view
19 | * - Abstract tests: distributeAndSize, spaceBetween, spaceAround etc
20 | */
21 | func createSpacingTestView() -> SpacingTestView {
22 | let view: SpacingTestView = .init()
23 | self.addSubview(view)
24 | view.backgroundColor = .green
25 | // view.applyAnchorAndSize(to: self)
26 | view.anchorAndSize(to: self)
27 | return view
28 | }
29 | /**
30 | * Test minimums
31 | */
32 | func createMinMaxView() -> MinMaxTestView {
33 | let view: MinMaxTestView = .init()
34 | self.addSubview(view)
35 | view.backgroundColor = .lightGray
36 | view.anchorAndSize(to: self, height: 120)
37 | return view
38 | }
39 | /**
40 | *
41 | */
42 | func createTestView() -> TestView {
43 | let view: TestView = .init()
44 | self.addSubview(view)
45 | view.backgroundColor = .lightGray
46 | view.anchorAndSize(to: self, height: 120)
47 | return view
48 | }
49 | /**
50 | * sizeTestingView
51 | * - Abstract: Creates an inner box with padding of 24px
52 | */
53 | func createSizeTestingView() -> SizeTestingView {
54 | with(.init()) {
55 | addSubview($0)
56 | $0.anchorAndSize(to: self)
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/SpatialExample/MainVC/MainView/MainView.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | class MainView: UIView {
4 | lazy var cardView: CardView = createCardView()
5 | // lazy var spacingTestView: UIView = createSpacingTestView()
6 | // lazy var minMaxTestView: UIView = createMinMaxView()
7 | // lazy var testView: UIView = createTestView()
8 | // lazy var sizeTestingView: SizeTestingView = createSizeTestingView()
9 | override init(frame: CGRect) {
10 | super.init(frame: frame)
11 | _ = cardView
12 | // _ = spacingTestView
13 | // _ = minMaxTestView
14 | // _ = testView
15 | // _ = sizeTestingView
16 | }
17 | /**
18 | * Boilerplate
19 | */
20 | @available(*, unavailable)
21 | required init?(coder aDecoder: NSCoder) {
22 | fatalError("init(coder:) has not been implemented")
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/SpatialExample/MainVC/MainView/MinMaxTestView/MinMaxTestView.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import With
3 | /**
4 | * Seems to be out of order
5 | */
6 | class MinMaxTestView: UIView {
7 | lazy var descLabel: UILabel = createDescLabel()
8 | lazy var inputTextField: UITextField = createInputTextField()
9 | override init(frame: CGRect = .zero) {
10 | super.init(frame: frame)
11 | _ = descLabel
12 | // descLabel.text = "Desc"
13 | _ = inputTextField
14 | inputTextField.text = "Some details about"
15 | }
16 | @available(*, unavailable)
17 | required init?(coder aDecoder: NSCoder) {
18 | fatalError("init(coder:) has not been implemented")
19 | }
20 | }
21 | /**
22 | * Create
23 | */
24 | extension MinMaxTestView {
25 | /**
26 | * Title
27 | */
28 | func createDescLabel() -> UILabel {
29 | with(.init()) {
30 | let text = "Description:"
31 | $0.text = text
32 | let font = UIFont.boldSystemFont(ofSize: 20.0)
33 | $0.font = font
34 | $0.textColor = .black
35 | $0.textAlignment = .left
36 | self.addSubview($0)
37 | $0.backgroundColor = UIColor.green.withAlphaComponent(0.5)
38 | $0.layer.borderWidth = 0.5
39 | $0.layer.borderColor = UIColor.black.cgColor
40 | $0.activateConstraints { view in
41 | let y = Constraint.anchor(view, to: self, align: .centerY, alignTo: .centerY)
42 | let left = Constraint.anchor(view, to: self, align: .left, alignTo: .left, offset: 20)
43 | let size = text.size(withAttributes: [.font: font])
44 | let width: NSLayoutConstraint = Constraint.width(view, width: size.width)
45 | return [y, left, width]
46 | }
47 | }
48 | }
49 | /**
50 | * TextField
51 | */
52 | func createInputTextField() -> UITextField {
53 | with(.init()) {
54 | $0.font = .systemFont(ofSize: 16)
55 | $0.textColor = .gray
56 | $0.textAlignment = .right
57 | $0.tintColor = .blue
58 | $0.backgroundColor = UIColor.yellow.withAlphaComponent(0.5)
59 | $0.layer.borderWidth = 0.5
60 | $0.layer.borderColor = UIColor.green.cgColor
61 | $0.text = "test content"
62 | self.self.addSubview($0)
63 | //$0.autoresizingMask = [.flexibleWidth, .flexibleHeight]
64 | $0.activateConstraints { view in
65 | //let height:NSLayoutConstraint = Constraint.height(view, to: contentView)//length(view, to:self, viewAxis: .ver, toAxis: .ver )
66 | let y = Constraint.anchor(view, to: self, align: .centerY, alignTo: .centerY)
67 | let width = Constraint.width(view, width: 100)
68 | //let left = Constraint.anchor(view, to: descLabel, align: .left, alignTo: .right/*, relation:.lessThanOrEqual*/)
69 | //left.priority = .init(rawValue: 900)
70 | let right = Constraint.anchor(view, to: self, align: .right, alignTo: .right, offset: -20)
71 | //right.priority = UILayoutPriority(rawValue: 1000)
72 | return [y, /*height,, left*/ right, width]
73 | }
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/SpatialExample/MainVC/MainView/SizeTestingView/SizeTestingView.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import With
3 | /**
4 | * Creates an inner box with padding of 24px
5 | */
6 | class SizeTestingView: UIView {
7 | lazy var box: UIView = createBox()
8 | override init(frame: CGRect) {
9 | super.init(frame: frame)
10 | self.backgroundColor = .purple
11 | _ = box
12 | }
13 | /**
14 | * Boilerplate
15 | */
16 | @available(*, unavailable)
17 | required init?(coder aDecoder: NSCoder) {
18 | fatalError("init(coder:) has not been implemented")
19 | }
20 | }
21 | /**
22 | * Create
23 | */
24 | extension SizeTestingView {
25 | /**
26 | * Title
27 | */
28 | func createBox() -> UIView {
29 | with(.init()) {
30 | self.addSubview($0)
31 | $0.backgroundColor = UIColor.green.withAlphaComponent(0.5)
32 | $0.anchorAndSize(to: self, align: .topLeft, alignTo: .topLeft, offset: .init(x: 24, y: 24), sizeOffset: .init(width: -48, height: -48))//(to: self, width: size.width, align: .centerLeft, alignTo: .centerLeft, offset: .init(x: 20, y: 0) )
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/SpatialExample/MainVC/MainView/SpacingTestView/SpacingTestView+Create.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | /**
3 | * Create
4 | */
5 | extension SpacingTestView {
6 | /**
7 | * Creates horizontal items
8 | */
9 | func createVerticalItems() -> [UIView] {
10 | let size: CGSize = .init(width: 120, height: 48)
11 | let views: [ConstraintView] = [UIColor.purple, .orange/*,.red,.blue*/].map {
12 | let view: ConstraintView = .init(frame: .zero) // .init(origin: .zero, size: size)
13 | self.addSubview(view)
14 | view.backgroundColor = $0
15 | return view
16 | }
17 | views.applySizes(width: size.width, height: size.height)
18 | views.applyAnchors(to: self, align: .top, alignTo: .top, offset: 20)
19 | // views.distribute(dir: .horizontal)
20 | // (dir: .hor, width: size.width, height: size.height)
21 | // views.distributeAndSize(dir: .hor, height: size.height, multiplier:.init(width:0.25,height:1), offset:20, sizeOffset:.init(width:-10,height:0))
22 | // Swift.print("self.bounds.size: \(self.bounds.size)")
23 | // let inset: UIEdgeInsets = .init(top: 0, left: 20, bottom: 0, right: 20)
24 | // views.spaceBetween(dir: .hor, parent: self, inset: .zero)
25 | // views.spaceBetween(dir: .vertical, parent: self)
26 | views.spaceAround(dir: .hor, parent: self)
27 | // views.spaceAround(dir: .ver, parent: self)
28 | return views
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/SpatialExample/MainVC/MainView/SpacingTestView/SpacingTestView.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | class SpacingTestView: UIView {
4 | lazy var verticalItems: [UIView] = createVerticalItems()
5 | override func layoutSubviews() {
6 | super.layoutSubviews()
7 | _ = verticalItems
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/SpatialExample/MainVC/MainView/TestView/TestView+Create.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import With
3 | /**
4 | * Create
5 | */
6 | extension TestView {
7 | /**
8 | * Title
9 | */
10 | func createDescLabel() -> UILabel {
11 | with(.init()) {
12 | let text = "title"
13 | $0.text = "title"
14 | $0.font = .boldSystemFont(ofSize: 20.0)
15 | $0.textColor = .black
16 | $0.textAlignment = .left
17 | self.addSubview($0)
18 | $0.backgroundColor = UIColor.green.withAlphaComponent(0.5)
19 | $0.layer.borderWidth = 0.5
20 | $0.layer.borderColor = UIColor.black.cgColor
21 | let size: CGSize = text.size(withAttributes: [.font: UIFont.systemFont(ofSize: 18.0)])
22 | $0.anchorAndSize(to: self, width: size.width, align: .centerLeft, alignTo: .centerLeft, offset: .init(x: 20, y: 0) )
23 | }
24 | }
25 | /**
26 | * TextField
27 | */
28 | func createInputTextField() -> UITextField {
29 | with(.init()) {
30 | $0.font = .systemFont(ofSize: 16)
31 | $0.textColor = .gray
32 | $0.textAlignment = .right
33 | $0.tintColor = .blue
34 | $0.backgroundColor = UIColor.yellow.withAlphaComponent(0.5)
35 | $0.layer.borderWidth = 0.5
36 | $0.layer.borderColor = UIColor.green.cgColor
37 | self.self.addSubview($0)
38 | $0.activateConstraints { view in
39 | let y = Constraint.anchor(view, to: self, align: .centerY, alignTo: .centerY)
40 | let left = Constraint.anchor(view, to: descLabel, align: .left, alignTo: .right)
41 | left.priority = .init(rawValue: 250)
42 | let right = Constraint.anchor(view, to: self, align: .right, alignTo: .right, offset: -20)
43 | return [y, left, right]
44 | }
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/SpatialExample/MainVC/MainView/TestView/TestView.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import With
3 | /**
4 | * Out of order
5 | */
6 | class TestView: UIView {
7 | lazy var descLabel: UILabel = createDescLabel()
8 | lazy var inputTextField: UITextField = createInputTextField()
9 | override init(frame: CGRect) {
10 | super.init(frame: frame)
11 | self.backgroundColor = .purple
12 | _ = descLabel
13 | descLabel.text = "Desc"
14 | _ = inputTextField
15 | inputTextField.text = "Some details about"
16 | }
17 | /**
18 | * Boilerplate
19 | */
20 | @available(*, unavailable)
21 | required init?(coder aDecoder: NSCoder) {
22 | fatalError("init(coder:) has not been implemented")
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/SpatialExample/common/Constants.swift:
--------------------------------------------------------------------------------
1 | #if os(iOS)
2 | import UIKit
3 |
4 | /**
5 | * Move to own class, margins, insets etc
6 | * - Remark: Access all colors via: Constants.Colors.allCases
7 | */
8 | class Constants {
9 | enum Colors: String, CaseIterable {
10 | case blue = "FB1B4D"
11 | case yellow = "1DE3E6"
12 | case red = "22FFA0"
13 | case green = "FED845"
14 | var uiColor: UIColor {
15 | .init(hex: self.rawValue)
16 | }
17 | }
18 | }
19 | #endif
20 |
--------------------------------------------------------------------------------
/SpatialExample/common/Extension.swift:
--------------------------------------------------------------------------------
1 | #if os(iOS)
2 | import UIKit.UIColor
3 |
4 | extension UIColor {
5 | /**
6 | * ## Examples:
7 | * let color = UIColor(hex: "ff0000")
8 | */
9 | internal convenience init(hex: String) {
10 | let scanner = Scanner(string: hex)
11 | scanner.scanLocation = 0
12 | var rgbValue: UInt64 = 0
13 | scanner.scanHexInt64(&rgbValue)
14 | let red = (rgbValue & 0xff0000) >> 16
15 | let green = (rgbValue & 0xff00) >> 8
16 | let blue = rgbValue & 0xff
17 | self.init(
18 | red: CGFloat(red) / 0xff,
19 | green: CGFloat(green) / 0xff,
20 | blue: CGFloat(blue) / 0xff,
21 | alpha: 1
22 | )
23 | }
24 | }
25 | #endif
26 |
--------------------------------------------------------------------------------
/SpatialExampleMac/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | @NSApplicationMain
4 | class AppDelegate: NSObject, NSApplicationDelegate {
5 | @IBOutlet weak var window: NSWindow!
6 | /**
7 | * Creates the view
8 | */
9 | lazy var view: NSView = createView()
10 | func applicationDidFinishLaunching(_ aNotification: Notification) {
11 | _ = view
12 | }
13 | }
14 | extension AppDelegate {
15 | func createView() -> NSView {
16 | let contentRect = window.contentRect(forFrameRect: window.frame) // Get the size of the window without the title bar
17 | let view: MainView = .init(frame: contentRect) // Create a new instance of MainView with the specified frame
18 | window.contentView = view // Set the content view of the window to the MainView instance
19 | view.layer?.backgroundColor = NSColor.white.cgColor // Set the background color of the MainView instance to white
20 | return view // Return the MainView instance
21 | }
22 | }
23 | open class MainView: NSView {
24 | override open var isFlipped: Bool { true }/*TopLeft orientation*/
25 | /**
26 | * - Remark: animating to new alignments also works
27 | */
28 | override public init(frame: CGRect) {
29 | super.init(frame: frame)
30 | Swift.print("Hello world")
31 | self.wantsLayer = true/*if true then view is layer backed*/
32 | let box = Box()
33 | addSubview(box)
34 | box.applyAnchorAndSize(to: self, width: 100, height: 100)
35 | // swiftlint:disable multiline_arguments
36 | // - Fixme: ⚠️️ weakify the bellow?
37 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
38 | View.animate({
39 | box.update(offset: .init(x: 100, y: 0), align: .topLeft, alignTo: .topLeft)
40 | }, onComplete: { Swift.print("done") }, dur: 0.2)
41 | }
42 | // swiftlint:enable multiline_arguments
43 | }
44 | /**
45 | * Boilerplate
46 | */
47 | public required init?(coder decoder: NSCoder) {
48 | fatalError("init(coder:) has not been implemented")
49 | }
50 | }
51 | /**
52 | * Test box
53 | */
54 | final class Box: NSView, ConstraintKind {
55 | var anchorAndSize: AnchorAndSize?
56 | override init(frame frameRect: NSRect = .zero) {
57 | super.init(frame: frameRect)
58 | wantsLayer = true
59 | layer?.backgroundColor = NSColor.systemPurple.cgColor
60 | }
61 | /**
62 | * Boilerplate
63 | */
64 | required init?(coder: NSCoder) {
65 | fatalError("init(coder:) has not been implemented")
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/SpatialExampleMac/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "size" : "16x16",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "size" : "16x16",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "size" : "32x32",
16 | "scale" : "1x"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "size" : "32x32",
21 | "scale" : "2x"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "size" : "128x128",
26 | "scale" : "1x"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "size" : "128x128",
31 | "scale" : "2x"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "size" : "256x256",
36 | "scale" : "1x"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "size" : "256x256",
41 | "scale" : "2x"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "size" : "512x512",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "size" : "512x512",
51 | "scale" : "2x"
52 | }
53 | ],
54 | "info" : {
55 | "version" : 1,
56 | "author" : "xcode"
57 | }
58 | }
--------------------------------------------------------------------------------
/SpatialExampleMac/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/SpatialExampleMac/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | 1
23 | LSMinimumSystemVersion
24 | $(MACOSX_DEPLOYMENT_TARGET)
25 | NSHumanReadableCopyright
26 | Copyright © 2019 FutureLab. All rights reserved.
27 | NSMainNibFile
28 | MainMenu
29 | NSPrincipalClass
30 | NSApplication
31 |
32 |
33 |
--------------------------------------------------------------------------------
/SpatialExampleMac/SpatialExampleMac.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Tests/SpatialTests/SpatialTests.swift:
--------------------------------------------------------------------------------
1 | import SpatialLib
2 | #if canImport(XCTest)
3 | import XCTest
4 |
5 | class SpatialTests: XCTestCase {
6 | func test() {
7 | Self.testAlignment()
8 | }
9 | }
10 | // Test the alignment of objects within a canvas
11 | extension SpatialTests {
12 | fileprivate static func testAlignment() {
13 | // Test center-center alignment of a circle within a 400x300 rectangle
14 | let equalsA: Bool = Align.alignmentPoint(
15 | objectSize: .init(width: 200, height: 200), // The size of the object
16 | canvasSize: .init(width: 400, height: 300), // The size of the canvas
17 | canvasAlign: .centerCenter, // The alignment of the canvas
18 | objectAlign: .topLeft // The alignment of the object
19 | ) == .init(x: 200.0, y: 150.0) // The expected alignment point
20 | Swift.print("equalsA: \(equalsA ? "✅" : "🚫")")
21 | XCTAssertTrue(equalsA)
22 | // Test center-right alignment of a circle within a 400x300 rectangle
23 | let equalsB: Bool = Align.alignmentPoint(
24 | objectSize: .init(width: 200, height: 200), // The size of the object
25 | canvasSize: .init(width: 400, height: 300), // The size of the canvas
26 | canvasAlign: .centerRight, // The alignment of the canvas
27 | objectAlign: .centerRight // The alignment of the object
28 | ) == .init(x: 200.0, y: 50.0) // The expected alignment point
29 | Swift.print("equalsB: \(equalsB ? "✅" : "🚫")")
30 | XCTAssertTrue(equalsB)
31 | }
32 | }
33 | #endif
34 |
--------------------------------------------------------------------------------