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.
| 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 |
π 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
π¦ 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
π 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
π 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
πΎ 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
π 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
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"]
)- File β Add Package Dependencies
- Enter:
https://github.com/pelagornis/swift-layout.git - Select version and add to your project
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)
}
}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)
}
}
}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 is the main container that manages your layout hierarchy. It provides automatic view management, content centering, and animation protection.
- 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
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
}// 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 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 3Arranges 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)
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)
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)
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 = trueFlexible 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
}// 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 heightEdge 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// Uniform padding
VStack { ... }
.padding(20)
// Edge-specific padding
VStack { ... }
.padding(UIEdgeInsets(top: 20, left: 16, bottom: 40, right: 16))// Move view from its calculated position
myView.layout()
.size(width: 100, height: 100)
.offset(x: 10, y: -5)VStack { ... }
.layout()
.size(width: 300, height: 200)
.background(.systemBlue)
.cornerRadius(16)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 provides access to its container's size and position, enabling dynamic layouts.
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)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)
}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
}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)Layout provides SwiftUI-style animation support with withAnimation and animation modifiers.
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)")
})// 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))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
}// 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
}// 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()
}// Check for RTL languages
let direction = LayoutDirection.current
if direction == .rightToLeft {
// Adjust layout for RTL
stackView.semanticContentAttribute = .forceRightToLeft
}// 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
}// 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()// 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)")// 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)")
}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
}
}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 provides an incremental layout system that only recalculates parts of the layout that have changed, significantly improving performance for complex UIs.
- 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
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 = falseWhen 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 passclass 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])
}
}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()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
β 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
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.)
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- Initial State: New
LayoutNodeinstances start withisDirty = true - Calculation:
calculateLayout()is called, node is marked clean - Children Cleanup: After parent calculation, dirty children are marked clean
- Incremental Updates: Only dirty nodes are recalculated in subsequent passes
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.
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
}
}- View Tracking: Views with identities are tracked in a map
- Diff Calculation: When layout updates, the system compares old and new identity maps
- 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
β
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
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!
}
}- 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
Hashabletype can be used as identity (String, Int, UUID, etc.)
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
// 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()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)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
| Category | Description |
|---|---|
| π§ Layout | Layout calculation process |
| ποΈ Hierarchy | View hierarchy structure |
| π Frame | Frame setting and changes |
| π² Spacer | Spacer calculation details |
| β‘ Performance | Performance metrics |
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.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 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 β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
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
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
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 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 β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 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
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β 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 β
βββββββββββββββββββββββββ
- Two-Phase Layout System: Separates measurement (size calculation) from placement (position calculation)
- Incremental Updates: Only recalculates parts of the layout that have changed
- Identity-Based Diffing: Efficiently updates views by tracking identities
- Modifier Storage: Modifiers stored as properties, not as new layout nodes
- Platform Abstraction: Supports both UIKit and AppKit through conditional compilation
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 viewsextractViews(): Returns the views managed by this layoutintrinsicContentSize: Provides natural size for the layout
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 definitionuseIncrementalLayout: Enables incremental updatesrootNode: Root of the layout tree for incremental updates
ViewLayout wraps individual UIView instances and provides modifier support:
Two-Phase Layout:
- Measure Phase: Calculates the size the view wants based on modifiers
- Layout Phase: Calculates the final position based on size and alignment
Modifier Storage:
- Modifiers stored as associated objects on the view itself
- No new
ViewLayoutinstances created for modifier chains - Efficient property-based storage
Stack Components:
VStack: Vertical stacking with spacing and alignmentHStack: Horizontal stacking with spacing and alignmentZStack: Z-axis stacking with multiple alignment options
Container Components:
ScrollView: Scrollable container with content offset preservationSpacer: Flexible space component
Dynamic Components:
ForEach: Dynamic list generation with identity-based diffing
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
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
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 treeIncrementalLayoutCache: Caches layout resultsDirtyRegionTracker: Tracks which regions need recalculation
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
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.layoutModifiersarray - No new
ViewLayoutinstances 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') β
ββββββββββββββββββββ
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 animatingstopAnimating(_:): Removes animation protection- Layout system skips frame updates for animating views
withAnimationprovides SwiftUI-style animation blocksLayoutAnimationEngine: Manages animation stateLayoutAnimation: Defines animation parametersAnimationTimingFunction: Timing functions (ease, spring, etc.)- Platform-specific animation APIs (UIView.animate / NSAnimationContext)
The environment system provides context values throughout the layout hierarchy:
Environment Values:
ColorScheme: Light/dark mode detectionLayoutDirection: RTL/LTR supportFont: Typography settingsForegroundColor: 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)
Multiple caching layers optimize performance:
Layout Cache:
- Caches
LayoutResultfor 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
The library is built on UIKit and designed for iOS applications:
UIKit Foundation:
- Uses
UIViewas the base view type - Leverages UIKit's view hierarchy and lifecycle
- Integrates with
UIViewControllerlifecycle 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
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:
-
CPU Cache Efficiency:
- Contiguous memory improves cache locality
- Sequential access patterns are significantly faster
- Reduced cache misses during tree traversal
- Better CPU prefetching behavior
-
Memory Allocation:
- Single allocation instead of many small heap allocations
- Reduced ARC overhead (no reference counting needed)
- More efficient memory utilization
-
SIMD Optimization:
- Vector operations possible on contiguous arrays
- Parallel processing of layout calculations
- Batch operations on multiple nodes
-
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 changesAPI 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 changesBest Practices:
-
Enable Incremental Layout: Always enable
useIncrementalLayoutfor complex layoutslayoutContainer.useIncrementalLayout = true
-
Use Identity for Views: Provide stable identities for views to enable efficient diffing
view.ui().id("stable-id")
-
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 }
-
Profile Performance: Use
PerformanceProfilerto identify bottlenecksPerformanceProfiler.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:
- Layout not updating: Ensure
layoutContainer.updateBody()is called after state changes - Views not appearing: Check that views are properly added to the layout hierarchy
- Performance issues: Enable incremental layout and use identity for views
- 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.
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()Common Issues and Solutions:
-
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 } }
-
Views not appearing
// Solution: Ensure views are in the layout hierarchy layoutContainer.updateBody { VStack { myView.ui() } }
-
Performance issues with complex layouts
// Solution: Enable incremental layout and use identity layoutContainer.useIncrementalLayout = true view.ui().id("stable-id")
-
Animation conflicts with layout
// Solution: Protect views during animation layoutContainer.startAnimating(view) // ... animate view ... layoutContainer.stopAnimating(view)
-
Layout calculations taking too long
// Solution: Profile and optimize PerformanceProfiler.shared.startProfiling() // ... identify bottlenecks ... PerformanceProfiler.shared.stopProfiling()
The architecture is designed for extensibility:
Custom Layouts:
- Conform to
Layoutprotocol - Implement
calculateLayout(in:)andextractViews() - Integrate with existing layout system
Custom Modifiers:
- Conform to
LayoutModifierprotocol - Store as associated objects
- Apply in measure/layout phases
Custom Components:
- Build on existing stack components
- Use
LayoutBuilderfor composition - Integrate with environment system
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
// 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)
])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()
}
}
}| 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) |
Layout now supports percentage-based sizing and positioning:
- Postfix
%Operator: Use80%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)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
Layout is inspired by:
- SwiftUI - Declarative syntax and result builders
- Yoga - Flexbox layout concepts
- ComponentKit - Declarative UI for iOS
swift-layout is released under the MIT license. See the LICENSE file for more info.