Search code examples
swiftswiftuiprivateswiftui-previews

Make SwiftUI previews working with view models


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


Solution

  • 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)))
        }
    }