I have a swiftui view that depends on a view model, the view model has some published properties. I want define a protocol and default implementation for the view model hierarchy, and make the view dependent on the protocol not the concrete class?
I want to be able to write the following:
protocol ItemViewModel: ObservableObject { @Published var title: String func save() func delete() } extension ItemViewModel { @Published var title = "Some default Title" func save() { // some default behaviour } func delete() { // some default behaviour } } struct ItemView: View { @ObservedObject var viewModel: ItemViewModel var body: some View { TextField($viewModel.title, text: "Item Title") Button("Save") { self.viewModel.save() } } } // What I have now is this: class AbstractItemViewModel: ObservableObject { @Published var title = "Some default Title" func save() { // some default behaviour } func delete() { // some default behaviour } } class TestItemViewModel: AbstractItemViewModel { func delete() { // some custom behaviour } } struct ItemView: View { @ObservedObject var viewModel: AbstractItemViewModel var body: some View { TextField($viewModel.title, text: "Item Title") Button("Save") { self.viewModel.save() } } }
To conform to ObservableObject, simply add it to the class definition. ObservableObject provides a default implementation for objectWillChange using Combine/ObservableObjectPublisher. To trigger objectWillChange events when your data changes, annotate your properties with the @Published property wrapper.
A property wrapper type that subscribes to an observable object and invalidates a view whenever the observable object changes.
Wrappers and stored properties are not allowed in swift protocols and extensions, at least for now. So I would go with the following approach mixing protocols, generics and classes... (all compilable and tested with Xcode 11.2 / iOS 13.2)
// base model protocol protocol ItemViewModel: ObservableObject { var title: String { get set } func save() func delete() } // generic view based on protocol struct ItemView<Model>: View where Model: ItemViewModel { @ObservedObject var viewModel: Model var body: some View { VStack { TextField("Item Title", text: $viewModel.title) Button("Save") { self.viewModel.save() } } } } // extension with default implementations extension ItemViewModel { var title: String { get { "Some default Title" } set { } } func save() { // some default behaviour } func delete() { // some default behaviour } } // concrete implementor class SomeItemModel: ItemViewModel { @Published var title: String init(_ title: String) { self.title = title } } // testing view struct TestItemView: View { var body: some View { ItemView(viewModel: SomeItemModel("test")) } }
We have found a solution in our small library by writing a custom property wrapper. You can have a look at XUI.
There are essentially two issues at hand:
ObservableObject
ObservedObject
By creating a similar protocol to ObservableObject
(without associated type) and a protocol wrapper similar to ObservedObject
(without the generic constraint), we can make this work!
Let me show you the protocol first:
protocol AnyObservableObject: AnyObject { var objectWillChange: ObservableObjectPublisher { get } }
That is essentially the default form of ObservableObject
, which makes it quite easy for new and existing components to conform to that protocol.
Secondly, the property wrapper - it is a bit more complex, which is why I will simply add a link. It has a generic attribute without a constraint, which means that we can use it with protocols as well (simply a language restriction as of now). However, you will need to make sure to only use this type with objects conforming to AnyObservableObject
. We call that property wrapper @Store
.
Okay, now let's go through the process of creating and using a view model protocol:
protocol ItemViewModel: AnyObservableObject { var title: String { get set } func save() func delete() }
class MyItemViewModel: ItemViewModel, ObservableObject { @Published var title = "" func save() {} func delete() {} }
@Store
property wrapper in your view:struct ListItemView: View { @Store var viewModel: ListItemViewModel var body: some View { // ... } }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With