Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Reviews, improves, and writes SwiftUI code following state management, view composition, performance, and iOS 26+ Liquid Glass best practices.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/layout-best-practices.md
1# SwiftUI Layout Best Practices Reference23## Table of Contents45- [Relative Layout Over Constants](#relative-layout-over-constants)6- [Context-Agnostic Views](#context-agnostic-views)7- [Own Your Container](#own-your-container)8- [Layout Performance](#layout-performance)9- [View Logic and Testability](#view-logic-and-testability)10- [Full-Width Views](#full-width-views)11- [Action Handlers](#action-handlers)12- [Summary Checklist](#summary-checklist)1314## Relative Layout Over Constants1516**Use dynamic layout calculations instead of hard-coded values.**1718```swift19// Good - relative to actual layout20GeometryReader { geometry in21VStack {22HeaderView()23.frame(height: geometry.size.height * 0.2)24ContentView()25}26}2728// Avoid - magic numbers that don't adapt29VStack {30HeaderView()31.frame(height: 150) // Doesn't adapt to different screens32ContentView()33}34```3536**Why**: Hard-coded values don't account for different screen sizes, orientations, or dynamic content (like status bars during phone calls).3738## Context-Agnostic Views3940**Views should work in any context.** Never assume presentation style or screen size.4142```swift43// Good - adapts to given space44struct ProfileCard: View {45let user: User4647var body: some View {48VStack {49Image(user.avatar)50.resizable()51.aspectRatio(contentMode: .fit)52Text(user.name)53Spacer()54}55.padding()56}57}5859// Avoid - assumes full screen60struct ProfileCard: View {61let user: User6263var body: some View {64VStack {65Image(user.avatar)66.frame(width: UIScreen.main.bounds.width) // Wrong!67Text(user.name)68}69}70}71```7273**Why**: Views should work as full screens, modals, sheets, popovers, or embedded content.7475## Own Your Container7677**Custom views should own static containers but not lazy/repeatable ones.**7879```swift80// Good - owns static container81struct HeaderView: View {82var body: some View {83HStack {84Image(systemName: "star")85Text("Title")86Spacer()87}88}89}9091// Avoid - missing container92struct HeaderView: View {93var body: some View {94Image(systemName: "star")95Text("Title")96// Caller must wrap in HStack97}98}99100// Good - caller owns lazy container101struct FeedView: View {102let items: [Item]103104var body: some View {105LazyVStack {106ForEach(items) { item in107ItemRow(item: item)108}109}110}111}112```113114## Layout Performance115116### Avoid Layout Thrash117118**Minimize deep view hierarchies and excessive layout dependencies.**119120```swift121// Bad - deep nesting, excessive layout passes122VStack {123HStack {124VStack {125HStack {126VStack {127Text("Deep")128}129}130}131}132}133134// Good - flatter hierarchy135VStack {136Text("Shallow")137Text("Structure")138}139```140141**Avoid excessive `GeometryReader` and preference chains:**142143```swift144// Bad - multiple geometry readers cause layout thrash145GeometryReader { outerGeometry in146VStack {147GeometryReader { innerGeometry in148// Layout recalculates multiple times149}150}151}152153// Good - single geometry reader or use alternatives (iOS 17+)154containerRelativeFrame(.horizontal) { width, _ in155width * 0.8156}157```158159**Gate frequent geometry updates:**160161```swift162// Bad - updates on every pixel change163.onPreferenceChange(ViewSizeKey.self) { size in164currentSize = size165}166167// Good - gate by threshold168.onPreferenceChange(ViewSizeKey.self) { size in169let difference = abs(size.width - currentSize.width)170if difference > 10 { // Only update if significant change171currentSize = size172}173}174```175176## View Logic and Testability177178### Keep Business Logic in Services and Models179180**Business logic belongs in services and models, not in views.** Views should stay simple and declarative — orchestrating UI state, not implementing business rules. This makes logic independently testable without requiring view instantiation.181182> **iOS 17+**: Use `@Observable` with `@State`.183184```swift185@Observable186final class AuthService {187var email = ""188var password = ""189var isValid: Bool {190!email.isEmpty && password.count >= 8191}192193func login() async throws {194// Business logic here — testable without the view195}196}197198struct LoginView: View {199@State private var authService = AuthService()200201var body: some View {202Form {203TextField("Email", text: $authService.email)204SecureField("Password", text: $authService.password)205Button("Login") {206Task {207try? await authService.login()208}209}210.disabled(!authService.isValid)211}212}213}214```215216For iOS 16 and earlier, use `ObservableObject` with `@StateObject` -- see `state-management.md` for the legacy pattern.217218Avoid embedding business logic directly in view closures (e.g., validation checks inside a `Button` action). This makes logic untestable without view instantiation.219220**Note**: This is about making business logic testable, not about enforcing a specific architecture. The key is that logic lives outside views where it can be tested independently.221222## Full-Width Views223224**When a single view needs to fill the available width, use `.frame(maxWidth: .infinity, alignment:)` instead of wrapping it in a stack with a `Spacer`.**225226```swift227// Good - frame modifier228Text("Hello")229.frame(maxWidth: .infinity, alignment: .leading)230231// Avoid - unnecessary stack and spacer232HStack {233Text("Hello")234Spacer()235}236```237238**Why**: `.frame(maxWidth:alignment:)` is a single modifier that clearly communicates intent. Wrapping in an `HStack` with a `Spacer` adds an extra container to the view hierarchy for no benefit.239240## Action Handlers241242**Separate layout from logic.** View body should reference action methods, not contain inline logic.243244```swift245// Good - action references method246Button("Publish Project", action: publishService.handlePublish)247248// Avoid - multi-line logic in closure249Button("Publish Project") {250isLoading = true251apiService.publish(project) { result in /* ... */ }252}253```254255## Summary Checklist256257- [ ] Use relative layout over hard-coded constants258- [ ] Views work in any context (don't assume screen size)259- [ ] Custom views own static containers260- [ ] Avoid deep view hierarchies (layout thrash)261- [ ] Gate frequent geometry updates by thresholds262- [ ] Business logic kept in services and models (not in views)263- [ ] Action handlers reference methods, not inline logic264- [ ] Use `.frame(maxWidth: .infinity, alignment:)` for full-width views (not `HStack` + `Spacer`)265- [ ] Avoid excessive `GeometryReader` usage266- [ ] Use `containerRelativeFrame()` when appropriate267