Gertjan.com
Gertjan.com

Reputation: 440

Using protocol in SwiftUI for providing "some View" / Generics?

I'm trying to get my head something in SwiftUI. I want to build a SwiftUI view and have something you could call a ViewProvider as a @State var. something like this:

protocol ViewProvider {
    associatedtype ViewOne = View
    associatedtype ViewTwo = View
    
    @ViewBuilder var viewOne: ViewOne { get }
    @ViewBuilder var viewTwo: ViewTwo { get }
}    

struct ContentView: View {
    @State private var parent: ViewProvider?
    
    var body: some View {
        VStack {
            HStack {
                Button(action: { parent = Father() }, label: { Text("Father") })
                Button(action: { parent = Mother() }, label: { Text("Mother") })
            }
            
            if let p = parent {
                p.viewOne
                p.viewTwo
            }
        }
    }
}

class Father: ViewProvider {
    @ViewBuilder var viewOne: some View {
        Text("Father One!")
    }
    
    @ViewBuilder var viewTwo: some View {
        Text("Father Two!")
    }
}


class Mother: ViewProvider {
    @ViewBuilder var viewOne: some View {
        Text("Mother One!")
    }
    
    @ViewBuilder var viewTwo: some View {
        Text("Mother Two!")
    }
}

This produces 2 different compiler errors.

@State private var parent: ViewProvider?
// Protocol 'ViewProvider' can only be used as a generic constraint because it has Self or associated type requirements

and

p.viewOne
p.viewTwo
// 2x Member 'viewOne' cannot be used on value of protocol type 'ViewProvider'; use a generic constraint instead

I have a vague idea of what I'm doing wrong, but no idea on how to solve it :) What syntax should I use to get something like this to work?

Upvotes: 2

Views: 3669

Answers (1)

kid_x
kid_x

Reputation: 1475

Assuming you're on Swift 5.6 or lower, the problem is that you can only use protocols with associated types for conformance, ie you can't use them as types to pass around. The reasoning is that their associated types will be different for different conformers.

Say you have the following:

protocol P {
    associatedtype T
    var prop: T 
}

class MyClass: P {
    var prop: Int 
}

class MyOtherClass: P { 
    var prop: String
}

What would the result of the following be?

let arr: [P] = [MyClass(), MyOtherClass()]
let myMappedArr = arr.map { $0.prop }

prop is of a different type for each conformer.


In Swift 5.7, however, you actually can pass around protocols of this sort. In later versions of Swift, you will have to use the keyword any to pass these protocols around as types.

See the proposal for unlocked existentials to learn more about it.


Lastly to address opaque types here:

Since you can't pass around protocols with associated types, you can't have something like

@State var myState: ViewProvider or even @State var myState: some ViewProvider, because your state variable is assigned, and you can't assign something of an opaque type.

In SwiftUI's View, this works because the view property is computed, and thus the type can be inferred

// type is inferred to be (something like) Group<Text>
var body: some View {
    Group {
       Text("something")
    }
} 

whereas here, you can't find a suitable type to assign to a property whose type is opaque

@State var myState: some ViewProvider
...

// You don't know myState's type, so you can't assign anything to it
myState = ... // error - you won't be able to find a matching type to assign to this property

To wit, the line @State private var parent: ViewProvider? in your code simply won't compile in Swift 5.6 or lower, because you're not allowed to use your ViewProvider protocol as a type for anything other than conformance or as an opaque return type when used in functions or computed properties.


Sorry for all the edits. Wanted to provide a couple of potential solutions:

One way is to simply make your ContentView generic over the type of its ViewProvider

struct ContentView<ViewProviderType: ViewProvider> {
     @State private var parent: ViewProviderType?
     ... 
}

The other would be to simply remove the associatedtype from your protocol and just erase the view type you're trying to return:

protocol ViewProvider {
     var viewOne: AnyView { get }
     var viewTwo: AnyView { get }
}

If you're working with Swift 5.7, you may be able to use your type-constrained protocols as property types, or you can also use primary associated types, wherein you could declare properties of type ViewProvider<MyView> (though that doesn't necessarily solve your problem).

Generics or type erasure over ViewProvider's view types are probably the best candidates for what you're trying to do, even in a Swift 5.7 world.

Upvotes: 9

Related Questions