swiftswiftuiswift-protocolsswiftui-view

How can I return a struct which indirectly inherits the View protocol via another protocol in SwiftUI?


Suppose I have the following protocol which conforms to View:

protocol Foo: View {
    init(field: Binding<Bool>)
}

I then have two structs which conform to this protocol:

struct BarView: Foo {
    @Binding private var field: Bool
    
    init(field: Binding<Bool>) {
        self._field = field
    }
    
    var body: some View {
        Text(field ? "Some text" : "Some other text")
    }
    
}

struct QuxView: Foo {
    @Binding private var field: Bool
    
    init(field: Binding<Bool>) {
        self._field = field
    }
    
    var body: some View {
        Text(field ? "Some text" : "Some other text")
    }
    
}

Now, in my main view I have a collection of types which conform to Foo. When I try and initialise a view of one of these types, I get the error Type 'any Foo' cannot conform to 'View'. How do I avoid this?

struct MainView: View {
    static let fooViews: [any Foo.Type] = [
        BarView.self,
        QuxView.self
    ]
    @State private var field = false
    
    var body: some View {
        if let fooView = MainView.fooViews.first {
            fooView.init(field: $field)
        }
    }
}

Thanks! (Bear in mind this is a minimal example of the problem I'm trying to solve)


Solution

  • Add an extension to Foo that returns a concrete view type - AnyView.

    extension Foo {
        static func create(field: Binding<Bool>) -> AnyView {
            AnyView(Self.init(field: field))
        }
    }
    

    and use this extension in the body instead:

    if let fooView = MainView.fooViews.first {
        fooView.create(field: $field)
    }
    

    Note that if all you need is a factory to create views, consider getting rid of the protocol, and jut using an array of (Binding<Bool>) -> AnyView:

    static let fooViewFactories: [(Binding<Bool>) -> AnyView] = [
        { AnyView(BarView(field: $0)) },
        { AnyView(QuxView(field: $0)) },
    ]
    
    @State private var field = false
    
    var body: some View {
        if let factory = MainView.fooViewFactories.first {
            factory($field)
        }
    }