I'm providing an extremely simplified example to illustrate my point:
View
displaying the same properties of a ViewModel
ViewModel
s 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()
)