I'm providing an extremely simplified example to illustrate my point:
View displaying the same properties of a ViewModelViewModels which are ObservableObject subclassesHow 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.
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()
)