Protocols and @ObservedObject properties

The @ObservedObject property wrapper can only be applied to properties that are classes. If you attempt to create a property that has a protocol as a type and you want this property to be wrapped in a @ObservedObject property wrapper you’ll get a compilation error message when you try to compile.

The protocol & view:

protocol Searchable {
    var searchResults: [String] { get }
    func search()
}

class LocalSearch : Searchable, ObservableObject {
    @Published private(set) var searchResults: [String] = []
    
    func search() {
        searchResults = ["Hello", "World"]
    }
}

The view referencing a class as an @ObservedObject property

struct ProtocolObservableObjectView: View {
        @ObservedObject var service: LocalSearch
        
        var body: some View {
            VStack {
                Text("ProtocolObservableObjectView").font(.largeTitle)
                
                Button(action: { self.service.search() }) {
                    Text("Search")
                }
                
                if (self.service.searchResults.count > 0) {
                    Text("Search Results")
                    
                    // https://www.hackingwithswift.com/articles/210/how-to-fix-slow-list-updates-in-swiftui
                    List(service.searchResults, id: \.self) {
                        Text("\($0)")
                    }
                    .id(UUID()) // crashes if this is missing
                }
            }
        }
    }

The view uses the property wrapper to define the service as a class. If we wanted to define the service as a protocol instead, by changing

@ObservedObject var service: LocalSearch

to

@ObservedObject var service: Searchable

you would get the following error:

Property type ‘Searchable’ does not match that of the ‘wrappedValue’ property of its wrapper type ‘ObservedObject’

The reason for this is that the ObservableObject protocol can only be annotated to class properties, not struct or protocol types. If you made LocalSearch a struct instead, you’d get the following error during compilation

Non-class type ‘SwiftSection.LocalSearch’ cannot conform to class protocol ‘ObservableObject’

Solution

To specify a protocol as a type that is to be wrapped in an @ObservedObject property wrapper, you can modify the view to use a generic type that is constrained to the protocol.

  1. The protocol should adopt ObservableObject
  2. Make the view specialized by a generic type that adopts your protocol.
  3. The type of the object that the property wrapper wraps should be the generic type
  4. Add an init method that sets the wrapped property to the input parameter that is typed as the generic type
protocol Searchable: ObservableObject {
    var searchResults: [String] { get }
    func search()
}

class LocalSearch : Searchable {
    @Published private(set) var searchResults: [String] = []
    
    func search() {
        searchResults = ["LocalSearch"]
    }
}
    
class RemoteSearch : Searchable {
    @Published private(set) var searchResults: [String] = []
    
    func search() {
        searchResults = ["RemoteSearch"]
    }
}

struct ProtocolObservableObjectView<Service: Searchable>: View {
    @ObservedObject var service: Service
    
    init(service: Service) {
        self.service = service
    }
    
    var body: some View {
        VStack {
            Text("ProtocolObservableObjectView").font(.largeTitle)
            
            Button(action: { self.service.search() }) {
                Text("Search")
            }
            
            if (self.service.searchResults.count > 0) {
                Text("Search Results")
                
                // https://www.hackingwithswift.com/articles/210/how-to-fix-slow-list-updates-in-swiftui
                List(service.searchResults, id: \.self) {
                    Text("\($0)")
                }
                .id(UUID()) // crashes if this is missing
            }
        }
    }
}

The caller can now pass to the view adopters of Searchable, either LocalSearch or RemoteSearch.