UIKit Horizontal-First Layouting
A systematic approach to UIKit layout design that prioritizes horizontal slicing and requires ASCII visualization before any code implementation.
Core Methodology (ALWAYS FOLLOW)
- •Identify horizontal bands - Slice UI into rows first
- •ASCII preview - ALWAYS show layout before writing ANY code
- •Implement top-to-bottom - Build each row sequentially
- •User confirmation - Get approval before generating code
MANDATORY: ASCII Preview Before Code
CRITICAL: Before writing ANY UIKit layout code, you MUST:
- •Draw an ASCII diagram of the entire screen
- •Label each horizontal row
- •Show content within each row
- •Mark fixed vs flexible heights
- •Get user confirmation
NEVER skip this step. NEVER write layout code first.
ASCII Preview Format
Full Screen Template
┌─────────────────────────────────────┐ │ [Status Bar - 44pt/54pt] │ <- Safe area top ├─────────────────────────────────────┤ │ [Navigation Bar - 44pt] │ <- Fixed height ├─────────────────────────────────────┤ │ [Row 1: Content description] │ <- Describe content ├─────────────────────────────────────┤ │ [Row 2: Content description] │ ├─────────────────────────────────────┤ │ [ │ │ Flexible Content Area │ <- Mark as flexible │ │ ├─────────────────────────────────────┤ │ [Row N: Content description] │ ├─────────────────────────────────────┤ │ [Tab Bar / Safe Area - 34pt/49pt] │ <- Safe area bottom └─────────────────────────────────────┘
Row Detail Template
When a row has horizontal content, show the internal layout:
Row: [Leading | Center | Trailing] Example: ┌────────────────────────────────────────┐ │ [48px] │ [Flexible] │ [32px] │ │ Icon │ Title + Subtitle│ Chevron │ └────────────────────────────────────────┘
Complete Example: Login Screen
Step 1: ASCII Preview (REQUIRED)
┌─────────────────────────────────────┐ │ [Safe Area Top] │ ├─────────────────────────────────────┤ │ [Row 1: Logo - 120pt] │ <- Fixed │ ┌──────┐ │ │ │ Logo │ │ │ └──────┘ │ ├─────────────────────────────────────┤ │ [Row 2: Title - 60pt] │ <- Fixed │ "Welcome Back" │ ├─────────────────────────────────────┤ │ [Row 3: Email Field - 80pt] │ <- Fixed │ ┌─────────────────────────────┐ │ │ │ Email │ │ │ └─────────────────────────────┘ │ │ [Error label] │ ├─────────────────────────────────────┤ │ [Row 4: Password Field - 80pt] │ <- Fixed │ ┌─────────────────────────────┐ │ │ │ Password [Eye] │ │ │ └─────────────────────────────┘ │ │ [Error label] │ ├─────────────────────────────────────┤ │ [Row 5: Forgot Password - 44pt] │ <- Fixed │ "Forgot Password?" │ ├─────────────────────────────────────┤ │ [ │ │ Flexible Spacer │ <- Flexible │ │ ├─────────────────────────────────────┤ │ [Row 6: Login Button - 56pt] │ <- Fixed │ ┌─────────────────────────────┐ │ │ │ Login │ │ │ └─────────────────────────────┘ │ ├─────────────────────────────────────┤ │ [Row 7: Signup Link - 44pt] │ <- Fixed │ "Don't have account? Sign up" │ ├─────────────────────────────────────┤ │ [Safe Area Bottom] │ └─────────────────────────────────────┘
Step 2: Row Breakdown
| Row | Content | Height | Horizontal Layout |
|---|---|---|---|
| 1 | Logo | 120pt fixed | Centered |
| 2 | Title | 60pt fixed | Centered |
| 3 | Email field + error | 80pt fixed | Full width with padding |
| 4 | Password field + error | 80pt fixed | TextField + eye button |
| 5 | Forgot password | 44pt fixed | Centered link |
| Spacer | - | Flexible | - |
| 6 | Login button | 56pt fixed | Full width with padding |
| 7 | Signup link | 44pt fixed | Centered |
Step 3: User Confirms → Generate Code
final class LoginViewController: UIViewController {
// MARK: - UI Elements
private let rootStackView: UIStackView = {
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 0
stack.translatesAutoresizingMaskIntoConstraints = false
return stack
}()
// Row 1: Logo
private let logoRow: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let logoImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.image = UIImage(named: "logo")
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
// Row 2: Title
private let titleLabel: UILabel = {
let label = UILabel()
label.text = "Welcome Back"
label.font = .systemFont(ofSize: 28, weight: .bold)
label.textAlignment = .center
return label
}()
// Row 3: Email
private let emailRow: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let emailTextField: UITextField = {
let tf = UITextField()
tf.placeholder = "Email"
tf.borderStyle = .roundedRect
tf.keyboardType = .emailAddress
tf.autocapitalizationType = .none
tf.translatesAutoresizingMaskIntoConstraints = false
return tf
}()
private let emailErrorLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 12)
label.textColor = .systemRed
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
// Row 4: Password
private let passwordRow: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let passwordTextField: UITextField = {
let tf = UITextField()
tf.placeholder = "Password"
tf.borderStyle = .roundedRect
tf.isSecureTextEntry = true
tf.translatesAutoresizingMaskIntoConstraints = false
return tf
}()
private let eyeButton: UIButton = {
let button = UIButton(type: .system)
button.setImage(UIImage(systemName: "eye"), for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private let passwordErrorLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 12)
label.textColor = .systemRed
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
// Row 5: Forgot Password
private let forgotPasswordButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Forgot Password?", for: .normal)
return button
}()
// Flexible Spacer
private let spacerView: UIView = {
let view = UIView()
view.setContentHuggingPriority(.defaultLow, for: .vertical)
return view
}()
// Row 6: Login Button
private let loginRow: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let loginButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Login", for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 17, weight: .semibold)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 12
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
// Row 7: Signup Link
private let signupButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Don't have account? Sign up", for: .normal)
return button
}()
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
setupHierarchy()
setupConstraints()
}
// MARK: - Setup
private func setupHierarchy() {
view.addSubview(rootStackView)
// Row 1: Logo
logoRow.addSubview(logoImageView)
rootStackView.addArrangedSubview(logoRow)
// Row 2: Title
rootStackView.addArrangedSubview(titleLabel)
// Row 3: Email
emailRow.addSubview(emailTextField)
emailRow.addSubview(emailErrorLabel)
rootStackView.addArrangedSubview(emailRow)
// Row 4: Password
passwordRow.addSubview(passwordTextField)
passwordRow.addSubview(eyeButton)
passwordRow.addSubview(passwordErrorLabel)
rootStackView.addArrangedSubview(passwordRow)
// Row 5: Forgot Password
rootStackView.addArrangedSubview(forgotPasswordButton)
// Flexible Spacer
rootStackView.addArrangedSubview(spacerView)
// Row 6: Login Button
loginRow.addSubview(loginButton)
rootStackView.addArrangedSubview(loginRow)
// Row 7: Signup Link
rootStackView.addArrangedSubview(signupButton)
}
private func setupConstraints() {
let padding: CGFloat = 24
NSLayoutConstraint.activate([
// Root stack pinned to safe area
rootStackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
rootStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
rootStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
rootStackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
// Row 1: Logo - 120pt height
logoRow.heightAnchor.constraint(equalToConstant: 120),
logoImageView.centerXAnchor.constraint(equalTo: logoRow.centerXAnchor),
logoImageView.centerYAnchor.constraint(equalTo: logoRow.centerYAnchor),
logoImageView.widthAnchor.constraint(equalToConstant: 80),
logoImageView.heightAnchor.constraint(equalToConstant: 80),
// Row 2: Title - intrinsic (wrapped by stack)
// Row 3: Email - 80pt height
emailRow.heightAnchor.constraint(equalToConstant: 80),
emailTextField.topAnchor.constraint(equalTo: emailRow.topAnchor, constant: 8),
emailTextField.leadingAnchor.constraint(equalTo: emailRow.leadingAnchor, constant: padding),
emailTextField.trailingAnchor.constraint(equalTo: emailRow.trailingAnchor, constant: -padding),
emailTextField.heightAnchor.constraint(equalToConstant: 44),
emailErrorLabel.topAnchor.constraint(equalTo: emailTextField.bottomAnchor, constant: 4),
emailErrorLabel.leadingAnchor.constraint(equalTo: emailTextField.leadingAnchor),
emailErrorLabel.trailingAnchor.constraint(equalTo: emailTextField.trailingAnchor),
// Row 4: Password - 80pt height
passwordRow.heightAnchor.constraint(equalToConstant: 80),
passwordTextField.topAnchor.constraint(equalTo: passwordRow.topAnchor, constant: 8),
passwordTextField.leadingAnchor.constraint(equalTo: passwordRow.leadingAnchor, constant: padding),
passwordTextField.trailingAnchor.constraint(equalTo: passwordRow.trailingAnchor, constant: -padding),
passwordTextField.heightAnchor.constraint(equalToConstant: 44),
eyeButton.centerYAnchor.constraint(equalTo: passwordTextField.centerYAnchor),
eyeButton.trailingAnchor.constraint(equalTo: passwordTextField.trailingAnchor, constant: -12),
eyeButton.widthAnchor.constraint(equalToConstant: 24),
passwordErrorLabel.topAnchor.constraint(equalTo: passwordTextField.bottomAnchor, constant: 4),
passwordErrorLabel.leadingAnchor.constraint(equalTo: passwordTextField.leadingAnchor),
passwordErrorLabel.trailingAnchor.constraint(equalTo: passwordTextField.trailingAnchor),
// Row 5: Forgot Password - 44pt (intrinsic)
forgotPasswordButton.heightAnchor.constraint(equalToConstant: 44),
// Spacer takes remaining space (low hugging priority)
// Row 6: Login Button - 56pt height
loginRow.heightAnchor.constraint(equalToConstant: 56),
loginButton.topAnchor.constraint(equalTo: loginRow.topAnchor),
loginButton.leadingAnchor.constraint(equalTo: loginRow.leadingAnchor, constant: padding),
loginButton.trailingAnchor.constraint(equalTo: loginRow.trailingAnchor, constant: -padding),
loginButton.bottomAnchor.constraint(equalTo: loginRow.bottomAnchor),
// Row 7: Signup Link - 44pt (intrinsic)
signupButton.heightAnchor.constraint(equalToConstant: 44),
])
}
}
Horizontal Slicing Rules
Rule 1: Top-Level is Always Vertical
The root container is a vertical stack. Each child is a horizontal row.
let rootStack = UIStackView() rootStack.axis = .vertical // ALWAYS
Rule 2: Rows Contain Horizontal Content
Each row manages its own horizontal layout:
Row: [Leading Content | Center Content | Trailing Content]
Rule 3: Fixed vs Flexible Heights
Mark heights clearly in ASCII:
- •
← Fixed 44pt- explicit height constraint - •
← Flexible- uses content hugging/compression resistance
Rule 4: Safe Areas Are Respected
Always show safe area boundaries:
- •Top: Status bar + notch
- •Bottom: Home indicator
Common Row Patterns
Navigation Row
┌────────────────────────────────────────┐ │ [Back] │ [Title] │ [Action] │ │ 44pt │ Flexible │ 44pt │ └────────────────────────────────────────┘
List Cell Row
┌────────────────────────────────────────┐ │ [Avatar] │ [Title ] │ [Accessory] │ │ 48pt │ [Subtitle ] │ 24pt │ │ │ Flexible │ │ └────────────────────────────────────────┘
Form Field Row
┌────────────────────────────────────────┐ │ [Label] │ [TextField ] │ │ 80pt │ Flexible │ └────────────────────────────────────────┘
Button Bar Row
┌────────────────────────────────────────┐ │ [Cancel] │ [Spacer] │ [Confirm] │ │ Equal │ Flexible │ Equal │ └────────────────────────────────────────┘
Constraint Naming Convention
Use descriptive names that match the ASCII diagram:
// Row names let headerRow, contentRow, footerRow // Element positions let leadingElement, centerElement, trailingElement // Sections let topSection, middleSection, bottomSection
Workflow Summary
1. User Request → "Create a settings screen"
↓
2. ASCII Diagram → Draw full screen with all rows
↓
3. Row Breakdown → Table listing content & heights
↓
4. User Confirms → "Looks good" / "Change X"
↓
5. Generate Code → UIKit with proper constraints
Anti-Patterns (NEVER DO)
❌ Write code without ASCII preview ❌ Guess at layout structure ❌ Start with individual views before overall structure ❌ Mix SwiftUI and UIKit for layout ❌ Use Auto Layout without clear row hierarchy
Quick Reference: Heights
| Element | Height |
|---|---|
| Status bar | 44pt (54pt with notch) |
| Navigation bar | 44pt |
| Large title nav | 96pt |
| Tab bar | 49pt |
| Toolbar | 44pt |
| Standard button | 44pt |
| Large button | 56pt |
| Text field | 44pt |
| Table cell | 44pt minimum |
| Safe area bottom | 34pt (home indicator) |
Pre-Layout Interview
Before creating any UI layout, gather requirements using AskUserQuestion:
Layout Questions
Question 1: Screen Type
- •Header: "Screen Type"
- •Question: "What type of screen is this?"
- •Options:
- •Form screen - Input fields, labels, buttons
- •List screen - Scrollable list of items
- •Detail screen - Display content with actions
- •Dashboard - Multiple sections/cards
Question 2: Key Components (multiSelect: true)
- •Header: "Components"
- •Question: "What UI elements do you need?"
- •Options:
- •Text inputs - Text fields, text views
- •Buttons - Action buttons, links
- •Images - Photos, icons, avatars
- •Lists/Tables - Scrollable content
Question 3: Layout Complexity
- •Header: "Complexity"
- •Question: "How complex is the layout?"
- •Options:
- •Simple - Few rows, straightforward
- •Medium - Multiple sections, some nesting
- •Complex - Many elements, intricate layout
Question 4: Scrolling Behavior
- •Header: "Scrolling"
- •Question: "Does the content need to scroll?"
- •Options:
- •No scrolling - Fixed content fits screen
- •Vertical scroll - Content exceeds screen height
- •Horizontal scroll - Side-scrolling content
- •Both directions - Complex scrollable content
Interview Flow
- •Ask questions using AskUserQuestion
- •Summarize: "Creating a [screen type] with [components], [complexity] layout, [scrolling] behavior"
- •ALWAYS show ASCII preview before any code
- •Get user confirmation
- •Generate UIKit code
Skip Interview If:
- •User provided detailed layout specifications
- •User says "skip questions" or "just do it"
- •User shared wireframe or design