2

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
}


Houdi
  • 136
  • 10

2 Answers2

2

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()
    }
}
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • Interesting. The first solution gives me the word the user typed but with the last letter missing in address.location_name. The second solution outputs OK twice each time a key is pressed. Its given me another approach to ponder though. Thanks. – Houdi Sep 13 '20 at 10:55
2

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 {}
New Dev
  • 48,427
  • 12
  • 87
  • 129
  • I like the separation of duties with using a subclass. Currently there is no requirement to have more than one autocomplete field in a form but it could get messy if there were more than one in time I guess. – Houdi Sep 14 '20 at 07:46