├── .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 [](https://github.com/sindresorhus/awesome)
2 |
3 | []()
4 | []()
5 | []()
6 | []()
7 | []()
8 |
9 | **Last Update: 02/January/2020.**
10 |
11 | 
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
--------------------------------------------------------------------------------