swiftswiftuiswift-protocols

Make a list of Structs conform to a protocol SwiftUI


I have a custom protocol say

protocol CustomProtocol {}

and I have a custom struct say

struct CustomStruct: View, CustomProtocol

How can I make (CustomStruct, CustomStruct) conform to the CustomProtocol

I have a Custom ViewBuilder which has an init function,

init<views>(@ViewBuilder content: @escaping () -> TupleView<Views>)

Now I want only Views that conform to the CustomProtocol to be accepted

Example:

struct CustomStruct: View {
var views: [AnyView]

init<Views: CustomProtocol>(@ViewBuilder content: @escaping () -> TupleView<Views>) {
    self.views = content().getViews
}

I added an extension to tuple view for the getViews variable:

extension TupleView {
var getViews: [AnyView] {
    makeArray(from: value)
}

private struct GenericView {
    let body: Any
    
    var anyView: AnyView? {
        AnyView(_fromValue: body)
    }
}

private func makeArray<Tuple>(from tuple: Tuple) -> [AnyView] {
    func convert(child: Mirror.Child) -> AnyView? {
        withUnsafeBytes(of: child.value) { ptr -> AnyView? in
            let binded = ptr.bindMemory(to: GenericView.self)
            return binded.first?.anyView
        }
    }
    
    let tupleMirror = Mirror(reflecting: tuple)
    return tupleMirror.children.compactMap(convert)
}

}


Solution

  • The only way I can think of is to reinvent your own ViewBuilder:

    @resultBuilder
    struct MyCustomViewBuilder {
        static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> TupleView<(C0, C1)> where C0 : View & CustomProtocol, C1 : View & CustomProtocol {
            ViewBuilder.buildBlock(c0, c1)
        }
        static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> TupleView<(C0, C1, C2)> where C0 : View & CustomProtocol, C1 : View & CustomProtocol, C2: View & CustomProtocol {
            ViewBuilder.buildBlock(c0, c1, c2)
        }
    
        // and so on... add every method in ViewBuilder here
    }
    

    In Swift 5.9+, you can write buildBlock using a parameter pack.

    static func buildBlock<each Content>(_ content: repeat each Content) -> TupleView<(repeat each Content)> where repeat each Content : View & CustomProtocol {
        ViewBuilder.buildBlock(repeat each content)
    }
    

    Before Swift 5.9, ViewBuilder had overloads of buildBlock of up to 10 views (which is why you couldn't put more than 10 views in a ViewBuilder), so you needed to write 10 overloads of buildBlock if you want the same functionality as ViewBuilder.

    Then you can do for example:

    struct CustomStack<Views>: View {
        var body: some View {
            content
        }
        
        let views: [AnyView]
        let content: TupleView<Views>
    
        // note the change from @ViewBuilder to @CustomViewBuilder
        init(@MyCustomViewBuilder content: @escaping () -> TupleView<Views>) {
            let view = content()
            self.views = view.getViews
            self.content = view
        }
    }
    

    Now if you do:

    CustomStack {
        Text("Hello")
        Text("World")
    }
    

    The compiler would complain that Text does not conform to CustomProtocol.