Search code examples
swiftuiswiftui-previews

How to mock view model for SwiftUI previews?


My SwiftUI app consists of different views with their own view models which handle data fetching etc. This works fine in the simulator, but I'd really like to be able to use previews and have each view appear in it's different state (loading, success, error etc.)

Here's a simple example of how a page might be built:

import SwiftUI
import Observation

@Observable
class TestViewModel {
    enum State {
        case loading
        case success
    }
    
    private(set) var state = State.loading
    
    func loadData() async {
        // Make async network request here and update state
    }
}

struct TestView: View {
    
    @State private var viewModel = TestViewModel()
    
    var body: some View {
        Group {
            switch viewModel.state {
            case .loading: Text("Loading...")
            case .success: Text("Success!")
            }
        }.task {
            await viewModel.loadData()
        }
    }
}

#Preview {
    TestView()
}

How would I go about mocking the view model/network response for previews?


Solution

  • You can add an init and then inject another view model for the preview that is either a sub class of your current view model or you can create a protocol that your view model and any mock object conforms to.

    Below is an example using the protocol approach

    protocol DataLoading: Observable {
        var state: TestViewModel.State { get }
        func loadData() async -> Void
    }
    
    @Observable
    class TestViewModel: DataLoading {
        //...
    }
    
    struct TestView: View {
        @State private var viewModel: DataLoading
        
        init(viewModel: DataLoading = TestViewModel()) {
            self.viewModel = viewModel
        }
    
        //...
    }
    

    And then you can create a mock like this

    @Observable
    class MockViewModel: DataLoading {
        private(set) var state = TestViewModel.State.loading
        func loadData() async {
            try? await Task.sleep(for: .milliseconds(300))
            state = .success
        }
    }
    

    If you do this then I would strongly suggest you move the enum declaration outside of the view model declaration