Reputation: 2901
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?
Upvotes: 3
Views: 2062
Reputation: 2901
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.
Upvotes: 1
Reputation: 1088
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)
}
Upvotes: 2