iOS SwiftUI Function returning some view not showing up in UI

I am currently attempting assignment 3 of stanford's 193p 2020 course. I am having trouble in trying to get my function createCardContent to actually display the Shape object it should be displaying. Instead of displaying a shape object on the cards, it is not displaying anything, which ends up just showing empty cards (and I don't know why) image here.

If I remove the text @ViewBuilder, then I get an error

Function declares an opaque return type, but has no return statements
in its body from which to infer an underlying type

If I add the return inside the if-else if statements, I get an error saying

Function declares an opaque return type, but the return statements in
its body do not have matching underlying types

struct CardView: View {
        var card: SetGameEngine.Card
    
    var body: some View {
        GeometryReader { geometry in
            self.testBody(for: geometry.size)
        }
    }
    
    func testBody(for size: CGSize) -> some View {
        ZStack {
            HStack {
                ForEach(0..<card.symbolCount) { _ in
                    self.createCardContent()
                }
                
            }
       } .cardify()
    }


    @ViewBuilder
    func createCardContent() -> some View {
        let colour = self.colour()
        let symbol = self.card.symbol
        let shade = self.card.shade
        
        if symbol == .oval, shade == .solid {
            Capsule().fill(colour)
        } else if symbol == .squiggle, shade == .solid {
            Rectangle().fill(colour)
        } else if symbol == .diamond, shade == .solid {
            Diamond().fill(colour)
        } else if symbol == .oval, shade == .empty {
            Capsule().stroke(colour)
        } else if symbol == .squiggle, shade == .empty {
            Rectangle().stroke(colour)
        } else if symbol == .diamond, shade == .empty {
            Diamond().stroke(colour)
        } else {
            Capsule().fill(Color.pink)
        }
            
    }

func colour() -> Color {
        switch self.card.colour {
        case .red:
            return Color.red
        case .green:
            return Color.green
        case .purple:
            return Color.purple
        }
    }

EDIT: This is very weird. When I remove the local variables I wrote in the function createCardContent and their references, the card content gets populated by the shapes I want. Here is my new function createCardContent. Can anyone explain to me why this is working but the previous function is not?

@ViewBuilder
func createCardContent() -> some View {
    
    if card.symbol == .oval && card.shade == .solid {
        Capsule().fill(colour())
    } else if card.symbol == .squiggle && card.shade  == .solid {
         return Rectangle().fill(colour())
    } else if card.symbol == .diamond && card.shade  == .solid {
         return Diamond().fill(colour())
    } else if card.symbol == .oval && card.shade  == .empty {
          return Capsule().stroke(colour())
    } else if card.symbol == .squiggle && card.shade  == .empty {
         return Rectangle().stroke(colour())
    } else if card.symbol == .diamond && card.shade == .empty {
         return Diamond().stroke(colour())
    } else {
        Diamond().stroke(colour())
    }
        
}

You can’t put arbitrary code inside a @ViewBuilder but need each line (up to a max of ten) to return some View or be one of the few flow supported flow control options available (mostly just if statement with else also supported).

The @ViewBuilder attribute is actually just syntactic sugar wrapping a function that returns a TupleView of up to ten contained other Views.

1 Like

Sorry, I am not sure I explained it properly. The version with the constants defined at the start of the function didn’t work because each of those let statements are not a closure or a function that returns some View to wrap inside the TupleView that @ViewBuilder makes.

Apple’s documentation for ViewBuilder may help illustrate it better when you see the various factory methods it uses to turn your input into a View for SwiftUI to display.

Here’s the example that takes two arguments:

static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> TupleView<(C0, C1)> where C0 : View, C1 : View

You can see that the two arguments are generic (what Prof. Hegarty calls “don’t cares”) but have a constraint that they must conform to the View protocol (so what Prof. Hegarty calls “cares a little bit”). Each line inside your @ViewBuilder must return something that conforms to View to be passed through these buildBlock factory methods and let colour = self.colour() is a statement that defines a constant, not one that returns some View, so putting it inside the ViewBuilder is invalid.

1 Like

Ahhhh! Thank you for explaining it so well! Appreciate is very much!!

1 Like

Also, since I didn’t address it in my earlier answer: The error you get without @ViewBuilder is because the opaque return type of some View has to be a type that the compiler can figure out what it actually is and you have runtime checks that change the concrete type that your function returns. Wrap the ifs in a Group { } block (you’re then beholden to the control flow restrictions, since Group is a ViewBuilder, so keep your constant and variable declarations outside the Group) and the compiler will be able to infer a specific return type.

Swift 5.3, let is now allowed in @​ViewBuilder

must change to:

if symbol == .oval && shade == .solid

"," does not work in @​ViewBuilder, must use &&

1 Like

I haven’t had a chance to play with 5.3. Do you know what else has changed? Is there an official list of new features somewhere? I heard you could use switch now, too.

This seems to be the latest update regarding implementation.