Binding accepting enums that conform to one protocol in Swift

Viewed 225

I have two enums that conform to one protocol, an observable object that holds arrays of those enums and a view that I want to accept both enums.

The observable object:

class OptionsStore: ObservableObject {
    @Published var topicOptions: [TopicOption]
    @Published var levelOptions: [LevelOption]
}

One of the enums and the protocol:

enum TopicOption: String, CaseIterable, OptionCompatible {
    case music, food, traffic

    ...
}

protocol OptionCompatible {
    ...
}

The views:

struct MainView: View {
    @EnvironmentObject var optionsStore: OptionsStore

    var body: some View {
        OptionsView(
            options: TopicOption.allCases,                   // no error
            activeOptions: self.$optionsStore.topicOptions   // error
        )
    }
}

struct OptionsView: View {
    let options: [OptionCompatible]
    @Binding var activeOptions: [OptionCompatible]
}

The error is

Cannot convert value of type 'Binding<[TopicOption]>' to expected argument type 'Binding<[OptionCompatible]>'

What I want is the OptionsView to accept both enums. Why does it go well by calling the static allCases, but not when I pass an array of those enums? How do I fix this?

1 Answers

Note that the difference between these two lines:

options: TopicOption.allCases,                   // no error
activeOptions: self.$optionsStore.topicOptions   // error

is that the first one does not involve a Binding. Normally an array of subtypes can be assigned to a variable of the type of an array of supertypes. This is not true for Binding<[Subtype]> and Binding<[Supertype]>.

This is because not only does a Binding needs to get the value that you pass in. It also sets it. Recall the common scenario of passing a Binding<String> to a TextField. The value of the Binding will change when the user enters text, right?

OptionsView declares a Binding<[OptionCompatible]>. This is saying that it might add some kind of OptionCompatible type to the array. But you are passing it a [TopicOptions]! If OptionsView wants to add a LevelOption to the array, for example, it can't!

You might say "but I am sure my OptionsView will only add objects of the correct type!" Well, the way to say this to the compiler is to make OptionsView generic:

struct OptionsView<OptionType: OptionCompatible>: View {
    let options: [OptionType]
    @Binding var activeOptions: [OptionType]

}
Related