├── .github └── workflows │ └── Build.yml ├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── ConstraintsKit │ ├── Anchor.swift │ ├── Attribute.swift │ ├── Axis.swift │ ├── AxisX.swift │ ├── AxisY.swift │ ├── ConstraintKitError.swift │ ├── NSLayoutConstraint+Utils.swift │ ├── Relation.swift │ ├── SperviewObject.swift │ ├── UILayoutArea.swift │ ├── UIView+ConstraintsKit.swift │ └── UIView+RoundedCorners.swift ├── Tests ├── ConstraintsKitTests │ ├── ConstraintsKitTests.swift │ └── XCTestManifests.swift └── LinuxMain.swift └── logo-constraints_kit.png /.github/workflows/Build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: macOS-latest 6 | steps: 7 | - uses: actions/checkout@v1 8 | 9 | - name: Switch to Xcode 11 10 | run: sudo xcode-select --switch /Applications/Xcode_11.3.app 11 | # Since we want to be running our tests from Xcode, we need to 12 | # generate an .xcodeproj file. Luckly, Swift Package Manager has 13 | # build in functionality to do so. 14 | - name: Generate xcodeproj 15 | run: swift package generate-xcodeproj 16 | - name: Run tests 17 | run: xcodebuild test -destination 'name=iPhone 11' -scheme 'ConstraintsKit-Package' 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Astemir Eleev 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. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "ConstraintsKit", 8 | platforms: [ 9 | .iOS(.v12) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 13 | .library( 14 | name: "ConstraintsKit", 15 | targets: ["ConstraintsKit"]), 16 | ], 17 | dependencies: [ 18 | // Dependencies declare other packages that this package depends on. 19 | // .package(url: /* package url */, from: "1.0.0"), 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 23 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 24 | .target( 25 | name: "ConstraintsKit", 26 | dependencies: []), 27 | .testTarget( 28 | name: "ConstraintsKitTests", 29 | dependencies: ["ConstraintsKit"]), 30 | ], 31 | swiftLanguageVersions: [ 32 | .v5 33 | ] 34 | ) 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # constraints-kit [![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/sindresorhus/awesome) 2 | 3 | [![Build](https://github.com/jvirus/constraints-kit/workflows/Build/badge.svg)]() 4 | [![Platform](https://img.shields.io/badge/Platform-iOS-yellow.svg)]() 5 | [![Language](https://img.shields.io/badge/Language-Swift_5.1-orange.svg)]() 6 | [![SPM](https://img.shields.io/badge/SPM-Supported-lightblue.svg)]() 7 | [![License](https://img.shields.io/badge/License-MIT-blue.svg)]() 8 | 9 | **Last Update: 02/January/2020.** 10 | 11 | ![](logo-constraints_kit.png) 12 | 13 | ### If you like the project, please give it a star ⭐ It will show the creator your appreciation and help others to discover the repo. 14 | 15 | # ✍️ About 16 | 🏗 Declarative, Chainable & Lightweight Auto Layout constraints framework for iOS. The framework offers a rich set of methods for defining `Auto Layout` constraints (see `Contents`) without any other external dependencies. 17 | 18 | # 💡 Movitation 19 | The purpose of this framework is to provide a very lightweight solution for `Auto Layout` and to make the development of programmatic `UI` much simpler, hiding the boilerplate code under the framework. Primary usage is for internal developments, however I decided to share the framework with the community, since it offers some uniqueness and I'm going to continue work on this development. 20 | 21 | # 📺 Demo 22 | 23 | In order to create something like in the following screenshot: 24 | 25 | 26 | 27 | You need to write just a few lines of code: 28 | 29 | ```swift 30 | // 1. First you need to add all your views somewhere. That means your views must have a superview. Let's assume that you have done that. 31 | 32 | // 2. Then we assume that visually your views are fully configured. 33 | 34 | // 3. And finally, all we need to do is to specify the constraints: 35 | 36 | cardView.pinInside(view: self.view, offset: 16) 37 | 38 | imageView.pinInside(view: cardView, offset: 8) 39 | 40 | blurView.pinInside(view: cardView) 41 | 42 | titleLabel.pinTopToTopCenter(of: imageView, offset: 24) 43 | 44 | button 45 | .bottom(with: imageView, anchor: .bottom, offset: -34) 46 | .center(in: imageView, axis: .horizontal) 47 | .set(height: 60) 48 | .set(aspect: 2/1) 49 | 50 | label 51 | .center(in: imageView) 52 | .left(with: imageView, anchor: .left, offset: 16) 53 | .right(with: imageView, anchor: .right, offset: -16) 54 | ``` 55 | 56 | # 🏗 Installation 57 | 58 | ## Swift Package Manager 59 | 60 | ### Xcode 11+ 61 | 62 | 1. Open `MenuBar` → `File` → `Swift Packages` → `Add Package Dependency...` 63 | 2. Paste the package repository url `https://github.com/jVirus/constraints-kit` and hit `Next`. 64 | 3. Select the installment rules. 65 | 66 | After specifying which version do you want to install, the package will be downloaded and attached to your project. 67 | 68 | ### Package.swift 69 | If you already have a `Package.swift` or you are building your own package simply add a new dependency: 70 | 71 | ```swift 72 | dependencies: [ 73 | .package(url: "`https://github.com/jVirus/constraints-kit", from: "1.0.0") 74 | ] 75 | ``` 76 | 77 | ## Manual 78 | You can always use copy-paste the sources method 😄. Or you can compile the framework and include it with your project. 79 | 80 | 81 | # ✈️ Usage 82 | The framework is pretty easy to use, however I do recommend to learn the basics of `Auto Layout` before installing the framework - it will help you to understand what the minimum number of constraints a `UIView` needs to have and to avoid common pitfalls. 83 | 84 | #### Import 85 | After adding the framework to a project, simply add an import statemt: 86 | 87 | ```swift 88 | import ConstraintsKit 89 | ``` 90 | 91 | #### Setting size 92 | 93 | ```swift 94 | view.set(size: CGSize(width: 300, height: 300)) 95 | ``` 96 | 97 | ```swift 98 | view.set(width: 400) 99 | ``` 100 | 101 | ```swift 102 | view.set(height: 200) 103 | ``` 104 | 105 | ```swift 106 | view 107 | .set(width: 200) 108 | .set(aspect: 2/1) 109 | ``` 110 | 111 | #### Constraining 112 | 113 | A `UIImageView` fills the parent `UIView` with offset, until the bottom anchor reaches the `top` anchor of the `UIButton`: 114 | 115 | ```swift 116 | imageView 117 | .constrain(using: .top, to: .top, of: view, offset: 24) 118 | .constrain(using: .right, to: .right, of: view, offset: -24) 119 | .constrain(using: .left, to: .left, of: view, offset: 24) 120 | .constrain(using: .bottom, to: .top, of: button, offset: -24) 121 | ``` 122 | 123 | Also you can remove the `of: view` part in cases when you want to anchor a `view` to its `superview`: 124 | ```swift 125 | imageView 126 | .constrain(using: .top, to: .top, , offset: 24) 127 | .constrain(using: .right, to: .right, offset: -24) 128 | .constrain(using: .left, to: .left, offset: 24) 129 | .constrain(using: .bottom, to: .top, offset: -24) 130 | ``` 131 | 132 | A `UIImageView` is anchored at the center of the parent view, it's stretched to the `horizontal` axis by anchoring `left` & `right` sides with the `aspect ratio` of `3 to 2`: 133 | 134 | ```swift 135 | imageView 136 | .center(in: view, axis: .vertical) 137 | .left(with: view, anchor: .left) 138 | .right(with: view, anchor: .right) 139 | .set(aspect: 3/2) 140 | ``` 141 | 142 | #### Anchoring 143 | 144 | A custom `UIView` fills a `UICollectionViewCell` to the `top` system spacing, with custom `left` & `right` offsets and to the `top` anchor of the `UIButton`: 145 | 146 | ```swift 147 | presenterView 148 | .topToSystemSpacing(with: view, anchor: .top) 149 | .right( with: view, anchor: .right, offset: -16) 150 | .left( with: view, anchor: .left, offset: 16) 151 | .bottom( with: button, anchor: .top, offset: -16) 152 | ``` 153 | 154 | A `UIButton` is placed at the `center` of the parent view, its `bottom` anchor is attached to the bottom of the parent view, `height` is set to `60` with `aspect ratio` of `2 to 1`: 155 | 156 | ```swift 157 | button 158 | .bottom(with: imageView, anchor: .bottom, offset: -34) 159 | .center(in: imageView, axis: .horizontal) 160 | .set(height: 60) 161 | .set(aspect: 2/1) 162 | ``` 163 | 164 | #### Pinning 165 | 166 | A custom `ActivityIndicator` view is anchored to the top left corner of the specified view with some `offset` and `size` equals to `20 to 20`: 167 | 168 | ```swift 169 | activityIndicator 170 | .pinInsideToTopLeftCorner(of: view, offset: 24) 171 | .size(CGSize(width: 20, height: 20)) 172 | ``` 173 | 174 | Advanced case where top left anchor of a custom `ProgressView` is attached to the top leading corner of the parent view and the bottom right anchor is attached to the top right anchor of the `LogIn` button: 175 | 176 | ```swift 177 | progressView 178 | .pin(anchors: [.left, .top], toTargetView: view, using: [.leading, .top]) 179 | .pin(anchors: [.bottom, .right], toTargetView: button, using: [.right, .top]) 180 | ``` 181 | 182 | ### Filling 183 | 184 | A `UITableView` is placed inside the parent `UIView` and fills the top half of it (using `left`, `top`, `right` and `centerX` anchors): 185 | 186 | ```swift 187 | tableView.fillTopHalf(of: parentView) 188 | ``` 189 | 190 | A `UICollectionView` is placed inside the parent `UIView` and fills the left half of it with the specified offset (using `left`, `top`, `bottom` and `centerY` anchors): 191 | 192 | ```swift 193 | collectionView.fillLeftHalf(of: splitView, offset: 16) 194 | ``` 195 | 196 | 197 | # 📝 Contents 198 | The kit contains several groups of methods, each with a specific purpose. All the groups can be chained together, however you should keep in mind that if you have conflicting constraints or you don't provide enough information for the `Auto Layout` engine, you will see no effect or some unexpected results. It's assumed that you already know the basics of `Auto Layout`. 199 | 200 | #### Common 201 | 202 | - `constrain` - constrains `self` using the specified `Attribute` to the specified `Attribute` with respect to the related `UIView`. You may set `Relation` (which is by default `.equal`), `offset` (default is `0.0`) and `multiplier` (default is `1.0`) 203 | - `fit` - places `self` inside the specified `UIView` with an optional `offset` (default is `0.0`) 204 | - `center` - centers `self` inside the specified `UIView` using a concrete `Axis` case, with an optional `multiplier` (default is `1.0`) 205 | - `width` - applies width equalization between `self` and the specified `UIView`. You may change the `Relation` (default is `equal`), `UILayoutPriority` (default is `required`), `multiplier` (default is `1.0`) and `constant` (default is `0.0`) 206 | - `height` - applies height equalization between `self` and the specified `UIView`. You may change the `Relation` (default is `equal`), `UILayoutPriority` (default is `required`), `multiplier` (default is `1.0`) and `constant` (default is `0.0`) 207 | 208 | #### Setters 209 | 210 | - `set(size:)` - sets a new `CGSize` for `self` by applying layout constaints for `width` & `height` anchors 211 | - `set(width:)` - sets a new `width` by applying layout constaint for `width` anchor 212 | - `set(height:)` - sets a new `height` by applying layout constraint for `height` anchor 213 | - `set(aspect:)` - sets a new `aspect ratio` by applying layout constaint for `aspect` anchor 214 | - `set(aspectOf:)` - sets a new `aspect ratio` by duplicating `aspect` of the specified `UIView` 215 | - `set(value: to:)` - sets a new offset `value` for the `Attribute` 216 | 217 | #### Anchoring 218 | 219 | - `top` - anchors top anchor to the specified `UIView` using `AxisY` anchor, `Relation` (defatul is `.equal`), NSLayoutPriority (default is `.required`) and `offset` (default is `0.0`) 220 | - `bottom` - anchors bottom anchor to the specified `UIView` using `AxisY` anchor, `Relation` (defatul is `.equal`), NSLayoutPriority (default is `.required`) and `offset` (default is `0.0`) 221 | - `left` - anchors left anchor to the specified `UIView` using `AxisY` anchor, `Relation` (defatul is `.equal`), NSLayoutPriority (default is `.required`) and `offset` (default is `0.0`) 222 | - `right` - anchors right anchor to the specified `UIView` using `AxisY` anchor, `Relation` (defatul is `.equal`), NSLayoutPriority (default is `.required`) and `offset` (default is `0.0`) 223 | 224 | #### Anchoring to System Spacing 225 | 226 | - `rightToSystemSpacing` - anchors right anchor to the specified `UIView` with respect to System Spacing using `AxisY` anchor, `Relation` (defatul is `.equal`), NSLayoutPriority (default is `.required`) and `offset` (default is `0.0`) 227 | - `leftToSystemSpacing` - anchors left anchor to the specified `UIView` with respect to System Spacing using `AxisY` anchor, `Relation` (defatul is `.equal`), NSLayoutPriority (default is `.required`) and `offset` (default is `0.0`) 228 | - `topToSystemSpacing` - anchors top anchor to the specified `UIView` with respect to System Spacing using `AxisY` anchor, `Relation` (defatul is `.equal`), NSLayoutPriority (default is `.required`) and `offset` (default is `0.0`) 229 | - `bottomToSystemSpacing` - anchors bottom anchor to the specified `UIView` with respect to System Spacing using `AxisY` anchor, `Relation` (defatul is `.equal`), NSLayoutPriority (default is `.required`) and `offset` (default is `0.0`) 230 | 231 | #### Pinning 232 | 233 | - `pinTopLeftToTopLeftCorner` - pins Top Left anchor to the Top Left corner of the specified `UIView` with a given `offset` (default is `0.0`) 234 | - `pinTopRightToTopRightCorner` - pins Top Right anchor to the Top Right corner of the specified `UIView` with a given `offset` (default is `0.0`) 235 | - `pinBottomRightToBottomRight` - pins Bottom Right anchor to the Bottom Right corner of the specified `UIView` with a given `offset` (default is `0.0`) 236 | - `pinBottomLeftToBottomLeft` - pins Bottom Left anchor to the Bottom Left corner of the specified `UIView` with a given `offset` (default is `0.0`) 237 | - `pinBottomRightToTopLeft` - pins Bottom Right anchor to the Top Left corner of the specified `UIView` with a given `offset` (default is `0.0`) 238 | - `pinBottomLeftToTopRight` - pins Bottom Left anchor to the Top Right corner of the specified `UIView` with a given `offset` (default is `0.0`) 239 | - `pinTopLeftToBottomRight` - pins Top Left anchor to the Bottom Right corner of the specified `UIView` with a given `offset` (default is `0.0`) 240 | - `pinBottomRightToTopLeft` - pins Bottom Right anchor to the Top Left corner of the specified `UIView` with a given `offset` (default is `0.0`) 241 | - `pinTopToTopCenter` - pins Top anchor to the Top Center anchor of the specified `UIView` with a given `offset` (default is `0.0`) 242 | - `pinBottomToBottomCenter` - pins Bottom anchor to the Bottom Center anchor of the specified `UIView` with a given `offset` (default is `0.0`) 243 | - `pinLeftToLeftCenter` - pins Left anchor to the Left Center anchor of the specified `UIView` with a given `offset` (default is `0.0`) 244 | - `pinRightToRightCenter` - pins Right anchor to the Right Center anchor of the specified `UIView` with a given `offset` (default is `0.0`) 245 | - `pinInside` - pins `self` inside the specified `UIView` with `Relation` (default is `.equal`), UILayoutPriority (default is `.required`) and `offset` (default is `0.0`) 246 | - `pinTo` - pins `self` to the specified `UIView` by using `Anchor` (which is an `OptionSet`) 247 | - `pin(anchors:, toTargetView: , using:)` - pins the specified `Anchors` of `self` to the `UIView` by using the related `Anchors` 248 | 249 | #### Filling 250 | 251 | - `fillBottomHalf` - fills the bottom half of the specified view by `self` with the given `offset` (default is `0.0`) 252 | - `fillTopHalf` - fills the top half of the specified view by `self` with the given `offset` (default is `0.0`) 253 | - `fillLeftHalf` - fills the left half of the specified view by `self` with the given `offset` (default is `0.0`) 254 | - `fillRightHalf` - fills the right half of the specified view by `self` with the given `offset` (default is `0.0`) 255 | 256 | # 👨‍💻 Author 257 | [Astemir Eleev](https://github.com/jVirus) 258 | 259 | # 🔖 Licence 260 | The project is available under [MIT Licence](https://github.com/jVirus/collection-flow-layout-ios/blob/master/LICENSE) 261 | -------------------------------------------------------------------------------- /Sources/ConstraintsKit/Anchor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Anchor.swift 3 | // ConstraintsKit 4 | // 5 | // Created by Astemir Eleev on 08/11/2018. 6 | // Copyright © 2018 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public struct Anchor: OptionSet { 12 | 13 | // MARK: Conformance to OptionSet protoocl 14 | 15 | public let rawValue: UInt 16 | 17 | // MARK: - Properties 18 | 19 | public static let left = Anchor(rawValue: 1 << 0) 20 | public static let right = Anchor(rawValue: 1 << 1) 21 | public static let top = Anchor(rawValue: 1 << 2) 22 | public static let bottom = Anchor(rawValue: 1 << 3) 23 | public static let width = Anchor(rawValue: 1 << 4) 24 | public static let height = Anchor(rawValue: 1 << 5) 25 | public static let centerX = Anchor(rawValue: 1 << 6) 26 | public static let centerY = Anchor(rawValue: 1 << 7) 27 | public static let lastBaseline = Anchor(rawValue: 1 << 8) 28 | public static let firstBaseline = Anchor(rawValue: 1 << 9) 29 | public static let trailing = Anchor(rawValue: 1 << 10) 30 | public static let leading = Anchor(rawValue: 1 << 11) 31 | 32 | // MARK: - Private properties 33 | 34 | private static let allCases: [Anchor] = [.left, .right, .top, .bottom, .width, .height, .centerX, .centerY, .lastBaseline, .firstBaseline, .trailing, .leading] 35 | 36 | // MARK: - Initializers 37 | 38 | public init(rawValue: UInt) { 39 | self.rawValue = rawValue 40 | } 41 | 42 | // MARK: - Methods 43 | 44 | func convert() -> [NSLayoutConstraint.Attribute] { 45 | var constraints = [NSLayoutConstraint.Attribute]() 46 | 47 | for `case` in Anchor.allCases where contains(`case`) { 48 | switch `case` { 49 | case .top: 50 | constraints += [.top] 51 | case .bottom: 52 | constraints += [.bottom] 53 | case .left: 54 | constraints += [.left] 55 | case .right: 56 | constraints += [.right] 57 | case .centerX: 58 | constraints += [.centerX] 59 | case .centerY: 60 | constraints += [.centerY] 61 | case .firstBaseline: 62 | constraints += [.firstBaseline] 63 | case .lastBaseline: 64 | constraints += [.lastBaseline] 65 | case .trailing: 66 | constraints += [.trailing] 67 | case .leading: 68 | constraints += [.leading] 69 | case .width: 70 | constraints += [.width] 71 | case .height: 72 | constraints += [.height] 73 | default: 74 | continue 75 | } 76 | } 77 | return constraints 78 | } 79 | } 80 | 81 | 82 | -------------------------------------------------------------------------------- /Sources/ConstraintsKit/Attribute.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Attribute.swift 3 | // ConstraintsKit 4 | // 5 | // Created by Astemir Eleev on 06/11/2018. 6 | // Copyright © 2018 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | public enum Attribute { 13 | case top, bottom, left, right, aspect, width, height, centerX, centerY, lastBaseline, firstBaseline, leading, trailing, notAnAttribute 14 | } 15 | 16 | extension Attribute { 17 | 18 | func convert() -> NSLayoutConstraint.Attribute { 19 | switch self { 20 | case .top: 21 | return .top 22 | case .bottom: 23 | return .bottom 24 | case .left: 25 | return .leading 26 | case .right: 27 | return .trailing 28 | case .width: 29 | return .width 30 | case .height: 31 | return .height 32 | case .centerX: 33 | return .centerX 34 | case .centerY: 35 | return .centerY 36 | case .lastBaseline: 37 | return .lastBaseline 38 | case .firstBaseline: 39 | return .firstBaseline 40 | case .leading: 41 | return .leading 42 | case .trailing: 43 | return .trailing 44 | default: 45 | return .notAnAttribute 46 | } 47 | } 48 | 49 | func toAxisX() -> AxisX? { 50 | switch self { 51 | case .left: 52 | return AxisX.left 53 | case .right: 54 | return AxisX.right 55 | default: 56 | return nil 57 | } 58 | } 59 | 60 | func toAxisY() -> AxisY? { 61 | switch self { 62 | case .top: 63 | return AxisY.top 64 | case .bottom: 65 | return AxisY.bottom 66 | default: 67 | return nil 68 | } 69 | } 70 | 71 | func toAxis() -> Axis? { 72 | switch self { 73 | case .centerX: 74 | return Axis.horizontal 75 | case .centerY: 76 | return Axis.vertical 77 | default: 78 | return nil 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/ConstraintsKit/Axis.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Axis.swift 3 | // ConstraintsKit 4 | // 5 | // Created by Astemir Eleev on 06/11/2018. 6 | // Copyright © 2018 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol AxisType { 12 | // Empty protocol, used as Marker Design Pattern 13 | } 14 | 15 | public enum Axis: AxisType { 16 | case horizontal, vertical 17 | } 18 | 19 | extension Axis { 20 | 21 | func convert() -> Attribute { 22 | switch self { 23 | case .horizontal: 24 | return .centerX 25 | case .vertical: 26 | return .centerY 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/ConstraintsKit/AxisX.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AxisX.swift 3 | // ConstraintsKit 4 | // 5 | // Created by Astemir Eleev on 06/11/2018. 6 | // Copyright © 2018 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum AxisX: AxisType { 12 | case left, right 13 | } 14 | -------------------------------------------------------------------------------- /Sources/ConstraintsKit/AxisY.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AxisY.swift 3 | // ConstraintsKit 4 | // 5 | // Created by Astemir Eleev on 06/11/2018. 6 | // Copyright © 2018 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum AxisY: AxisType { 12 | case top, bottom 13 | } 14 | -------------------------------------------------------------------------------- /Sources/ConstraintsKit/ConstraintKitError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConstraintKitError.swift 3 | // ConstraintsKit 4 | // 5 | // Created by Astemir Eleev on 02/01/2019. 6 | // Copyright © 2019 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum ConstraintsKitError: Error { 12 | case missingSuperview 13 | } 14 | -------------------------------------------------------------------------------- /Sources/ConstraintsKit/NSLayoutConstraint+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSLayoutConstraint+Utils.swift 3 | // ConstraintsKit 4 | // 5 | // Created by Astemir Eleev on 06/11/2018. 6 | // Copyright © 2018 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | extension NSLayoutConstraint { 13 | 14 | @discardableResult public func set(priority: UILayoutPriority, isActive: Bool) -> Self { 15 | self.priority = priority 16 | self.isActive = isActive 17 | return self 18 | } 19 | 20 | @discardableResult public func set(priority: UILayoutPriority) -> Self { 21 | self.priority = priority 22 | return self 23 | } 24 | 25 | @discardableResult public func activate() -> Self { 26 | isActive = true 27 | return self 28 | } 29 | 30 | @discardableResult public func deactivate() -> Self { 31 | isActive = false 32 | return self 33 | } 34 | 35 | } 36 | 37 | extension NSLayoutConstraint.Attribute { 38 | func convert() -> Attribute { 39 | switch self { 40 | case .left: 41 | return .left 42 | case .right: 43 | return .right 44 | case .top: 45 | return .top 46 | case .bottom: 47 | return .bottom 48 | case .centerX: 49 | return .centerX 50 | case .centerY: 51 | return .centerY 52 | case .firstBaseline: 53 | return .firstBaseline 54 | case .lastBaseline: 55 | return .lastBaseline 56 | case .height: 57 | return .height 58 | case .width: 59 | return .width 60 | case .leading: 61 | return .leading 62 | case .trailing: 63 | return .trailing 64 | default: 65 | return .notAnAttribute 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/ConstraintsKit/Relation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Relation.swift 3 | // ConstraintsKit 4 | // 5 | // Created by Astemir Eleev on 06/11/2018. 6 | // Copyright © 2018 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | public enum Relation: Int { 13 | case lessThanOrEqual = -1 14 | case equal = 0 15 | case greaterThanOrEqual = 1 16 | } 17 | 18 | extension Relation { 19 | 20 | func convert() -> NSLayoutConstraint.Relation { 21 | switch self { 22 | case .equal: 23 | return .equal 24 | case .lessThanOrEqual: 25 | return .lessThanOrEqual 26 | case .greaterThanOrEqual: 27 | return .greaterThanOrEqual 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/ConstraintsKit/SperviewObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SperviewObject.swift 3 | // ConstraintsKit 4 | // 5 | // Created by Astemir Eleev on 02/01/2019. 6 | // Copyright © 2019 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import UIKit.UIView 10 | 11 | /// Acts as a `NullObject` pattern but for the `UIView`, in order to be able to use a default parameter in methods that use superview as the default value. The class intends to be more descriptive than optional parameter with the default nil values. 12 | public class SuperviewObject: UIView { 13 | // Empty implementation 14 | } 15 | -------------------------------------------------------------------------------- /Sources/ConstraintsKit/UILayoutArea.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UILayoutArea.swift 3 | // ConstraintsKit 4 | // 5 | // Created by Astemir Eleev on 08/11/2018. 6 | // Copyright © 2018 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | /// The following struct is an experimental development and is not used anywhere in the framework. It was intentionally marked as `private` so you will not be able to accidentally get access to this struct. Please, don't use or try to modify this struct. If a modification will be made, such a pull requrest will be declined. 13 | private struct UILayoutArea: OptionSet { 14 | 15 | // MARK: - Conformance to OptionSet protocol 16 | 17 | public let rawValue: UInt 18 | 19 | // MARK: - Properties 20 | 21 | public static let topLeft = UILayoutArea(rawValue: 1 << 0) 22 | public static let topRight = UILayoutArea(rawValue: 1 << 1) 23 | public static let topCenter = UILayoutArea(rawValue: 1 << 2) 24 | public static let midLeft = UILayoutArea(rawValue: 1 << 3) 25 | public static let midRight = UILayoutArea(rawValue: 1 << 4) 26 | public static let bottomLeft = UILayoutArea(rawValue: 1 << 5) 27 | public static let bottomRight = UILayoutArea(rawValue: 1 << 6) 28 | public static let bottomCenter = UILayoutArea(rawValue: 1 << 7) 29 | 30 | // MARK: - Private properties 31 | 32 | private static let allCases: [UILayoutArea] = [ .topLeft, .topRight, .topCenter, .midLeft, .midRight, .bottomLeft, .bottomRight, .bottomCenter] 33 | 34 | // MARK: - Initialiezers 35 | 36 | public init(rawValue: UInt) { 37 | self.rawValue = rawValue 38 | } 39 | 40 | // MARK: - Methods 41 | 42 | internal func convert() -> [(AxisType, AxisType)] { 43 | var areas = [(AxisType, AxisType)]() 44 | 45 | for `case` in UILayoutArea.allCases where contains(`case`) { 46 | switch self { 47 | case .topLeft: 48 | areas += [(AxisY.top, AxisX.left)] 49 | case .topRight: 50 | areas += [(AxisY.top, AxisX.right)] 51 | case .topCenter: 52 | areas += [(AxisY.top, Axis.horizontal)] 53 | case .midLeft: 54 | areas += [(Axis.vertical, AxisX.left)] 55 | case .midRight: 56 | areas += [(Axis.vertical, AxisX.right)] 57 | case .bottomLeft: 58 | areas += [(AxisY.bottom, AxisX.left)] 59 | case .bottomRight: 60 | areas += [(AxisY.bottom, AxisX.right)] 61 | case .bottomCenter: 62 | areas += [(AxisY.bottom, Axis.horizontal)] 63 | default: 64 | continue 65 | } 66 | } 67 | 68 | return areas 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/ConstraintsKit/UIView+ConstraintsKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+ConstraintsKit.swift 3 | // extensions-kit 4 | // 5 | // Created by Astemir Eleev on 15/10/2018. 6 | // Copyright © 2018 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIView { 12 | 13 | // MARK: Methods 14 | 15 | // MARK: - Constraining 16 | 17 | /// Constrains `self` using the specified `Attribute` to the specified `Attribute` with respect to the related `UIView`. You may set `Relation` (which is by default `.equal`), `offset` (default is `0.0`) and `multiplier` (default is `1.0`) 18 | @discardableResult public func constrain(using attribute: Attribute, to viewAttribute: Attribute, of relatedView: UIView, relatedBy relation: Relation = .equal, offset: CGFloat = 0, multiplier: CGFloat = 1) -> UIView { 19 | enableAutoLayout() 20 | 21 | if let superview = self.superview { 22 | let constraint = NSLayoutConstraint(item: self, 23 | attribute: attribute.convert(), 24 | relatedBy: relation.convert(), 25 | toItem: relatedView, 26 | attribute: viewAttribute.convert(), 27 | multiplier: multiplier, 28 | constant: offset) 29 | superview.addConstraint(constraint) 30 | } 31 | return self 32 | } 33 | 34 | /// Places `self` inside the specified `UIView` with an optional `offset` (default is `0.0`) 35 | public func fit(inside view: UIView = SuperviewObject(), offset: CGFloat = 0) throws { 36 | let constrainView = try resolveSuperviewObject(for: view) 37 | 38 | constrain(using: .top, to: .top, of: constrainView, offset: offset, multiplier: 1) 39 | .constrain(using: .bottom, to: .bottom, of: constrainView, offset: -offset, multiplier: 1) 40 | .constrain(using: .left, to: .left, of: constrainView, offset: offset, multiplier: 1) 41 | .constrain(using: .right, to: .right, of: constrainView, offset: -offset, multiplier: 1) 42 | } 43 | 44 | /// Centers `self` inside the specified `UIView` 45 | @discardableResult public func center(in view: UIView = SuperviewObject()) throws -> UIView { 46 | let constrainView = try resolveSuperviewObject(for: view) 47 | enableAutoLayout() 48 | 49 | centerXAnchor.constraint(equalTo: constrainView.centerXAnchor).isActive = true 50 | centerYAnchor.constraint(equalTo: constrainView.centerYAnchor).isActive = true 51 | 52 | return self 53 | } 54 | 55 | /// Centers `self` inside the specified `UIView` using a concrete `Axis` case, with an optional `multiplier` (default is `1.0`) 56 | @discardableResult public func center(in view: UIView = SuperviewObject(), axis: Axis, multiplier: CGFloat = 1.0) throws -> UIView { 57 | let constrainView = try resolveSuperviewObject(for: view) 58 | enableAutoLayout() 59 | 60 | let anchor = axis.convert() 61 | constrain(using: anchor, to: anchor, of: constrainView, offset: 0, multiplier: multiplier) 62 | return self 63 | } 64 | 65 | /// Applies width equalization between `self` and the specified `UIView`. You may change the `Relation` (default is `equal`), `UILayoutPriority` (default is `required`), `multiplier` (default is `1.0`) and `constant` (default is `0.0`) 66 | @discardableResult public func width(to view: UIView = SuperviewObject(), 67 | relatedBy relatioin: Relation = .equal, 68 | priority: UILayoutPriority = .required, 69 | multiplier: CGFloat = 1.0, 70 | constant: CGFloat = 0.0) throws -> UIView { 71 | let constrainView = try resolveSuperviewObject(for: view) 72 | enableAutoLayout() 73 | 74 | let constraint: NSLayoutConstraint 75 | 76 | switch relatioin { 77 | case .equal: 78 | constraint = self.widthAnchor.constraint(equalTo: constrainView.widthAnchor, multiplier: multiplier, constant: constant) 79 | case .lessThanOrEqual: 80 | constraint = self.widthAnchor.constraint(lessThanOrEqualTo: constrainView.widthAnchor, multiplier: multiplier, constant: constant) 81 | case .greaterThanOrEqual: 82 | constraint = self.widthAnchor.constraint(greaterThanOrEqualTo: constrainView.widthAnchor, multiplier: multiplier, constant: constant) 83 | } 84 | constraint.set(priority: priority, isActive: true) 85 | 86 | return self 87 | } 88 | 89 | /// Applies height equalization between `self` and the specified `UIView`. You may change the `Relation` (default is `equal`), `UILayoutPriority` (default is `required`), `multiplier` (default is `1.0`) and `constant` (default is `0.0`) 90 | @discardableResult public func height(to view: UIView = SuperviewObject(), 91 | relatedBy relatioin: Relation = .equal, 92 | priority: UILayoutPriority = .required, 93 | multiplier: CGFloat = 1.0, 94 | constant: CGFloat = 0.0) throws -> UIView { 95 | let constrainView = try resolveSuperviewObject(for: view) 96 | 97 | enableAutoLayout() 98 | let constraint: NSLayoutConstraint 99 | 100 | switch relatioin { 101 | case .equal: 102 | constraint = self.heightAnchor.constraint(equalTo: constrainView.heightAnchor, multiplier: multiplier, constant: constant) 103 | case .lessThanOrEqual: 104 | constraint = self.widthAnchor.constraint(lessThanOrEqualTo: constrainView.heightAnchor, multiplier: multiplier, constant: constant) 105 | case .greaterThanOrEqual: 106 | constraint = self.widthAnchor.constraint(greaterThanOrEqualTo: constrainView.heightAnchor, multiplier: multiplier, constant: constant) 107 | } 108 | constraint.set(priority: priority, isActive: true) 109 | 110 | return self 111 | } 112 | 113 | // MARK: - Setting 114 | 115 | /// Sets a new `CGSize` for `self` by applying layout constaints for `width` & `height` anchors 116 | @discardableResult public func set(size: CGSize) -> UIView { 117 | set(width: size.width) 118 | set(height: size.height) 119 | return self 120 | } 121 | 122 | /// Sets a new `width` by applying layout constaint for `width` anchor 123 | @discardableResult public func set(width value: CGFloat) -> UIView { 124 | set(value: value, to: .width) 125 | return self 126 | } 127 | 128 | /// Sets a new `height` by applying layout constraint for `height` anchor 129 | @discardableResult public func set(height value: CGFloat) -> UIView { 130 | set(value: value, to: .height) 131 | return self 132 | } 133 | 134 | /// Sets a new `aspect ratio` by applying layout constaint for `aspect` anchor 135 | @discardableResult public func set(aspect value: CGFloat) -> UIView { 136 | set(value: value, to: .aspect) 137 | return self 138 | } 139 | 140 | /// Sets a new `aspect ratio` by duplicating `aspect` of the specified `UIView` 141 | @discardableResult public func set(aspectOf view: UIView) -> UIView { 142 | set(aspect: view.aspect) 143 | return self 144 | } 145 | 146 | /// Sets a new offset `value` for the `Attribute` 147 | @discardableResult public func set(value: CGFloat, to attribute: Attribute) -> UIView { 148 | guard let superview = self.superview else { return self } 149 | enableAutoLayout() 150 | 151 | let constraint = attribute != .aspect ? 152 | NSLayoutConstraint(item: self, attribute: attribute.convert() , relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: value) : 153 | NSLayoutConstraint(item: self, attribute: .width, relatedBy: .equal, toItem: self, attribute: .height, multiplier: value, constant: 0) 154 | superview.addConstraint(constraint) 155 | 156 | return self 157 | } 158 | } 159 | 160 | private extension UIView { 161 | 162 | private func enableAutoLayout() { 163 | translatesAutoresizingMaskIntoConstraints = false 164 | } 165 | 166 | private func anchored(to view: UIView, using anchor: AxisY) -> NSLayoutAnchor { 167 | guard #available(iOS 11.0, *) else { 168 | return (anchor == .bottom) ? view.layoutMarginsGuide.bottomAnchor : view.layoutMarginsGuide.topAnchor 169 | } 170 | return (anchor == .bottom) ? view.safeAreaLayoutGuide.bottomAnchor : view.safeAreaLayoutGuide.topAnchor 171 | } 172 | 173 | private func anchored(to view: UIView, using anchor: AxisX, useEdge edge: Bool = false) -> NSLayoutAnchor { 174 | guard #available(iOS 11.0, *) else { 175 | if edge { 176 | return (anchor == .left) ? view.layoutMarginsGuide.leadingAnchor : view.layoutMarginsGuide.trailingAnchor 177 | } else { 178 | return (anchor == .left) ? view.layoutMarginsGuide.leftAnchor : view.layoutMarginsGuide.rightAnchor 179 | } 180 | } 181 | 182 | if edge { 183 | return (anchor == .left) ? view.safeAreaLayoutGuide.leadingAnchor : view.safeAreaLayoutGuide.trailingAnchor 184 | } else { 185 | return (anchor == .left) ? view.safeAreaLayoutGuide.leftAnchor : view.safeAreaLayoutGuide.rightAnchor 186 | } 187 | } 188 | 189 | private func resolveSuperviewObject(for view: UIView) throws -> UIView { 190 | var constrainView = view 191 | 192 | switch view { 193 | case is SuperviewObject: 194 | guard let superview = self.superview else { throw ConstraintsKitError.missingSuperview } 195 | constrainView = superview 196 | default: () 197 | } 198 | return constrainView 199 | } 200 | } 201 | 202 | extension UIView { 203 | 204 | public var width: CGFloat { 205 | return bounds.width 206 | } 207 | 208 | public var height: CGFloat { 209 | return bounds.height 210 | } 211 | 212 | public var size: CGSize { 213 | return CGSize(width: width, height: height) 214 | } 215 | 216 | public var aspect: CGFloat { 217 | return bounds.width / bounds.height 218 | } 219 | } 220 | 221 | // MARK: - Anchoring 222 | extension UIView { 223 | 224 | /// Anchors top anchor to the specified `UIView` using `AxisY` anchor, `Relation` (defatul is `.equal`), NSLayoutPriority (default is `.required`) and `offset` (default is `0.0`) 225 | @discardableResult public func top(with view: UIView = SuperviewObject(), 226 | anchor: AxisY, 227 | relatedBy relation: Relation = .equal, 228 | priority: UILayoutPriority = .required, 229 | offset: CGFloat = 0) throws -> UIView { 230 | let constaintView = try resolveSuperviewObject(for: view) 231 | enableAutoLayout() 232 | 233 | let computedAnchor = anchored(to: constaintView, using: anchor) 234 | let constraint: NSLayoutConstraint 235 | 236 | switch relation { 237 | case .equal: 238 | constraint = topAnchor.constraint(equalTo: computedAnchor, constant: offset) 239 | case .greaterThanOrEqual: 240 | constraint = topAnchor.constraint(greaterThanOrEqualTo: computedAnchor, constant: offset) 241 | case .lessThanOrEqual: 242 | constraint = topAnchor.constraint(lessThanOrEqualTo: computedAnchor, constant: offset) 243 | } 244 | constraint.set(priority: priority, isActive: true) 245 | 246 | return self 247 | } 248 | 249 | /// Anchors botom anchor to the specified `UIView` using `AxisY` anchor, `Relation` (defatul is `.equal`), NSLayoutPriority (default is `.required`) and `offset` (default is `0.0`) 250 | @discardableResult public func bottom(with view: UIView = SuperviewObject(), 251 | anchor: AxisY, 252 | relatedBy relation: Relation = .equal, 253 | priority: UILayoutPriority = .required, 254 | offset: CGFloat = 0) throws -> UIView { 255 | let constainView = try resolveSuperviewObject(for: view) 256 | enableAutoLayout() 257 | 258 | let computedAnchor = anchored(to: constainView, using: anchor) 259 | let constraint: NSLayoutConstraint 260 | 261 | switch relation { 262 | case .equal: 263 | constraint = bottomAnchor.constraint(equalTo: computedAnchor, constant: offset) 264 | case .lessThanOrEqual: 265 | constraint = bottomAnchor.constraint(lessThanOrEqualTo: computedAnchor, constant: offset) 266 | case .greaterThanOrEqual: 267 | constraint = bottomAnchor.constraint(greaterThanOrEqualTo: computedAnchor, constant: offset) 268 | } 269 | constraint.set(priority: priority, isActive: true) 270 | 271 | return self 272 | } 273 | 274 | /// Anchors left anchor to the specified `UIView` using `AxisY` anchor, `Relation` (defatul is `.equal`), NSLayoutPriority (default is `.required`) and `offset` (default is `0.0`) 275 | @discardableResult public func left(with view: UIView = SuperviewObject(), 276 | anchor: AxisX, 277 | relatedBy realtion: Relation = .equal, 278 | priority: UILayoutPriority = .required, 279 | offset: CGFloat = 0) throws -> UIView { 280 | let constainView = try resolveSuperviewObject(for: view) 281 | enableAutoLayout() 282 | 283 | let computedAnchor = anchored(to: constainView, using: anchor) 284 | let constraint: NSLayoutConstraint 285 | 286 | switch realtion { 287 | case .equal: 288 | constraint = leftAnchor.constraint(equalTo: computedAnchor, constant: offset) 289 | case .greaterThanOrEqual: 290 | constraint = leftAnchor.constraint(greaterThanOrEqualTo: computedAnchor, constant: offset) 291 | case .lessThanOrEqual: 292 | constraint = leftAnchor.constraint(lessThanOrEqualTo: computedAnchor, constant: offset) 293 | } 294 | constraint.set(priority: priority, isActive: true) 295 | 296 | return self 297 | } 298 | 299 | /// Anchors right anchor to the specified `UIView` using `AxisY` anchor, `Relation` (defatul is `.equal`), NSLayoutPriority (default is `.required`) and `offset` (default is `0.0`) 300 | @discardableResult public func right(with view: UIView = SuperviewObject(), 301 | anchor: AxisX, 302 | relatedBy relation: Relation = .equal, 303 | priority: UILayoutPriority = .required, 304 | offset: CGFloat = 0) throws -> UIView { 305 | let constainView = try resolveSuperviewObject(for: view) 306 | enableAutoLayout() 307 | 308 | let computedAnchor = anchored(to: constainView, using: anchor) 309 | let constraint: NSLayoutConstraint 310 | 311 | switch relation { 312 | case .equal: 313 | constraint = rightAnchor.constraint(equalTo: computedAnchor, constant: offset) 314 | case .greaterThanOrEqual: 315 | constraint = rightAnchor.constraint(greaterThanOrEqualTo: computedAnchor, constant: offset) 316 | case .lessThanOrEqual: 317 | constraint = rightAnchor.constraint(lessThanOrEqualTo: computedAnchor, constant: offset) 318 | } 319 | constraint.set(priority: priority, isActive: true) 320 | 321 | return self 322 | } 323 | 324 | } 325 | 326 | // MARK: - Anchoring to System Spacing 327 | extension UIView { 328 | 329 | /// Anchors left anchor to the specified `UIView` with respect to System Spacing using `AxisY` anchor, `Relation` (defatul is `.equal`), NSLayoutPriority (default is `.required`) and `offset` (default is `0.0`) 330 | @discardableResult public func leftToSystemSpacing(with view: UIView = SuperviewObject(), 331 | anchor: AxisX, 332 | relatedBy relation: Relation = .equal, 333 | priority: UILayoutPriority = .required, 334 | multiplier: CGFloat = 1.0) throws -> UIView { 335 | let constainView = try resolveSuperviewObject(for: view) 336 | enableAutoLayout() 337 | 338 | // We can be sure that this force-casting will succed sunce NSLayoutYAxisAnchor is a subclass of NSLayoutAnchor class 339 | let computedAnchor = anchored(to: constainView, using: anchor) as! NSLayoutXAxisAnchor 340 | let constraint: NSLayoutConstraint 341 | 342 | switch relation { 343 | case .equal: 344 | constraint = leftAnchor.constraint(equalToSystemSpacingAfter: computedAnchor, multiplier: multiplier) 345 | case .greaterThanOrEqual: 346 | constraint = leftAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: computedAnchor, multiplier: multiplier) 347 | case .lessThanOrEqual: 348 | constraint = leftAnchor.constraint(lessThanOrEqualToSystemSpacingAfter: computedAnchor, multiplier: multiplier) 349 | } 350 | constraint.set(priority: priority, isActive: true) 351 | 352 | return self 353 | } 354 | 355 | /// Anchors bottom anchor to the specified `UIView` with respect to System Spacing using `AxisY` anchor, `Relation` (defatul is `.equal`), NSLayoutPriority (default is `.required`) and `offset` (default is `0.0`) 356 | @discardableResult public func bottomToSystemSpacing(with view: UIView = SuperviewObject(), 357 | anchor: AxisY, 358 | relatedBy relation: Relation = .equal, 359 | priority: UILayoutPriority = .required, 360 | multiplier: CGFloat = 1.0) throws -> UIView { 361 | let constainView = try resolveSuperviewObject(for: view) 362 | enableAutoLayout() 363 | 364 | // We can be sure that this force-casting will succed sunce NSLayoutYAxisAnchor is a subclass of NSLayoutAnchor class 365 | let computedAnchor = anchored(to: constainView, using: anchor) as! NSLayoutYAxisAnchor 366 | let constraint: NSLayoutConstraint 367 | 368 | switch relation { 369 | case .equal: 370 | constraint = bottomAnchor.constraint(equalToSystemSpacingBelow: computedAnchor, multiplier: multiplier) 371 | case .lessThanOrEqual: 372 | constraint = bottomAnchor.constraint(lessThanOrEqualToSystemSpacingBelow: computedAnchor, multiplier: multiplier) 373 | case .greaterThanOrEqual: 374 | constraint = bottomAnchor.constraint(greaterThanOrEqualToSystemSpacingBelow: computedAnchor, multiplier: multiplier) 375 | } 376 | constraint.set(priority: priority, isActive: true) 377 | 378 | return self 379 | } 380 | 381 | /// Anchors top anchor to the specified `UIView` with respect to System Spacing using `AxisY` anchor, `Relation` (defatul is `.equal`), NSLayoutPriority (default is `.required`) and `offset` (default is `0.0`) 382 | @discardableResult public func topToSystemSpacing(with view: UIView = SuperviewObject(), 383 | anchor: AxisY, 384 | relatedBy relation: Relation = .equal, 385 | priority: UILayoutPriority = .required, 386 | multiplier: CGFloat = 1.0) throws -> UIView { 387 | let constainView = try resolveSuperviewObject(for: view) 388 | enableAutoLayout() 389 | 390 | // We can be sure that this force-casting will succed sunce NSLayoutYAxisAnchor is a subclass of NSLayoutAnchor class 391 | let computedAnchor = anchored(to: constainView, using: anchor) as! NSLayoutYAxisAnchor 392 | let constraint: NSLayoutConstraint 393 | 394 | switch relation { 395 | case .equal: 396 | constraint = topAnchor.constraint(equalToSystemSpacingBelow: computedAnchor, multiplier: multiplier) 397 | case .lessThanOrEqual: 398 | constraint = topAnchor.constraint(lessThanOrEqualToSystemSpacingBelow: computedAnchor, multiplier: multiplier) 399 | case .greaterThanOrEqual: 400 | constraint = topAnchor.constraint(greaterThanOrEqualToSystemSpacingBelow: computedAnchor, multiplier: multiplier) 401 | } 402 | constraint.set(priority: priority, isActive: true) 403 | 404 | return self 405 | } 406 | 407 | /// Anchors right anchor to the specified `UIView` with respect to System Spacing using `AxisY` anchor, `Relation` (defatul is `.equal`), NSLayoutPriority (default is `.required`) and `offset` (default is `0.0`) 408 | @discardableResult public func rightToSystemSpacing(with view: UIView = SuperviewObject(), 409 | anchor: AxisX, 410 | relatedBy relation: Relation = .equal, 411 | priority: UILayoutPriority = .required, 412 | multiplier: CGFloat = 1.0) throws -> UIView { 413 | let constainView = try resolveSuperviewObject(for: view) 414 | enableAutoLayout() 415 | 416 | // We can be sure that this force-casting will succed sunce NSLayoutYAxisAnchor is a subclass of NSLayoutAnchor class 417 | let computedAnchor = anchored(to: constainView, using: anchor) as! NSLayoutXAxisAnchor 418 | let constraint: NSLayoutConstraint 419 | 420 | switch relation { 421 | case .equal: 422 | constraint = rightAnchor.constraint(equalToSystemSpacingAfter: computedAnchor, multiplier: multiplier) 423 | case .greaterThanOrEqual: 424 | constraint = rightAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: computedAnchor, multiplier: multiplier) 425 | case .lessThanOrEqual: 426 | constraint = rightAnchor.constraint(lessThanOrEqualToSystemSpacingAfter: computedAnchor, multiplier: multiplier) 427 | } 428 | constraint.set(priority: priority, isActive: true) 429 | 430 | return self 431 | } 432 | 433 | } 434 | 435 | // MARK: - Pin extension 436 | extension UIView { 437 | 438 | /// Pins Top Left anchor to the Top Left corner of the specified `UIView` with a given `offset` (default is `0.0`) 439 | @discardableResult public func pinTopLeftToTopLeftCorner(cornerOf view: UIView = SuperviewObject(), offset: CGFloat = 0.0) throws -> UIView { 440 | let constainView = try resolveSuperviewObject(for: view) 441 | try top(with: constainView, anchor: .top, offset: offset) 442 | try left(with: constainView, anchor: .left, offset: offset) 443 | 444 | return self 445 | } 446 | 447 | /// Pins Top Right anchor to the Top Right corner of the specified `UIView` with a given `offset` (default is `0.0`) 448 | @discardableResult public func pinTopRightToTopRightCorner(of view: UIView = SuperviewObject(), offset: CGFloat = 0.0) throws -> UIView { 449 | let constainView = try resolveSuperviewObject(for: view) 450 | try top(with: constainView, anchor: .top, offset: offset) 451 | try right(with: constainView, anchor: .right, offset: offset) 452 | 453 | return self 454 | } 455 | 456 | /// Pins Bottom Right anchor to the Bottom Right corner of the specified `UIView` with a given `offset` (default is `0.0`) 457 | @discardableResult public func pinBottomRightToBottomRight(cornerOf view: UIView = SuperviewObject(), offset: CGFloat = 0.0) throws -> UIView { 458 | let constainView = try resolveSuperviewObject(for: view) 459 | try bottom(with: constainView, anchor: .bottom, offset: offset) 460 | try right(with: constainView, anchor: .right, offset: offset) 461 | 462 | return self 463 | } 464 | 465 | /// Pins Bottom Left anchor to the Bottom Left corner of the specified `UIView` with a given `offset` (default is `0.0`) 466 | @discardableResult public func pinBottomLeftToBottomLeft(cornerOf view: UIView = SuperviewObject(), offset: CGFloat = 0.0) throws -> UIView { 467 | let constainView = try resolveSuperviewObject(for: view) 468 | try bottom(with: constainView, anchor: .bottom, offset: offset) 469 | try left(with: constainView, anchor: .left, offset: offset) 470 | 471 | return self 472 | } 473 | 474 | /// Pins Bottom Right anchor to the Top Left corner of the specified `UIView` with a given `offset` (default is `0.0`) 475 | @discardableResult public func pinBottomRightToTopLeft(of view: UIView = SuperviewObject(), offset: CGFloat = 0.0) throws -> UIView { 476 | let constainView = try resolveSuperviewObject(for: view) 477 | try bottom(with: constainView, anchor: .top, offset: offset) 478 | try right(with: constainView, anchor: .left, offset: offset) 479 | 480 | return self 481 | } 482 | 483 | /// Pins Bottom Left anchor to the Top Right corner of the specified `UIView` with a given `offset` (default is `0.0`) 484 | @discardableResult public func pinBottomLeftToTopRight(cornerOf view: UIView = SuperviewObject(), offset: CGFloat = 0.0) throws -> UIView { 485 | let constainView = try resolveSuperviewObject(for: view) 486 | try bottom(with: constainView, anchor: .top, offset: offset) 487 | try left(with: constainView, anchor: .right, offset: offset) 488 | 489 | return self 490 | } 491 | 492 | /// Pins Top Left anchor to the Bottom Right corner of the specified `UIView` with a given `offset` (default is `0.0`) 493 | @discardableResult public func pinTopLeftToBottomRight(cornerOf view: UIView = SuperviewObject(), offset: CGFloat = 0.0) throws -> UIView { 494 | let constainView = try resolveSuperviewObject(for: view) 495 | try top(with: constainView, anchor: .bottom, offset: offset) 496 | try left(with: constainView, anchor: .right, offset: offset) 497 | 498 | return self 499 | } 500 | 501 | /// Pins Bottom Right anchor to the Top Left corner of the specified `UIView` with a given `offset` (default is `0.0`) 502 | @discardableResult public func pinBottomRightToTopLeft(cornerOf view: UIView = SuperviewObject(), offset: CGFloat = 0.0) throws -> UIView { 503 | let constainView = try resolveSuperviewObject(for: view) 504 | try top(with: constainView, anchor: .bottom, offset: offset) 505 | try right(with: constainView, anchor: .left, offset: offset) 506 | 507 | return self 508 | } 509 | 510 | /// Pins Top anchor to the Top Center anchor of the specified `UIView` with a given `offset` (default is `0.0`) 511 | @discardableResult public func pinTopToTopCenter(of view: UIView = SuperviewObject(), offset: CGFloat = 0.0) throws -> UIView { 512 | let constainView = try resolveSuperviewObject(for: view) 513 | try top(with: constainView, anchor: .top, offset: offset) 514 | try center(in: constainView, axis: .horizontal) 515 | 516 | return self 517 | } 518 | 519 | /// Pins Bottom anchor to the Bottom Center anchor of the specified `UIView` with a given `offset` (default is `0.0`) 520 | @discardableResult public func pinBottomToBottomCenter(of view: UIView = SuperviewObject(), offset: CGFloat = 0.0) throws -> UIView { 521 | let constainView = try resolveSuperviewObject(for: view) 522 | try bottom(with: constainView, anchor: .bottom, offset: -offset) 523 | try center(in: constainView, axis: .horizontal) 524 | 525 | return self 526 | } 527 | 528 | /// Pins Left anchor to the Left Center anchor of the specified `UIView` with a given `offset` (default is `0.0`) 529 | @discardableResult public func pinLeftToLeftCenter(of view: UIView = SuperviewObject(), offset: CGFloat = 0.0) throws -> UIView { 530 | let constainView = try resolveSuperviewObject(for: view) 531 | try left(with: constainView, anchor: .left, offset: offset) 532 | try center(in: constainView, axis: .vertical) 533 | 534 | return self 535 | } 536 | 537 | /// Pins Right anchor to the Right Center anchor of the specified `UIView` with a given `offset` (default is `0.0`) 538 | @discardableResult public func pinRightToRightCenter(of view: UIView = SuperviewObject(), offset: CGFloat = 0.0) throws -> UIView { 539 | let constainView = try resolveSuperviewObject(for: view) 540 | try right(with: constainView, anchor: .right, offset: -offset) 541 | try center(in: constainView, axis: .vertical) 542 | 543 | return self 544 | } 545 | 546 | /// Pins `self` inside the specified `UIView` with `Relation` (default is `.equal`), UILayoutPriority (default is `.required`) and `offset` (default is `0.0`) 547 | @discardableResult public func pinInside(view: UIView = SuperviewObject(), relatedBy relation: Relation = .equal, priority: UILayoutPriority = .required, offset: CGFloat = 0.0 ) throws -> UIView { 548 | let constainView = try resolveSuperviewObject(for: view) 549 | 550 | try left( with: constainView, anchor: .left, relatedBy: relation, priority: priority, offset: offset) 551 | try top( with: constainView, anchor: .top, relatedBy: relation, priority: priority, offset: offset) 552 | try right( with: constainView, anchor: .right, relatedBy: relation, priority: priority, offset: -offset) 553 | try bottom( with: constainView, anchor: .bottom, relatedBy: relation, priority: priority, offset: -offset) 554 | 555 | return self 556 | } 557 | 558 | /// Pins `self` to the specified `UIView` by using `Anchor` (which is an `OptionSet`) 559 | @discardableResult public func pinTo(view: UIView = SuperviewObject(), using anchor: Anchor) throws -> UIView { 560 | let constainView = try resolveSuperviewObject(for: view) 561 | let constraints = anchor.convert() 562 | 563 | for constraint in constraints { 564 | switch constraint { 565 | case .left: 566 | try left(with: constainView, anchor: .left) 567 | case .right: 568 | try right(with: constainView, anchor: .right) 569 | case .bottom: 570 | try bottom(with: constainView, anchor: .bottom) 571 | case .top: 572 | try top(with: constainView, anchor: .top) 573 | case .centerY: 574 | try center(in: constainView, axis: .vertical) 575 | case .centerX: 576 | try center(in: constainView, axis: .horizontal) 577 | case .firstBaseline: 578 | constrain(using: .firstBaseline, to: .firstBaseline, of: constainView) 579 | case .lastBaseline: 580 | constrain(using: .lastBaseline, to: .lastBaseline, of: constainView) 581 | case .trailing: 582 | constrain(using: .trailing, to: .trailing, of: constainView) 583 | case .leading: 584 | constrain(using: .leading, to: .leading, of: constainView) 585 | case .width: 586 | try width(to: constainView) 587 | case .height: 588 | try height(to: constainView) 589 | default: 590 | continue 591 | } 592 | } 593 | return self 594 | } 595 | 596 | /// Pins the specified `Anchors` of `self` to the `UIView` by using the related `Anchors` 597 | @discardableResult public func pin(anchors: Anchor, toTargetView view: UIView = SuperviewObject(), using viewAnchors: Anchor) throws -> UIView { 598 | let constainView = try resolveSuperviewObject(for: view) 599 | let constraints = anchors.convert() 600 | let targetConstraints = viewAnchors.convert() 601 | 602 | for (index, constraint) in constraints.enumerated() where index < targetConstraints.count { 603 | let targetConstraint = targetConstraints[index].convert() 604 | 605 | switch constraint { 606 | case .left: 607 | try left(with: constainView, anchor: targetConstraint.toAxisX() ?? .left) 608 | case .right: 609 | try right(with: constainView, anchor: targetConstraint.toAxisX() ?? .right) 610 | case .bottom: 611 | try bottom(with: constainView, anchor: targetConstraint.toAxisY() ?? .bottom) 612 | case .top: 613 | try top(with: constainView, anchor: targetConstraint.toAxisY() ?? .top) 614 | case .centerY: 615 | try center(in: constainView, axis: targetConstraint.toAxis() ?? .vertical) 616 | case .centerX: 617 | try center(in: constainView, axis: targetConstraint.toAxis() ?? .horizontal) 618 | case .firstBaseline: 619 | constrain(using: .firstBaseline, to: targetConstraint, of: constainView) 620 | case .lastBaseline: 621 | constrain(using: .lastBaseline, to: targetConstraint, of: constainView) 622 | case .trailing: 623 | constrain(using: .trailing, to: targetConstraint, of: constainView) 624 | case .leading: 625 | constrain(using: .leading, to: targetConstraint, of: constainView) 626 | case .width: 627 | try width(to: constainView) 628 | case .height: 629 | try height(to: constainView) 630 | default: 631 | continue 632 | } 633 | } 634 | return self 635 | } 636 | 637 | } 638 | 639 | 640 | // MARK: - Fill extension 641 | extension UIView { 642 | 643 | 644 | /// Fills the bottom half of the specified view by `self` with the given `offset` (default is `0.0`) 645 | @discardableResult public func fillBottomHalf(of view: UIView = SuperviewObject(), offset: CGFloat = 0.0) throws -> UIView { 646 | let constainView = try resolveSuperviewObject(for: view) 647 | 648 | try left( with: constainView, anchor: .left, offset: offset) 649 | try right( with: constainView, anchor: .right, offset: -offset) 650 | try bottom( with: constainView, anchor: .bottom, offset: -offset) 651 | 652 | constrain(using: .top, to: .centerY, of: view, offset: offset) 653 | 654 | return self 655 | } 656 | /// Fills the top half of the specified view by `self` with the given `offset` (default is `0.0`) 657 | @discardableResult public func fillTopHalf(of view: UIView = SuperviewObject(), offset: CGFloat = 0.0) throws -> UIView { 658 | let constainView = try resolveSuperviewObject(for: view) 659 | 660 | try left( with: constainView, anchor: .left, offset: offset) 661 | try right( with: constainView, anchor: .right, offset: -offset) 662 | try top( with: constainView, anchor: .top, offset: offset) 663 | 664 | constrain(using: .bottom, to: .centerY, of: view, offset: -offset) 665 | 666 | return self 667 | } 668 | 669 | /// Fills the left half of the specified view by `self` with the given `offset` (default is `0.0`) 670 | @discardableResult public func fillLeftHalf(of view: UIView = SuperviewObject(), offset: CGFloat = 0.0) throws -> UIView { 671 | let constainView = try resolveSuperviewObject(for: view) 672 | 673 | try left( with: constainView, anchor: .left, offset: offset) 674 | try bottom( with: constainView, anchor: .bottom, offset: -offset) 675 | try top( with: constainView, anchor: .top, offset: offset) 676 | 677 | constrain(using: .right, to: .centerX, of: view, offset: -offset) 678 | 679 | return self 680 | } 681 | 682 | /// Fills the right half of the specified view by `self` with the given `offset` (default is `0.0`) 683 | @discardableResult public func fillRightHalf(cornerOf view: UIView = SuperviewObject(), offset: CGFloat = 0.0) throws -> UIView { 684 | let constainView = try resolveSuperviewObject(for: view) 685 | 686 | try right( with: constainView, anchor: .right, offset: -offset) 687 | try bottom( with: constainView, anchor: .bottom, offset: -offset) 688 | try top( with: constainView, anchor: .top, offset: offset) 689 | 690 | constrain(using: .left, to: .centerX, of: view, offset: offset) 691 | 692 | return self 693 | } 694 | 695 | } 696 | -------------------------------------------------------------------------------- /Sources/ConstraintsKit/UIView+RoundedCorners.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+RoundedCorners.swift 3 | // ConstraintsKit 4 | // 5 | // Created by Astemir Eleev on 05/11/2018. 6 | // Copyright © 2018 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @available(iOS, introduced: 11.0) 12 | public extension UIView { 13 | 14 | /// Wrapps `CACornerMask` for more consistent naming. Serves as a proxy for `CACornerMask` and simplifies naming convension for rounded corners. 15 | /// 16 | /// Rather than doing this: 17 | /// 18 | ///`CACornerMask.layerMinXMinYCorner` 19 | /// 20 | /// you do this: 21 | /// 22 | /// `UICorner.topLeft` 23 | /// 24 | /// The struct provides all the capabilities of the standard API `CACornerMask` through `OptoinSet` protocol. 25 | /// 26 | /// There are `5` different constants that can be chained together: 4 constants for each corner and the 5th one for all corners. 27 | struct UICorner: OptionSet { 28 | 29 | // MARK: - Conformance to OptionSet protocol 30 | 31 | public let rawValue: UInt 32 | 33 | // MARK: - Static properties 34 | 35 | public static let topLeft = UICorner(rawValue: 1 << 0) 36 | public static let topRight = UICorner(rawValue: 1 << 1) 37 | public static let bottomLeft = UICorner(rawValue: 1 << 2) 38 | public static let bottomRight = UICorner(rawValue: 1 << 3) 39 | 40 | public static let all: UICorner = [.topLeft, .topRight, .bottomLeft, .bottomRight] 41 | 42 | // MARK: - Initializers 43 | 44 | public init(rawValue: UInt) { 45 | self.rawValue = rawValue 46 | } 47 | 48 | // MARK: - Fileprivate methods for internal usage 49 | 50 | fileprivate func convert() -> CACornerMask { 51 | switch self { 52 | case .topLeft: 53 | return CACornerMask.layerMinXMinYCorner 54 | case .topRight: 55 | return CACornerMask.layerMaxXMinYCorner 56 | case .bottomLeft: 57 | return CACornerMask.layerMinXMaxYCorner 58 | case .bottomRight: 59 | return CACornerMask.layerMaxXMaxYCorner 60 | case .all: 61 | return [CACornerMask.layerMinXMinYCorner, CACornerMask.layerMaxXMinYCorner, CACornerMask.layerMinXMaxYCorner, CACornerMask.layerMaxXMaxYCorner] 62 | default: 63 | return CACornerMask(rawValue: rawValue) 64 | } 65 | } 66 | 67 | static fileprivate func encode(cornerMask: CACornerMask) -> UICorner { 68 | return UICorner(rawValue: cornerMask.rawValue) 69 | } 70 | 71 | } 72 | 73 | func round(corners: UICorner, radius: CGFloat) { 74 | layer.cornerRadius = radius 75 | layer.maskedCorners = corners.convert() 76 | clipsToBounds = true 77 | } 78 | 79 | func getRoundedCorners() -> UIView.UICorner { 80 | return UICorner.encode(cornerMask: layer.maskedCorners) 81 | } 82 | 83 | func resetRoundedCorners() { 84 | layer.cornerRadius = 0 85 | layer.maskedCorners = .init(rawValue: 0) 86 | } 87 | } 88 | 89 | -------------------------------------------------------------------------------- /Tests/ConstraintsKitTests/ConstraintsKitTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ConstraintsKit 3 | 4 | final class ConstraintsKitTests: XCTestCase { 5 | func testExample() { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | } 10 | 11 | static var allTests = [ 12 | ("testExample", testExample), 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /Tests/ConstraintsKitTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(ConstraintsKitTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import ConstraintsKitTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += ConstraintsKitTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /logo-constraints_kit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eleev/constraints-kit/8f9deae6db9ceae25210ecc49aef158a146b59b4/logo-constraints_kit.png --------------------------------------------------------------------------------