/ PROGRAMMING, IOS, SWIFT, SWIFTUI

MVVM Architecture in SwiftUI Using Generics

Have you ever been frustrated with Xcode’s live previewing of SwiftUI views when working with a MVVM architecture?

As your project grows, and your views and view models become more complex, you might find yourself running into some challenges with being able to use Xcode’s live previewing. As your view model gains more responsibilities it becomes more difficult to preview different view states. Here’s an example.

class SomeViewModel: ObservableObject {
    @Published var data: String = "hello world"

    @Injected(\.service) var service: Service

    private var cancelBag = Set<AnyCancellable>()

    public func upload() {
        service.uploadData(data)
            .recieve(on: RunLoop.main)
            .sink { result
                // Do something with result
            } receiveValue: { success in
                // Do something with value
            }
            .store(in: &cancelBag)
    }
}

There’s obviously a few ways to be able to approach this problem, maybe you mock some service layer dependencies that are injected into your view model when initializing from a preview, or some other creative solution. Ideally, you want a solution that is as flexible as possible when working with previews, because you might have some complex state transitions, or just want to test how a view’s animations react to changes in the view model. In this post, we’ll go over how to use Swift generics to create a protocol for your view model, that can be implemented as a mock for your previews for easier state change testing.

Using Generics

I feel as though generics in Swift aren’t seen as commonly as they are in other languages, however, generics are extremely powerful when writing flexible, reusable functions across your codebase. If you haven’t worked with generics before, I recommend checking out the Swift language documentation before continuing.

The General Idea

When working with generics, you’ll want to define a protocol that your view models will conform to, this can be named whatever is relevant for your use-case, but for this example we’ll just name our protocol with the naming convention of our view model + “protocol”.

The View Model Protocol
import Combine

protocol UserInfoViewModelProtocol: ObservableObject {
    var userName: String { get set }
    var userDetailsAreUpdating: Bool { get set }

    func updateUserDetails()
}

Since we’re going to be using our protocol as a state object, the protocol itself will need to conform to ObservableObject. Now, all of our view model implementations can conform to this protocol and allow us to use the protocol as a generic in our view. I’ve also added a sample variable userName that stores a user’s name, and a sample function updateUserDetails to send the user’s updated name to the application’s service layer that for example, might update a user’s name on the product’s back-end.

The View

To create our view, we’ll use a generic type parameter ViewModel on our view example UserInfoView. We can also define what type our generic type parameter conforms to, in our example it will be UserInfoViewModelProtocol, the protocol we just created.

import SwiftUI

struct UserInfoView<ViewModel>: View where ViewModel: UserInfoViewModelProtocol {
    
    @StateObject var viewModel: ViewModel
    
    var body: some View {
        Form {
            if viewModel.userDetailsAreUpdating {
                VStack {
                    Text("Updating User Details")
                    ProgressView()
                }
            } else {
                Section {
                    TextField("Name", $viewModel.userName)
                }
            
                Section {
                    Button {
                        viewModel.updateUserDetails()
                    } label: {
                        Text("Save Changes")
                    }
                }
            }
        }
    }
    
    public init(viewModel: ViewModel) {
        self._viewModel = StateObject(wrappedValue: viewModel)
    }
}

#if DEBUG
struct UserInfoView_Previews: PreviewProvider {
    static var previews: some View {
        UserInfoView(viewModel: MockUserInfoViewModel())
    }
}
#endif

Our view has three UI elements, a text field for a user to enter their name, a button so that the user can save the changes to their name, and a text element that displays the status that a user has performed an action, and there are asynchronous tasks happening in the background.

The View Model Mock Implementation

As you’ve seen in the view example, I went ahead and stubbed out in the preview where the mock view model would be implemented. All that’s left to do is create the mock view model.

import Combine

class MockUserInfoViewModel: UserInfoViewModelProtocol {
    @Published var userName: String = ""
    @Published var userDetailsAreUpdating: Bool = false

    func updateUserDetails() {
        userDetailsAreUpdating = true
        print("noop")
    }
}

Our mock view model implements the protocol by updating our variable userDetailsAreUpdating whenever updateUserDetails() is called, the view should then react to this state change and show a status message that user details are updating.

The REAL View Model Implementation

Now that our mock is setup, we’ll want to create our “real” implementation of our protocol, so that we can use it in the live version of our app.

import Combine

class UserInfoViewModel: UserInfoViewModelProtocol {
    @Published var userName: String = ""
    @Published var userDetailsAreUpdating: Bool = false

    func updateUserDetails() {
        userDetailsAreUpdating = true
        // Do something to save user details.
    }
}

Now, whenever we instantiate our view from somewhere else in our app we can use UserInfoView(viewModel: UserInfoViewModel()) to get the “real” view model setup to use. This also gives us some serious flexibility as we can now use other implementations of UserInfoViewModelProtocol to build more robust features in the future.

Reusability

When using this pattern in some of my indie dev projects, I’ve created code snippets to easily boilerplate this code that you can import yourself, which will are available on Github.

charlemagne

Charles Fager

Charles is the founder and lead developer at Norfare. He spends his days working as a fulltime developer, and enjoys working on new app concepts with Swift. He is also an avid gamer.

Read More