Search code examples
protocolsswiftui

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


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() } 
    }
}

Solution

  • 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"))
        }
    }
    

    backup