SwiftUI Opinion: ViewModel doesn't belong in Previews

SwiftUI Opinion: ViewModel doesn't belong in Previews

SwiftUI Previews are key to making SwiftUI development fast and easy. However, if your views require a ViewModel, you are impeding your own development speed and re-usability of the view.

Let’s imagine that you create a view and it’s view model. Everything works, the project is released and everyone is happy. A couple months after, a new feature is added and you need another view that looks 100% like the one you implemented, except the logic in how the view is used is entirely different.

You find yourself having to either duplicate the SwiftUI view, or refactor the one you had created so it’s no longer tightly coupled, jeopardizing the stability of the already developed feature.

If your view requires interaction with a view model, you should extract all the visible parts of the view into its own SwiftUI view, keeping it agnostic of the view model. You end up with two SwiftUI views, one with knowledge of the view model that doesn’t have any design whatsoever in it, and another with all the design and no reference to the view model.


Example of what NOT to do:

import SwiftUI

struct BadDesignView: View {
    @StateObject var viewModel: LoginViewModel

    var body: some View {
        VStack {
            if viewModel.showLoginForm {
                TextField("Username", text: $viewModel.enteredUserName)
                Button("Login", action: viewModel.login)
            }
            if let username = viewModel.loggedInUser {
                Text("Greetings, \(username)")
            }
        }
    }
}

struct BadDesignView_Previews: PreviewProvider {

    static let loggedInVm: LoginViewModel = {
        let vm = LoginViewModel()
        vm.enteredUserName = "Mike Mousey"
        vm.login()
        return vm
    }()

    static let loggedOutVm: LoginViewModel = LoginViewModel()

    static var previews: some View {
        BadDesignView(viewModel: loggedOutVm)
            .previewDisplayName("Logged Out")
            .previewLayout(.sizeThatFits)

        BadDesignView(viewModel: loggedInVm)
            .previewDisplayName("Logged In")
            .previewLayout(.sizeThatFits)
    }
}

Example of reusable design:

import SwiftUI

struct ReusableDesignView: View {

    let showLoginForm: Bool
    @Binding var enteredUserName: String
    let loggedInUser: String?
    let loginAction: () -> Void

    var body: some View {
        VStack {
            if showLoginForm {
                TextField("Username", text: $enteredUserName)
                Button("Login", action: loginAction)
            }
            if let username = loggedInUser {
                Text("Greetings, \(username)")
            }
        }
    }
}

struct ReusableDesignView_Previews: PreviewProvider {
    static var previews: some View {
        ReusableDesignView(
            showLoginForm: true,
            enteredUserName: .constant(""),
            loggedInUser: nil,
            loginAction: { /* do nothing */ })

        ReusableDesignView(
            showLoginForm: false,
            enteredUserName: .constant(""),
            loggedInUser: "Mike Mousey",
            loginAction: { /* do nothing */ })
    }
}
import SwiftUI

struct LoginView: View {
    @StateObject var viewModel = LoginViewModel()

    var body: some View {
        ReusableDesignView(
            showLoginForm: viewModel.showLoginForm,
            enteredUserName: $viewModel.enteredUserName,
            loggedInUser: viewModel.loggedInUser,
            loginAction: viewModel.login)
    }
}

In a MVVM architecture, the logic is moved into a view model to allow testing the logic separately from the user interface.

Isolating a designed view from the view model improves re-usability of the view.

As a side effect, creating your Previews will be easier. You no longer have to find a way to bring the view model into the expected state, or have to create mock view models.