This document provides comprehensive guidelines, standards, and best practices for SwiftUI development. Following these principles will help maintain code quality, consistency, and readability across your SwiftUI projects. These standards are designed to leverage SwiftUI's declarative syntax while avoiding common pitfalls.
As SwiftUI continues to evolve, adhering to established patterns becomes increasingly important for creating maintainable, performant, and scalable applications. This guide aims to serve as a reference for developers of all experience levels working with SwiftUI.
The examples provided demonstrate both recommended approaches and anti-patterns to avoid. By following these guidelines, development teams can ensure consistency in code style, improve collaboration, and reduce technical debt.
Proper view structure is crucial in SwiftUI projects. Well-organized views improve readability, maintainability, and performance. This section covers how to structure and organize your SwiftUI views effectively.
- Keep views small and focused on a single responsibility
- Extract reusable components into separate views
- Use private extensions to organize view modifiers
- Implement computed properties for complex view elements
- Follow a consistent naming convention for views and their components
- Use ViewBuilders for reusable view compositions
- Creating large, monolithic views with multiple responsibilities
- Duplicating view code instead of extracting reusable components
- Nesting too many views, which can impact performance
- Mixing business logic with view code
- Balance between small components and over-fragmentation
- Performance implications of view extraction (SwiftUI optimizes many extractions automatically)
- Readability vs. conciseness
struct ProductDetailView: View {
let product: Product
@State private var quantity = 1
var body: some View {
VStack(alignment: .leading, spacing: 16) {
productHeader
productDescription
quantitySelector
addToCartButton
}
.padding()
}
private var productHeader: some View {
HStack {
productImage
productTitlePrice
}
}
private var productImage: some View {
Image(product.imageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100, height: 100)
.cornerRadius(8)
}
private var productTitlePrice: some View {
VStack(alignment: .leading) {
Text(product.name)
.font(.headline)
Text("$\(product.price, specifier: "%.2f")")
.font(.subheadline)
.foregroundColor(.secondary)
}
}
private var productDescription: some View {
Text(product.description)
.font(.body)
}
private var quantitySelector: some View {
HStack {
Text("Quantity:")
Stepper("\(quantity)", value: $quantity, in: 1...10)
}
}
private var addToCartButton: some View {
Button(action: addToCart) {
Text("Add to Cart")
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
}
private func addToCart() {
// Cart logic
}
}struct ProductDetailView: View {
let product: Product
@State private var quantity = 1
var body: some View {
VStack(alignment: .leading, spacing: 16) {
// All code in one place, making it difficult to read and maintain
HStack {
Image(product.imageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100, height: 100)
.cornerRadius(8)
VStack(alignment: .leading) {
Text(product.name)
.font(.headline)
Text("$\(product.price, specifier: "%.2f")")
.font(.subheadline)
.foregroundColor(.secondary)
}
}
Text(product.description)
.font(.body)
HStack {
Text("Quantity:")
Stepper("\(quantity)", value: $quantity, in: 1...10)
}
Button(action: {
// Cart logic mixed directly in the view
print("Adding \(quantity) of \(product.name) to cart")
// More logic here...
}) {
Text("Add to Cart")
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
}
.padding()
}
}Proper state management is critical in SwiftUI applications. SwiftUI offers several property wrappers and approaches to manage state, each with specific use cases and implications.
- Use the most appropriate property wrapper for each use case:
@Statefor simple, view-local state@Bindingfor state passed from a parent@ObservedObjectfor external reference types that can change@StateObjectfor owned reference types that need to persist through view updates@EnvironmentObjectfor dependency injection of shared objects@AppStoragefor user defaults@SceneStoragefor UI state persistence between app launches
- Keep state at the highest necessary level, not higher
- Prefer value types (structs) for modeling state where appropriate
- Use readonly computed properties for derived state
- Using
@Statefor data that should be shared across views - Overusing
@EnvironmentObjectfor data that only needs to be passed to a few views - Creating deep property paths with multiple
@Bindingreferences - Using mutable global state instead of proper state management
- Mixing different state management approaches unnecessarily
- Performance impact of property wrappers, especially with large objects
- Memory management and potential retain cycles
- Debugging complexity with deeply nested state
- View lifecycle and when state is initialized or reset
// Parent view creates and owns the state
struct ParentView: View {
@StateObject private var viewModel = ShoppingCartViewModel()
var body: some View {
VStack {
CartSummaryView(itemCount: viewModel.itemCount)
CartItemsView(items: $viewModel.items)
// Local state for UI elements
CartActionsView(onCheckout: viewModel.checkout)
}
}
}
// Child view receives only what it needs
struct CartSummaryView: View {
let itemCount: Int
var body: some View {
Text("Your cart contains \(itemCount) item(s)")
}
}
// Child view that needs to modify the parent's state
struct CartItemsView: View {
@Binding var items: [CartItem]
var body: some View {
List {
ForEach(items) { item in
CartItemRow(item: item)
}
.onDelete(perform: removeItems)
}
}
private func removeItems(at offsets: IndexSet) {
items.remove(atOffsets: offsets)
}
}
// View with local actions that calls back to parent
struct CartActionsView: View {
let onCheckout: () -> Void
@State private var showingConfirmation = false
var body: some View {
Button("Checkout") {
showingConfirmation = true
}
.alert(isPresented: $showingConfirmation) {
Alert(
title: Text("Confirm Checkout"),
message: Text("Do you want to proceed with checkout?"),
primaryButton: .default(Text("Yes"), action: onCheckout),
secondaryButton: .cancel()
)
}
}
}// Global state - avoid this pattern
var globalCart = ShoppingCart()
struct BadParentView: View {
// Using @State for shared data that should be in a model
@State private var items: [CartItem] = []
@State private var isCheckingOut = false
var body: some View {
VStack {
// Passing too many properties instead of a cohesive model
BadCartSummaryView(items: items)
BadCartItemsView(items: $items, isCheckingOut: $isCheckingOut)
Button("Checkout") {
// Directly modifying global state
globalCart.items = items
isCheckingOut = true
// Business logic embedded in the view
processCheckout()
}
}
}
func processCheckout() {
// Complex business logic in the view
print("Processing checkout...")
}
}
struct BadCartSummaryView: View {
// Receiving the entire array when only needing the count
let items: [CartItem]
var body: some View {
Text("Your cart contains \(items.count) item(s)")
}
}
struct BadCartItemsView: View {
// Receiving state not relevant to this view
@Binding var items: [CartItem]
@Binding var isCheckingOut: Bool
var body: some View {
List {
ForEach(items) { item in
Text(item.name)
}
.onDelete(perform: { offsets in
// Directly modifying global state alongside binding
items.remove(atOffsets: offsets)
globalCart.items.remove(atOffsets: offsets)
})
}
.disabled(isCheckingOut) // Using a binding that could be a simple parameter
}
}