Skip to content

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

πŸš€ High Performance - Pure frame-based calculations, zero Auto Layout overhead
πŸ“± SwiftUI-Style API - Familiar declarative syntax with @LayoutBuilder
πŸ“ GeometryReader - Access container size and position dynamically
πŸ”„ Automatic View Management - Smart view hierarchy handling
πŸŒ‰ UIKit ↔ SwiftUI Bridge - Seamless integration between frameworks
πŸ“¦ Flexible Layouts - VStack, HStack, ZStack, ScrollView, and more
🎯 Zero Dependencies - Pure UIKit with optional SwiftUI integration
⚑ Animation Engine - Built-in spring and timing animations
πŸ”§ Environment System - Color scheme, layout direction support
πŸ“Š Performance Profiler - Real-time FPS and layout metrics
πŸ’Ύ Layout Caching - Intelligent caching for repeated layouts
🎨 Preferences System - Pass values up the view hierarchy


πŸ“¦ 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. Add container to view
        view.addSubview(layoutContainer)
        layoutContainer.frame = view.bounds
        layoutContainer.autoresizingMask = [.flexibleWidth, .flexibleHeight]

        // 5. Set the layout body
        layoutContainer.setBody { self.body }
    }

    // 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 cleaner code, extend BaseViewController:

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

    override func viewDidLoad() {
        super.viewDidLoad()
        setupViews()
    }

    override func setLayout() {
        layoutContainer.setBody { 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)
        }
    }
}

πŸ“¦ 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 and updates

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

// Update layout manually
layoutContainer.setBody { self.body }

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

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

🎨 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.

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

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)

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)

πŸ“ 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:

GeometryReader { proxy in
    contentView.layout()
}
.onGeometryChange { proxy in
    print("Size changed: \(proxy.size)")
    print("Global position: \(proxy.globalFrame.origin)")
}

⚑ 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, duration: 0.5))

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

πŸ” 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.enableFrameLogging = true

// Disable all
LayoutDebugger.shared.disableAll()

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

πŸ—οΈ 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
β”‚   β”œβ”€β”€ 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+SwiftUI.swift
β”‚   └── ArraryExtension.swift
β”‚
β”œβ”€β”€ LayoutContainer.swift  # Main container class
β”œβ”€β”€ ViewLayout.swift       # View layout wrapper
└── LayoutDebugger.swift   # Debugging utilities

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

// 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 frames
Debugging Constraint conflicts Simple frame inspection
Flexibility Rigid constraints Dynamic calculations

πŸ™ Inspiration

Layout is inspired by:

  • SwiftUI - Declarative syntax and result builders
  • PinLayout - Performance-first philosophy
  • 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