├── .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 | ![mit](https://img.shields.io/badge/License-MIT-brightgreen.svg) 3 | ![platform](https://img.shields.io/badge/Platform-iOS/macOS-blue.svg) 4 | ![Lang](https://img.shields.io/badge/Language-Swift%205.0-orange.svg) 5 | [![SPM compatible](https://img.shields.io/badge/SPM-compatible-4BC51D.svg?style=flat)](https://github.com/apple/swift) 6 | ![Tests](https://github.com/eonist/Spatial/workflows/Tests/badge.svg) 7 | [![codebeat badge](https://codebeat.co/badges/d73a742e-037b-4423-8a02-fb8050a1c21d)](https://codebeat.co/projects/github-com-eonist-spatial-master) 8 | 9 | img 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 | --------------------------------------------------------------------------------