Jon Vogel
Jon Vogel

Reputation: 5644

Change model view state in SwiftUI PreviewProvider

I'm struggling to find a clean way to preview SwiftUI views that have a view model with some state that can change through out the life of the view. Here is some slightly contrived code to illustrate the pattern I use.

import SwiftUI

enum NetworkState {
    case idle
    case loading
    case hasData
    case error
}

public class MyViewModel: ObservableObject {
    
    @Published var results: [String] = []
    
    @Published var state: NetworkState = NetworkState.idle
    
    
    func load() {
        self.results = ["One", "Two", "Three"]
        self.state = .hasData
    }
}

public struct MyView: View {
    
    //MARK: View Model
    @ObservedObject var viewModel: MyViewModel
    
    
    //MARK: Body
    public var body: some View {
        switch self.viewModel.state {
        case NetworkState.idle:
            Text("Tap to load").onTapGesture {
                self.viewModel.load()
            }
        case NetworkState.loading:
            Text("Loading ...")
        case NetworkState.hasData:
            ScrollView {
                ForEach(self.viewModel.results, id: \.self) { string in
                    Text(string)
                }
            }
        case NetworkState.error:
            Text("There was an error")
        }
    }
    
    //MARK: Init
    public init(viewModel model: MyViewModel) {
        self.viewModel = model
    }
    
}

Now, I wan't to be able to preview and change the .state variable to see different permutations of my view through out its life. Here are my two unsuccessful attempts.

//Does not compile
struct MyView_Previews: PreviewProvider {
    static var previews: some View {
        let viewModel = MyViewModel()
        viewModel.state = .error
        MyView(viewModel: viewModel)
    }
}

//Can not manipulate state after initalization. Also does not compile.
struct MyViewTwo_Previews: PreviewProvider {
    static let viewModel = MyViewModel()
    static var previews: some View {
        viewModel.state = .error
        MyView(viewModel: viewModel)
    }
}

They both fail with this error

Type '()' cannot conform to 'View'; only struct/enum/class types can conform to protocols

I'm sure there is a way and I would like it to be as clean as possible.

Upvotes: 3

Views: 2098

Answers (2)

jnpdx
jnpdx

Reputation: 52575

You're missing return statements in your Previews. If you don't have a return, the compiler assumes it's a ViewBuilder and will treat the top level as an implicit return, but you're doing work assigning variables, etc.

struct MyView_Previews: PreviewProvider {
    static var previews: some View {
        let viewModel = MyViewModel()
        viewModel.state = .idle
        return MyView(viewModel: viewModel)
    }
}

struct MyViewTwo_Previews: PreviewProvider {
    static let viewModel = MyViewModel()
    static var previews: some View {
        viewModel.state = .idle
        return MyView(viewModel: viewModel)
    }
}

Update, showing multiple views in one preview:

struct MyView_Previews: PreviewProvider {
    static var previews: some View {
        let viewModel = MyViewModel()
        viewModel.state = .idle
        let viewModel2 = MyViewModel()
        viewModel2.state = .hasData
        let viewModel3 = MyViewModel()
        viewModel3.state = .error
        
        return Group {
            MyView(viewModel: viewModel)
            MyView(viewModel: viewModel2)
            MyView(viewModel: viewModel3)
        }
    }

Upvotes: 5

Asperi
Asperi

Reputation: 258413

Use dependency injection via constructor with default arguments, like

public class MyViewModel: ObservableObject {
    
    @Published var results: [String]
    @Published var state: NetworkState
    
    init(results: [String] = [], state: NetworkState = .idle) {
        self.results = results
        self.state = state
    }
    func load() {
        self.results = ["One", "Two", "Three"]
        self.state = .hasData
    }
}

then you can use it as

struct MyView_Previews: PreviewProvider {
    static var previews: some View {
        MyView(viewModel: MyViewModel(state: .error))
    }
}

Upvotes: 1

Related Questions