GetLaid is a lean framework for laying out complex UIs through short readable code.
- Why Oh Why?
- Install
- Add Subviews and Layout Guides
- Constrain a Position
- Constrain Multiple Positions
- Constrain a Dimension
- Constrain Both Dimensions
- Constrain to the Parent
- Constrain to the Safe Area on iOS
- System Spacing on iOS and tvOS
- âś… Readability
- The syntax is close to natural language instead of technically fancy.
- All constraining takes the form
source.constrain(to: target)
. - The operator
>>
can add further clarity:source >> target
- âś… Brevity
- Code lines are super short and involve few function arguments.
- A single code line can do a lot, via combined targets like
allButTop
andsize
.
- âś… Simplicity / Flexibility
- Simple consistent systematic design: Understand 1 thing to do everything.
- Seemless coverage of parent views, safe areas and system spacings
- âś… Easy Advanced Layouting
- Modify any constrain target with
offset(CGFLoat)
,min
,max
andat(_ factor: CGFloat)
. - Chain target modifications together:
item1 >> item2.size.at(0.5).min
- Modify any constrain target with
- âś… Compatibility
- Works on iOS, tvOS and macOS.
- Works on UILayoutGuide and NSLayoutGuide just as well as on views.
Well, that would be insane.
Programmatic AutoLayout without any such frameworks was never hard. It's all about creating objects of NSLayoutConstraint
, which has only one powerful initializer.
Since iOS/tvOS 9.0 and macOS 10.11, we also have NSLayoutAnchor
, which adds a native abstraction layer on top of NSLayoutConstraint
, further reducing the need for any AutoLayout wrappers at all.
At this point, all an AutoLayout wrapper can do is to make layout code even more meaningful, readable and succinct at the point of use. GetLaid does exactly that.
Modern AutoLayout wrappers like SnapKit are almost too clever for the simple task at hand. The first example from the SnapKit README:
box.snp.makeConstraints { (make) -> Void in
make.width.height.equalTo(50)
make.center.equalTo(self.view)
}
Classic AutoLayout wrappers like PureLayout, have easier syntax but are still wordy:
box.autoSetDimensions(to: CGSize(width: 50, height: 50))
box.autoCenterInSuperView()
GetLaid trims AutoLayout further down to the essence. Just read the operator >>
as "constrain to":
box >> 50
box >> view.center
So, which is prettier, mh? If you can spare fancyness but appreciate readability, GetLaid might be for you.
Here is also a richer comparison of how layout code looks with GetLaid and its alternatives.
With the Swift Package Manager, you can just add the GetLaid package via Xcode (11+).
Or you manually adjust the Package.swift file of your project:
// swift-tools-version:5.1
import PackageDescription
let package = Package(
name: "MyApp",
dependencies: [
.package(url: "https://github.com/flowtoolz/GetLaid.git",
.upToNextMajor(from: "3.0.0"))
],
targets: [
.target(name: "MyAppTarget",
dependencies: ["GetLaid"])
]
)
Then run $ swift build
or $ swift run
.
With Cocoapods, adjust your Podfile:
target "MyAppTarget" do
pod "GetLaid", "~> 3.0"
end
Then run $ pod install
.
Finally, in your Swift files:
import GetLaid
The generic function addForAutoLayout
adds a subview and prepares it for AutoLayout. It returns the subview it takes as its exact type. Use this function to add subviews:
class List: UIView {
// ... other code, including call to addSubviews() ...
func addSubviews() {
addForAutoLayout(header) >> allButBottom // add header to the top
}
private let header = Header()
}
If you don't use addForAutoLayout
, remember to set translatesAutoresizingMaskIntoConstraints = false
on the views you incorporate in AutoLayout.
There's also a helper function for adding a new layout guide to a view:
let guide = view.addLayoutGuide()
You would always call constrain(to:)
on exactly the thing you want to constrain. And you can always replace that function with the shorthand operator >>
, which we'll do in the examples. These lines are equivalent :
view1.top.constrain(to: view2.lastBaseline)
view1.top >> view2.lastBaseline
All layout attributes can be used in that way, while baselines are not available on layout guides.
If source and target refer to the same attribute, you may omit the attribute on one side. These are equivalent:
item1.left >> item2.left
item1.left >> item2
item1 >> item2.left
You may modify the constrain target and also chain these modifications:
item1 >> item2.left.offset(8)
item1 >> item2.left.min // >= item2.left
item1 >> item2.left.max // <= item2.left
item1 >> item2.left.at(0.5) // at 0.5 of item2.left
item1 >> item2.left.min.offset(8)
You may constrain multiple positions at once:
item1 >> item2.allButTop(leadingOffset: 5, // leading, bottom, trailing
bottomOffset: -5)
item1 >> item2.center // centerX, centerY
item1 >> item2.all // all edges
item1 >> item2 // shorthand for .all
Available position target combinations are:
all
allButTop
allButLeading
allButLeft
allButBottom
allButTrailing
allButRight
center
All of them take offsets as arguments for exactly the constrained positions, in counter-clockwise order.
You constrain width and height just like positions:
item1.width >> item2.height
As with positions, you can omit redundant attributes, modify the target, and chain modifications:
item1 >> item2.height.at(0.6).min // >= 60% of item2.height
You can constrain a dimension to a constant size. These are equivalent:
item.width >> .size(100)
item.width >> 100
Omit the dimension to constrain both dimensions to the same constant. These are equivalent:
item >> .size(100) // square with edge length 100
item >> 100 // same
You can modify the constant size target like any other target, for one or both dimensions. And there are shorthand notations for minimum and maximum constants. These are equivalent:
item >> .size(100).max // width, height <= 100
item >> .max(100) // same
The size
target combines width and height. It works fully equivalent to those single dimensions:
item1 >> item2.size.min // at least as big as item2
A size target can also represent a constant size. These are equivalent:
item >> .size(100, 50) // size target with constants
item >> (100, 50) // same
And there are also shorthand notations for minimum and maximum size. These are equivalent:
item >> .size(100, 50).min // at least 100 by 50
item >> .min(100, 50) // same
Normally, in well structured code, views add and layout their own subviews. In those contexts, the parent (superview) of the constrained subviews is self
, which makes it easy to constrain those subviews to any of their parent's attributes:
class MySuperview: UIView {
// ... other code, including call to addSubviews() ...
func addSubviews() {
let subview = addForAutoLayout(UIView())
subview >> allButBottom // constrain 3 edges to parent (self)
subview >> height.at(0.2) // constrain height to 20% of parent (self)
}
}
Sometimes, not all superviews are implemented as their own custom view class. In other words, some custom view- or controller classes add and layout whole subview hierarchies. In those contexts, the enclosing custom view or view controller controls the parent-child relation of its subviews and can directly constrain subviews to their parents:
class MySuperview: UIView {
// ... other code, including call to addSubviews() ...
func addSubviews() {
let subview = addForAutoLayout(UIView())
let subsubview = subview.addForAutoLayout(UIView())
subsubview >> subview.allButBottom // constrain 3 edges to parent
subsubview >> subview.height.at(0.2) // constrain height to 20% of parent
}
}
If you still want to explicitly constrain a layout item to its parent, you can use the parent
property. On a view, parent
is its superView
. On a layout guide, parent
is its owningView
. Of course, parent
is optional, but all layout item based constrain targets can just be optional:
item >> item.parent?.top.offset(10) // constrain top to parent, inset 10
item >> item.parent?.allButBottom // constrain 3 edges to parent
item >> item.parent?.size.at(0.3) // constrain width and height to 30% of parent
item >> item.parent?.all(leadingOffset: 10) // constrain all edges to parent, leading inset 10
item >> item.parent // constrain all edges to parent
On iOS 11 and above, you can access the safe area of a view via the safeArea
property and the parent's safe area via the optional parentSafeArea
property.
Normally, in well structured code where views add and layout their own subviews, you would simply call safeArea
on self
:
class MyView: UIView {
// ... other code, including call to addSubviews() ...
func addSubviews() {
addForAutoLayout(MyContentView()) >> safeArea // constrain content to safe area
}
}
If you find youself constraining many subviews to the safe area, there should probably be a content view containing them.
With Apple's NSLayoutAnchor
, you can make use of a mysterious "system spacing". Apple does not disclose how that is calculated and does not offer any concrete values you could access. Using system spacings through the NSLayoutAnchor
API is a bit awkward, limited in how it is applied and limited in what it can be applied to.
GetLaid exposes the system spacing as two global CGFLoat
constants. It calls the actual Apple API to calculate the constants the first time you access them:
systemSiblingSpacing
is the gap the user's system wants between sibling views.systemParentSpacing
is the inset the user's system wants from a view's edge to a contained subview.
It seems that on iOS both these system spacings are always the same. At least, I checked that from iPhone SE up to the newest 13" iPad Pro, and from iOS 12.0 to iOS 13.3. So GetLaid also offers a universal systemSpacing
which just returns systemSiblingSpacing
.
The system spacing as a constant offers loads of flexibility:
item2.left >> item1.right.offset(systemSpacing) // gap between views
item >> item.parent?.top.offset(systemSpacing) // inset to parent
spacer.width >> .min(systemSpacing) // minimum spacer width
Remember that these constants are not hardcoded but dynamically calculated on the actual user device, so they are absolutely true to what Apple intents for sibling gaps and parent insets, on any system and on any iOS/tvOS version. But also note that these two values do not capture the system spacing magic that NSLayoutAnchor
offers in conjunction with baselines and font sizes and possibly in other contexts.