Protocol as a property type, SwiftUI's View protocol. (any View vs View)

Apologies - this is sort of about SwiftUI, but also about protocols, so I thought that it might be OK to post here...

I would like to have a type that is a simple wrapper to a type that conforms to View (a SwiftUI view). This type has properties that are the view and a name for the view. This is so that I can have an array of homogeneous heterogeneous Views each with their own name, and these can be shown in a list for pushing onto a SwiftUI NavStack. (Basically an array of views that can list itself and, present those views when selected.)

Question 1 - Can protocols be Types for struct properties?

The following code compiles in a playground, showing that you can have a property of a struct which is of a type that is a protocol:

protocol P {
    func test()
}

extension P {
    func test() {
        print("test called in type that conforms to P")
    }
}

struct Container {
    var pConformer: P
}

struct S: P {
}

let c = Container(pConformer: S())

However, this code in a SwiftUI app:

struct ViewHolder: Identifiable {
    let view: View
    var id: String {
        return String(describing: view)
    }
    let name: String
}

fails to compile and informs me that:
Use of protocol 'View' as a type must be written 'any View'

Question 2: Why must I box-up the protocol type?

If I do so and change
let view: View to let view: any View
then I'm unable to use the view property in a SwiftUI NavigationLink as it's not of type View and gives the error:
Type '() -> any View' cannot conform to 'View'

It seems impossible to satisfy both SwiftUI's ViewBuilder's need for everything to be a View and the any boxing requirement....

Thanks for any help.

With some protocols such as P, you can use it as a type by writing either any P or just P. The latter is a legacy spelling that's accepted for source compatibility reasons.

With other protocols such as View, you must spell the type as any View. Previously, not all protocols could not be used as types (such as protocols with Self or associated type requirements), so when Swift eventually gained this feature there was no need to support the legacy spelling.

With limited (hardcoded) exceptions, protocols used as types don't conform to any protocols, even themselves.

This has nothing to do with the previous point about spelling. For example, whether you spell the type as P or any P doesn't change the fact that it doesn't conform to P.

That we used to have to say "P doesn't conform to P" was one motivation for adopting a new spelling to distinguish the type from the protocol: now, as you show, you can clearly understand that there is a box involved.

Unlike spelling rules, the fact that protocols used as types do not conform to their own protocols is not an artificial limitation of the language. In the general case, it is simply impossible. For example, a variable of type any FixedWidthInteger can hold a value of any fixed-width integer type (for example, an Int16 or an Int32)—therefore, any FixedWidthInteger is not a FixedWidthInteger.

We do not currently have a feature to manually conform a protocol used as a type to any protocols, but it is conceivable that this will come in the future. For now, you can manually write or use a manually written wrapper type. For example, SwiftUI has AnyView, which conforms to View.

5 Likes

View has an associated type requirement—Body. Add one to your own protocol and you'll see the same error:

protocol P { associatedtype T }

struct Container {
  var pConformer: P // Use of protocol 'P' as a type must be written 'any P'
}

struct ViewHolder<View: SwiftUI.View>: Identifiable {
  let view: View
  var id: String { .init(describing: view) }
  let name: String
}
1 Like

Thank-you for such a detailed reply.
I think I’ll need to do some more reading to update my internal mental model…

You don't need views if they're homogeneous, just the type.

protocol HolderBody: View { init(name: String) }
struct Holder<Body: HolderBody> { let name: String }
extension Holder: Hashable & Identifiable & View {
  var body: some View { Body(name: name) }
  var id: String { name }
}
struct NameView: HolderBody {
  let name: String
  var body: some View { Text(name) }
}
struct ContentView<Destination: HolderBody>: View {
  let holders: [Holder<Destination>]

  var body: some View {
    NavigationStack {
      List(holders) { holder in
        NavigationLink(holder.name, value: holder)
      }
      .navigationDestination(
        for: Holder<Destination>.self,
        destination: \.self
      )
    }
  }
}

#Preview {
  ContentView<NameView>(
    holders: [
      .init(name: "Yakko"),
      .init(name: "Wakko"),
      .init(name: "Dot"),
    ]
  )
}

Apologies, a small brain hiccough. I meant heterogeneous…

struct Holder {
  let name: String
  let _body: () -> any View
}
extension Holder: Hashable & Identifiable & View {
  static func == (lhs: Holder, rhs: Holder) -> Bool { lhs.name == rhs.name }
  func hash(into hasher: inout Hasher) { hasher.combine(name) }
  var body: some View { AnyView(_body()) }
  var id: String { name }
}
struct ContentView: View {
  let holders: [Holder]

  var body: some View {
    NavigationStack {
      List(holders) { holder in
        NavigationLink(holder.name, value: holder)
      }
      .navigationDestination(for: Holder.self, destination: \.self)
    }
  }
}

#Preview {
  ContentView(
    holders: [
      .init(name: "Yakko") { Text("Yakko") },
      .init(name: "Wakko") { Button("Wakko") { } },
      .init(name: "Dot", _body: EmptyView.init)
    ]
  )
}
1 Like

Thanks for your concrete solution.
I think I need to keep reading-up on how SwiftUI works so that I properly understand all the details from this thread. (And brush up on generics...)

Thanks all!