Skip to content

A manual layout system for UIKit based on a SwiftUI-like declarative DSL

License

Notifications You must be signed in to change notification settings

pelagornis/swift-layout

Repository files navigation

Layout

Official Swift Version iOS Version License CodeCov Swift Package Manager

A high-performance, SwiftUI-style declarative layout system built on frame-based calculations β€” no Auto Layout constraints. Layout combines the readability of SwiftUI with the blazing speed of direct frame manipulation.

Why Layout?

Feature Auto Layout Layout
Performance Constraint solving overhead Direct frame calculation
Syntax Imperative constraints Declarative SwiftUI-style
Debugging Complex constraint conflicts Simple frame inspection
Learning Curve Steep Familiar to SwiftUI users

✨ Features

Core Features

πŸš€ High Performance - Pure frame-based calculations, zero Auto Layout overhead
πŸ“± SwiftUI-Style API - Familiar declarative syntax with @LayoutBuilder
πŸ”„ Automatic View Management - Smart view hierarchy handling with automatic add/remove
🎯 Zero Dependencies - Pure UIKit with optional SwiftUI integration
βš™οΈ Two-Phase Layout - Separate measure and layout phases for optimal performance
🎯 Efficient Modifiers - Modifiers stored as properties, not new nodes

Layout Components

πŸ“¦ VStack - Vertical stacking with spacing and alignment options
πŸ“¦ HStack - Horizontal stacking with spacing and alignment options
πŸ“¦ ZStack - Z-axis stacking with multiple alignment options
πŸ“œ ScrollView - Vertical and horizontal scrolling with content offset preservation
πŸ“ Spacer - Flexible space component with minimum length support
πŸ”„ ForEach - Dynamic list generation with identity-based diffing

Layout Modifiers

πŸ“ Size - Fixed or flexible width/height sizing
πŸ“¦ Padding - Uniform or edge-specific padding
πŸ“ Offset - Position adjustment from calculated layout
🎨 Background - Background color and view support
πŸ”² Corner Radius - Rounded corner styling
πŸ†” Identity - View identity for efficient diffing and reuse

Advanced Features

πŸ“ GeometryReader - Access container size and position dynamically
⚑ Animation Engine - Built-in spring and timing animations with withAnimation
πŸ”§ Environment System - Color scheme, layout direction, and custom environment values
πŸŒ‰ UIKit ↔ SwiftUI Bridge - Seamless integration between frameworks
🎨 Preferences System - Pass values up the view hierarchy
🌳 Layout Tree & Dirty Propagation - Incremental layout updates with partial recalculation
πŸ†” Identity & Diff - Efficient view updates based on identity tracking
πŸ”„ Layout Invalidation Rules - Clear rules for when and how layouts update

Performance Features

πŸ’Ύ Layout Caching - Intelligent caching for repeated layouts
πŸ“Š Performance Profiler - Real-time FPS and layout metrics
πŸ“ SwiftUI-Style Size Proposals - Accurate size negotiation with unconstrained dimensions
⚑ Incremental Layout - Only recalculate changed parts of the layout tree

Developer Tools

πŸ› Debugging Hooks - Custom hooks for debugging and monitoring
πŸ” View Hierarchy Analysis - Detailed layout structure inspection
πŸ“Š Frame Rate Monitor - Real-time FPS tracking
πŸ”— UIKit Lifecycle Integration - Seamless integration with view controller lifecycle


πŸ“¦ Installation

Swift Package Manager

Add the following to your Package.swift:

dependencies: [
    .package(url: "https://github.com/pelagornis/swift-layout.git", from: "vTag")
]

Then add Layout to your target dependencies:

.target(
    name: "YourTarget",
    dependencies: ["Layout"]
)

Xcode

  1. File β†’ Add Package Dependencies
  2. Enter: https://github.com/pelagornis/swift-layout.git
  3. Select version and add to your project

πŸš€ Quick Start

Basic Setup

import Layout

class MyViewController: UIViewController, Layout {
    // 1. Create a layout container
    let layoutContainer = LayoutContainer()

    // 2. Create your UI components
    let titleLabel = UILabel()
    let subtitleLabel = UILabel()
    let actionButton = UIButton(type: .system)

    override func viewDidLoad() {
        super.viewDidLoad()

        // 3. Configure views
        titleLabel.text = "Welcome to Layout!"
        titleLabel.font = .systemFont(ofSize: 28, weight: .bold)

        subtitleLabel.text = "High-performance declarative layouts"
        subtitleLabel.font = .systemFont(ofSize: 16)
        subtitleLabel.textColor = .secondaryLabel

        actionButton.setTitle("Get Started", for: .normal)
        actionButton.backgroundColor = .systemBlue
        actionButton.setTitleColor(.white, for: .normal)
        actionButton.layer.cornerRadius = 12

        // 4. Setup container using pure Manual Layout (no Auto Layout)
        setupLayoutContainer(layoutContainer)

        // 5. Set and update the layout body
        layoutContainer.updateBody { self.body }
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        // Update container frame when bounds change
        updateLayoutContainer(layoutContainer)
    }

    // 6. Define your layout declaratively
    @LayoutBuilder var body: some Layout {
        VStack(alignment: .center, spacing: 16) {
            Spacer(minLength: 100)

            titleLabel.layout()
                .size(width: 300, height: 34)

            subtitleLabel.layout()
                .size(width: 300, height: 20)

            Spacer(minLength: 40)

            actionButton.layout()
                .size(width: 280, height: 50)

            Spacer()
        }
        .padding(20)
    }
}

Using BaseViewController (Recommended)

For the cleanest code, inherit from BaseViewController:

class MyViewController: BaseViewController, Layout {
    let titleLabel = UILabel()
    let actionButton = UIButton()

    override func viewDidLoad() {
        super.viewDidLoad()

        // Setup views
        titleLabel.text = "Welcome"
        actionButton.setTitle("Get Started", for: .normal)

        // Layout container is automatically set up
        // Just override setLayout() to define your layout
    }

    override func setLayout() {
        layoutContainer.updateBody { self.body }
    }

    @LayoutBuilder var body: some Layout {
        VStack(alignment: .center, spacing: 24) {
            titleLabel.layout().size(width: 280, height: 30)
            actionButton.layout().size(width: 240, height: 50)
        }
    }
}

Using UIViewController Extension

For minimal integration without a base class:

class MyViewController: UIViewController, Layout {
    let layoutContainer = LayoutContainer()
    let titleLabel = UILabel()
    let actionButton = UIButton()

    override func viewDidLoad() {
        super.viewDidLoad()

        // Setup views
        titleLabel.text = "Welcome"
        actionButton.setTitle("Get Started", for: .normal)

        // Setup container (pure Manual Layout, no Auto Layout)
        setupLayoutContainer(layoutContainer)

        // Set and update layout
        layoutContainer.updateBody { self.body }
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        updateLayoutContainer(layoutContainer)
    }

    @LayoutBuilder var body: some Layout {
        VStack(alignment: .center, spacing: 24) {
            titleLabel.layout().size(width: 280, height: 30)
            actionButton.layout().size(width: 240, height: 50)
        }
    }
}

πŸ“¦ LayoutContainer

LayoutContainer is the main container that manages your layout hierarchy. It provides automatic view management, content centering, and animation protection.

Key Features

  • Automatic View Management: Views are automatically added/removed based on layout changes
  • Content Centering: Content is automatically centered like SwiftUI
  • Animation Protection: Prevents layout system from overriding animated views
  • Layout Updates: Smart layout invalidation with clear rules
  • Two-Phase Layout: Separate measure and layout phases for optimal performance
  • Pure Manual Layout: Zero Auto Layout constraints, only frame-based calculations

Animation Protection

When animating views directly, use startAnimating and stopAnimating to prevent the layout system from overriding your animations:

// Mark view as animating
layoutContainer.startAnimating(myView)

// Animate the view
withAnimation(.easeInOut(duration: 0.3)) {
    myView.frame.size = CGSize(width: 300, height: 200)
}

// Stop animating after completion
withAnimation(.easeInOut(duration: 0.3), {
    myView.frame.size = CGSize(width: 300, height: 200)
}, completion: { _ in
    layoutContainer.stopAnimating(myView)
})

// Check if any views are animating
if layoutContainer.isAnimating {
    // Layout updates are automatically paused
}

Layout Updates

// Set body and update immediately (recommended)
layoutContainer.updateBody { self.body }

// Or set body first, then update separately
layoutContainer.setBody { self.body }
layoutContainer.updateBody()

// Update existing body without changing it
layoutContainer.updateBody()

// Force layout update
layoutContainer.setNeedsLayout()
layoutContainer.layoutIfNeeded()

// Update layout for orientation changes
layoutContainer.updateLayoutForOrientationChange()

Layout Invalidation Rules

Layout uses clear invalidation rules to determine when layouts need to be recalculated:

// Invalidation rules are automatically applied
let rules = LayoutInvalidationRules.default

// Check if a reason should trigger invalidation
if rules.shouldInvalidate(for: .hierarchyChanged) {
    // Layout will be invalidated
}

// Check invalidation priority
let priority = rules.priority(for: .sizeChanged) // Returns 3

🎨 Layout Components

VStack (Vertical Stack)

Arranges children vertically from top to bottom.

VStack(alignment: .center, spacing: 16) {
    headerView.layout()
        .size(width: 300, height: 60)

    contentView.layout()
        .size(width: 300, height: 200)

    footerView.layout()
        .size(width: 300, height: 40)
}

Parameters:

  • alignment: .leading, .center, .trailing (default: .center)
  • spacing: Space between children (default: 0)

HStack (Horizontal Stack)

Arranges children horizontally from leading to trailing.

HStack(alignment: .center, spacing: 12) {
    iconView.layout()
        .size(width: 44, height: 44)

    VStack(alignment: .leading, spacing: 4) {
        titleLabel.layout().size(width: 200, height: 20)
        subtitleLabel.layout().size(width: 200, height: 16)
    }

    Spacer()

    chevronIcon.layout()
        .size(width: 24, height: 24)
}
.padding(16)

Parameters:

  • alignment: .top, .center, .bottom (default: .center)
  • spacing: Space between children (default: 0)

ZStack (Overlay Stack)

Overlays children on top of each other.

ZStack(alignment: .topTrailing) {
    // Background (bottom layer)
    backgroundImage.layout()
        .size(width: 300, height: 200)

    // Content (middle layer)
    contentView.layout()
        .size(width: 280, height: 180)

    // Badge (top layer, positioned at top-trailing)
    badgeView.layout()
        .size(width: 30, height: 30)
        .offset(x: -10, y: 10)
}

Parameters:

  • alignment: Any combination of vertical (.top, .center, .bottom) and horizontal (.leading, .center, .trailing)

ScrollView

Adds scrolling capability to content with automatic content offset preservation.

// Vertical scrolling (default)
ScrollView {
    VStack(alignment: .center, spacing: 20) {
        // Header
        headerView.layout()
            .size(width: 350, height: 200)

        // Multiple content sections
        ForEach(sections) { section in
            sectionView.layout()
                .size(width: 350, height: 150)
        }

        // Bottom spacing
        Spacer(minLength: 100)
    }
}

// Horizontal scrolling
ScrollView(.horizontal) {
    HStack(spacing: 16) {
        ForEach(items) { item in
            itemView.layout()
                .size(width: 200, height: 150)
        }
    }
}

// With safe area offset adjustment
let scrollView = ScrollView {
    contentView.layout()
}
scrollView.adjustsContentOffsetForSafeArea = true

Spacer

Flexible space that expands to fill available room.

VStack(alignment: .center, spacing: 0) {
    Spacer(minLength: 20)  // At least 20pt, can grow

    titleLabel.layout()

    Spacer()  // Flexible space, takes remaining room

    buttonView.layout()

    Spacer(minLength: 40)  // Safe area padding
}

πŸŽ›οΈ Layout Modifiers

Size

// Fixed size
myView.layout()
    .size(width: 200, height: 100)

// Width only (height flexible)
myView.layout()
    .size(width: 200)

// Height only (width flexible)
myView.layout()
    .size(height: 50)

// Percentage-based size
myView.layout()
    .size(width: 90%, height: 100)

// Mixed fixed and percentage
myView.layout()
    .size(width: 80%, height: 50)

// Percentage syntax with postfix operator
let width: Percent = 80%  // Using postfix % operator
myView.layout()
    .size(width: width, height: 100)

Percentage-based Sizing:

Layout supports percentage-based sizing using the % postfix operator:

// Direct percentage syntax
myView.layout()
    .size(width: 90%, height: 100)

// Percentage is calculated relative to parent container's size
VStack(alignment: .center, spacing: 16) {
    headerView.layout()
        .size(width: 90%, height: 60)  // 90% of VStack's width

    contentView.layout()
        .size(width: 80%, height: 200)  // 80% of VStack's width
}

// Mix percentage and fixed sizes
cardView.layout()
    .size(width: 50%, height: 140)  // 50% width, fixed 140pt height

Edge Positioning with Percentages:

// Position views using percentage offsets
myView.layout()
    .size(width: 100, height: 100)
    .top(10%)      // 10% from top
    .leading(20%)  // 20% from leading edge
    .centerX()     // Center horizontally

Padding

// Uniform padding
VStack { ... }
    .padding(20)

// Edge-specific padding
VStack { ... }
    .padding(UIEdgeInsets(top: 20, left: 16, bottom: 40, right: 16))

Offset

// Move view from its calculated position
myView.layout()
    .size(width: 100, height: 100)
    .offset(x: 10, y: -5)

Background & Corner Radius

VStack { ... }
    .layout()
    .size(width: 300, height: 200)
    .background(.systemBlue)
    .cornerRadius(16)

Chaining Modifiers

cardView.layout()
    .size(width: 320, height: 180)
    .padding(16)
    .background(.tertiarySystemBackground)
    .cornerRadius(20)
    .offset(y: 10)

Performance Note: Modifiers are stored as properties on the view itself (using Associated Objects), not as new nodes. This means modifier chains don't create new ViewLayout instances, providing optimal performance.


πŸ“ GeometryReader

GeometryReader provides access to its container's size and position, enabling dynamic layouts.

Declarative Style (with @LayoutBuilder)

GeometryReader { proxy in
    // Use proxy.size for dynamic sizing
    VStack(alignment: .center, spacing: 8) {
        topBox.layout()
            .size(width: proxy.size.width * 0.8, height: 60)

        bottomBox.layout()
            .size(width: proxy.size.width * 0.6, height: 40)
    }
}
.layout()
.size(width: 360, height: 140)

Imperative Style (for Complex Layouts)

When you need direct control over view placement:

GeometryReader { proxy, container in
    // Calculate dimensions based on container size
    let availableWidth = proxy.size.width - 32
    let columnWidth = (availableWidth - 16) / 2

    // Create and position views manually
    let leftColumn = createColumn()
    leftColumn.frame = CGRect(x: 16, y: 16, width: columnWidth, height: 100)
    container.addSubview(leftColumn)

    let rightColumn = createColumn()
    rightColumn.frame = CGRect(x: 16 + columnWidth + 16, y: 16, width: columnWidth, height: 100)
    container.addSubview(rightColumn)
}

GeometryProxy Properties

GeometryReader { proxy, container in
    // Container dimensions
    let width = proxy.size.width      // CGFloat
    let height = proxy.size.height    // CGFloat

    // Safe area information
    let topInset = proxy.safeAreaInsets.top
    let bottomInset = proxy.safeAreaInsets.bottom

    // Position in global coordinate space
    let globalX = proxy.globalFrame.minX
    let globalY = proxy.globalFrame.minY

    // Local bounds (origin is always 0,0)
    let bounds = proxy.bounds  // CGRect
}

Geometry Change Callback

React to size changes:

let geometryReader = GeometryReader { proxy in
    contentView.layout()
        .size(width: proxy.size.width, height: proxy.size.height)
}
.onGeometryChange { proxy in
    print("Size changed: \(proxy.size)")
    print("Global position: \(proxy.globalFrame.origin)")
}

// Use in layout
geometryReader.layout()
    .size(width: 300, height: 200)

⚑ Animation Engine

Layout provides SwiftUI-style animation support with withAnimation and animation modifiers.

withAnimation Function

The withAnimation function provides SwiftUI-like animation blocks:

// Basic animation
withAnimation {
    self.view.alpha = 1.0
    self.view.frame.size = CGSize(width: 200, height: 200)
}

// Custom animation
withAnimation(.spring(damping: 0.7, velocity: 0.5)) {
    self.cardView.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
}

// With completion handler
withAnimation(.easeInOut(duration: 0.3), {
    self.view.frame.origin = CGPoint(x: 100, y: 100)
}, completion: { finished in
    print("Animation completed: \(finished)")
})

Animation Presets

// Predefined animations
withAnimation(.default)      // 0.3s easeInOut
withAnimation(.spring)        // Spring animation with damping 0.7
withAnimation(.quick)         // 0.15s easeOut

// Custom timing functions
withAnimation(.easeIn(duration: 0.4))
withAnimation(.easeOut(duration: 0.3))
withAnimation(.easeInOut(duration: 0.5))
withAnimation(.linear(duration: 0.3))

// Custom spring
withAnimation(.spring(damping: 0.6, velocity: 0.8))

Protecting Animations from Layout System

When animating views directly, protect them from layout system interference:

// Mark view as animating
layoutContainer.startAnimating(myView)

// Animate the view
withAnimation(.easeInOut(duration: 0.3)) {
    myView.frame.size = CGSize(width: 300, height: 200)
}

// Stop animating after completion
withAnimation(.easeInOut(duration: 0.3), {
    myView.frame.size = CGSize(width: 300, height: 200)
}, completion: { _ in
    layoutContainer.stopAnimating(myView)
})

// Check if any views are animating
if layoutContainer.isAnimating {
    // Layout updates are paused
}

LayoutAnimation Structure

// Create custom animation
let customAnimation = LayoutAnimation(
    duration: 0.5,
    delay: 0.1,
    timingFunction: .easeInOut,
    repeatCount: 1,
    autoreverses: false
)

// Use with withAnimation
withAnimation(customAnimation) {
    // Your animations
}

πŸ”§ Environment System

Color Scheme Detection

// Get current color scheme
let colorScheme = ColorScheme.current

switch colorScheme {
case .light:
    view.backgroundColor = .white
case .dark:
    view.backgroundColor = .black
}

// React to changes
override func traitCollectionDidChange(_ previous: UITraitCollection?) {
    super.traitCollectionDidChange(previous)
    EnvironmentProvider.shared.updateSystemEnvironment()

    // Update your UI based on new color scheme
    updateColorsForCurrentScheme()
}

Layout Direction

// Check for RTL languages
let direction = LayoutDirection.current

if direction == .rightToLeft {
    // Adjust layout for RTL
    stackView.semanticContentAttribute = .forceRightToLeft
}

Environment Values

// Access shared environment
let env = EnvironmentValues.shared

// Custom environment keys
extension EnvironmentValues {
    var customSpacing: CGFloat {
        get { self[CustomSpacingKey.self] }
        set { self[CustomSpacingKey.self] = newValue }
    }
}

struct CustomSpacingKey: EnvironmentKey {
    static let defaultValue: CGFloat = 16
}

πŸ“Š Performance Monitoring

Frame Rate Monitor

// Start monitoring
FrameRateMonitor.shared.start()

// Check current FPS (updated in real-time)
let currentFPS = FrameRateMonitor.shared.currentFPS
let averageFPS = FrameRateMonitor.shared.averageFPS

// Display in UI
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
    let fps = FrameRateMonitor.shared.averageFPS
    self.fpsLabel.text = String(format: "%.0f FPS", fps)
    self.fpsLabel.textColor = fps >= 55 ? .systemGreen : .systemRed
}

// Stop when done
FrameRateMonitor.shared.stop()

Layout Cache

// Check cache performance
let hitRate = LayoutCache.shared.hitRate  // 0.0 - 1.0
print("Cache hit rate: \(Int(hitRate * 100))%")

// Clear cache if needed
LayoutCache.shared.clearCache()

// Get cache statistics
let stats = LayoutCache.shared.statistics
print("Hits: \(stats.hits), Misses: \(stats.misses)")

Performance Profiler

// Profile a layout operation
let profiler = PerformanceProfiler.shared

profiler.startProfiling(name: "ComplexLayout")

// ... perform layout operations ...

profiler.endProfiling(name: "ComplexLayout")

// Get all profiles
let profiles = profiler.allProfiles
for profile in profiles {
    print("\(profile.name): \(profile.duration)ms")
}

// Check for warnings
let warnings = profiler.allWarnings
for warning in warnings {
    print("⚠️ \(warning.message)")
}

πŸŒ‰ UIKit ↔ SwiftUI Bridge

UIKit View in SwiftUI

import SwiftUI
import Layout

struct MySwiftUIView: View {
    var body: some View {
        VStack {
            Text("SwiftUI Header")
                .font(.title)

            // Use any UIKit view in SwiftUI
            createCustomChart()
                .swiftui  // ← Converts to SwiftUI View
                .frame(height: 200)

            // UIKit labels, buttons, etc.
            UILabel().configure {
                $0.text = "UIKit Label"
                $0.textAlignment = .center
            }
            .swiftui
            .frame(height: 44)
        }
    }

    func createCustomChart() -> UIView {
        let chart = CustomChartView()
        chart.data = [10, 20, 30, 40, 50]
        return chart
    }
}

SwiftUI View in UIKit

class MyViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        // Create SwiftUI view
        let swiftUIContent = MySwiftUIView()

        // Convert to UIKit hosting controller
        let hostingController = swiftUIContent.uikit

        // Add as child view controller
        addChild(hostingController)
        view.addSubview(hostingController.view)
        hostingController.view.frame = view.bounds
        hostingController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        hostingController.didMove(toParent: self)
    }
}

🌳 Layout Tree & Dirty Propagation

Layout provides an incremental layout system that only recalculates parts of the layout that have changed, significantly improving performance for complex UIs.

Key Concepts

  • Layout Tree: A hierarchical representation of layout components, allowing efficient traversal and targeted updates
  • Dirty Propagation: When a child layout changes, it marks its parent and ancestors as "dirty," indicating they need recalculation
  • Incremental Layout: Only dirty nodes are recalculated, avoiding full layout passes

Enabling Incremental Layout

By default, incremental layout is enabled. You can toggle it:

// Enable incremental layout (default)
layoutContainer.useIncrementalLayout = true

// Disable for full recalculation every time
layoutContainer.useIncrementalLayout = false

Marking Views as Dirty

When a view's content changes, mark it as dirty to trigger incremental recalculation:

// Mark a specific view as dirty
layoutContainer.markViewDirty(myLabel)

// The layout system will:
// 1. Find the LayoutNode containing this view
// 2. Mark that node and its ancestors as dirty
// 3. Recalculate only the dirty parts in the next layout pass

Example: Dynamic Content Updates

class MyViewController: BaseViewController, Layout {
    let layoutContainer = LayoutContainer()
    let cardLabels: [UILabel] = (0..<6).map { _ in UILabel() }
    var cardCounts = Array(repeating: 0, count: 6)

    override func viewDidLoad() {
        super.viewDidLoad()
        layoutContainer.useIncrementalLayout = true
        setupLayoutContainer(layoutContainer)
        layoutContainer.updateBody { self.body }
    }

    @LayoutBuilder var body: some Layout {
        VStack(alignment: .center, spacing: 16) {
            // Multiple cards in a grid
            VStack(alignment: .center, spacing: 12) {
                HStack(alignment: .center, spacing: 12) {
                    createCard(index: 0).layout()
                    createCard(index: 1).layout()
                }
                HStack(alignment: .center, spacing: 12) {
                    createCard(index: 2).layout()
                    createCard(index: 3).layout()
                }
            }

            // Update buttons
            createUpdateButton(title: "Update Card 1") {
                self.updateCard(at: 0)
            }
        }
    }

    private func updateCard(at index: Int) {
        // Update the card's content
        cardCounts[index] += 1
        cardLabels[index].text = "Count: \(cardCounts[index])"

        // Mark only this view as dirty - only this card will be recalculated!
        layoutContainer.markViewDirty(cardLabels[index])
    }
}

Invalidating the Entire Tree

For major changes, invalidate the entire layout tree:

// Invalidate entire tree (forces full recalculation)
layoutContainer.invalidateLayoutTree()

// Rebuild the layout tree (useful when toggling incremental layout)
layoutContainer.rebuildLayoutTree()

Performance Benefits

With incremental layout enabled:

  • Partial Recalculation: Only changed nodes are recalculated
  • Cached Results: Clean nodes use cached layout results
  • Dirty Propagation: Changes automatically propagate up the tree
  • View Preservation: Views remain in the hierarchy during updates

When to Use

βœ… Use incremental layout when:

  • You have complex layouts with many views
  • Only small parts of the UI change frequently
  • You want optimal performance for dynamic content

❌ Disable incremental layout when:

  • Layout structure changes frequently
  • You need full recalculation for debugging
  • Performance is not a concern

Layout Tree Structure

The layout tree mirrors your layout hierarchy:

LayoutContainer (rootNode)
└── VStack (LayoutNode)
    β”œβ”€β”€ HStack (LayoutNode)
    β”‚   β”œβ”€β”€ Card 1 (LayoutNode)
    β”‚   └── Card 2 (LayoutNode)
    └── HStack (LayoutNode)
        β”œβ”€β”€ Card 3 (LayoutNode)
        └── Card 4 (LayoutNode)

Each LayoutNode tracks:

  • Dirty State: Whether the node needs recalculation (new nodes start dirty)
  • Cached Result: Layout result when node is clean
  • Parent-Child Relationships: Tree structure for efficient traversal
  • Child Nodes: Automatically built from layout hierarchy (VStack, HStack, etc.)

Dirty Propagation

When a child node is marked dirty, it automatically propagates to its parent:

// Child node becomes dirty
childNode.markDirty()

// Parent is automatically marked dirty due to propagation
// This ensures parent layout is recalculated when child changes

Layout Calculation Flow

  1. Initial State: New LayoutNode instances start with isDirty = true
  2. Calculation: calculateLayout() is called, node is marked clean
  3. Children Cleanup: After parent calculation, dirty children are marked clean
  4. Incremental Updates: Only dirty nodes are recalculated in subsequent passes

πŸ†” Identity & Diff

Layout provides identity-based diffing to efficiently update views when layout changes. By assigning identities to views, the system can track and reuse views across updates, minimizing unnecessary view creation and removal.

Setting View Identity

Use the .id() modifier to assign an identity to a view:

@LayoutBuilder var body: some Layout {
    VStack(spacing: 10) {
        // Use item ID as identity
        ForEach(items) { item in
            ItemView(item: item)
                .layout()
                .id(item.id)  // Identity based on item ID
        }

        // Use string identity
        headerView.layout()
            .id("header")

        // Use any Hashable type
        footerView.layout()
            .id(123)  // Integer identity
    }
}

How Identity Diffing Works

  1. View Tracking: Views with identities are tracked in a map
  2. Diff Calculation: When layout updates, the system compares old and new identity maps
  3. Efficient Updates:
    • Same Identity: Reuses existing view instance (no removal/addition)
    • New Identity: Adds new view
    • Removed Identity: Removes view that no longer exists
    • Changed Identity: Replaces view when identity changes

Benefits

βœ… View Reuse: Views with matching identities are reused, preserving state
βœ… Efficient Updates: Only changed views are added/removed
βœ… State Preservation: View state (scroll position, selection, etc.) is maintained
βœ… Performance: Reduces view creation/destruction overhead

Example: Dynamic List

class ItemListViewController: BaseViewController, Layout {
    var items: [Item] = []

    @LayoutBuilder var body: some Layout {
        ScrollView {
            VStack(spacing: 12) {
                ForEach(items) { item in
                    ItemCard(item: item)
                        .layout()
                        .id(item.id)  // Identity ensures efficient updates
                        .size(width: 350, height: 80)
                }
            }
        }
    }

    func updateItems(_ newItems: [Item]) {
        items = newItems
        layoutContainer.updateBody { self.body }
        // Only changed items are updated, others are reused!
    }
}

Identity Best Practices

  • Use Stable Identifiers: Use IDs that don't change for the same logical view
  • Unique Identities: Each view should have a unique identity within its parent
  • Optional Identity: Views without identity still work, but won't benefit from diffing
  • Hashable Types: Any Hashable type can be used as identity (String, Int, UUID, etc.)

Integration with Layout Tree

Identity diffing works seamlessly with the Layout Tree system:

  • Identity-based updates trigger dirty propagation
  • Only views with changed identities cause recalculation
  • Clean views with matching identities use cached results

πŸ” Debugging

Enable Debugging

// Enable all debugging
LayoutDebugger.shared.enableAll()

// Enable specific features
LayoutDebugger.shared.isEnabled = true
LayoutDebugger.shared.enableViewHierarchy = true
LayoutDebugger.shared.enableSpacerCalculation = true
LayoutDebugger.shared.enableFrameSettings = true

// Disable all
LayoutDebugger.shared.disableAll()

Custom Debugging Hooks

Set custom hooks to intercept and customize debug output:

// Set a custom hook for layout calculations
LayoutDebugger.shared.setDebuggingHook({ message, component in
    // Custom logging logic
    MyCustomLogger.log("\(component): \(message)")
}, for: .layout)

// Set a hook for view hierarchy
LayoutDebugger.shared.setDebuggingHook({ message, component in
    // Send to analytics
    Analytics.track("LayoutHierarchy", parameters: ["message": message])
}, for: .hierarchy)

View Hierarchy Analysis

LayoutDebugger.shared.analyzeViewHierarchy(
    layoutContainer,
    title: "MY LAYOUT"
)

Output:

πŸ” ===== MY LAYOUT =====
πŸ” LayoutContainer
β”œβ”€ Frame: (20.0, 100.0, 350.0, 600.0)
β”œβ”€ Background: systemBackground
β”œβ”€ Hidden: false
└─ Alpha: 1.0
  └─ VStack
    β”œβ”€ Frame: (0.0, 20.0, 350.0, 560.0)
    β”œβ”€ Spacing: 16.0
    └─ Alignment: center
      β”œβ”€ UILabel "Welcome"
      β”‚   β”œβ”€ Frame: (25.0, 0.0, 300.0, 34.0)
      β”‚   └─ Font: .boldSystemFont(28)
      β”œβ”€ Spacer
      β”‚   └─ Frame: (0.0, 50.0, 350.0, 400.0)
      └─ UIButton "Get Started"
          β”œβ”€ Frame: (35.0, 466.0, 280.0, 50.0)
          └─ Background: systemBlue

Debug Categories

Category Description
πŸ”§ Layout Layout calculation process
πŸ—οΈ Hierarchy View hierarchy structure
πŸ“ Frame Frame setting and changes
πŸ”² Spacer Spacer calculation details
⚑ Performance Performance metrics

πŸ—οΈ Architecture

Overview

Layout is built on a frame-based calculation system that separates measurement from placement, enabling high-performance declarative layouts without Auto Layout constraints. The architecture is designed to be modular, extensible, and performant.

Architecture Diagram

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        User Code Layer                          β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                      β”‚
β”‚  β”‚UIViewController│──────►│@LayoutBuilderβ”‚                      β”‚
β”‚  β”‚                β”‚       β”‚    body      β”‚                      β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                      β”‚
β”‚         β”‚                        β”‚                              β”‚
β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                              β”‚
β”‚                      β–Ό                                          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                      β”‚
                      β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      LayoutContainer                            β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚   β”‚
β”‚  β”‚  β”‚View Hierarchyβ”‚  β”‚Layout Calc   β”‚  β”‚Identity Map  β”‚    β”‚   β”‚
β”‚  β”‚  β”‚  Management  β”‚  β”‚Orchestration β”‚  β”‚   Tracking   β”‚    β”‚   β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚   β”‚
β”‚  β”‚                                                          β”‚   β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                      β”‚   β”‚
β”‚  β”‚  β”‚Animation     β”‚  β”‚Incremental   β”‚                      β”‚   β”‚
β”‚  β”‚  β”‚Protection    β”‚  β”‚Layout Updatesβ”‚                      β”‚   β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                      β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                      β”‚
                      β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      Layout System                              β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                      β”‚
β”‚  β”‚ Layout Tree  │────────►│ LayoutNodes  β”‚                      β”‚
β”‚  β”‚              β”‚         β”‚              β”‚                      β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                      β”‚
β”‚         β”‚                        β”‚                              β”‚
β”‚         β”‚                        β–Ό                              β”‚
β”‚         β”‚              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                     β”‚
β”‚         β”‚              β”‚ Dirty State      β”‚                     β”‚
β”‚         β”‚              β”‚ Cache Management β”‚                     β”‚
β”‚         β”‚              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                     β”‚
β”‚         β”‚                                                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚
          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Layout Components                            β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚
β”‚  β”‚ VStack β”‚  β”‚ HStack β”‚  β”‚ ZStack β”‚  β”‚ScrollViewβ”‚  β”‚ViewLayout β”‚β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
β”‚                                                                 β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”                                         β”‚
β”‚  β”‚ Spacer β”‚  β”‚ForEach β”‚                                         β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                         β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                      β”‚
                      β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  Two-Phase Layout System                        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚
β”‚  β”‚  Measure Phase       │────────►│  Layout Phase        β”‚      β”‚
β”‚  β”‚  Calculate Size      β”‚         β”‚  Calculate Position  β”‚      β”‚
β”‚  β”‚                      β”‚         β”‚                      β”‚      β”‚
β”‚  β”‚  β€’ Intrinsic Size    β”‚         β”‚  β€’ Alignment         β”‚      β”‚
β”‚  β”‚  β€’ Size Modifiers    β”‚         β”‚  β€’ Position Mods     β”‚      β”‚
β”‚  β”‚  β€’ Percentage Calc   β”‚         β”‚  β€’ Final Frame       β”‚      β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                      β”‚
                      β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Modifier System                              β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚  UIView ──(Associated Object)──► layoutModifiers[]              β”‚
β”‚                                    β”‚                            β”‚
β”‚                                    β”œβ”€β–Ί SizeModifier             β”‚
β”‚                                    β”œβ”€β–Ί OffsetModifier           β”‚
β”‚                                    β”œβ”€β–Ί BackgroundModifier       β”‚
β”‚                                    └─► PaddingModifier          β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Layout Calculation Flow

sequenceDiagram
    participant User
    participant LC as LayoutContainer
    participant Tree as Layout Tree
    participant Node as LayoutNode
    participant Measure as Measure Phase
    participant Layout as Layout Phase
    participant Views as Views

    User->>LC: updateBody { body }
    LC->>Tree: Build Layout Tree
    Tree->>Node: Create LayoutNodes
    Node->>Node: Check Dirty State

    alt Node is Dirty
        Node->>Measure: Calculate Size
        Measure->>Node: Return Measured Size
        Node->>Layout: Calculate Position
        Layout->>Node: Return Final Frame
        Node->>Node: Mark as Clean
        Node->>Node: Cache Result
    else Node is Clean
        Node->>Node: Use Cached Result
    end

    Node->>Views: Apply Frames
    Views->>User: UI Updated
Loading

Layout Tree Structure

graph TD
    Root[LayoutContainer<br/>rootNode]

    Root --> V1[VStack<br/>LayoutNode]
    V1 -->|Child| H1[HStack<br/>LayoutNode]
    V1 -->|Child| SV1[ScrollView<br/>LayoutNode]

    H1 -->|Child| VL1[ViewLayout<br/>Label<br/>LayoutNode]
    H1 -->|Child| VL2[ViewLayout<br/>Button<br/>LayoutNode]

    SV1 -->|Child| V2[VStack<br/>LayoutNode]
    V2 -->|Child| VL3[ViewLayout<br/>Card<br/>LayoutNode]
    V2 -->|Child| FE1[ForEach<br/>LayoutNode]

    FE1 -->|Child| VL4[ViewLayout<br/>Item 1<br/>LayoutNode]
    FE1 -->|Child| VL5[ViewLayout<br/>Item 2<br/>LayoutNode]

    style Root fill:#e1f5ff
    style V1 fill:#fff4e1
    style H1 fill:#fff4e1
    style SV1 fill:#fff4e1
    style V2 fill:#fff4e1
    style VL1 fill:#e8f5e9
    style VL2 fill:#e8f5e9
    style VL3 fill:#e8f5e9
    style VL4 fill:#e8f5e9
    style VL5 fill:#e8f5e9
    style FE1 fill:#f3e5f5
Loading

Two-Phase Layout System

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Phase 1: Measure                         β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                             β”‚
β”‚  Proposed Bounds                                            β”‚
β”‚       β”‚                                                     β”‚
β”‚       β–Ό                                                     β”‚
β”‚  Intrinsic Size                                             β”‚
β”‚       β”‚                                                     β”‚
β”‚       β–Ό                                                     β”‚
β”‚  Size Modifiers                                             β”‚
β”‚  (Fixed, Percentage, Flexible)                              β”‚
β”‚       β”‚                                                     β”‚
β”‚       β–Ό                                                     β”‚
β”‚  Measured Size ───────────────────┐                         β”‚
β”‚                                   β”‚                         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                    β”‚
                                    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Phase 2: Layout                          β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                             β”‚
β”‚  Measured Size                                              β”‚
β”‚       β”‚                                                     β”‚
β”‚       β–Ό                                                     β”‚
β”‚  Position Modifiers                                         β”‚
β”‚  (Offset, Center, Alignment)                                β”‚
β”‚       β”‚                                                     β”‚
β”‚       β–Ό                                                     β”‚
β”‚  Alignment Rules                                            β”‚
β”‚       β”‚                                                     β”‚
β”‚       β–Ό                                                     β”‚
β”‚  Final Frame                                                β”‚
β”‚                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Incremental Layout System

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Initial State                            β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                             β”‚
β”‚  All Nodes Clean βœ“                                          β”‚
β”‚       β”‚                                                     β”‚
β”‚       β–Ό                                                     β”‚
β”‚  Layout Calculated                                          β”‚
β”‚       β”‚                                                     β”‚
β”‚       β–Ό                                                     β”‚
β”‚  Results Cached                                             β”‚
β”‚                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    β”‚
                    β”‚ Change Detected
                    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  Change Detected                            β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                             β”‚
β”‚  View Content Changes                                       β”‚
β”‚       β”‚                                                     β”‚
β”‚       β–Ό                                                     β”‚
β”‚  Mark View as Dirty                                         β”‚
β”‚       β”‚                                                     β”‚
β”‚       β–Ό                                                     β”‚
β”‚  Propagate to Parent                                        β”‚
β”‚  (Dirty State Propagation)                                  β”‚
β”‚                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    β”‚
                    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                Incremental Update                           β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                             β”‚
β”‚  Find Dirty Nodes                                           β”‚
β”‚       β”‚                                                     β”‚
β”‚       β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”               β”‚
β”‚       β”‚                  β”‚                  β”‚               β”‚
β”‚       β–Ό                  β–Ό                  β–Ό               β”‚
β”‚  Recalculate        Use Cache          Skip Clean           β”‚
β”‚  Only Dirty         for Clean          Nodes                β”‚
β”‚       β”‚                  β”‚                  β”‚               β”‚
β”‚       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜               β”‚
β”‚                       β”‚                                     β”‚
β”‚                       β–Ό                                     β”‚
β”‚              Update Frames                                  β”‚
β”‚                       β”‚                                     β”‚
β”‚                       β–Ό                                     β”‚
β”‚              Mark as Clean βœ“                                β”‚
β”‚                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    β”‚
                    β”‚ (Loop back)
                    β–Ό
            Return to Initial State

Identity-Based Diffing

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Old Layout   β”‚         β”‚ Diff Engine  β”‚         β”‚ New Layout   β”‚
β”‚              β”‚         β”‚              β”‚         β”‚              β”‚
β”‚ View A (id:  │────────►│ Extract      │◄────────│ View A (id:  β”‚
β”‚  'header')   β”‚         β”‚ Identities   β”‚         β”‚  'header')   β”‚
β”‚              β”‚         β”‚              β”‚         β”‚              β”‚
β”‚ View B (id:  │────────►│              │◄────────│ View D (id:  β”‚
β”‚  'content')  β”‚         β”‚ Compare      β”‚         β”‚  'content')  β”‚
β”‚              β”‚         β”‚ Maps         β”‚         β”‚              β”‚
β”‚ View C (id:  │────────►│              │◄────────│ View E (id:  β”‚
β”‚  'footer')   β”‚         β”‚              β”‚         β”‚  'sidebar')  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                β”‚
                                β”‚ Calculate Diff
                                β–Ό
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚   Diff Results        β”‚
                    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
                    β”‚                       β”‚
                    β”‚ Match: View A ────────┼──► Reuse (State Preserved)
                    β”‚                       β”‚
                    β”‚ Changed: B β†’ D ───────┼──► Replace
                    β”‚                       β”‚
                    β”‚ New: View E ──────────┼──► Create
                    β”‚                       β”‚
                    β”‚ Removed: View C ──────┼──► Remove
                    β”‚                       β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                β”‚
                                β–Ό
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚ Updated View          β”‚
                    β”‚ Hierarchy             β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Core Architecture Principles

  1. Two-Phase Layout System: Separates measurement (size calculation) from placement (position calculation)
  2. Incremental Updates: Only recalculates parts of the layout that have changed
  3. Identity-Based Diffing: Efficiently updates views by tracking identities
  4. Modifier Storage: Modifiers stored as properties, not as new layout nodes
  5. Platform Abstraction: Supports both UIKit and AppKit through conditional compilation

Core Components

1. Layout Protocol

The Layout protocol is the foundation of the entire system. All layout components conform to this protocol:

@MainActor
public protocol Layout {
    func calculateLayout(in bounds: CGRect) -> LayoutResult
    func extractViews() -> [UIView]
    var intrinsicContentSize: CGSize { get }
}

Key Responsibilities:

  • calculateLayout(in:): Calculates frame positions for all managed views
  • extractViews(): Returns the views managed by this layout
  • intrinsicContentSize: Provides natural size for the layout

2. LayoutContainer

LayoutContainer is the main orchestrator that manages the entire layout hierarchy:

Responsibilities:

  • View hierarchy management (automatic add/remove)
  • Layout calculation orchestration
  • Identity-based view diffing
  • Animation protection
  • Incremental layout updates
  • Content centering

Key Properties:

  • body: The root layout definition
  • useIncrementalLayout: Enables incremental updates
  • rootNode: Root of the layout tree for incremental updates

3. ViewLayout

ViewLayout wraps individual UIView instances and provides modifier support:

Two-Phase Layout:

  1. Measure Phase: Calculates the size the view wants based on modifiers
  2. Layout Phase: Calculates the final position based on size and alignment

Modifier Storage:

  • Modifiers stored as associated objects on the view itself
  • No new ViewLayout instances created for modifier chains
  • Efficient property-based storage

4. Layout Components

Stack Components:

  • VStack: Vertical stacking with spacing and alignment
  • HStack: Horizontal stacking with spacing and alignment
  • ZStack: Z-axis stacking with multiple alignment options

Container Components:

  • ScrollView: Scrollable container with content offset preservation
  • Spacer: Flexible space component

Dynamic Components:

  • ForEach: Dynamic list generation with identity-based diffing

Layout Calculation Flow

The following diagram shows the complete flow from user code to rendered UI:

User Code
    β”‚
    β”‚ @LayoutBuilder body
    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ LayoutContainer     β”‚
β”‚   updateBody()      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    β”‚
    β”‚ Build Layout Tree
    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Layout Tree         β”‚
β”‚ (LayoutNode         β”‚
β”‚  hierarchy)         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    β”‚
    β”‚ View Hierarchy Diffing
    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Identity-based      β”‚
β”‚ Diffing             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    β”‚
    β”‚ Incremental Layout?
    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ Yes     β”‚ No      β”‚
    β–Ό         β–Ό         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Check   β”‚ β”‚ Full            β”‚
β”‚ Dirty   β”‚ β”‚ Calculation     β”‚
β”‚ State   β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    β”‚
    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ Dirty   β”‚ Clean   β”‚
    β–Ό         β–Ό         β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ Measure β”‚ β”‚ Use     β”‚ β”‚
β”‚ Phase   β”‚ β”‚ Cached  β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
    β”‚         β”‚         β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚
              β–Ό
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ Layout Phase    β”‚
    β”‚ (Calculate      β”‚
    β”‚  Positions)     β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚
              β–Ό
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ Apply Frames    β”‚
    β”‚ to Views        β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚
              β–Ό
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ Update View     β”‚
    β”‚ Hierarchy       β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚
              β–Ό
         UI Rendered

Two-Phase Layout System

The layout system separates measurement from placement:

Phase 1: Measure

  • Calculate desired size for each view
  • Respects size modifiers (fixed, percentage, flexible)
  • Uses intrinsic content size as base
  • Results cached for reuse

Phase 2: Layout

  • Calculate final position based on measured size
  • Applies alignment and positioning modifiers
  • Respects parent container constraints
  • Sets final frame on views

Benefits:

  • Measurement can be cached independently
  • Placement can be recalculated without remeasuring
  • Supports responsive layouts with percentage-based sizing
  • Enables efficient incremental updates

Incremental Layout System

The incremental layout system only recalculates parts of the layout that have changed:

Layout Tree Structure:

The layout tree mirrors your view hierarchy, with each component represented as a LayoutNode:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    LayoutContainer                              β”‚
β”‚                    (rootNode)                                   β”‚
β”‚                    isDirty: false βœ“                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                             β”‚
                             β–Ό
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚      VStack (LayoutNode)      β”‚
              β”‚      isDirty: false βœ“         β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                β”‚                           β”‚
                β–Ό                           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  HStack (LayoutNode)      β”‚  β”‚  ScrollView (LayoutNode)  β”‚
β”‚  isDirty: true            β”‚  β”‚  isDirty: false βœ“         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
            β”‚                                β”‚
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”                        β”‚
    β”‚               β”‚                        β”‚
    β–Ό               β–Ό                        β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ViewLayoutβ”‚  β”‚ViewLayoutβ”‚          β”‚ VStack (LayoutNode)β”‚
β”‚  Label   β”‚  β”‚  Button  β”‚          β”‚ isDirty: false βœ“   β”‚
β”‚          β”‚  β”‚          β”‚          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚isDirty:  β”‚  β”‚isDirty:  β”‚                     β”‚
β”‚true      β”‚  β”‚false βœ“   β”‚                     β–Ό
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                    β”‚ ViewLayout Card  β”‚
                                    β”‚                  β”‚
                                    β”‚ isDirty: false βœ“ β”‚
                                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                            Legend                               β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚  Dirty (will be recalculated in next layout pass)               β”‚
β”‚  Clean (uses cached result, skipped in calculation)             β”‚
β”‚                                                                 β”‚
β”‚  When a node is dirty:                                          β”‚
β”‚    β€’ It and all its children are recalculated                   β”‚
β”‚    β€’ Results are cached after calculation                       β”‚
β”‚    β€’ Parent nodes are marked dirty (propagation)                β”‚
β”‚                                                                 β”‚
β”‚  When a node is clean:                                          β”‚
β”‚    β€’ Uses cached layout result                                  β”‚
β”‚    β€’ Skips calculation entirely                                 β”‚
β”‚    β€’ Improves performance significantly                         β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Dirty Propagation:

  • When a child node changes, it marks itself as dirty
  • Dirty state propagates up to parent nodes
  • Only dirty nodes are recalculated
  • Clean nodes use cached results

Key Components:

  • LayoutNode: Represents a node in the layout tree
  • IncrementalLayoutCache: Caches layout results
  • DirtyRegionTracker: Tracks which regions need recalculation

Identity-Based Diffing

Views are tracked by identity for efficient updates:

Identity System:

  • Views can have explicit identity via .id() modifier
  • Identity used for view reuse across layout updates
  • Views with matching identities are reused (not recreated)
  • Enables state preservation (scroll position, selection, etc.)

Diffing Process:

The identity-based diffing process efficiently updates views:

flowchart LR
    subgraph "Old Layout"
        O1[View A<br/>id: 'header']
        O2[View B<br/>id: 'content']
        O3[View C<br/>id: 'footer']
    end

    subgraph "Diff Engine"
        D1[Extract Identities]
        D2[Compare Maps]
        D3[Calculate Diff]
    end

    subgraph "New Layout"
        N1[View A<br/>id: 'header']
        N2[View D<br/>id: 'content']
        N3[View E<br/>id: 'sidebar']
    end

    subgraph "Actions"
        A1[Reuse: View A]
        A2[Replace: B β†’ D]
        A3[Add: View E]
        A4[Remove: View C]
    end

    O1 --> D1
    O2 --> D1
    O3 --> D1
    N1 --> D1
    N2 --> D1
    N3 --> D1

    D1 --> D2
    D2 --> D3

    D3 -->|Match| A1
    D3 -->|Changed| A2
    D3 -->|New| A3
    D3 -->|Removed| A4

    style A1 fill:#e8f5e9
    style A2 fill:#fff4e1
    style A3 fill:#e1f5ff
    style A4 fill:#ffebee
Loading

Modifier System

Modifiers are stored as properties on views, not as new layout nodes:

Storage Mechanism:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    View Instance                            β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                             β”‚
β”‚  UIView                                                     β”‚
β”‚    β”‚                                                        β”‚
β”‚    β”‚ (Associated Object)                                    β”‚
β”‚    β–Ό                                                        β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                   β”‚
β”‚  β”‚ layoutModifiers[]    β”‚                                   β”‚
β”‚  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€                                   β”‚
β”‚  β”‚ β€’ SizeModifier       β”‚                                   β”‚
β”‚  β”‚ β€’ OffsetModifier     β”‚                                   β”‚
β”‚  β”‚ β€’ BackgroundModifier β”‚                                   β”‚
β”‚  β”‚ β€’ PaddingModifier    β”‚                                   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                   β”‚
β”‚                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    β–²
                    β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Modifier Chain (User Code)                       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                               β”‚
β”‚  view.ui()                                                    β”‚
β”‚    .size(width: 200, height: 100)  ────┐                      β”‚
β”‚    .offset(x: 10, y: 20)            ─────                     β”‚
β”‚    .background(.systemBlue)         ────┼──► Stores in        β”‚
β”‚    .padding(16)                      ─────   layoutModifiers[]β”‚
β”‚                                         β”‚                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                          β”‚
                                          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚            Layout Calculation                               β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                             β”‚
β”‚  calculateLayout(in:)                                       β”‚
β”‚    β”‚                                                        β”‚
β”‚    β”‚ Reads layoutModifiers[]                                β”‚
β”‚    β–Ό                                                        β”‚
β”‚  Applies modifiers in order:                                β”‚
β”‚    1. Size modifiers (Measure Phase)                        β”‚
β”‚    2. Position modifiers (Layout Phase)                     β”‚
β”‚    3. Style modifiers                                       β”‚
β”‚    β”‚                                                        β”‚
β”‚    β–Ό                                                        β”‚
β”‚  Final Frame                                                β”‚
β”‚                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key Benefits:

  • Uses Associated Objects to store modifiers
  • Modifiers stored in view.layoutModifiers array
  • No new ViewLayout instances for modifier chains
  • Efficient property-based access
  • Modifiers persist across layout updates

Modifier Types:

                    Modifiers
                         β”‚
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚                β”‚                β”‚
        β–Ό                β–Ό                β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Size         β”‚ β”‚ Position     β”‚ β”‚ Style        β”‚
β”‚ Modifiers    β”‚ β”‚ Modifiers    β”‚ β”‚ Modifiers    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚              β”‚ β”‚              β”‚ β”‚              β”‚
β”‚ β€’ Fixed      β”‚ β”‚ β€’ Offset     β”‚ β”‚ β€’ Background β”‚
β”‚   width: 200 β”‚ β”‚   x: 10      β”‚ β”‚   .systemBlueβ”‚
β”‚              β”‚ β”‚   y: 20      β”‚ β”‚              β”‚
β”‚ β€’ Percentage β”‚ β”‚              β”‚ β”‚ β€’ Corner     β”‚
β”‚   width: 80% β”‚ β”‚ β€’ Center     β”‚ β”‚   Radius: 12 β”‚
β”‚              β”‚ β”‚   centerX    β”‚ β”‚              β”‚
β”‚ β€’ Flexible   β”‚ β”‚   centerY    β”‚ β”‚ β€’ Padding    β”‚
β”‚   height: nilβ”‚ β”‚              β”‚ β”‚   insets: 16 β”‚
β”‚              β”‚ β”‚ β€’ Alignment  β”‚ β”‚              β”‚
β”‚              β”‚ β”‚   leading    β”‚ β”‚              β”‚
β”‚              β”‚ β”‚   trailing   β”‚ β”‚              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        β”‚                β”‚                β”‚
        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                         β–Ό
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚ Identity         β”‚
              β”‚ Modifiers        β”‚
              β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
              β”‚ β€’ .id('unique')  β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Animation System

The animation system protects animated views from layout interference:

Animation Protection Flow:

User Code
    β”‚
    β”‚ startAnimating(view)
    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ LayoutContainer     β”‚
β”‚                     β”‚
β”‚ Add to              β”‚
β”‚ animatingViews set  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    β”‚
    β”‚ withAnimation { ... }
    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Animation Engine    β”‚
β”‚                     β”‚
β”‚ Apply animation     β”‚
β”‚ to view             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    β”‚
    β”‚ Animate frame/transform
    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Animated View       β”‚
β”‚ (frame changing)    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    β–²
    β”‚
    β”‚ Layout System attempts
    β”‚ frame update
    β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ LayoutContainer     β”‚
β”‚                     β”‚
β”‚ Check isAnimating   β”‚
β”‚                     β”‚
β”‚ Skip frame update   β”‚
β”‚ (view is animating) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    β”‚
    β”‚ Animation completes
    β”‚ stopAnimating(view)
    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ LayoutContainer     β”‚
β”‚                     β”‚
β”‚ Remove from         β”‚
β”‚ animatingViews      β”‚
β”‚                     β”‚
β”‚ Resume frame        β”‚
β”‚ updates             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Animation Architecture:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    User API                                 β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”‚
β”‚  β”‚withAnimation β”‚  β”‚startAnimatingβ”‚  β”‚stopAnimating β”‚       β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β”‚
β”‚         β”‚                β”‚                β”‚                 β”‚
β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                 β”‚
β”‚                         β”‚                                   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                          β”‚
                          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                LayoutContainer                              β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”‚
β”‚  β”‚ animatingViews Set   β”‚  β”‚animatingViewIdentifiersβ”‚       β”‚
β”‚  β”‚                      β”‚  β”‚                        β”‚       β”‚
β”‚  β”‚ β€’ Tracks animating   β”‚  β”‚ β€’ ObjectIdentifier     β”‚       β”‚
β”‚  β”‚   views              β”‚  β”‚   tracking             β”‚       β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β”‚
β”‚         β”‚                                                   β”‚
β”‚         β”‚ Protects from layout updates                      β”‚
β”‚         β–Ό                                                   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                          β”‚
                          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Animation Engine                               β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                   β”‚
β”‚  β”‚LayoutAnimationEngine β”‚                                   β”‚
β”‚  β”‚                      β”‚                                   β”‚
β”‚  β”‚ β€’ Manages animation  β”‚                                   β”‚
β”‚  β”‚   state              β”‚                                   β”‚
β”‚  β”‚ β€’ Coordinates timing β”‚                                   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                   β”‚
β”‚         β”‚                                                   β”‚
β”‚         β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                     β”‚
β”‚         β–Ό              β–Ό              β–Ό                     β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚  β”‚Layout    β”‚  β”‚Timing    β”‚  β”‚Platform  β”‚                   β”‚
β”‚  β”‚Animation β”‚  β”‚Function  β”‚  β”‚APIs      β”‚                   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β”‚                                                             β”‚
β”‚         β”‚              β”‚              β”‚                     β”‚
β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                     β”‚
β”‚                          β”‚                                  β”‚
β”‚                          β–Ό                                  β”‚
β”‚              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                       β”‚
β”‚              β”‚ UIView.animate       β”‚                       β”‚
β”‚              β”‚ NSAnimationContext   β”‚                       β”‚
β”‚              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                       β”‚
β”‚                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key Features:

  • startAnimating(_:): Marks view as animating
  • stopAnimating(_:): Removes animation protection
  • Layout system skips frame updates for animating views
  • withAnimation provides SwiftUI-style animation blocks
  • LayoutAnimationEngine: Manages animation state
  • LayoutAnimation: Defines animation parameters
  • AnimationTimingFunction: Timing functions (ease, spring, etc.)
  • Platform-specific animation APIs (UIView.animate / NSAnimationContext)

Environment System

The environment system provides context values throughout the layout hierarchy:

Environment Values:

  • ColorScheme: Light/dark mode detection
  • LayoutDirection: RTL/LTR support
  • Font: Typography settings
  • ForegroundColor: Text color
  • Custom environment keys

Propagation:

  • Environment values flow down the layout tree
  • Child layouts inherit parent environment
  • Can be overridden at any level
  • Platform-specific detection (UITraitCollection / NSApp.effectiveAppearance)

Caching System

Multiple caching layers optimize performance:

Layout Cache:

  • Caches LayoutResult for repeated calculations
  • Keyed by layout type and bounds
  • Reduces redundant calculations

Incremental Cache:

  • Caches results for clean nodes in layout tree
  • Only dirty nodes recalculated
  • Enables partial updates

View Layout Cache:

  • Caches modifier calculations
  • Stores measured sizes
  • Reuses measurements when possible

Platform Support

The library is built on UIKit and designed for iOS applications:

UIKit Foundation:

  • Uses UIView as the base view type
  • Leverages UIKit's view hierarchy and lifecycle
  • Integrates with UIViewController lifecycle methods
  • Supports UIKit-specific features (safe areas, trait collections, etc.)

Future Platform Support:

  • Architecture designed to support platform abstraction
  • Conditional compilation patterns in place for future expansion
  • Can be extended to support AppKit/macOS with platform-specific types

Performance Optimizations

Frame-Based Calculations:

  • Direct frame manipulation (no constraint solver)
  • O(n) complexity for most layouts
  • No Auto Layout overhead

Incremental Updates:

  • Only dirty nodes recalculated
  • Cached results for clean nodes
  • Minimal view hierarchy changes

Efficient Modifiers:

  • Property-based storage (no new nodes)
  • Lazy evaluation when possible
  • Cached measurements

View Reuse:

  • Identity-based view reuse
  • Minimal view creation/destruction
  • State preservation across updates

Value-Type Layout Buffer:

The library implements LayoutNodeBuffer, a high-performance layout tree buffer using a contiguous memory array instead of object references. This provides significant performance benefits for complex layouts.

Current (Object-Based):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Node 1   │───►│ Node 2   │───►│ Node 3   β”‚
β”‚ (Heap)   β”‚    β”‚ (Heap)   β”‚    β”‚ (Heap)   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
   β”‚              β”‚              β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
   (Non-contiguous memory, cache misses)

Optimized (Array-Based):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ [Node 1] [Node 2] [Node 3] [Node 4] ... β”‚
β”‚  (Contiguous memory, cache-friendly)    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Benefits:

  1. CPU Cache Efficiency:

    • Contiguous memory improves cache locality
    • Sequential access patterns are significantly faster
    • Reduced cache misses during tree traversal
    • Better CPU prefetching behavior
  2. Memory Allocation:

    • Single allocation instead of many small heap allocations
    • Reduced ARC overhead (no reference counting needed)
    • More efficient memory utilization
  3. SIMD Optimization:

    • Vector operations possible on contiguous arrays
    • Parallel processing of layout calculations
    • Batch operations on multiple nodes
  4. Memory Access Patterns:

    • Predictable access patterns enable CPU optimizations
    • Better branch prediction
    • Reduced pointer chasing

Trade-offs:

  • More complex tree structure representation (indices instead of pointers)
  • Dynamic resizing requires array reallocation
  • Parent-child relationships managed via indices
  • More complex implementation and debugging

Implementation:

LayoutNodeBuffer is automatically used by LayoutContainer when useIncrementalLayout is enabled. The buffer manages layout nodes in a contiguous array, providing efficient tree operations:

// LayoutContainer automatically uses LayoutNodeBuffer internally
layoutContainer.useIncrementalLayout = true

// The buffer is managed automatically:
// - Created when layout tree is built
// - Updated on layout changes
// - Cleared when hierarchy changes

API Overview:

public class LayoutNodeBuffer {
    // Node management
    func addNode(layout: any Layout, parentIndex: Int?) -> Int
    func removeNode(at index: Int)
    func getNode(at index: Int) -> LayoutNodeData?

    // Tree operations
    func buildTree(rootIndex: Int)
    func calculateLayout(at index: Int, in bounds: CGRect) -> LayoutResult

    // Dirty state management
    func markDirty(at index: Int, propagateToParent: Bool)
    func invalidate(at index: Int)

    // Statistics
    func activeNodeCount() -> Int
    func totalCapacity() -> Int
}

Performance Characteristics:

  • Faster layout calculations for complex trees
  • Improved CPU cache utilization through contiguous memory access
  • Reduced memory allocation overhead
  • Better performance for dynamic content updates

User Experience Benefits:

  • βœ… Smooth animations: No choppiness during rotation or transitions
  • βœ… Faster response: Layout updates complete more quickly
  • βœ… Stable performance: Consistent frame rates even with complex layouts
  • βœ… Better battery life: More efficient CPU usage

Implementation Status:

LayoutNodeBuffer is currently implemented and used by LayoutContainer when useIncrementalLayout is enabled. The library automatically manages the buffer internally, so you don't need to interact with it directly in most cases.

Usage:

// Enable incremental layout (uses LayoutNodeBuffer internally)
layoutContainer.useIncrementalLayout = true

// The buffer is automatically managed:
// - Created when layout tree is built
// - Updated on layout changes
// - Cleared when hierarchy changes

Best Practices:

  1. Enable Incremental Layout: Always enable useIncrementalLayout for complex layouts

    layoutContainer.useIncrementalLayout = true
  2. Use Identity for Views: Provide stable identities for views to enable efficient diffing

    view.ui().id("stable-id")
  3. Minimize Layout Changes: Batch layout updates when possible

    // Good: Single update
    layoutContainer.updateBody { self.body }
    
    // Avoid: Multiple updates
    layoutContainer.updateBody { self.body1 }
    layoutContainer.updateBody { self.body2 }
  4. Profile Performance: Use PerformanceProfiler to identify bottlenecks

    PerformanceProfiler.shared.startProfiling()
    // ... layout operations ...
    let report = PerformanceProfiler.shared.stopProfiling()

Debugging & Performance Monitoring:

The library provides comprehensive debugging and performance monitoring tools:

// Enable layout debugging
LayoutDebugger.shared.enableAll()

// Monitor performance
PerformanceProfiler.shared.startProfiling()
// ... layout operations ...
let report = PerformanceProfiler.shared.stopProfiling()

// Measure specific layout operations
LayoutPerformanceMonitor.measureLayout(name: "Complex Layout") {
    layoutContainer.layoutSubviews()
}

Troubleshooting Common Issues:

  1. Layout not updating: Ensure layoutContainer.updateBody() is called after state changes
  2. Views not appearing: Check that views are properly added to the layout hierarchy
  3. Performance issues: Enable incremental layout and use identity for views
  4. Animation conflicts: Use layoutContainer.startAnimating() to protect views during animation

When to Use:

βœ… Array-based buffer is beneficial for:

  • Complex layouts with many nodes (100+ nodes)
  • Frequent layout updates
  • Performance-critical applications
  • Applications requiring smooth scrolling and animations

❌ Object-based approach is sufficient for:

  • Simple layouts (< 50 nodes)
  • Infrequent updates
  • Development/prototyping phase
  • Code simplicity is priority

Note: The LayoutNodeBuffer is automatically used by LayoutContainer when incremental layout is enabled. You don't need to interact with it directly in most cases. The library handles buffer management internally.

Debugging & Performance Tools

The library includes comprehensive debugging and performance monitoring tools:

Layout Debugger:

// Enable debugging
LayoutDebugger.shared.enableAll()

// Enable specific categories
LayoutDebugger.shared.enableViewHierarchy = true
LayoutDebugger.shared.enableLayoutCalculation = true
LayoutDebugger.shared.enableFrameSettings = true

// Analyze view hierarchy
LayoutDebugger.shared.analyzeViewHierarchy(
    layoutContainer,
    title: "Layout Analysis"
)

Performance Profiler:

// Profile layout operations
PerformanceProfiler.shared.startProfiling()
// ... perform layout operations ...
let report = PerformanceProfiler.shared.stopProfiling()

// Get performance statistics
let profiles = PerformanceProfiler.shared.allProfiles
for profile in profiles {
    print("\(profile.name): \(profile.averageTime)ms")
}

Performance Monitor:

// Measure specific operations
LayoutPerformanceMonitor.measureLayout(name: "Complex Layout") {
    layoutContainer.layoutSubviews()
}

// Print performance summary
LayoutPerformanceMonitor.printSummary()

Frame Rate Monitor:

// Monitor frame rate
FrameRateMonitor.shared.start()
// ... later ...
let fps = FrameRateMonitor.shared.averageFPS
FrameRateMonitor.shared.stop()

Troubleshooting

Common Issues and Solutions:

  1. Layout not updating after state change

    // Solution: Call updateBody() after state changes
    @State private var count = 0
    
    Button("Increment") {
        count += 1
        layoutContainer.updateBody { self.body }
    }
  2. Views not appearing

    // Solution: Ensure views are in the layout hierarchy
    layoutContainer.updateBody {
        VStack {
            myView.ui()
        }
    }
  3. Performance issues with complex layouts

    // Solution: Enable incremental layout and use identity
    layoutContainer.useIncrementalLayout = true
    view.ui().id("stable-id")
  4. Animation conflicts with layout

    // Solution: Protect views during animation
    layoutContainer.startAnimating(view)
    // ... animate view ...
    layoutContainer.stopAnimating(view)
  5. Layout calculations taking too long

    // Solution: Profile and optimize
    PerformanceProfiler.shared.startProfiling()
    // ... identify bottlenecks ...
    PerformanceProfiler.shared.stopProfiling()

Extension Points

The architecture is designed for extensibility:

Custom Layouts:

  • Conform to Layout protocol
  • Implement calculateLayout(in:) and extractViews()
  • Integrate with existing layout system

Custom Modifiers:

  • Conform to LayoutModifier protocol
  • Store as associated objects
  • Apply in measure/layout phases

Custom Components:

  • Build on existing stack components
  • Use LayoutBuilder for composition
  • Integrate with environment system

πŸ—οΈ Project Structure

Sources/Layout/
β”œβ”€β”€ Animation/              # Animation engine & timing functions
β”‚   β”œβ”€β”€ AnimationTimingFunction.swift
β”‚   β”œβ”€β”€ LayoutAnimation.swift
β”‚   β”œβ”€β”€ LayoutAnimationEngine.swift
β”‚   β”œβ”€β”€ LayoutTransition.swift
β”‚   β”œβ”€β”€ TransitionConfig.swift
β”‚   β”œβ”€β”€ AnimatedLayout.swift
β”‚   β”œβ”€β”€ Animated.swift
β”‚   β”œβ”€β”€ AnimationToken.swift
β”‚   β”œβ”€β”€ VectorArithmetic.swift
β”‚   └── WithAnimation.swift
β”‚
β”œβ”€β”€ Cache/                  # Layout caching system
β”‚   β”œβ”€β”€ LayoutCache.swift
β”‚   β”œβ”€β”€ LayoutCacheKey.swift
β”‚   β”œβ”€β”€ IncrementalLayoutCache.swift
β”‚   β”œβ”€β”€ CacheableLayout.swift
β”‚   └── ViewLayoutCache.swift
β”‚
β”œβ”€β”€ Components/            # Layout components
β”‚   β”œβ”€β”€ VStack.swift
β”‚   β”œβ”€β”€ HStack.swift
β”‚   β”œβ”€β”€ ZStack.swift
β”‚   β”œβ”€β”€ ScrollView.swift
β”‚   β”œβ”€β”€ Spacer.swift
β”‚   └── ForEach.swift
β”‚
β”œβ”€β”€ Environment/           # Environment values & providers
β”‚   β”œβ”€β”€ EnvironmentValues.swift
β”‚   β”œβ”€β”€ EnvironmentKey.swift
β”‚   β”œβ”€β”€ EnvironmentKeys.swift
β”‚   β”œβ”€β”€ EnvironmentProvider.swift
β”‚   β”œβ”€β”€ EnvironmentObject.swift
β”‚   β”œβ”€β”€ EnvironmentPropertyWrapper.swift
β”‚   β”œβ”€β”€ EnvironmentModifierLayout.swift
β”‚   β”œβ”€β”€ ColorScheme.swift
β”‚   └── LayoutDirection.swift
β”‚
β”œβ”€β”€ Geometry/              # Geometry system
β”‚   β”œβ”€β”€ GeometryReader.swift
β”‚   β”œβ”€β”€ GeometryProxy.swift
β”‚   β”œβ”€β”€ CoordinateSpace.swift
β”‚   β”œβ”€β”€ CoordinateSpaceRegistry.swift
β”‚   β”œβ”€β”€ Anchor.swift
β”‚   └── UnitPoint.swift
β”‚
β”œβ”€β”€ Invalidation/          # Layout invalidation system
β”‚   β”œβ”€β”€ LayoutInvalidating.swift
β”‚   β”œβ”€β”€ LayoutInvalidationContext.swift
β”‚   β”œβ”€β”€ InvalidationReason.swift
β”‚   └── DirtyRegionTracker.swift
β”‚
β”œβ”€β”€ Layout/                # Core layout protocol & builders
β”‚   β”œβ”€β”€ Layout.swift
β”‚   β”œβ”€β”€ LayoutBuilder.swift
β”‚   β”œβ”€β”€ LayoutResult.swift
β”‚   β”œβ”€β”€ LayoutModifier.swift
β”‚   β”œβ”€β”€ LayoutNode.swift   # Layout tree node for incremental updates
β”‚   β”œβ”€β”€ LayoutNodeBuffer.swift  # High-performance array-based layout buffer
β”‚   β”œβ”€β”€ EmptyLayout.swift
β”‚   β”œβ”€β”€ TupleLayout.swift
β”‚   β”œβ”€β”€ ArrayLayout.swift
β”‚   β”œβ”€β”€ OptionalLayout.swift
β”‚   β”œβ”€β”€ ConditionalLayout.swift
β”‚   β”œβ”€β”€ BackgroundLayout.swift
β”‚   β”œβ”€β”€ OverlayLayout.swift
β”‚   └── CornerRadius.swift
β”‚
β”œβ”€β”€ Modifiers/             # Layout modifiers
β”‚   β”œβ”€β”€ SizeModifier.swift
β”‚   β”œβ”€β”€ PaddingModifier.swift
β”‚   β”œβ”€β”€ OffsetModifier.swift
β”‚   β”œβ”€β”€ PositionModifier.swift
β”‚   β”œβ”€β”€ CenterModifier.swift
β”‚   β”œβ”€β”€ BackgroundModifier.swift
β”‚   β”œβ”€β”€ CornerRadiusModifier.swift
β”‚   β”œβ”€β”€ AspectRatioModifier.swift
β”‚   └── AnimationModifier.swift
β”‚
β”œβ”€β”€ Performance/           # Performance monitoring
β”‚   β”œβ”€β”€ FrameRateMonitor.swift
β”‚   β”œβ”€β”€ PerformanceProfiler.swift
β”‚   β”œβ”€β”€ PerformanceProfile.swift
β”‚   β”œβ”€β”€ PerformanceReport.swift
β”‚   β”œβ”€β”€ PerformanceThreshold.swift
β”‚   β”œβ”€β”€ PerformanceWarning.swift
β”‚   └── ProfilingToken.swift
β”‚
β”œβ”€β”€ Preferences/           # Preference system
β”‚   β”œβ”€β”€ PreferenceKey.swift
β”‚   β”œβ”€β”€ PreferenceKeys.swift
β”‚   β”œβ”€β”€ PreferenceRegistry.swift
β”‚   β”œβ”€β”€ PreferenceValues.swift
β”‚   └── PreferenceModifierLayout.swift
β”‚
β”œβ”€β”€ Priority/              # Layout priority system
β”‚   β”œβ”€β”€ LayoutPriority.swift
β”‚   β”œβ”€β”€ ContentPriority.swift
β”‚   β”œβ”€β”€ PriorityLayout.swift
β”‚   β”œβ”€β”€ FlexibleLayout.swift
β”‚   β”œβ”€β”€ FixedSizeLayout.swift
β”‚   β”œβ”€β”€ LayoutAxis.swift
β”‚   β”œβ”€β”€ PrioritySizeCalculator.swift
β”‚   └── StackPriorityDistributor.swift
β”‚
β”œβ”€β”€ Snapshot/              # Snapshot testing
β”‚   β”œβ”€β”€ SnapshotConfig.swift
β”‚   β”œβ”€β”€ SnapshotEngine.swift
β”‚   β”œβ”€β”€ SnapshotResult.swift
β”‚   └── SnapshotAsserter.swift
β”‚
β”œβ”€β”€ Utils/                 # Utility extensions
β”‚   β”œβ”€β”€ UIView+Layout.swift
β”‚   β”œβ”€β”€ UIView+Modifiers.swift  # Modifier storage (Associated Objects)
β”‚   β”œβ”€β”€ UIView+SwiftUI.swift
β”‚   β”œβ”€β”€ UIViewController+Layout.swift  # UIKit lifecycle integration
β”‚   └── ArraryExtension.swift
β”‚
β”œβ”€β”€ Invalidation/          # Layout invalidation system
β”‚   β”œβ”€β”€ LayoutInvalidating.swift
β”‚   β”œβ”€β”€ LayoutInvalidationContext.swift
β”‚   β”œβ”€β”€ LayoutInvalidationRules.swift  # Invalidation rules
β”‚   β”œβ”€β”€ InvalidationReason.swift
β”‚   └── DirtyRegionTracker.swift
β”‚
β”œβ”€β”€ LayoutContainer.swift  # Main container class
β”œβ”€β”€ ViewLayout.swift       # View layout wrapper (two-phase layout)
└── LayoutDebugger.swift   # Debugging utilities with hooks

🎯 Migration from Auto Layout

Before (Auto Layout)

// Complex constraint setup
titleLabel.translatesAutoresizingMaskIntoConstraints = false
subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
button.translatesAutoresizingMaskIntoConstraints = false

NSLayoutConstraint.activate([
    titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40),
    titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
    titleLabel.widthAnchor.constraint(equalToConstant: 280),

    subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 16),
    subtitleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
    subtitleLabel.widthAnchor.constraint(equalToConstant: 280),

    button.topAnchor.constraint(equalTo: subtitleLabel.bottomAnchor, constant: 40),
    button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
    button.widthAnchor.constraint(equalToConstant: 240),
    button.heightAnchor.constraint(equalToConstant: 50)
])

After (Layout - Pure Manual Layout)

class MyViewController: BaseViewController, Layout {
    let titleLabel = UILabel()
    let subtitleLabel = UILabel()
    let button = UIButton()

    override func viewDidLoad() {
        super.viewDidLoad()
        // BaseViewController automatically sets up layoutContainer
    }

    override func setLayout() {
        layoutContainer.updateBody { self.body }
    }

    // Clean, declarative layout
    @LayoutBuilder var body: some Layout {
        VStack(alignment: .center, spacing: 16) {
            Spacer(minLength: 40)

            titleLabel.layout()
                .size(width: 280, height: 30)

            subtitleLabel.layout()
                .size(width: 280, height: 20)

            Spacer(minLength: 40)

            button.layout()
                .size(width: 240, height: 50)

            Spacer()
        }
    }
}

Benefits

Aspect Auto Layout Layout
Lines of code ~15 lines ~10 lines
Readability Constraint pairs Visual hierarchy
Performance Constraint solver Direct frame calculations
Debugging Constraint conflicts Simple frame inspection + hooks
Flexibility Rigid constraints Dynamic calculations
Layout System Auto Layout engine Pure Manual Layout (no constraints)
Modifiers N/A Stored as properties (no new nodes)
Layout Phases Single phase Two-phase (measure + layout)

πŸ†• Recent Updates

Percentage-Based Sizing

Layout now supports percentage-based sizing and positioning:

  • Postfix % Operator: Use 80% syntax for intuitive percentage values
  • Mixed Types: Combine percentage and fixed sizes (e.g., size(width: 80%, height: 50))
  • Edge Positioning: Position views using percentage offsets (e.g., .top(10%), .leading(20%))
  • Responsive Layouts: Automatically adapts to screen rotation and size changes
// Simple percentage syntax
myView.layout()
    .size(width: 90%, height: 100)
    .centerX()

// Mixed fixed and percentage
cardView.layout()
    .size(width: 50%, height: 140)

DSL-First Approach

Example app components have been refactored to use DSL syntax instead of imperative methods:

  • Before: UIView creation with manual frame calculations
  • After: Declarative VStack/HStack with percentage-based sizing
  • Benefits: Better readability, automatic layout updates, responsive design

πŸ™ Inspiration

Layout is inspired by:

  • SwiftUI - Declarative syntax and result builders
  • Yoga - Flexbox layout concepts
  • ComponentKit - Declarative UI for iOS

πŸ“„ License

swift-layout is released under the MIT license. See the LICENSE file for more info.

About

A manual layout system for UIKit based on a SwiftUI-like declarative DSL

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •  

Languages