Unboxing protocol to concrete type in SwiftUI View

Using Swift 5.7 we're trying to use protocols for testing and mocking in our SwiftUI App.

With any and some we're able to hold a heterogeneous list of protocols with self constraints which works perfectly. What we're running into now is that we can't undox the any Protocol into a concrete type for the view.

Here's a basic example:


protocol ItemProtocol: ObservableObject {
    var id: String { get }
}

struct ListSection {
    var id: Int
    let title: String
    let items: [any ItemProtocol]
}

protocol ViewModelProtocol: ObservableObject {
    var sections: [ListSection] { get }
}

struct MyView<T: ViewModelProtocol>: View {
   @ObservedObject
    var viewModel: T

    init(viewModel: T) {
        self.viewModel = viewModel
    }

    var body: some View {
        List(viewModel.sections, id: \.id) { section in
            Section {
                ForEach(section.items, id: \.id) { item in
                    RowView(item: item)
                    // create view for some ItemProtocol
                    Text("Hello Item")
                }

            } header: {
                Text(section.title)
            }
        }
    }
}

struct RowView<T: ItemProtocol>: View {

    @ObservedObject
    var item: T

    init(item: T) {
        self.item = item
    }

    var body: some View {
        Text("Row View")
    }

}


This will result in an error:

Type 'any ItemProtocol' cannot conform to 'ItemProtocol'

I had hoped that the any ItemProtocol would be unboxed to it's concrete type and a concrete type of View would be created.

Post not yet marked as solved Up vote post of utahwithak Down vote post of utahwithak
1.5k views
  • IMHO it would be best to define what you are testing. If you are testing business logic, then it would be best to test only the model without SwiftUI code. You can be guaranteed the dependent views will change when the model changes

Add a Comment

Replies

I have a similar problem. There is a not elegant solution: force cast.

switch item:
    case is Item1
        let binding = Binding(...)
        RowView(item: binding)
    default:
        fatelError("Unknown Item")
}
struct RowView: View {
    var item: any ItemProtocol
    init(item: any ItemProtocol) {
        self.item = item
    }

    var body: some View {
        Text("Row View")
    }
}
  • This doesn’t address the concern above. 1) item is never used by the view. 2) if any properties of item are published they won’t trigger a rebuild of view

Add a Comment