SwiftUI Development
This skill covers best practices for building clear, performant, and maintainable SwiftUI applications, including architecture, state management, Swift 6 concurrency, layout, animation, and testing.
Key Principles
- Write correct, up-to-date, bug-free, fully functional, secure, and performant code
- Favor readability, but not at the expense of correctness or safety
- Fully implement all requested functionality — leave no TODOs, placeholders, or missing pieces
- Target Swift 6.0+ with strict concurrency checking enabled; treat concurrency warnings as errors
- Target iOS 17+ where possible to use modern APIs (
@Observable,NavigationStack,#Preview); note the minimum deployment target when it forces older patterns
Architecture
- Use MVVM (Model-View-ViewModel) or Clean Architecture; introduce a Coordinator layer for complex, multi-screen navigation flows
- Apply SOLID principles to views and view models:
- Single Responsibility — each view or view model has one reason to change
- Open/Closed — extend behavior via composition and protocols rather than modifying existing types
- Liskov Substitution — protocol conformances must be substitutable without surprising callers
- Interface Segregation — keep protocols small and focused rather than one large "do everything" interface
- Dependency Inversion — depend on protocol abstractions, not concrete types, and inject dependencies via
init
- Implement protocol-oriented programming; prefer structs over classes for data models
- Use extensions for code organization and separation of concerns
SwiftUI View Structure
- Keep views small and focused on a single responsibility
- Treat 50 lines as a practical ceiling for a
bodyproperty — past that, extract subviews into small, reusable structs or private extension functions - Use
@ViewBuilderfor custom container views and complex conditional view logic - Implement proper view composition patterns
State Management
@State— local, value-type view state (Bool, Int, String, small structs)@Binding— two-way data binding with child views@StateObject— use only in the view that creates/owns the object's lifecycle@ObservedObject— use in child views that react to changes but don't own the object@EnvironmentObject/@Environment— use sparingly for broadly shared dependencies or system values; prefer explicit dependency injection viainitwhen a dependency belongs to a specific view hierarchy rather than the whole app@Published— for observable properties onObservableObjectclasses (pre-iOS 17 or whenObservableObjectis otherwise required)@Observablemacro (iOS 17+) — prefer this overObservableObject/@Publishedfor new view models; it removes the need for@Publishedon every property and only triggers view updates for properties actually read by that view, which reduces unnecessary redraws
Naming Conventions
- camelCase for variables, functions, and methods; PascalCase for types (classes, structs, enums, protocols)
- Use descriptive, verbose names —
fetchUserDataovergetData - Prefix boolean variables with
is,has,should, etc. - Use verb phrases for function names
SwiftUI Best Practices
- Use SF Symbols for system icons; use semantic colors from the asset catalog for automatic dark mode support
- Support Dynamic Type and implement proper keyboard avoidance
- Use
NavigationStack(iOS 16+) over the deprecatedNavigationView - Prefer type-inferred shorthand dot syntax where available (e.g.,
.background(.blue)) - Add
.contentShape(Rectangle())toHStack/VStackrows that have transparent backgrounds, so taps register across the whole row rather than only on opaque content
Layout and Styling
- Use native SwiftUI layout containers (VStack, HStack, ZStack, Grid); use
LazyVStack/LazyHStackfor dynamic or long lists - Use
GeometryReadersparingly — it expands to fill all available space and can hurt layout performance; prefer.background(GeometryReader { ... })scoped to a single view, or theLayoutprotocol for custom layouts - Give list items stable, meaningful
ids (fromIdentifiable) rather than\.self - Implement adaptive layouts for different screen sizes
- Use
ViewModifiers for reusable styling; create customButtonStyle,TextFieldStyle, etc.
Animations and Transitions
- Prefer
.animation(_:value:)scoped to a specific state value over broadwithAnimationcalls, especially insidebody - Reserve
withAnimationfor animations explicitly triggered by user interaction - Implement custom transitions using
AnyTransition; usematchedGeometryEffectfor hero animations - Use
TimelineViewfor high-frequency, time-driven visual updates instead of aTimer+ published property
Swift 6 Concurrency & Data Flow
- Annotate view models with
@MainActor; all UI updates must happen on the main actor - Prefer
.task(id:)over.onAppearfor starting async work — it automatically cancels the task when the view disappears or the id changes, avoiding orphaned work - Use
async/awaitfor asynchronous operations andResult(or typed throws) for error handling - Prefer
actortypes for shared mutable state or services accessed from multiple tasks - Mark pure logic functions
nonisolatedwhen they don't touch the main actor, to avoid unnecessary hops - Never block the main thread — move heavy computation to a detached
Task - Handle loading, error, and success states explicitly rather than leaving implicit/undefined states
Memory Management & Safety
- Default to
[weak self]in closures that outlive the current scope; useguard let self else { return }at the start of async closures - Only use
[unowned self]when the closure's lifetime is provably shorter thanself's - Handle optionals safely — no force unwrapping; use
guardfor early returns - For remote images, use
AsyncImage(with a caching layer, or a library like Nuke/Kingfisher for production apps) and apply.resizable()immediately
Performance Optimization
- Minimize view body recalculations; adopt
Equatableconformance where it helps SwiftUI skip redundant diffing - Implement proper list diffing with
Identifiableitems and stable ids - Profile with Instruments before optimizing; cache expensive computations rather than recomputing them in
body
Accessibility
- Add accessibility labels, hints, and traits appropriately; support VoiceOver
- Assign distinct
accessibilityIdentifierstrings to interactive elements so UI tests can target them reliably - Test with accessibility features (Dynamic Type, VoiceOver, Reduce Motion) enabled
Testing and Previews
- Always provide a preview using
#Preview(Xcode 15+) orPreviewProvideron older toolchains, injecting realistic mock data - Preview in multiple color schemes and device sizes
- Structure unit tests with Given-When-Then; generate protocol-based mocks for external dependencies so view models can be tested without hitting real services
Code Quality
- Write self-documenting code; add comments only for non-obvious logic
- Follow the Swift API Design Guidelines
- Use
MARK: - Section Nameto organize longer files, and place private helpers in aprivate extension
Common Patterns
View with @Observable ViewModel (iOS 17+)
@Observable
@MainActor
final class ContentViewModel {
var items: [Item] = []
var isLoading = false
func loadItems() async {
isLoading = true
defer { isLoading = false }
// Load items
}
}
struct ContentView: View {
@State private var viewModel = ContentViewModel()
var body: some View {
List(viewModel.items) { item in
Text(item.name)
}
.task(id: viewModel.items.count) {
await viewModel.loadItems()
}
}
}
View with ObservableObject ViewModel (pre-iOS 17 / legacy)
struct ContentView: View {
@StateObject private var viewModel = ContentViewModel()
var body: some View {
// View implementation
}
}
@MainActor
class ContentViewModel: ObservableObject {
@Published var items: [Item] = []
@Published var isLoading = false
func loadItems() async {
isLoading = true
// Load items
isLoading = false
}
}
Reusable View Modifier
struct CardModifier: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(radius: 4)
}
}
extension View {
func cardStyle() -> some View {
modifier(CardModifier())
}
}
</content>