Home > other >  Declare a temporary variable or constant inside a closure that returns some View (SwiftUI)
Declare a temporary variable or constant inside a closure that returns some View (SwiftUI)

Time:01-22

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:

  • This 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.
  • I'm not depending on the implicit return when a closure contains a single expression - I've added my own explicit return.
  • It doesn't matter whether I use the declared variable or not. Just the presence of the statement upsets the compiler.

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?

CodePudding user response:

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

  1. Using Group:
var body: some View {
        MyListView(items: cards) { card in
            Group {
                let label = label(for: card)
                Text(label)
            }
        }
    }

  1. Using func with @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)
    }

CodePudding user response:

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.

  •  Tags:  
  • Related