Agent skill
programmatic-uikit-layout
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
Install this agent skill to your Project
npx add-skill https://github.com/dagba/ios-mcp/tree/main/skills/programmatic-uikit-layout
SKILL.md
Programmatic UIKit Layout with Auto Layout
Overview
Programmatic UIKit layout with anchors provides type-safe, maintainable UI code used by production apps at scale. Stack views solve 50%+ of layout problems, anchors provide compile-time safety, design systems ensure consistency.
Core principle: Anchors-only (no Visual Format Language), stack-first composition, reusable components with centralized spacing/colors.
Foundation Setup
Step 1: Disable Storyboards
Remove Main.storyboard:
- Delete Main.storyboard file
- In Info.plist, delete "Main storyboard file base name" entry
- Delete "Storyboard Name" in Target → Info → Custom iOS Target Properties
Configure SceneDelegate:
class 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()
}
}
Step 2: Essential UIView Setup
CRITICAL: Every view needs 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 = true
Rule: Set translatesAutoresizingMaskIntoConstraints = false BEFORE adding constraints.
Layout Anchors (Preferred Method)
Why Anchors Over NSLayoutConstraint
| 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 = true
Anchor Types
X-axis: leadingAnchor, trailingAnchor, leftAnchor, rightAnchor, centerXAnchor
Y-axis: topAnchor, bottomAnchor, centerYAnchor
Dimension: widthAnchor, heightAnchor
Type safety:
// ✅ Compiles: Same axis
label.leadingAnchor.constraint(equalTo: view.leadingAnchor)
// ❌ Compiler error: Mixed axes
label.leadingAnchor.constraint(equalTo: view.topAnchor) // Won't compile!
Basic Constraint Patterns
Pin to edges:
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)
])
Center with size:
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)
])
Aspect ratio:
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
])
Safe Area Layout Guide
CRITICAL: Use safeAreaLayoutGuide for modern iPhones (notch, dynamic island).
// ❌ WRONG: Content hidden behind notch
label.topAnchor.constraint(equalTo: view.topAnchor)
// ✅ CORRECT: Respects safe area
label.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
Standard pattern:
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)
])
Stack Views (Stack-First Philosophy)
CRITICAL: Stack views solve 50%+ of layout problems. Use them liberally.
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)"];
}
Basic Stack View
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)
Nested Stack Views (Complex Layouts)
// 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
Spacer Views
// 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)
Design System Pattern
Theme (Centralized Spacing/Colors)
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.text
Reusable Components
Card View:
final 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)
])
}
}
Primary Button:
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
)
}
}
Layout Priority & Content Hugging/Compression
Content Hugging Priority
Controls how much view resists growing beyond intrinsic content size.
// 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
Content Compression Resistance
Controls how much view resists shrinking below intrinsic content size.
// Default: 750 (high)
// Higher priority = resists shrinking more
titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal) // 1000
subtitleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) // 250
// Result: Subtitle truncates before title
Common pattern: Label in horizontal stack
let 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)
Common Mistakes
| 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() |
Debugging Layout Issues
Ambiguous Layout
// 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
}
}
#endif
Constraint Conflicts
Runtime error: "Unable to simultaneously satisfy constraints..."
Debug:
- Read error message for constraint IDs
- Add identifiers to constraints:
let constraint = label.topAnchor.constraint(equalTo: view.topAnchor)
constraint.identifier = "label-top"
constraint.isActive = true
- Lower priority of less important constraint:
let constraint = label.heightAnchor.constraint(equalToConstant: 50)
constraint.priority = .defaultHigh // 750 instead of 1000
constraint.isActive = true
Quick Reference
Basic setup:
let 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)
])
Stack view:
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = Theme.Spacing.medium
stack.addArrangedSubview(view1)
stack.addArrangedSubview(view2)
Design system:
view.backgroundColor = Theme.Colors.background
stackView.spacing = Theme.Spacing.medium
button.layer.cornerRadius = Theme.CornerRadius.large
Red Flags - STOP and Reconsider
- Using Visual Format Language → Switch to anchors
- Not disabling
translatesAutoresizingMaskIntoConstraints→ Constraint conflicts - Ignoring safe area layout guide → Content hidden on modern iPhones
- Creating 5+ constraints manually → Consider UIStackView
- Hard-coded spacing values (8, 16, 24) scattered everywhere → Centralize in Theme
- Activating constraints individually → Batch with activate([])
- Ambiguous layout in production → Add intrinsic size or missing constraints
Real-World Impact
Before: Mixed VFL + anchors + raw NSLayoutConstraint. 500+ lines for profile screen. Frequent constraint conflicts.
After: Anchors-only + stack views. 200 lines, zero conflicts, easier to maintain.
Before: Hard-coded spacing (8px, 12px, 16px) across 30 view controllers. Design update = edit 30 files.
After: Theme.Spacing.medium. Design update = edit 1 file, affects entire app.
Before: Custom complex constraint logic for card layout. 100 lines, hard to debug.
After: Reusable CardView component. 30 lines, used in 15 places consistently.
Sources
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
gh-issue-fix-flow
End-to-end GitHub issue fix workflow using gh, local code changes, builds/tests, and git push. Use when asked to take an issue number, inspect the issue via gh, implement a fix, run XcodeBuildMCP builds/tests, commit with a closing message, and push.
realm-persistence
Use when implementing Realm database in iOS apps, encountering thread-safety errors, async/await crashes, performance issues with sync/writes, or integrating with Codable APIs
viper-architecture-rambler
Use when architecting complex iOS apps with multiple features, long-term maintenance requirements, or team scalability needs. Use when refactoring Massive View Controllers or implementing testable architecture. Do NOT use for simple single-screen apps, rapid prototypes, or small utility tools.
swiftui-ui-patterns
Best practices and example-driven guidance for building SwiftUI views and components. Use when creating or refactoring SwiftUI UI, designing tab architecture with TabView, composing screens, or needing component-specific patterns and examples.
swiftui-liquid-glass
Implement, review, or improve SwiftUI features using the iOS 26+ Liquid Glass API. Use when asked to adopt Liquid Glass in new SwiftUI UI, refactor an existing feature to Liquid Glass, or review Liquid Glass usage for correctness, performance, and design alignment.
swiftui-view-refactor
Refactor and review SwiftUI view files for consistent structure, dependency injection, and Observation usage. Use when asked to clean up a SwiftUI view’s layout/ordering, handle view models safely (non-optional when possible), or standardize how dependencies and @Observable state are initialized and passed.
Didn't find tool you were looking for?