Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to define a protocol as a type for a @ObservedObject property?

Tags:

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() }      } } 
like image 389
M.Serag Avatar asked Dec 27 '19 16:12

M.Serag


People also ask

How do you conform to ObservableObject?

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.

What is Observedobject?

A property wrapper type that subscribes to an observable object and invalidates a view whenever the observable object changes.


2 Answers

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"))     } } 
like image 84
Asperi Avatar answered Sep 27 '22 21:09

Asperi


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:

  1. the associated type requirement in ObservableObject
  2. the generic constraint on 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:

  1. Create view model protocol
protocol ItemViewModel: AnyObservableObject {     var title: String { get set }      func save()     func delete() } 
  1. Create view model implementation
class MyItemViewModel: ItemViewModel, ObservableObject {      @Published var title = ""      func save() {}     func delete() {}  } 
  1. Use the @Store property wrapper in your view:
struct ListItemView: View {     @Store var viewModel: ListItemViewModel      var body: some View {         // ...     }  } 
like image 43
Paul Avatar answered Sep 27 '22 20:09

Paul