I'm building a SwiftUI based view and I'd like to store a temporary value (so it can be used multiple times) in a closure that returns some View. The compiler is giving me the following error:
Unable to infer complex closure return type; add explicit type to disambiguate
struct GameView: View {
@State private var cards = [
Card(value: 100),
Card(value: 20),
Card(value: 80),
]
var body: some View {
MyListView(items: cards) { card in // Error is on this line, at the starting curly brace
let label = label(for: card)
return Text(label)
}
}
func label(for card: Card) -> String {
return "Card with value \(card.value)"
}
}
struct MyListView<Item: Identifiable, ItemView: View>: View {
let items: [Item]
let content: (Item) -> ItemView
var body: some View {
List {
ForEach(items) { item in
content(item)
}
}
}
}
struct Card: Identifiable {
let value: Int
let id = UUID()
}
If I inline the call to the label(for:) method, the build succeeds. Obviously, the example above is my simplified reproduction for the issue. In my actual app, I'm trying to store the return value of the method because it gets used more than once while creating the view for the individual item and that operation requires a potentially expensive evaluation in my model. It's wasteful to make that method call several times.
A couple notes:
content closure passed to MyListView is not a @ViewBuilder, but even if it was, I thought using a let as I've done should be okay.return when a closure contains a single expression - I've added my own explicit return.How can I write this so that I don't have to call the potentially expensive method more than once? Can someone explain what's happening at a language/syntax level to cause the error?
This is unfortunately too complex for Swift to grasp, but there are several solutions:
First, you can manually declare what function it is:
MyListView(items: cards) { (card: Card) -> Text in
let label = label(for: card)
return Text(label)
}
Or you need to use the power of @ViewBuilder to make it work. Therefore, I have 2 working suggestions of equal quality
Group:var body: some View {
MyListView(items: cards) { card in
Group {
let label = label(for: card)
Text(label)
}
}
}
@ViewBuilder tag
@ViewBuilder func cardView(card: Card) -> some View {
let label = label(for: card)
Text(label)
}
var body: some View {
MyListView(items: cards, content: cardView)
}
Additionally, you can simplify the second example and not use ViewBuilder, as you can manually say you will return Text, e.g.:
func cardView(card: Card) -> Text {
let label = label(for: card)
return Text(label)
}
This is due to a limitation where the Swift compiler only tries to infer a closure's return type if it is a single expression. Closure's that are processed by a result builder, such as @ViewBuilder, are not subject to this limitation. Importantly, this limitation also doesn't affect functions (only closures).
I was able to make this work by moving the closure to a method inside the structure. Note: this is the same as @cluelessCoder's second solution, just excluding the @ViewBuilder attribute.
struct GameView: View {
@State private var cards = [
Card(value: 100),
Card(value: 20),
Card(value: 80),
]
var body: some View {
MyListView(items: cards, content: cardView)
}
func cardView(for card: Card) -> some View {
let label = label(for: card) // only called once, and can be reused.
return Text(label)
}
func label(for card: Card) -> String {
return "Card with value \(card.value)"
}
}
Thanks to @cluelessCoder. I would have never stumbled upon this discovery without their input and helpful answer.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With