```html SwiftUI: How to Implement Preview Mocking (iOS 17+, 2026)

How to implement preview mocking in SwiftUI

iOS 17+ Xcode 16+ Beginner APIs: #Preview, PreviewProvider Updated: May 11, 2026
TL;DR

Pass a lightweight mock struct—conforming to a protocol your view depends on—into your view's initializer, then call it from a named #Preview block. This lets you preview any UI state (loaded, empty, error) without a network or database.

protocol ProfileServiceProtocol {
    func loadProfile() -> UserProfile
}

struct MockProfileService: ProfileServiceProtocol {
    func loadProfile() -> UserProfile {
        UserProfile(name: "Alice", bio: "iOS developer")
    }
}

#Preview("Loaded State") {
    ProfileView(service: MockProfileService())
}

Full implementation

The pattern centers on three pieces: a protocol that abstracts the real dependency, a mock struct that returns deterministic sample data, and an @Observable ViewModel that accepts either implementation through its initializer. Because the mock is injected at the call site rather than hardcoded inside the view, you can spin up multiple #Preview blocks—each representing a distinct UI state—without changing any production code.

import SwiftUI

// MARK: - Model

struct UserProfile: Equatable {
    let name: String
    let bio: String
    let followerCount: Int
    let isVerified: Bool
}

// MARK: - Protocol

protocol ProfileServiceProtocol {
    func loadProfile() -> UserProfile
}

// MARK: - Mock Implementations

struct MockProfileService: ProfileServiceProtocol {
    func loadProfile() -> UserProfile {
        UserProfile(
            name: "Alice Wonderland",
            bio: "SwiftUI enthusiast. Building apps with Soarias.",
            followerCount: 3_400,
            isVerified: true
        )
    }
}

struct EmptyProfileService: ProfileServiceProtocol {
    func loadProfile() -> UserProfile {
        UserProfile(name: "New User", bio: "", followerCount: 0, isVerified: false)
    }
}

// MARK: - ViewModel

@Observable
final class ProfileViewModel {
    var profile: UserProfile?
    var isLoading = false

    private let service: ProfileServiceProtocol

    init(service: ProfileServiceProtocol) {
        self.service = service
    }

    func load() {
        isLoading = true
        // Simulate async work; replace with real async/await call in production.
        profile = service.loadProfile()
        isLoading = false
    }
}

// MARK: - View

struct ProfileView: View {
    @State private var viewModel: ProfileViewModel

    init(service: ProfileServiceProtocol) {
        _viewModel = State(initialValue: ProfileViewModel(service: service))
    }

    var body: some View {
        Group {
            if viewModel.isLoading {
                ProgressView("Loading…")
                    .accessibilityLabel("Loading profile")
            } else if let profile = viewModel.profile {
                VStack(alignment: .leading, spacing: 12) {
                    HStack {
                        Image(systemName: "person.circle.fill")
                            .font(.system(size: 56))
                            .foregroundStyle(.blue)
                            .accessibilityHidden(true)
                        VStack(alignment: .leading, spacing: 4) {
                            HStack(spacing: 4) {
                                Text(profile.name)
                                    .font(.title3.bold())
                                if profile.isVerified {
                                    Image(systemName: "checkmark.seal.fill")
                                        .foregroundStyle(.blue)
                                        .accessibilityLabel("Verified")
                                }
                            }
                            Text("\(profile.followerCount.formatted()) followers")
                                .font(.caption)
                                .foregroundStyle(.secondary)
                        }
                    }
                    if !profile.bio.isEmpty {
                        Text(profile.bio)
                            .font(.body)
                            .foregroundStyle(.primary)
                    } else {
                        Text("No bio yet.")
                            .font(.body)
                            .foregroundStyle(.tertiary)
                    }
                }
                .padding()
            }
        }
        .onAppear { viewModel.load() }
    }
}

// MARK: - Previews

#Preview("Loaded – Verified User") {
    ProfileView(service: MockProfileService())
}

#Preview("Empty – New User") {
    ProfileView(service: EmptyProfileService())
}

#Preview("Dark Mode") {
    ProfileView(service: MockProfileService())
        .preferredColorScheme(.dark)
}

How it works

  1. Protocol abstraction. ProfileServiceProtocol defines the contract (loadProfile() -> UserProfile). Neither the view nor the ViewModel knows—or cares—whether the conforming type hits a real API or returns hardcoded data. This single step unlocks every preview and test benefit below.
  2. Deterministic mock structs. MockProfileService and EmptyProfileService are value types with no state, so they're instantaneous and never flaky. They model distinct real-world situations: a fully populated account versus a brand-new one with no bio.
  3. Initializer injection on the ViewModel. ProfileViewModel.init(service:) accepts any ProfileServiceProtocol. The @Observable macro (iOS 17+) propagates changes automatically—no ObservableObject or @Published boilerplate required.
  4. State wrapper in the view init. Because ProfileViewModel is marked @Observable and owned locally, the view uses @State private var viewModel. The custom init(service:) wraps it with the _viewModel = State(initialValue:) pattern, giving Xcode's canvas a fresh instance per preview.
  5. Named #Preview blocks. Each block gives Xcode's canvas a distinct label ("Loaded – Verified User", "Empty – New User", "Dark Mode"). You can jump between them in the preview pane without changing any source—exactly the rapid iteration loop that makes SwiftUI previews valuable.

Variants

Simulating an error state

Add an error-throwing variant to your protocol to preview failure UI without ever touching the network.

protocol ProfileServiceProtocol {
    func loadProfile() throws -> UserProfile
}

struct FailingProfileService: ProfileServiceProtocol {
    func loadProfile() throws -> UserProfile {
        throw URLError(.notConnectedToInternet)
    }
}

// In ViewModel:
func load() {
    do {
        profile = try service.loadProfile()
    } catch {
        errorMessage = error.localizedDescription
    }
}

#Preview("Error State") {
    ProfileView(service: FailingProfileService())
}

Previewing async services with withCheckedContinuation

When your real service is async, keep the mock synchronous by returning data immediately from func loadProfile() async -> UserProfile—Swift allows a non-throwing, non-awaiting body inside an async function. The preview canvas runs the mock without needing any task delay, keeping iteration instant. In production, the real implementation performs the actual network call.

Common pitfalls

Prompt this with Claude Code

When using Soarias or Claude Code directly to implement this:

Implement preview mocking in SwiftUI for iOS 17+.
Use the #Preview macro and PreviewProvider pattern.
Create protocol-based mock services with at least three
preview states: loaded, empty, and error.
Make it accessible (VoiceOver labels on all interactive elements).
Add a #Preview with realistic sample data for each state.

In Soarias's Build phase, drop this prompt into the implementation canvas after your screen mockups are approved—Claude Code will scaffold the protocol, mock structs, and all three preview states in one pass, leaving you to wire up the real service.

Related

FAQ

Does this work on iOS 16?

The protocol-injection pattern and mock structs work on iOS 16, but the #Preview macro requires Xcode 15 and a minimum deployment target of iOS 17. On iOS 16 targets you must use the older struct Foo_Previews: PreviewProvider syntax. If you need to support both, write your mocks independently of the preview registration syntax so the same structs work in both old and new preview styles.

Can I reuse preview mocks in XCTest unit tests?

Yes—and you should. Place your mock structs in a dedicated Swift package target (e.g. AppMocks) and list it as a dependency for both your main app target and your test target. That way the same MockProfileService that powers your preview also runs in your XCTestCase, keeping your test suite honest without duplicating fixture data.

What's the UIKit equivalent?

UIKit has no first-party canvas preview system. The closest analog is instantiating your UIViewController with a mock service in a separate Storyboard or via @IBDesignable, but neither matches SwiftUI's live, multi-state preview workflow. Teams using UIKit typically rely on snapshot testing libraries (e.g. swift-snapshot-testing) plus unit tests against the ViewModel to achieve comparable coverage.

Last reviewed: 2026-05-11 by the Soarias team.

```