UIKit Horizontal-First Layouting · v1.0.0 · 2026-03-29 · sha256 d85d416fb6e2d68d

UIKit Horizontal-First Layouting v1.0.0A

Immutable. This exact content is served forever at /api/v1/blob/d85d416fb6e2d68d.

---
name: UIKit Horizontal-First Layouting
description: Horizontal-first slicing methodology with mandatory ASCII preview for UIKit layout design
version: 1.0.0
---

# 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)

1. **Identify horizontal bands** - Slice UI into rows first
2. **ASCII preview** - ALWAYS show layout before writing ANY code
3. **Implement top-to-bottom** - Build each row sequentially
4. **User confirmation** - Get approval before generating code

## MANDATORY: ASCII Preview Before Code

**CRITICAL**: Before writing ANY UIKit layout code, you MUST:

1. Draw an ASCII diagram of the entire screen
2. Label each horizontal row
3. Show content within each row
4. Mark fixed vs flexible heights
5. 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

```swift
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.

```swift
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:

```swift
// 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

1. Ask questions using AskUserQuestion
2. Summarize: "Creating a [screen type] with [components], [complexity] layout, [scrolling] behavior"
3. **ALWAYS show ASCII preview** before any code
4. Get user confirmation
5. Generate UIKit code

### Skip Interview If:
- User provided detailed layout specifications
- User says "skip questions" or "just do it"
- User shared wireframe or design