Make SwiftUI previews working with view models

795 Views Asked by At

I have a SwiftUI view with a view model associated to it.

struct BookmarksView: View {
    @StateObject private var viewModel = BookmarksViewModel()
    
    var body: some View {
        switch viewModel.viewState {
        case .empty:
            BookmarksEmptyView()
        case .content(let newsLetters):
            ListView(newsLetters: newsLetters)
        }
    }
}

With the PreviewProvider represented below I'm able to test just the empty case. Not the content one.

struct BookmarksView_Previews: PreviewProvider {
    static var previews: some View {
        BookmarksView()
    }
}

Are you able to suggest a way to test BookmarksView for both cases (i.e. empty and content)?

Thanks, Lorenzo

2

There are 2 best solutions below

5
Joakim Danielson On BEST ANSWER

You could create an init so you can inject the view model but use a default value so you don't need to use it normally.

init(viewModel: BookmarksViewModel = BookmarksViewModel() {
   _viewModel = StateObject(wrappedValue: viewModel)
}

Now you can create and inject an instance in your preview code

For previews I would add two static properties for creating different versions with different configurations of the view model to be used in the previews. I did this inside a #if DEBUG/#endif so they can't be used by mistake in a release build.

#if DEBUG
extension BookmarksViewModel {
    static let emptyState: BookmarksViewModel = {
        BookmarksViewModel(viewState: .empty)
    }()

    static let contentState: BookmarksViewModel = {
        let newsLetters = Newsletter.previews
        return BookmarksViewModel(viewState: .content(newsLetters))
    }()
}
#endif

Note that since I don't know how the view model is declared I made my own version

This can then be used directly in the previews

struct BookmarksViewEmpty_Previews: PreviewProvider {
    static var previews: some View {
        BookmarksView(viewModel: .emptyState)
    }
}

struct BookmarksViewContent_Previews: PreviewProvider {
    static var previews: some View {
        BookmarksView(viewModel: .contentState)
    }
}

The solution posted by OP is another good way to solve this since the sub-view is now decoupled from the view model which makes it much easier to create previews but there is no need to use @State properties in a preview since the properties will not change, instead we can create a binding using constant()

struct BookmarksViewEmpty_Previews: PreviewProvider {
    static var previews: some View {
        BookmarksView(viewState: .constant(.empty))
    }
}

struct BookmarksViewContent_Previews: PreviewProvider {
    static var previews: some View {
        BookmarksView(viewState: .constant(.content(Newsletter.previews)))
    }
}
0
Andrew Bogaevskyi On

The best way is to pass an instance of the view model into the view. You will be able to configure the view model as you want. To do that you need to add a custom init. But be careful with @StateObject (check my answer about @StateObject here for more details).

struct BookmarksView: View {
    @StateObject private var viewModel: BookmarksViewModel
    
    init(viewModel: @autoclosure @escaping () -> BookmarksViewModel) {
        _viewModel = StateObject(wrappedValue: viewModel())
    }

    var body: some View {
        switch viewModel.viewState {
        case .empty:
            BookmarksEmptyView()
        case .content(let newsLetters):
            ListView(newsLetters: newsLetters)
        }
    }
}

And then:

struct BookmarksView_Previews: PreviewProvider {
    static var previews: some View {
        BookmarksView(
            viewModel: BookmarksViewModel(initialState: . content([...]))
        )
    }
}