How to be notified of change to ObservedObject which conforms to a protocol

Viewed 171

I'm building an AutoCompletion view and would like to pass an object which contains the field to be autocompleted. Currently I have two different types which I need to autocomplete on an attribute of, both attributes are named the same. I've created a Protocol and am using that to build a generic view to accept it.

The problem I'm encountering is that the onReceive breaks the compile. Unfortunately I can't get an error message apart from 'Unable to infer complex closure return type...' but if I comment out the onReceive the error clears.

If I replace LocationNameAutoComplete in the struct with Address then it compiles and runs fine - but that means I can't use it with the other type FactorySite.

If I could see the actual error message regarding the onReceive it would be a start...

Is there a better approach to doing this?

Thanks

struct LocationNameTextField<T>: View where T: LocationNameAutoComplete {

    @ObservedObject var address: T

    var body: some View {
        VStack {
            TextField("", text: $address.location_name)
                .onReceive(self.address.$location_name) { attr in
                    print("OK")
                }
        }
    }
}



protocol LocationNameAutoComplete: ObservableObject {
    var location_name: String {get set}
}

struct Address: LocationNameAutoComplete {
    @Published var location_name: String
}

struct FactorySite: LocationNameAutoComplete {
    @Published var location_name: String
}


2 Answers

Your location_name property in protocol is not a publisher, so you cannot refer to it in generic view, the only publisher you have is objectWillChange.

Here is compilable code, including some other fixes, (Xcode 11.7)

struct LocationNameTextField<T>: View where T: LocationNameAutoComplete {

    @ObservedObject var address: T

    var body: some View {
        VStack {
            TextField("", text: $address.location_name)
                .onReceive(self.address.objectWillChange) { _ in
                    print("OK")
                }
        }
    }
}

protocol LocationNameAutoComplete: ObservableObject {
    var location_name: String {get set}
}

class Address: LocationNameAutoComplete {
    @Published var location_name: String = ""
}

class FactorySite: LocationNameAutoComplete {
    @Published var location_name: String = ""
}

Update: here is possible approach if you need explicit generic publisher for some property (in this case location_name)

struct LocationNameTextField<T>: View where T: LocationNameAutoComplete {

    @ObservedObject var address: T

    var body: some View {
        VStack {
            TextField("", text: $address.location_name)
                .onReceive(self.address.location_name_publisher) { attr in
                    print("OK")
                }
        }
    }
}

protocol LocationNameAutoComplete: ObservableObject {
    var location_name: String {get set}
    var location_name_publisher: AnyPublisher<String, Never> { get }
}

class Address: LocationNameAutoComplete {
    @Published var location_name: String = ""

    var location_name_publisher: AnyPublisher<String, Never> {
        $location_name.eraseToAnyPublisher()
    }
}

class FactorySite: LocationNameAutoComplete {
    @Published var location_name: String = ""

    var location_name_publisher: AnyPublisher<String, Never> {
        $location_name.eraseToAnyPublisher()
    }
}

The error is because address.$location_name - which you expect to be a Published<String> publisher - doesn't exist for a generic T, because the protocol LocationNameAutoComplete doesn't require it, and you can't use a property wrapper @Published to auto-synthesize this requirement.

One approach is to manually define a publisher property and implement it in each conforming type (as shown by Asperi).

Another approach is to create a base class instead of a protocol that implements it:

class LocationNameAutoComplete: ObservableObject {
    @Published var location_name: String

    init(location: String) { self.location = location }
}

Then, pretty much everything else remains the same:

struct LocationNameTextField<T>: View where T: LocationNameAutoComplete {
    @ObservedObject var address: T

    var body: some View {
        VStack {
            TextField("", text: $address.location_name)
                .onReceive(self.address.$location_name) { attr in
                    print("OK")
                }
        }
    }
} 
class Address: LocationNameAutoComplete {}

class FactorySite: LocationNameAutoComplete {}
Related