iosswiftuimvvmobservable

SwiftUI MVVM: Single View, 2 different ViewModels that conform to ObservableObject


I'm providing an extremely simplified example to illustrate my point:

How to correctly initialize a View so that it would "own" the ViewModel? Normally I go about this as follows:


struct OverrideView: View {
    @Environment(\.dismiss) private var dismiss
    @StateObject private var viewModel: OverrideViewModelProtocol
    
    init(_ viewModel: @escaping @autoclosure () -> OverrideViewModelProtocol) {
        self._viewModel = StateObject(wrappedValue: viewModel())
    }
    
    var body: some View {
    }
}

However, obviously this doesn't work since I cannot initialize a non-concrete class, the init is expecting some sort of some OverrideViewModelProtocol:

protocol OverrideViewModelProtocol: ObservableObject {
    var mainTitle: String { get }
    var overrideSelectedSegmentIndex: Int { get set }

    var overrideCommentHeader: String { get }
    var overrideComment: String { get set }
    var overrideSubmitButtonEnabled: Bool { get }
    var overrideShouldDismiss: Bool { get }
    func submitButtonPressed()
}

Obviously, I cannot impose that the OverrideViewModelProtocol is also an ObservableObject, therefore I'm getting an issue:

Type 'any OverrideViewModelProtocol' cannot conform to 'ObservableObject'

One way to solve the problem is to create an abstract base class and use it instead of the protocol. But is there a way to use just the protocol and restrict only ObservableObject subclass to be able to conform to it, so that the View would know that it's a concrete ObservableObject subclass on initialization?

Use-case: 2 slightly different views, which differ only in text / button titles, so that I could use 2 different view models instead of if/else statements inside the views.


Solution

  • I've found a very simple and elegant solution:

    struct OverrideView<T: OverrideViewModelProtocol>: View {
        @Environment(\.dismiss) private var dismiss
        @StateObject private var viewModel: T
        
        init(_ viewModel: @escaping @autoclosure () -> T) {
            self._viewModel = StateObject(wrappedValue: viewModel())
        }
        
        var body: some View {
        }
    }
    

    The fact that the OverrideView was not specialized prevented the compiler from inferring the concrete type. Since the specific kind of ViewModel is known at a compile time, we just have to specialize that View over that type.

    Works like this:

    OverrideView(
        ViewModel1()
    )
    
    OverrideView(
        ViewModel2()
    )