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
174 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")
}