ios-swift
/

iOS SwiftUI – Complete Guide to Declarative UI Development

Last Sync: Today

On this page

10
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

ios-swift

iOS SwiftUI – Complete Guide to Declarative UI Development

SwiftUI – Apple's Declarative UI Framework

SwiftUI is Apple's modern framework for building user interfaces across all Apple platforms (iOS, macOS, watchOS, tvOS, visionOS). Unlike UIKit's imperative approach, SwiftUI uses a declarative syntax where you describe what the UI should look like based on the current state, and the framework automatically handles updates. In 2026, SwiftUI is mature and feature-complete for most production apps, though UIKit integration remains valuable for complex custom needs.

  1. SwiftUI Fundamentals – Your First View

SwiftUI views are lightweight structs that conform to the View protocol. The body property describes the view's content using a composition of built-in components.

SWIFTRead-only
1
import SwiftUI

// MARK: - Basic View Structure
struct ContentView: View {
    var body: some View {
        VStack(spacing: 20) {
            Text("Welcome to SwiftUI")
                .font(.largeTitle)
                .fontWeight(.bold)
            
            Image(systemName: "swift")
                .font(.system(size: 60))
                .foregroundColor(.orange)
            
            Button("Tap Me") {
                print("Button tapped!")
            }
            .buttonStyle(.borderedProminent)
            .controlSize(.large)
        }
        .padding()
    }
}

// MARK: - App Entry Point
@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

// MARK: - Common Layout Containers
struct LayoutExample: View {
    var body: some View {
        // VStack: Vertical stack
        VStack(alignment: .leading, spacing: 8) {
            Text("Vertical Stack")
            Divider()
        }
        
        // HStack: Horizontal stack
        HStack(spacing: 16) {
            Image(systemName: "heart")
            Text("Horizontal Stack")
        }
        
        // ZStack: Overlapping views (like a stack of cards)
        ZStack {
            Rectangle()
                .fill(Color.blue)
                .frame(width: 200, height: 200)
            Text("On Top")
                .foregroundColor(.white)
        }
        
        // Lazy stacks for scrolling content
        ScrollView {
            LazyVStack(alignment: .leading, spacing: 12) {
                ForEach(0..<100) { index in
                    Text("Item \(index)")
                        .padding()
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .background(Color.gray.opacity(0.1))
                }
            }
        }
    }
}

  1. State Management – The Heart of SwiftUI

SwiftUI uses property wrappers to manage state and data flow. Understanding when to use each wrapper is critical for building reactive, bug-free apps.

SWIFTRead-only
1
import SwiftUI

// MARK: - @State (Local view state)
struct CounterView: View {
    @State private var count = 0 // Owned by this view, value type
    
    var body: some View {
        VStack {
            Text("Count: \(count)")
                .font(.title)
            
            Button("Increment") {
                count += 1 // Triggers view re-render
            }
            .buttonStyle(.borderedProminent)
        }
    }
}

// MARK: - @Binding (Two-way connection to parent state)
struct ParentView: View {
    @State private var isOn = false
    
    var body: some View {
        VStack {
            Text("Parent: \(isOn ? "ON" : "OFF")")
            ChildSwitch(isOn: $isOn) // Pass binding
        }
    }
}

struct ChildSwitch: View {
    @Binding var isOn: Bool // References state from parent
    
    var body: some View {
        Toggle("Child Toggle", isOn: $isOn)
            .padding()
    }
}

// MARK: - @ObservedObject (External reference type state)
class UserViewModel: ObservableObject {
    @Published var name = "" // @Published triggers updates
    @Published var age = 0
    
    func incrementAge() {
        age += 1
    }
}

struct ProfileView: View {
    @ObservedObject var viewModel: UserViewModel // Observes external object
    
    var body: some View {
        Form {
            TextField("Name", text: $viewModel.name)
            HStack {
                Text("Age: \(viewModel.age)")
                Button("+", action: viewModel.incrementAge)
            }
        }
    }
}

// MARK: - @StateObject (Owned observable object – creates and owns lifecycle)
struct OwnerView: View {
    @StateObject private var viewModel = UserViewModel() // Owns the object
    
    var body: some View {
        ProfileView(viewModel: viewModel)
    }
}

// MARK: - @EnvironmentObject (Shared across view hierarchy)
class AppSettings: ObservableObject {
    @Published var isDarkMode = false
    @Published var language = "en"
}

struct RootView: View {
    @StateObject private var settings = AppSettings()
    
    var body: some View {
        ContentView()
            .environmentObject(settings) // Inject into environment
    }
}

struct SettingsToggle: View {
    @EnvironmentObject var settings: AppSettings // Access anywhere
    
    var body: some View {
        Toggle("Dark Mode", isOn: $settings.isDarkMode)
    }
}

// MARK: - @Environment (System-provided values)
struct SystemInfoView: View {
    @Environment(\.colorScheme) var colorScheme
    @Environment(\.locale) var locale
    @Environment(\.horizontalSizeClass) var sizeClass
    
    var body: some View {
        VStack {
            Text("Theme: \(colorScheme == .dark ? "Dark" : "Light")")
            Text("Language: \(locale.languageCode ?? "unknown")")
            Text("Size Class: \(sizeClass == .compact ? "Compact" : "Regular")")
        }
    }
}

// MARK: - @AppStorage (UserDefaults binding)
struct UserPreferencesView: View {
    @AppStorage("username") private var username = "Guest"
    @AppStorage("notificationsEnabled") private var notificationsEnabled = true
    
    var body: some View {
        Form {
            TextField("Username", text: $username)
            Toggle("Enable Notifications", isOn: $notificationsEnabled)
        }
    }
}

// MARK: - @SceneStorage (State restoration per scene/window)
struct DetailView: View {
    @SceneStorage("detail.selectedTab") private var selectedTab = 0
    
    var body: some View {
        TabView(selection: $selectedTab) {
            Text("Tab 1").tag(0)
            Text("Tab 2").tag(1)
        }
        // Selected tab persists across app launches
    }
}

  1. Navigation in SwiftUI

SwiftUI provides two navigation systems: the legacy NavigationView (deprecated in iOS 16+) and the modern NavigationStack (iOS 16+) with programmatic navigation and deep linking support.

SWIFTRead-only
1
import SwiftUI

// MARK: - Modern NavigationStack (iOS 16+, Recommended)
struct HomeView: View {
    @State private var path = NavigationPath() // Manages navigation stack
    
    var body: some View {
        NavigationStack(path: $path) {
            List {
                NavigationLink("Basic Detail", value: "Simple")
                NavigationLink("User Profile", value: User(id: 1, name: "Alice"))
                Button("Programmatic Push") {
                    path.append("Programmatic")
                }
            }
            .navigationTitle("Home")
            .navigationDestination(for: String.self) { stringValue in
                Text("Detail: \(stringValue)")
            }
            .navigationDestination(for: User.self) { user in
                UserProfileView(user: user)
            }
        }
    }
}

struct User: Hashable {
    let id: Int
    let name: String
}

struct UserProfileView: View {
    let user: User
    @Environment(\.dismiss) private var dismiss
    
    var body: some View {
        VStack {
            Text("Profile: \(user.name)")
            Button("Go Back") {
                dismiss() // Programmatic dismiss
            }
        }
    }
}

// MARK: - Sheet & FullScreenCover (Modals)
struct ModalExampleView: View {
    @State private var showingSheet = false
    @State private var showingFullScreen = false
    
    var body: some View {
        VStack(spacing: 20) {
            Button("Show Sheet") {
                showingSheet.toggle()
            }
            .sheet(isPresented: $showingSheet) {
                SheetContentView()
                    .presentationDetents([.medium, .large]) // iOS 16+
                    .presentationDragIndicator(.visible)
            }
            
            Button("Show Full Screen") {
                showingFullScreen.toggle()
            }
            .fullScreenCover(isPresented: $showingFullScreen) {
                FullScreenView()
            }
        }
    }
}

struct SheetContentView: View {
    @Environment(\.dismiss) private var dismiss
    
    var body: some View {
        NavigationStack {
            Text("Sheet Content")
                .toolbar {
                    ToolbarItem(placement: .cancellationAction) {
                        Button("Cancel") { dismiss() }
                    }
                }
        }
    }
}

// MARK: - Alert & Confirmation Dialog
struct AlertExampleView: View {
    @State private var showingAlert = false
    @State private var showingConfirmation = false
    
    var body: some View {
        VStack {
            Button("Show Alert") {
                showingAlert = true
            }
            .alert("Delete Item", isPresented: $showingAlert) {
                Button("Cancel", role: .cancel) { }
                Button("Delete", role: .destructive) {
                    // Perform delete
                }
            } message: {
                Text("This action cannot be undone.")
            }
            
            Button("Show Options") {
                showingConfirmation = true
            }
            .confirmationDialog("Choose Option", isPresented: $showingConfirmation) {
                Button("Option 1") { }
                Button("Option 2") { }
                Button("Cancel", role: .cancel) { }
            }
        }
    }
}

  1. Layout & Customization

SwiftUI offers powerful layout modifiers and custom layout capabilities for creating responsive, adaptive interfaces.

SWIFTRead-only
1
import SwiftUI

// MARK: - Layout Modifiers
struct LayoutModifiersExample: View {
    var body: some View {
        VStack(spacing: 20) {
            // Frame & sizing
            Text("Fixed Size")
                .frame(width: 200, height: 50)
                .background(Color.blue)
            
            Text("Max Width")
                .frame(maxWidth: .infinity)
                .background(Color.green)
            
            // Padding & margin
            Text("With Padding")
                .padding(.all, 16)
                .background(Color.yellow)
            
            // Overlay & background
            Text("Overlay")
                .padding()
                .overlay(
                    RoundedRectangle(cornerRadius: 8)
                        .stroke(Color.red, lineWidth: 2)
                )
                .background(
                    RoundedRectangle(cornerRadius: 8)
                        .fill(Color.gray.opacity(0.1))
                )
        }
        .padding()
    }
}

// MARK: - Custom Layout (iOS 16+)
struct EqualWidthHStack: Layout {
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        let maxHeight = subviews.map { $0.sizeThatFits(.unspecified).height }.max() ?? 0
        return CGSize(width: proposal.width ?? 0, height: maxHeight)
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        let width = bounds.width / CGFloat(subviews.count)
        for (index, subview) in subviews.enumerated() {
            let x = bounds.minX + (CGFloat(index) * width)
            subview.place(at: CGPoint(x: x, y: bounds.midY), anchor: .center, proposal: ProposedViewSize(width: width, height: nil))
        }
    }
}

struct CustomLayoutExample: View {
    var body: some View {
        EqualWidthHStack {
            Text("Short")
                .padding()
                .background(Color.red)
            Text("Much longer text")
                .padding()
                .background(Color.green)
            Text("Medium")
                .padding()
                .background(Color.blue)
        }
        .frame(height: 60)
    }
}

// MARK: - Size Classes & Adaptive Layout
struct AdaptiveLayoutView: View {
    @Environment(\.horizontalSizeClass) var horizontalSizeClass
    
    var body: some View {
        if horizontalSizeClass == .compact {
            // iPhone layout
            VStack {
                Text("Compact Layout")
                // Stack vertically
            }
        } else {
            // iPad layout
            HStack {
                Text("Regular Layout")
                // Side-by-side
            }
        }
    }
}

// MARK: - GeometryReader (Access parent dimensions)
struct GeometryReaderExample: View {
    var body: some View {
        GeometryReader { geometry in
            VStack {
                Text("Width: \(geometry.size.width)")
                Text("Height: \(geometry.size.height)")
                
                Rectangle()
                    .fill(Color.blue)
                    .frame(width: geometry.size.width * 0.7, height: 50)
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
        }
        .frame(height: 200)
        .background(Color.gray.opacity(0.1))
    }
}

  1. Animations & Transitions

SwiftUI makes animation incredibly simple with implicit, explicit, and custom animations.

SWIFTRead-only
1
import SwiftUI

// MARK: - Implicit Animation (automatic on state change)
struct ImplicitAnimationView: View {
    @State private var isExpanded = false
    
    var body: some View {
        VStack {
            RoundedRectangle(cornerRadius: 10)
                .fill(Color.blue)
                .frame(width: isExpanded ? 200 : 100, height: isExpanded ? 200 : 100)
                .animation(.spring(), value: isExpanded) // Animate when value changes
            
            Button("Toggle") {
                isExpanded.toggle()
            }
        }
    }
}

// MARK: - Explicit Animation (withAnimation closure)
struct ExplicitAnimationView: View {
    @State private var rotation = 0.0
    
    var body: some View {
        Button("Rotate") {
            withAnimation(.easeInOut(duration: 1)) {
                rotation += 360
            }
        }
        .rotationEffect(.degrees(rotation))
    }
}

// MARK: - Transition Animations
struct TransitionExampleView: View {
    @State private var showDetails = false
    
    var body: some View {
        VStack {
            Button("Toggle Details") {
                withAnimation(.easeInOut) {
                    showDetails.toggle()
                }
            }
            
            if showDetails {
                Text("Detailed Information")
                    .transition(.slide.combined(with: .opacity))
                // Available transitions: .slide, .scale, .opacity, .move(edge:), .asymmetric
            }
        }
    }
}

// MARK: - Matched Geometry Effect (hero animations)
struct MatchedGeometryExample: View {
    @Namespace private var namespace
    @State private var isExpanded = false
    
    var body: some View {
        if !isExpanded {
            VStack {
                Image(systemName: "photo")
                    .matchedGeometryEffect(id: "image", in: namespace)
                    .frame(width: 100, height: 100)
                
                Text("Tap to Expand")
                    .matchedGeometryEffect(id: "text", in: namespace)
            }
            .onTapGesture {
                withAnimation(.spring()) {
                    isExpanded.toggle()
                }
            }
        } else {
            VStack {
                Image(systemName: "photo")
                    .matchedGeometryEffect(id: "image", in: namespace)
                    .frame(width: 300, height: 300)
                
                Text("Expanded View")
                    .matchedGeometryEffect(id: "text", in: namespace)
                
                Button("Close") {
                    withAnimation(.spring()) {
                        isExpanded.toggle()
                    }
                }
            }
        }
    }
}

// MARK: - Custom Animations
struct CustomAnimation: View {
    @State private var animate = false
    
    var body: some View {
        Circle()
            .fill(Color.red)
            .frame(width: 100, height: 100)
            .scaleEffect(animate ? 1.5 : 1)
            .opacity(animate ? 0.5 : 1)
            .animation(.easeInOut(duration: 1).repeatForever(), value: animate)
            .onAppear {
                animate = true
            }
    }
}

  1. Lists & Forms

SwiftUI provides List, Form, and Lazy stacks for displaying collections of data efficiently.

SWIFTRead-only
1
import SwiftUI

// MARK: - List with Dynamic Data
struct Item: Identifiable {
    let id = UUID()
    let name: String
    let isCompleted: Bool
}

struct ListExample: View {
    @State private var items = [
        Item(name: "Buy groceries", isCompleted: false),
        Item(name: "Walk dog", isCompleted: true),
        Item(name: "Write code", isCompleted: false)
    ]
    
    @State private var newItemName = ""
    
    var body: some View {
        List {
            // Section header
            Section {
                ForEach(items) { item in
                    HStack {
                        Image(systemName: item.isCompleted ? "checkmark.circle.fill" : "circle")
                            .foregroundColor(item.isCompleted ? .green : .gray)
                        Text(item.name)
                            .strikethrough(item.isCompleted)
                    }
                    .swipeActions {
                        Button(role: .destructive) {
                            deleteItem(item)
                        } label: {
                            Label("Delete", systemImage: "trash")
                        }
                    }
                }
                .onDelete(perform: deleteItems)
                .onMove(perform: moveItems)
            } header: {
                Text("Tasks")
            } footer: {
                Text("\(items.filter { !$0.isCompleted }.count) remaining")
            }
            
            // Add new item
            HStack {
                TextField("New task", text: $newItemName)
                Button("Add") {
                    addItem()
                }
                .disabled(newItemName.isEmpty)
            }
        }
        .navigationTitle("Tasks")
        .toolbar {
            EditButton()
        }
    }
    
    private func deleteItems(at offsets: IndexSet) {
        items.remove(atOffsets: offsets)
    }
    
    private func moveItems(from source: IndexSet, to destination: Int) {
        items.move(fromOffsets: source, toOffset: destination)
    }
    
    private func deleteItem(_ item: Item) {
        items.removeAll { $0.id == item.id }
    }
    
    private func addItem() {
        let newItem = Item(name: newItemName, isCompleted: false)
        items.append(newItem)
        newItemName = ""
    }
}

// MARK: - Form (for settings & input screens)
struct SettingsFormView: View {
    @AppStorage("notifications") private var notifications = true
    @AppStorage("darkMode") private var darkMode = false
    @State private var selectedColor = "Blue"
    @State private var volume = 0.5
    
    let colors = ["Red", "Green", "Blue"]
    
    var body: some View {
        NavigationStack {
            Form {
                Section("Preferences") {
                    Toggle("Enable Notifications", isOn: $notifications)
                    Toggle("Dark Mode", isOn: $darkMode)
                }
                
                Section("Appearance") {
                    Picker("Theme Color", selection: $selectedColor) {
                        ForEach(colors, id: \.self) { color in
                            Text(color)
                        }
                    }
                    
                    HStack {
                        Text("Volume")
                        Slider(value: $volume, in: 0...1)
                        Text("\(Int(volume * 100))%")
                            .foregroundColor(.secondary)
                    }
                }
                
                Section("About") {
                    HStack {
                        Text("Version")
                        Spacer()
                        Text("1.0.0")
                            .foregroundColor(.secondary)
                    }
                    
                    Link("Visit Website", destination: URL(string: "https://apple.com")!)
                }
            }
            .navigationTitle("Settings")
        }
    }
}

  1. Data Fetching & Async/Await

SwiftUI works seamlessly with Swift concurrency. Use .task modifier for async operations and @Observable for modern data models (iOS 17+).

SWIFTRead-only
1
import SwiftUI

// MARK: - Async/Await with .task (iOS 15+)
struct Post: Codable, Identifiable {
    let id: Int
    let title: String
    let body: String
}

struct PostsListView: View {
    @State private var posts: [Post] = []
    @State private var errorMessage: String?
    @State private var isLoading = false
    
    var body: some View {
        NavigationStack {
            Group {
                if isLoading {
                    ProgressView("Loading...")
                } else if let error = errorMessage {
                    VStack {
                        Text("Error: \(error)")
                            .foregroundColor(.red)
                        Button("Retry") {
                            Task { await fetchPosts() }
                        }
                    }
                } else {
                    List(posts) { post in
                        VStack(alignment: .leading) {
                            Text(post.title)
                                .font(.headline)
                            Text(post.body)
                                .font(.caption)
                                .foregroundColor(.secondary)
                        }
                    }
                }
            }
            .navigationTitle("Posts")
            .refreshable {
                await fetchPosts()
            }
            .task {
                await fetchPosts() // Runs when view appears
            }
        }
    }
    
    private func fetchPosts() async {
        isLoading = true
        errorMessage = nil
        
        do {
            let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
            let (data, _) = try await URLSession.shared.data(from: url)
            posts = try JSONDecoder().decode([Post].self, from: data)
        } catch {
            errorMessage = error.localizedDescription
        }
        
        isLoading = false
    }
}

// MARK: - @Observable Macro (iOS 17+, modern replacement for ObservableObject)
@Observable
class UserRepository {
    var users: [String] = []
    var isLoading = false
    
    @ObservationIgnored // Don't track this property
    private let apiService = APIService()
    
    func fetchUsers() async {
        isLoading = true
        defer { isLoading = false }
        
        // Simulate network call
        try? await Task.sleep(nanoseconds: 1_000_000_000)
        users = ["Alice", "Bob", "Charlie"]
    }
}

struct UserListView: View {
    @State private var repository = UserRepository() // No @StateObject needed
    
    var body: some View {
        List(repository.users, id: \.self) { user in
            Text(user)
        }
        .overlay {
            if repository.isLoading {
                ProgressView()
            }
        }
        .task {
            await repository.fetchUsers()
        }
    }
}

// MARK: - Pull to Refresh & Pagination
struct PaginatedListView: View {
    @State private var items: [Int] = []
    @State private var currentPage = 1
    @State private var isLoadingMore = false
    
    var body: some View {
        List(items, id: \.self) { item in
            Text("Item \(item)")
                .onAppear {
                    if item == items.last && !isLoadingMore {
                        loadMore()
                    }
                }
        }
        .refreshable {
            await refresh()
        }
        .task {
            await loadInitial()
        }
    }
    
    private func loadInitial() async {
        items = Array(1...20)
        currentPage = 2
    }
    
    private func loadMore() {
        isLoadingMore = true
        Task {
            try? await Task.sleep(nanoseconds: 1_000_000_000)
            let newItems = Array((currentPage*20)...((currentPage+1)*20 - 1))
            await MainActor.run {
                items.append(contentsOf: newItems)
                currentPage += 1
                isLoadingMore = false
            }
        }
    }
    
    private func refresh() async {
        await loadInitial()
    }
}

  1. UIKit Integration

Use UIViewRepresentable and UIViewControllerRepresentable to embed UIKit components in SwiftUI views.

SWIFTRead-only
1
import SwiftUI
import UIKit

// MARK: - Embed UIKit UIView
struct MapView: UIViewRepresentable {
    @Binding var centerCoordinate: CLLocationCoordinate2D
    
    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView()
        mapView.delegate = context.coordinator
        return mapView
    }
    
    func updateUIView(_ uiView: MKMapView, context: Context) {
        // Update when SwiftUI state changes
        let region = MKCoordinateRegion(center: centerCoordinate, span: MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02))
        uiView.setRegion(region, animated: true)
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject, MKMapViewDelegate {
        var parent: MapView
        
        init(_ parent: MapView) {
            self.parent = parent
        }
        
        func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
            parent.centerCoordinate = mapView.centerCoordinate
        }
    }
}

// Usage in SwiftUI
struct ContentView: View {
    @State private var location = CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194)
    
    var body: some View {
        MapView(centerCoordinate: $location)
            .frame(height: 300)
    }
}

// MARK: - Embed UIKit UIViewController
class ImagePickerController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    var onImagePicked: ((UIImage) -> Void)?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        let picker = UIImagePickerController()
        picker.delegate = self
        present(picker, animated: true)
    }
    
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        if let image = info[.originalImage] as? UIImage {
            onImagePicked?(image)
        }
        dismiss(animated: true)
    }
}

struct ImagePicker: UIViewControllerRepresentable {
    @Binding var image: UIImage?
    
    func makeUIViewController(context: Context) -> ImagePickerController {
        let picker = ImagePickerController()
        picker.onImagePicked = { image in
            self.image = image
        }
        return picker
    }
    
    func updateUIViewController(_ uiViewController: ImagePickerController, context: Context) {}
}

// Usage
struct ProfileView: View {
    @State private var profileImage: UIImage?
    @State private var showingPicker = false
    
    var body: some View {
        VStack {
            if let image = profileImage {
                Image(uiImage: image)
                    .resizable()
                    .frame(width: 100, height: 100)
                    .clipShape(Circle())
            }
            
            Button("Select Photo") {
                showingPicker = true
            }
        }
        .sheet(isPresented: $showingPicker) {
            ImagePicker(image: $profileImage)
        }
    }
}

  1. SwiftUI Best Practices

Keep Views Small – Break large views into reusable components. Each view should have a single responsibility.

Use @State for Local UI State – Not for persisted data or complex app state.

Prefer @StateObject Over @ObservedObject – When the view creates the object, use @StateObject. When receiving from parent, use @ObservedObject.

Avoid Side Effects in body – The body property should be pure. Use .task, .onAppear, or .onChange for side effects.

Use @Observable for iOS 17+ – Modern replacement for ObservableObject with cleaner syntax.

Lazy Stacks for Large Lists – Use LazyVStack/LazyHStack instead of VStack/HStack when scrolling large data sets.

Accessibility by Default – Add .accessibilityLabel, .accessibilityHint, and test with VoiceOver.

Preview-Driven Development – Use Xcode previews extensively with mocked data for faster iteration.

SWIFTRead-only
1
// MARK: - Preview with Mock Data
struct PostListView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            PostListView(posts: mockPosts)
                .previewDisplayName("Light Mode")
            
            PostListView(posts: mockPosts)
                .preferredColorScheme(.dark)
                .previewDisplayName("Dark Mode")
            
            PostListView(posts: [])
                .previewDisplayName("Empty State")
        }
    }
    
    static var mockPosts: [Post] {
        [
            Post(id: 1, title: "Mock Post 1", body: "This is a mock post"),
            Post(id: 2, title: "Mock Post 2", body: "Another mock post")
        ]
    }
}

// MARK: - View Composition Pattern
struct UserAvatar: View {
    let imageURL: URL?
    let size: CGFloat
    
    var body: some View {
        AsyncImage(url: imageURL) { phase in
            switch phase {
            case .empty:
                ProgressView()
            case .success(let image):
                image.resizable()
            case .failure:
                Image(systemName: "person.circle.fill")
            @unknown default:
                EmptyView()
            }
        }
        .frame(width: size, height: size)
        .clipShape(Circle())
    }
}

// MARK: - Custom View Modifier
struct CardStyle: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding()
            .background(Color(.systemBackground))
            .cornerRadius(12)
            .shadow(color: .black.opacity(0.1), radius: 5, x: 0, y: 2)
    }
}

extension View {
    func cardStyle() -> some View {
        modifier(CardStyle())
    }
}

// Usage: Text("Hello").cardStyle()

Test Your Knowledge

Q1
of 4

Which property wrapper should you use for a simple boolean state owned by a view?

A
@ObservedObject
B
@State
C
@EnvironmentObject
D
@Published
Q2
of 4

What is the modern navigation component for iOS 16+?

A
NavigationView
B
UINavigationController
C
NavigationStack
D
NavigationLink
Q3
of 4

Which protocol must a class conform to in order to use @ObservedObject (pre-iOS 17)?

A
Observable
B
ObservableObject
C
AnyObject
D
View
Q4
of 4

How do you perform a side effect when a SwiftUI view appears?

A
viewDidLoad()
B
onAppear
C
task (with no await)
D
init()

Frequently Asked Questions

Should I use SwiftUI or UIKit for my next app?

In 2026, SwiftUI is production-ready for most apps. Use SwiftUI for new projects unless you need: (1) advanced custom UICollectionView layouts, (2) extensive camera/ARKit integration, or (3) maintaining a large existing UIKit codebase. Many apps use a hybrid approach: SwiftUI for standard screens, UIKit for complex custom components.

What's the difference between @State, @ObservedObject, and @StateObject?

@State is for simple value types owned by the view (strings, bools, ints). @ObservedObject is for reference types (classes) passed from a parent view. @StateObject is for reference types created by the view itself – use this instead of @ObservedObject when the view owns the object to ensure proper lifecycle management.

How do I navigate programmatically in SwiftUI?

Use NavigationStack with a NavigationPath binding. Append values to the path to push screens, and modify the path to pop (e.g., path.removeLast()). Avoid the deprecated NavigationView and isActive bindings.

How do I optimize List performance with large data?

Use List (not ScrollView + LazyVStack) for dynamic data. Ensure your data models are Identifiable. Avoid expensive computations inside ForEach closures. Use .id() to force recreation when needed, but rarely. For very large datasets (10k+ items), consider pagination or UICollectionView via UIViewRepresentable.

Previous

ios uikit

Next

ios layouts

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.