Initial Commit
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
7
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
generated
Normal file
7
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "container:../..">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,91 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1150"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "TextualAutoLayoutTests"
|
||||
BuildableName = "TextualAutoLayoutTests"
|
||||
BlueprintName = "TextualAutoLayoutTests"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "TextualAutoLayout"
|
||||
BuildableName = "TextualAutoLayout"
|
||||
BlueprintName = "TextualAutoLayout"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "TextualAutoLayoutTests"
|
||||
BuildableName = "TextualAutoLayoutTests"
|
||||
BlueprintName = "TextualAutoLayoutTests"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "TextualAutoLayout"
|
||||
BuildableName = "TextualAutoLayout"
|
||||
BlueprintName = "TextualAutoLayout"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
115
.swiftpm/xcode/xcshareddata/xcschemes/TextualAutoLayout.xcscheme
Normal file
115
.swiftpm/xcode/xcshareddata/xcschemes/TextualAutoLayout.xcscheme
Normal file
@@ -0,0 +1,115 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1150"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "TextualAutoLayout"
|
||||
BuildableName = "TextualAutoLayout"
|
||||
BlueprintName = "TextualAutoLayout"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "TextualAutoLayoutTests"
|
||||
BuildableName = "TextualAutoLayoutTests"
|
||||
BlueprintName = "TextualAutoLayoutTests"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "TextualAutoLayout"
|
||||
BuildableName = "TextualAutoLayout"
|
||||
BlueprintName = "TextualAutoLayout"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "TextualAutoLayoutTests"
|
||||
BuildableName = "TextualAutoLayoutTests"
|
||||
BlueprintName = "TextualAutoLayoutTests"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "TextualAutoLayoutTests"
|
||||
BuildableName = "TextualAutoLayoutTests"
|
||||
BlueprintName = "TextualAutoLayoutTests"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "TextualAutoLayout"
|
||||
BuildableName = "TextualAutoLayout"
|
||||
BlueprintName = "TextualAutoLayout"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
20
LICENSE
Normal file
20
LICENSE
Normal file
@@ -0,0 +1,20 @@
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2020 Oleg Geier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
16
Package.swift
Normal file
16
Package.swift
Normal file
@@ -0,0 +1,16 @@
|
||||
// swift-tools-version:5.0
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "TextualAutoLayout",
|
||||
platforms: [
|
||||
.macOS(.v10_11), .iOS(.v9), .tvOS(.v9) // watchOS does not support NSLayout
|
||||
],
|
||||
products: [
|
||||
.library(name: "TextualAutoLayout", targets: ["TextualAutoLayout"]),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "TextualAutoLayout", dependencies: []),
|
||||
.testTarget(name: "TextualAutoLayoutTests", dependencies: ["TextualAutoLayout"]),
|
||||
]
|
||||
)
|
||||
BIN
README-anatomy.png
Normal file
BIN
README-anatomy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
161
README.md
Normal file
161
README.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# Textual Auto Layout
|
||||

|
||||

|
||||

|
||||
[](LICENSE)
|
||||
|
||||
Textual Auto Layout (TAL) helps you create Auto Layout constraints that you can read, understand, and maintain.
|
||||
|
||||
In contrast to existing libraries ([SnapKit](https://github.com/SnapKit/SnapKit), [Cartography](https://github.com/robb/Cartography)), TAL follows a minimization approach. The package consists of a single ~120 lines source file, thus TAL won't have every imaginable feature. Instead you get a dependency ideal for security audits: short and tidy.
|
||||
|
||||
To install, use Swift Package Manager or simply copy the source file to your project and you're good to go.
|
||||
|
||||
|
||||
## Requirements
|
||||
|
||||
- macOS 10.11+ / iOS 9.0+ / tvOS 9.0+
|
||||
- Swift 5? (probably lower, can't test)
|
||||
|
||||
The swift package requires `swift-tools-version:5.0` for the new `platforms` attribute.
|
||||
|
||||
|
||||
## Long story
|
||||
|
||||
Lets take a look to Apples documentation archive on Auto Layout Guide, [Anatomy of a Constraint][1]. There is a wonderful picture that illustrates the constraint relation between two views.
|
||||
|
||||
> 
|
||||
> Copyright © 2018 Apple Inc ([source][2])
|
||||
|
||||
I love the simplicity of the depiction. However, when you start creating constraints programmatically, the same constraint looks more like this:
|
||||
|
||||
```swift
|
||||
let constraint = RedView.leadingAnchor.constraint(equalTo: BlueView.trailingAnchor, constant: 8.0)
|
||||
constraint.isActive = true
|
||||
```
|
||||
|
||||
Not only is this hard to read but it gets tedious when you have to create a bunch of constraints (and remember to activate all of them).
|
||||
**Textual Auto Layout** (TAL) to the rescue; This is what the same constraint looks like in TAL:
|
||||
|
||||
```swift
|
||||
RedView.leadingAnchor =&= BlueView.trailingAnchor + 8.0
|
||||
```
|
||||
|
||||
This sets all constraint attributes and activates the constraint too. The only slight deviation to the illustration it uses `=&=` instead of `=`.
|
||||
|
||||
There are two reasons for that. The obvious, `=` is already used for assignments. But also, you want to handle the other relations too. You have the `greaterThanOrEqualTo` and `lessThanOrEqualTo` relations at your disposal (`=>=` and `=<=` respectively).
|
||||
|
||||
|
||||
### Going into a different dimension
|
||||
|
||||
The example above uses a multiplier of `1.0`. Technically this is correct, though it does not make sense for location attributes. The documentation is clear:
|
||||
|
||||
> You cannot use a nonidentity multiplier (a value other than 1.0) with location attributes.
|
||||
|
||||
Thats why it is omitted altogether in the `=&=` relation above. But lets take a look where it does make sense, dimensional constraints:
|
||||
|
||||
```swift
|
||||
View.widthAnchor =>= 2 * View.heightAnchor
|
||||
View.widthAnchor =<= 250
|
||||
```
|
||||
|
||||
These two constraints ensure that the view is at least twice as wide as tall, but at most 250 pt wide. Readability, 100%.
|
||||
Again, what would you do without TAL?:
|
||||
|
||||
```swift
|
||||
View.widthAnchor.constraint(greaterThanOrEqualTo: View.heightAnchor, multiplier: 2).isActive = true
|
||||
View.widthAnchor.constraint(lessThanOrEqualToConstant: 250).isActive = true
|
||||
```
|
||||
|
||||
|
||||
### Prioritize your constraints
|
||||
|
||||
Sometimes requiring a specific is just too strong. You can adjust the priority in the same line. Use the pipe symbol (`|`)<a href="#n1" id="n1ref"><sup>1</sup></a> and append the priority as needed:
|
||||
|
||||
```swift
|
||||
View.widthAnchor =&= 250 | .defaultHigh
|
||||
```
|
||||
|
||||
--------------------
|
||||
<a id="n1" href="#n1ref"><sup>1</sup></a> To be consistent with Apple documentation, I'd like to use `@` for priority assignment (instead of `|`). But sadly you can't use `@` in an operator declaration.
|
||||
|
||||
|
||||
### Doing more things at the same time
|
||||
|
||||
Usually you need to set more than a single constraint. For example, you want to set a child view to the same size as the parent view, but inset by 5 px in all directions. You could create these four constraints individually, or use a little helper method:
|
||||
|
||||
```swift
|
||||
Child.anchor([.top, .bottom, .left, .right], to: Parent, padding: 5)
|
||||
```
|
||||
|
||||
The `anchor(_,to:)` method will use the same relation attribute for both views. In this example the code above is equivalent to:
|
||||
|
||||
```swift
|
||||
Child.topAnchor =&= Parent.topAnchor + 5
|
||||
Child.leftAnchor =&= Parent.leftAnchor + 5
|
||||
Parent.bottomAnchor =&= Child.bottomAnchor + 5
|
||||
Parent.rightAnchor =&= Child.rightAnchor + 5
|
||||
Child.translatesAutoresizingMaskIntoConstraints = false
|
||||
```
|
||||
|
||||
Notice how `bottomAnchor` and `rightAnchor` did change the relation direction instead of using `Child =&= Parent - 5` with a negative constant. This makes it easier when debugging constraints. You can simply look for your chosen `5 px` padding instead of thinking what edges are involved.
|
||||
|
||||
Further you should note that `anchor(_,to:)` will set `translatesAutoresizingMaskIntoConstraints` to `false`. Most of the time you'll want that. In fact you'll probably want to set it for single constraints too. But give it some time and you'll see that you often use both constraint types at the same time.
|
||||
|
||||
```swift
|
||||
ChildA.anchor([.leadingMargin, .trailingMargin], to: Parent)
|
||||
ChildB.anchor([.leadingMargin, .trailingMargin], to: Parent)
|
||||
ChildB.topAnchor =&= ChildA.bottomAnchor + 8
|
||||
```
|
||||
|
||||
In that case both relevant views (`ChildA` and `ChildB`) have `translatesAutoresizingMaskIntoConstraints` set. Not `Parent` though, which is intended. E.g., `Parent ` may have `autoresizingMask = [.flexibleWidth, .flexibleHeight]`.
|
||||
|
||||
|
||||
#### Limitations
|
||||
|
||||
Since the `anchor(_,to:)` reuses the same relation attribute for both sides, you can't connect two different attributes (e.g., `.left =&= .right`). You can resolve this issue with individual constraints.
|
||||
|
||||
|
||||
### Requiring greater control
|
||||
|
||||
Even though it wasn't mentioned in the previous example, you can chain `anchor(_,to:)` with a priority, same as with individual constraints. Or more general, the `anchor` method returns a list of constraints. And you can apply functions on constraint lists.
|
||||
|
||||
```swift
|
||||
let x: [NSLayoutConstraint]
|
||||
x = Child.anchor([.left, .right], to: Parent)
|
||||
x | .defaultLow
|
||||
// or short
|
||||
Child.anchor([.left, .right], to: Parent) | .defaultLow
|
||||
```
|
||||
|
||||
It doesn't stop there. I haven't mention it in the beginning, but every TAL expression has a return type.
|
||||
|
||||
```swift
|
||||
View.widthAnchor =<= 250 // returns NSLayoutConstraint
|
||||
```
|
||||
|
||||
This allows you to chain, reuse or store constraints for later usage.
|
||||
|
||||
```swift
|
||||
let compactConstraints = [
|
||||
View.widthAnchor =&= 120,
|
||||
View.heightAnchor =&= 35
|
||||
].setActive(false)
|
||||
let regularConstraints = [
|
||||
View.widthAnchor =<= 250,
|
||||
View.heightAnchor =<= 50
|
||||
] // implicitly isActive = true
|
||||
regularConstraints | .fittingSizeLevel
|
||||
```
|
||||
|
||||
And lastly, though not as helpful as the other features. You can constraint the intrinsic content size.
|
||||
|
||||
```swift
|
||||
Label.constrainHuggingCompression(.vertical, .required)
|
||||
// which is equivalent to:
|
||||
Label.setContentHuggingPriority(.vertical, for: .required)
|
||||
Label.setContentCompressionResistancePriority(.vertical, for: .required)
|
||||
```
|
||||
|
||||
|
||||
[1]: https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/AnatomyofaConstraint.html
|
||||
[2]: https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/Art/view_formula_2x.png
|
||||
124
Sources/TextualAutoLayout/TextualAutoLayout.swift
Normal file
124
Sources/TextualAutoLayout/TextualAutoLayout.swift
Normal file
@@ -0,0 +1,124 @@
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
public typealias ConstraintPriority = NSLayoutConstraint.Priority
|
||||
public typealias ConstraintAxis = NSLayoutConstraint.Orientation
|
||||
public typealias ConstraintView = NSView
|
||||
#else
|
||||
import UIKit
|
||||
public typealias ConstraintPriority = UILayoutPriority
|
||||
public typealias ConstraintAxis = NSLayoutConstraint.Axis
|
||||
public typealias ConstraintView = UIView
|
||||
#endif
|
||||
|
||||
/*
|
||||
Readable Auto Layout Constraints
|
||||
|
||||
Usage:
|
||||
A.anchor =&= multiplier * B.anchor + constant | priority
|
||||
*/
|
||||
|
||||
precedencegroup ReadableLayoutPrecedence {
|
||||
higherThan: AdditionPrecedence
|
||||
lowerThan: MultiplicationPrecedence
|
||||
}
|
||||
|
||||
infix operator =&= : ReadableLayoutPrecedence
|
||||
infix operator =<= : ReadableLayoutPrecedence
|
||||
infix operator =>= : ReadableLayoutPrecedence
|
||||
|
||||
/// Create and activate an `equal` constraint between left and right anchor. Format: `A.anchor =&= multiplier * B.anchor + constant | priority`
|
||||
@discardableResult public func =&= <T>(l: NSLayoutAnchor<T>, r: NSLayoutAnchor<T>) -> NSLayoutConstraint { l.constraint(equalTo: r).on() }
|
||||
/// Create and activate a `lessThan` constraint between left and right anchor. Format: `A.anchor =<= multiplier * B.anchor + constant | priority`
|
||||
@discardableResult public func =<= <T>(l: NSLayoutAnchor<T>, r: NSLayoutAnchor<T>) -> NSLayoutConstraint { l.constraint(lessThanOrEqualTo: r).on() }
|
||||
/// Create and activate a `greaterThan` constraint between left and right anchor. Format: `A.anchor =>= multiplier * B.anchor + constant | priority`
|
||||
@discardableResult public func =>= <T>(l: NSLayoutAnchor<T>, r: NSLayoutAnchor<T>) -> NSLayoutConstraint { l.constraint(greaterThanOrEqualTo: r).on() }
|
||||
|
||||
public extension NSLayoutDimension { // higher precedence, so multiply first
|
||||
/// Create intermediate anchor multiplier result.
|
||||
static func *(l: CGFloat, r: NSLayoutDimension) -> AnchorMultiplier { .init(anchor: r, m: l) }
|
||||
/// Create and activate an `equal` constraint with constant value. Format: `A.anchor =&= constant | priority`
|
||||
@discardableResult static func =&=(l: NSLayoutDimension, r: CGFloat) -> NSLayoutConstraint { l.constraint(equalToConstant: r).on() }
|
||||
/// Create and activate a `lessThan` constraint with constant value. Format: `A.anchor =<= constant | priority`
|
||||
@discardableResult static func =<=(l: NSLayoutDimension, r: CGFloat) -> NSLayoutConstraint { l.constraint(lessThanOrEqualToConstant: r).on() }
|
||||
/// Create and activate a `greaterThan` constraint with constant value. Format: `A.anchor =>= constant | priority`
|
||||
@discardableResult static func =>=(l: NSLayoutDimension, r: CGFloat) -> NSLayoutConstraint { l.constraint(greaterThanOrEqualToConstant: r).on() }
|
||||
}
|
||||
|
||||
/// Intermediate `NSLayoutConstraint` anchor with multiplier supplement
|
||||
public struct AnchorMultiplier {
|
||||
fileprivate let anchor: NSLayoutDimension, m: CGFloat
|
||||
}
|
||||
|
||||
public extension AnchorMultiplier {
|
||||
/// Create and activate an `equal` constraint between left and right anchor. Format: `A.anchor =&= multiplier * B.anchor + constant | priority`
|
||||
@discardableResult static func =&=(l: NSLayoutDimension, r: Self) -> NSLayoutConstraint { l.constraint(equalTo: r.anchor, multiplier: r.m).on() }
|
||||
/// Create and activate a `lessThan` constraint between left and right anchor. Format: `A.anchor =<= multiplier * B.anchor + constant | priority`
|
||||
@discardableResult static func =<=(l: NSLayoutDimension, r: Self) -> NSLayoutConstraint { l.constraint(lessThanOrEqualTo: r.anchor, multiplier: r.m).on() }
|
||||
/// Create and activate a `greaterThan` constraint between left and right anchor. Format: `A.anchor =>= multiplier * B.anchor + constant | priority`
|
||||
@discardableResult static func =>=(l: NSLayoutDimension, r: Self) -> NSLayoutConstraint { l.constraint(greaterThanOrEqualTo: r.anchor, multiplier: r.m).on() }
|
||||
}
|
||||
|
||||
public extension NSLayoutConstraint {
|
||||
/// Change `isActive`to `true` and return `self`
|
||||
func on() -> Self { isActive = true; return self }
|
||||
/// Change `isActive`to `false` and return `self`
|
||||
func off() -> Self { isActive = false; return self }
|
||||
/// Change `constant`attribute and return `self`
|
||||
@discardableResult static func +(l: NSLayoutConstraint, r: CGFloat) -> NSLayoutConstraint { l.constant = r; return l }
|
||||
/// Change `constant` attribute and return `self`
|
||||
@discardableResult static func -(l: NSLayoutConstraint, r: CGFloat) -> NSLayoutConstraint { l.constant = -r; return l }
|
||||
/// Change `priority` attribute and return `self`
|
||||
@discardableResult static func |(l: NSLayoutConstraint, r: ConstraintPriority) -> NSLayoutConstraint { l.priority = r; return l }
|
||||
}
|
||||
|
||||
/*
|
||||
UIView extension to generate multiple constraints at once
|
||||
|
||||
Usage:
|
||||
child.anchor([.width, .height], to: parent) | .defaultLow
|
||||
*/
|
||||
|
||||
public extension ConstraintView {
|
||||
#if os(macOS)
|
||||
/// Edges that need the relation to flip arguments. For these we need to inverse the constant value and relation.
|
||||
private static let inverseItem: [NSLayoutConstraint.Attribute] = [.right, .bottom, .trailing, .lastBaseline]
|
||||
#else
|
||||
/// Edges that need the relation to flip arguments. For these we need to inverse the constant value and relation.
|
||||
private static let inverseItem: [NSLayoutConstraint.Attribute] = [.right, .bottom, .trailing, .lastBaseline, .rightMargin, .bottomMargin, .trailingMargin]
|
||||
#endif
|
||||
|
||||
/// Create and active constraints for provided edges. Constraints will anchor the same edge on both `self` and `other`.
|
||||
/// - Note: Will set `translatesAutoresizingMaskIntoConstraints = false`
|
||||
/// - Parameters:
|
||||
/// - edges: List of constraint attributes, e.g. `[.top, .bottom, .left, .right]`
|
||||
/// - other: Instance to bind to, e.g. `UIView` or `UILayoutGuide`
|
||||
/// - padding: Used as constant value. Multiplier will always be `1.0`. If you need to change the multiplier, use single constraints instead. (Default: `0`)
|
||||
/// - rel: Constraint relation. (Default: `.equal`)
|
||||
/// - Returns: List of created and active constraints
|
||||
@discardableResult func anchor(_ edges: [NSLayoutConstraint.Attribute], to other: Any, padding: CGFloat = 0, if rel: NSLayoutConstraint.Relation = .equal) -> [NSLayoutConstraint] {
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
return edges.map {
|
||||
let (A, B) = Self.inverseItem.contains($0) ? (other, self) : (self, other)
|
||||
return NSLayoutConstraint(item: A, attribute: $0, relatedBy: rel, toItem: B, attribute: $0, multiplier: 1, constant: padding).on()
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the priority with which a view resists being made smaller and larger than its intrinsic size.
|
||||
func constrainHuggingCompression(_ axis: ConstraintAxis, _ priotity: ConstraintPriority) {
|
||||
setContentHuggingPriority(priotity, for: axis)
|
||||
setContentCompressionResistancePriority(priotity, for: axis)
|
||||
}
|
||||
}
|
||||
|
||||
public extension Array where Element: NSLayoutConstraint {
|
||||
/// set `priority` on all elements and return same list
|
||||
@discardableResult static func |(l: Self, r: ConstraintPriority) -> Self {
|
||||
for x in l { x.priority = r }
|
||||
return l
|
||||
}
|
||||
/// set `isActive` on all elements and return `self`
|
||||
@discardableResult func setActive(_ flag: Bool) -> Self {
|
||||
flag ? NSLayoutConstraint.activate(self) : NSLayoutConstraint.deactivate(self)
|
||||
return self
|
||||
}
|
||||
}
|
||||
76
Tests/TextualAutoLayoutTests/TextualAutoLayoutTests.swift
Normal file
76
Tests/TextualAutoLayoutTests/TextualAutoLayoutTests.swift
Normal file
@@ -0,0 +1,76 @@
|
||||
import XCTest
|
||||
@testable import TextualAutoLayout
|
||||
|
||||
final class TextualAutoLayoutTests: XCTestCase {
|
||||
var parent, A, B: ConstraintView!
|
||||
|
||||
override func setUp() {
|
||||
parent = ConstraintView()
|
||||
A = ConstraintView()
|
||||
B = ConstraintView()
|
||||
parent.addSubview(A)
|
||||
parent.addSubview(B)
|
||||
}
|
||||
|
||||
func testBasic() {
|
||||
let x = A.bottomAnchor =&= B.topAnchor + 20.3 | .defaultLow
|
||||
XCTAssertEqual(x.relation, NSLayoutConstraint.Relation.equal)
|
||||
XCTAssertEqual(x.priority, ConstraintPriority.defaultLow)
|
||||
XCTAssertEqual(x.constant, 20.3)
|
||||
XCTAssertEqual(x.isActive, true)
|
||||
}
|
||||
|
||||
func testDimensional() {
|
||||
let x = A.heightAnchor =<= 2 * B.widthAnchor - 4
|
||||
XCTAssertEqual(x.relation, NSLayoutConstraint.Relation.lessThanOrEqual)
|
||||
XCTAssertEqual(x.priority, ConstraintPriority.required)
|
||||
XCTAssertEqual(x.multiplier, 2)
|
||||
XCTAssertEqual(x.constant, -4)
|
||||
XCTAssertEqual(x.isActive, true)
|
||||
|
||||
XCTAssertEqual((A.widthAnchor =>= 2 * A.heightAnchor).constant, 0)
|
||||
XCTAssertEqual((A.widthAnchor =<= 250).constant, 250)
|
||||
}
|
||||
|
||||
func testIntrinsicContent() {
|
||||
A.constrainHuggingCompression(.vertical, .required)
|
||||
// newly set values
|
||||
XCTAssertEqual(A.contentCompressionResistancePriority(for: .vertical), ConstraintPriority.required)
|
||||
XCTAssertEqual(A.contentHuggingPriority(for: .vertical), ConstraintPriority.required)
|
||||
// default values
|
||||
XCTAssertEqual(A.contentCompressionResistancePriority(for: .horizontal), ConstraintPriority.defaultHigh)
|
||||
XCTAssertEqual(A.contentHuggingPriority(for: .horizontal), ConstraintPriority.defaultLow)
|
||||
}
|
||||
|
||||
func testReturnConstraint() {
|
||||
XCTAssertEqual((A.rightAnchor =&= B.leftAnchor).firstItem as? ConstraintView, A)
|
||||
}
|
||||
|
||||
func testViewMultiConstraint() {
|
||||
A.anchor([.left, .right, .top], to: parent!, padding: 76) | .defaultHigh
|
||||
let x = parent.constraints
|
||||
XCTAssertEqual(x.count, 3)
|
||||
XCTAssertEqual(A.translatesAutoresizingMaskIntoConstraints, false)
|
||||
XCTAssertEqual(parent.translatesAutoresizingMaskIntoConstraints, true)
|
||||
|
||||
for u in x {
|
||||
XCTAssertEqual(u.relation, NSLayoutConstraint.Relation.equal)
|
||||
XCTAssertEqual(u.priority, ConstraintPriority.defaultHigh)
|
||||
XCTAssertEqual(u.constant, 76)
|
||||
XCTAssertEqual(u.firstAttribute, u.secondAttribute)
|
||||
XCTAssertEqual(u.isActive, true)
|
||||
|
||||
let first = u.firstItem as? ConstraintView
|
||||
let second = u.secondItem as? ConstraintView
|
||||
if u.firstAttribute == .right {
|
||||
XCTAssertEqual(first, parent)
|
||||
XCTAssertEqual(second, A)
|
||||
} else {
|
||||
XCTAssertEqual(first, A)
|
||||
XCTAssertEqual(second, parent)
|
||||
}
|
||||
}
|
||||
x.setActive(false)
|
||||
for u in x { XCTAssertEqual(u.isActive, false) }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user