ospr
ospr

Reputation: 1671

“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?

Upvotes: 1

Views: 86

Answers (2)

Sweeper
Sweeper

Reputation: 273540

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.

Upvotes: 1

Naqeeb Alom
Naqeeb Alom

Reputation: 1

The error you're encountering suggests that the return type of the ForEach expression inside the content closure doesn't match the expected return type of the content closure itself modify the extension to ensure that the return type is compatible

extension Picker where SelectionValue: Identifiable, Label == Text {

init<Data>(_ title: String, selection: Binding

Upvotes: -3

Related Questions