Declarative UIKit view hierarchy and Auto Layout, in one small package.
Pin lets UIKit code describe containment and constraints together. Build nested layouts with a result builder, activate the complete tree in one call, and use the built-in lifecycle to cleanly reverse constraints, layout guides, and containment state. It has no third-party dependencies.
| Component | Requirement |
|---|---|
| iOS | 13.0 or later |
| Xcode | 16 or later |
| Swift toolchain | Swift 6 / Swift tools 6.0 or later |
| Language mode | Swift 5 compatibility mode or Swift 6 |
Swift 5 compatibility refers to the language mode supported by a Swift 6 toolchain; Pin does not support Swift 5 toolchains. UIKit-facing APIs are isolated to @MainActor.
Important
Pin is pre-1.0 and does not have a stable release tag yet. The following dependency tracks the moving main branch. Pin a full commit revision when you need reproducible builds.
In Xcode, choose File → Add Package Dependencies…, enter https://github.com/danielinoa/Pin.git, select Branch with main, and add the Pin product to your app target.
For a Swift package, declare both the package and target dependency:
dependencies: [
.package(url: "https://github.com/danielinoa/Pin.git", branch: "main")
],
targets: [
.target(
name: "YourApp",
dependencies: [.product(name: "Pin", package: "Pin")]
)
]Pin will adopt semantic versioning with its first tagged release.
Store the layout description when you may need to invalidate, deactivate, or replace it later.
import Pin
import UIKit
@MainActor
final class ProfileViewController: UIViewController {
private let avatarView = UIImageView()
private let nameLabel = UILabel()
private var layout: Pinnable?
override func viewDidLoad() {
super.viewDidLoad()
avatarView.backgroundColor = .secondarySystemBackground
avatarView.layer.cornerRadius = 36
avatarView.clipsToBounds = true
nameLabel.text = "Taylor"
layout = view.add {
avatarView
.size(square: 72)
.pin(to: .leadingMargin, .topMargin)
nameLabel
.pin(\.leadingAnchor, to: avatarView.trailingAnchor, constant: 16)
.pin(\.centerYAnchor, to: avatarView.centerYAnchor)
.pin(\.trailingAnchor, to: view.layoutMarginsGuide.trailingAnchor)
}
layout?.activate()
}
func removeLayout() {
layout?.deactivate()
layout = nil
}
}add inserts both views before resolving their constraints. There is no separate addSubview phase.
| Goal | API |
|---|---|
| Build a child hierarchy | parent.add { child; nestedLayout } |
| Pin ordinary edges | pin(to: .edges) or .horizontalEdges / .verticalEdges |
| Pin layout-margin edges | pin(to: .marginEdges) or its horizontal / vertical groups |
| Match parent size or center | pin(to: .size), pin(to: .center), or combine groups with + |
| Set a fixed size | size(square:), size(width:height:), size(width:), size(height:) |
| Tune a relationship | pin(to:relation:multiplier:padding:priority:) |
| Pin anchors or safe-area anchors | pin(\.topAnchor, to: targetAnchor, constant:priority:) |
| Center between two anchors | center(between:and:) for either axis |
| Add arranged subviews | stackView.stack { arrangedViews } |
| Manage the layout | activate(), invalidate(), and deactivate() |
Positive padding moves top and leading edges inward; Pin reverses its sign for bottom and trailing edges to produce a uniform inset.
Anchor pinning works with any compatible UIKit anchor, including guides owned by another view.
import Pin
import UIKit
let container = UIView()
let content = UIView()
let layout = container.add {
content
.pin(\.topAnchor, to: container.safeAreaLayoutGuide.topAnchor, constant: 16)
.pin(\.leadingAnchor, to: container.safeAreaLayoutGuide.leadingAnchor, constant: 16)
.pin(\.trailingAnchor, to: container.safeAreaLayoutGuide.trailingAnchor, constant: -16)
.pin(\.bottomAnchor, to: container.safeAreaLayoutGuide.bottomAnchor, constant: -16)
}
layout.activate()Anchor constants follow UIKit's sign convention, which is why the trailing and bottom insets above are negative. The semantic padding argument to pin(to:) handles that sign adjustment automatically.
The builder supports conditionals, optionals, arrays, and loops. stack uses arranged-subview containment rather than ordinary addSubview containment.
import Pin
import UIKit
let stackView = UIStackView()
let titleLabel = UILabel()
let detailLabel = UILabel()
let showsDetails = true
let layout = stackView.stack {
titleLabel
if showsDetails {
detailLabel
}
}
layout.activate()activate()composes the hierarchy before resolving constraints. Repeated activation is a no-op.deactivate()reverses activation order, including constraints, temporary layout guides, built-in containment, previous view or arranged-subview placement, andtranslatesAutoresizingMaskIntoConstraintsvalues. Repeated deactivation is a no-op.- A deactivated layout can be activated again. Each activation records a fresh set of reversible effects.
- An active description is immutable. A view may appear only once in a tree, and a node cannot belong to two active trees.
The built-in add() and stack() strategies restore hierarchy state they change. A custom containment strategy receives no automatic guarantee for its own side effects; its returned closure must completely undo them. Resolver authoring, custom containment, control-flow details, and lifecycle invariants are covered in the advanced guide.
- Advanced usage and extension points
- Contributing and local verification
- Changes and release notes
- Security policy
- Issues
A license has not yet been selected. Until a license file is added, copyright law reserves all rights to the author.
Pin is primarily the work of Daniel Inoa.
