Loading...
Loading...
Use when building UIKit interfaces without storyboards, setting up Auto Layout constraints with anchors, creating reusable UI components, or encountering layout constraint errors and ambiguous layout warnings
npx skill4agent add dagba/ios-mcp programmatic-uikit-layoutclass SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(windowScene: windowScene)
window?.rootViewController = MainViewController()
window?.makeKeyAndVisible()
}
}translatesAutoresizingMaskIntoConstraints = false// ❌ WRONG: Constraints conflict with autoresizing mask
let label = UILabel()
view.addSubview(label)
label.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
// Runtime error: conflicting constraints
// ✅ CORRECT: Disable autoresizing mask
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
label.topAnchor.constraint(equalTo: view.topAnchor).isActive = truetranslatesAutoresizingMaskIntoConstraints = false| Feature | Layout Anchors | NSLayoutConstraint |
|---|---|---|
| Type safety | ✅ Compile-time checks | ❌ Runtime errors |
| Readability | ✅ Fluent API | ❌ Verbose initializer |
| Error prevention | ✅ Can't mix X/Y axes | ❌ Easy to make mistakes |
// ❌ WRONG: NSLayoutConstraint (verbose, error-prone)
NSLayoutConstraint(
item: label,
attribute: .top,
relatedBy: .equal,
toItem: view,
attribute: .top,
multiplier: 1.0,
constant: 20
).isActive = true
// ✅ CORRECT: Layout anchors (concise, type-safe)
label.topAnchor.constraint(equalTo: view.topAnchor, constant: 20).isActive = trueleadingAnchortrailingAnchorleftAnchorrightAnchorcenterXAnchortopAnchorbottomAnchorcenterYAnchorwidthAnchorheightAnchor// ✅ Compiles: Same axis
label.leadingAnchor.constraint(equalTo: view.leadingAnchor)
// ❌ Compiler error: Mixed axes
label.leadingAnchor.constraint(equalTo: view.topAnchor) // Won't compile!label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
NSLayoutConstraint.activate([
label.topAnchor.constraint(equalTo: view.topAnchor, constant: 20),
label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
label.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
label.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -20)
])label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: view.centerYAnchor),
label.widthAnchor.constraint(equalToConstant: 200),
label.heightAnchor.constraint(equalToConstant: 50)
])imageView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(imageView)
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 9.0/16.0) // 16:9
])safeAreaLayoutGuide// ❌ WRONG: Content hidden behind notch
label.topAnchor.constraint(equalTo: view.topAnchor)
// ✅ CORRECT: Respects safe area
label.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)NSLayoutConstraint.activate([
contentView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
contentView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
])digraph layout_choice {
"Need to arrange multiple views?" [shape=diamond];
"All in line (row/column)?" [shape=diamond];
"Use UIStackView" [shape=box, style=filled, fillcolor=lightgreen];
"Use nested UIStackViews" [shape=box, style=filled, fillcolor=lightblue];
"Use anchors" [shape=box, style=filled, fillcolor=yellow];
"Need to arrange multiple views?" -> "All in line (row/column)?" [label="yes"];
"Need to arrange multiple views?" -> "Use anchors" [label="no"];
"All in line (row/column)?" -> "Use UIStackView" [label="yes"];
"All in line (row/column)?" -> "Use nested UIStackViews" [label="no (complex grid)"];
}let stackView = UIStackView()
stackView.axis = .vertical // or .horizontal
stackView.spacing = 16
stackView.alignment = .fill // .leading, .center, .trailing
stackView.distribution = .fill // .fillEqually, .equalSpacing, etc.
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
])
stackView.addArrangedSubview(titleLabel)
stackView.addArrangedSubview(subtitleLabel)
stackView.addArrangedSubview(button)// Horizontal stack with icon + labels
let horizontalStack = UIStackView()
horizontalStack.axis = .horizontal
horizontalStack.spacing = 12
horizontalStack.alignment = .center
let iconImageView = UIImageView(image: UIImage(systemName: "star.fill"))
iconImageView.widthAnchor.constraint(equalToConstant: 24).isActive = true
iconImageView.heightAnchor.constraint(equalToConstant: 24).isActive = true
// Vertical stack for title + subtitle
let verticalStack = UIStackView()
verticalStack.axis = .vertical
verticalStack.spacing = 4
verticalStack.addArrangedSubview(titleLabel)
verticalStack.addArrangedSubview(subtitleLabel)
horizontalStack.addArrangedSubview(iconImageView)
horizontalStack.addArrangedSubview(verticalStack)
view.addSubview(horizontalStack)
// Pin horizontalStack with anchors// Add flexible spacing in stack view
let spacer = UIView()
spacer.setContentHuggingPriority(.defaultLow, for: .vertical)
stackView.addArrangedSubview(spacer)
// Pattern: Button at bottom
stackView.addArrangedSubview(titleLabel)
stackView.addArrangedSubview(descriptionLabel)
stackView.addArrangedSubview(spacer) // Pushes button to bottom
stackView.addArrangedSubview(button)enum Theme {
enum Spacing {
static let tiny: CGFloat = 4
static let small: CGFloat = 8
static let medium: CGFloat = 16
static let large: CGFloat = 24
static let xLarge: CGFloat = 32
}
enum CornerRadius {
static let small: CGFloat = 4
static let medium: CGFloat = 8
static let large: CGFloat = 12
static let xLarge: CGFloat = 16
}
enum Colors {
static let primary = UIColor.systemBlue
static let secondary = UIColor.systemGray
static let background = UIColor.systemBackground
static let text = UIColor.label
static let textSecondary = UIColor.secondaryLabel
}
}
// Usage:
stackView.spacing = Theme.Spacing.medium
button.layer.cornerRadius = Theme.CornerRadius.medium
label.textColor = Theme.Colors.textfinal class CardView: UIView {
private let containerView = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) not implemented")
}
private func setupView() {
backgroundColor = Theme.Colors.background
layer.cornerRadius = Theme.CornerRadius.large
layer.shadowColor = UIColor.black.cgColor
layer.shadowOpacity = 0.1
layer.shadowOffset = CGSize(width: 0, height: 2)
layer.shadowRadius = 4
containerView.translatesAutoresizingMaskIntoConstraints = false
addSubview(containerView)
NSLayoutConstraint.activate([
containerView.topAnchor.constraint(equalTo: topAnchor, constant: Theme.Spacing.medium),
containerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Theme.Spacing.medium),
containerView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Theme.Spacing.medium),
containerView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -Theme.Spacing.medium)
])
}
func setContent(_ view: UIView) {
containerView.subviews.forEach { $0.removeFromSuperview() }
view.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(view)
NSLayoutConstraint.activate([
view.topAnchor.constraint(equalTo: containerView.topAnchor),
view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
])
}
}final class PrimaryButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
setupButton()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) not implemented")
}
private func setupButton() {
backgroundColor = Theme.Colors.primary
setTitleColor(.white, for: .normal)
layer.cornerRadius = Theme.CornerRadius.medium
titleLabel?.font = .systemFont(ofSize: 16, weight: .semibold)
contentEdgeInsets = UIEdgeInsets(
top: Theme.Spacing.medium,
left: Theme.Spacing.large,
bottom: Theme.Spacing.medium,
right: Theme.Spacing.large
)
}
}// Default: 250 (low)
// Higher priority = resists growing more
label.setContentHuggingPriority(.required, for: .horizontal) // 1000
button.setContentHuggingPriority(.defaultLow, for: .horizontal) // 250
// Result: Button expands, label stays at intrinsic width// Default: 750 (high)
// Higher priority = resists shrinking more
titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal) // 1000
subtitleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) // 250
// Result: Subtitle truncates before titlelet stackView = UIStackView()
stackView.axis = .horizontal
stackView.spacing = 8
let label = UILabel()
label.text = "Very long text that might truncate..."
label.setContentHuggingPriority(.defaultLow, for: .horizontal) // Allows expansion
label.setContentCompressionResistancePriority(.required, for: .horizontal) // Prevents shrinking
let button = UIButton()
button.setContentHuggingPriority(.required, for: .horizontal) // Stays at intrinsic width
stackView.addArrangedSubview(label)
stackView.addArrangedSubview(button)| Mistake | Reality | Fix |
|---|---|---|
| "Forgot translatesAutoresizingMaskIntoConstraints = false" | Conflicting constraints, layout breaks | Set to false BEFORE constraints |
| "Using view.topAnchor instead of safeAreaLayoutGuide" | Content hidden behind notch | Always use safeAreaLayoutGuide |
| "Individual .isActive = true for each constraint" | Verbose, inefficient | Use NSLayoutConstraint.activate([]) |
| "Visual Format Language is easier" | VFL is deprecated, error-prone | Use anchors only |
| "Don't need content hugging/compression" | Labels truncate unexpectedly, buttons expand wrong | Set priorities explicitly |
| "Adding subview after constraints" | Crash: view not in hierarchy | addSubview() BEFORE activate() |
// In UIViewController viewDidLoad or custom view init
#if DEBUG
DispatchQueue.main.async {
if self.view.hasAmbiguousLayout {
print("⚠️ Ambiguous layout detected")
self.view.exerciseAmbiguityInLayout() // Animate between valid layouts
}
}
#endiflet constraint = label.topAnchor.constraint(equalTo: view.topAnchor)
constraint.identifier = "label-top"
constraint.isActive = truelet constraint = label.heightAnchor.constraint(equalToConstant: 50)
constraint.priority = .defaultHigh // 750 instead of 1000
constraint.isActive = truelet view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
parentView.addSubview(view)
NSLayoutConstraint.activate([
view.topAnchor.constraint(equalTo: parentView.safeAreaLayoutGuide.topAnchor),
view.leadingAnchor.constraint(equalTo: parentView.leadingAnchor),
view.trailingAnchor.constraint(equalTo: parentView.trailingAnchor),
view.bottomAnchor.constraint(equalTo: parentView.bottomAnchor)
])let stack = UIStackView()
stack.axis = .vertical
stack.spacing = Theme.Spacing.medium
stack.addArrangedSubview(view1)
stack.addArrangedSubview(view2)view.backgroundColor = Theme.Colors.background
stackView.spacing = Theme.Spacing.medium
button.layer.cornerRadius = Theme.CornerRadius.largetranslatesAutoresizingMaskIntoConstraints