Reputation: 5644
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
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
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