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
π 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
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. 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)
}
}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 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 and updates
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
}// Update layout manually
layoutContainer.setBody { self.body }
// Force layout update
layoutContainer.setNeedsLayout()
layoutContainer.layoutIfNeeded()
// Update layout for orientation changes
layoutContainer.updateLayoutForOrientationChange()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)
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.
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)
}
}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
}// 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)// 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)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:
GeometryReader { proxy in
contentView.layout()
}
.onGeometryChange { proxy in
print("Size changed: \(proxy.size)")
print("Global position: \(proxy.globalFrame.origin)")
}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, duration: 0.5))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)
}
}// 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()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 |
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
// 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)
])// 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 frames |
| Debugging | Constraint conflicts | Simple frame inspection |
| Flexibility | Rigid constraints | Dynamic calculations |
Layout is inspired by:
- SwiftUI - Declarative syntax and result builders
- PinLayout - Performance-first philosophy
- Yoga - Flexbox layout concepts
- ComponentKit - Declarative UI for iOS
swift-layout is released under the MIT license. See the LICENSE file for more info.