swiftuiswiftui-picker

“Cannot convert return expression of type” error in custom Picker init method


I’m trying to build an extension on a SwiftUI Picker, but I’m getting the following error inside the content closure when trying to compile:

Cannot convert return expression of type 'ForEach<Data, SelectionValue.ID, some View>' to return type 'Content'

Here’s the code:

extension Picker where SelectionValue: Identifiable, Label == Text {
    
    init<Data>(_ title: String, selection: Binding<SelectionValue>, items: Data, @ViewBuilder itemContent: @escaping (Data.Element) -> some View) where Data: RandomAccessCollection, Data.Element == SelectionValue {
        self.init(title, selection: selection, content: {
            ForEach(items) { item in
                itemContent(item).tag(item)
            }
        })
    }
}

The ForEach should conform to View, and therefore satisfy the Content type requirement, right?


Solution

  • Your init is expected to be able to create Pickers where the Content type parameter is any View, because you did not constrain its Content to anything. However, the Picker you create in init has a Content type of ForEach<...>.

    This means that you should add a constraint on Content.

    // note that you should also parameterise this with ItemContent
    // you can't use (Data.Element) -> some View anymore, because you need the return type
    // of that to constrain Content
    init<Data, ItemContent>(
        _ title: String,
        selection: Binding<SelectionValue>,
        items: Data,
        @ViewBuilder itemContent: @escaping (Data.Element) -> ItemContent
    ) where
        Data: RandomAccessCollection,
        Data.Element == SelectionValue,
        Content == ??? // <=== here
    

    So what is the type of the ForEach(items) { ... }? We don't actually know, because we don't know the type that the .tag modifier returns.

    One simple way to work around this is to create your own view that wraps this ForEach:

    struct PickerContentHelper<Data, Content>: View
        where Data: RandomAccessCollection, 
            Data.Element: Hashable,
            Data.Element: Identifiable,
            Content: View
    {
        
        let items: Data
        let itemContent: (Data.Element) -> Content
        
        init(_ items: Data, @ViewBuilder itemContent: @escaping (Data.Element) -> Content) {
            self.items = items
            self.itemContent = itemContent
        }
        
        var body: some View {
            ForEach(items) { item in
                itemContent(item).tag(item)
            }
        }
    }
    

    You can then constrain Content to be PickerContentHelper<Data, ItemContent>:

    extension Picker where SelectionValue: Identifiable, Label == Text {
        
        init<Data, ItemContent>(
            _ title: String,
            selection: Binding<SelectionValue>,
            items: Data,
            @ViewBuilder itemContent: @escaping (Data.Element) -> ItemContent
        ) where
            Data: RandomAccessCollection,
            Data.Element == SelectionValue,
            Content == PickerContentHelper<Data, ItemContent>
        {
            self.init(title, selection: selection) {
                PickerContentHelper(items, itemContent: itemContent)
            }
        }
    }
    

    This is a common pattern seen in many other SwiftUI views. You can find many of these "helper views" in the "Supporting Types" section. For example, ShareLink has DefaultShareLinkLabel, and initialisers such as init(item:subject:message:) constrain the Label type parameter to be this type.