Computed property for Shape within the SwiftUI's views

Hello dear community! I'm a new to SwiftUI and going through the official Develop in Swift Tutorials. I've just finished the chapter Customize views with properties where it was show that it's possible to declare computed properties like this:

var iconName: String {
    if isRainy {
        return "cloud.rain.fill"
    } else {
        return "sun.max.fill"
    }
}

And then use it as follows:

var body: some View {
    VStack {
        Text(day)
        Image(systemName: iconName)
    }
}

This is really cool and I liked it and I decided that I want to do the similar thing for the project from the previous chapter (ChatBubble), so I decided to declare the following property in my custom ChatBubble view:

struct ChatBubble: View {
    let text: String
    let color: Color
    let isEllipsis: Bool

    var shape: Shape {
        if isEllipsis {
            Ellipse()
        } else {
            RoundedRectangle(cornerRadius: 8)
        }
    }

    var body: some View {
        Text(text)
            .padding()
            .background(color, in: shape)
    }
}

But first, I got a warning where I declare shape: Shape that it must be written any Shape like this:

var shape: any Shape { …

But in both cases I got a error in the body:

'buildExpression' is unavailable: this expression does not conform to 'View'

What does it mean? Why I can't use the computed property with shape like the computed property for String?

Thank you very much in advance.

Answered by BabyJ in 845618022

Hey there, welcome to the forums and SwiftUI!


You seem to have come across a common problem in SwiftUI when dealing with protocols, like Shape.

You're telling Swift that the shape property is some type that conforms to Shape, but the problem is that Ellipse and RoundedRectangle are two different concrete types — even though they both conform to Shape.

Swift needs to know the exact concrete type at compile time, so you can't return two different shape types from the same computed property unless you erase the type.

Swift suggests using any Shape, like this:

var shape: any Shape {
    if isEllipsis {
        Ellipse()
    } else {
        RoundedRectangle(cornerRadius: 8)
    }
}

But this approach doesn't work directly with modifiers like background(_:in:), because SwiftUI's view builder needs a specific Shape type, not a generic any Shape.



The solution is to use AnyShape – a type-erased shape value – and can be used by wrapping each Shape type like this:

var shape: some Shape {
    if isEllipsis {
        AnyShape(Ellipse())
    } else {
        AnyShape(RoundedRectangle(cornerRadius: 8))
    }
}

Now Swift can infer the concrete type as AnyShape, whilst also preserving the underlying shape's behaviour (like how it draws itself). That gives you flexibility without breaking Swift's strict type system.


You could also move the logic into the body and inline the condition using a ternary operator instead, like this:

.background(color, in: isEllipsis ? AnyShape(Ellipse()) : AnyShape(RoundedRectangle(cornerRadius: 8)))

// You can use the shorter syntax for creating shapes if you want to
.background(color, in: isEllipsis ? AnyShape(.ellipse) : AnyShape(.rect(cornerRadius: 8)))


I hope this explanation helps and also fixes your problem.

Accepted Answer

Hey there, welcome to the forums and SwiftUI!


You seem to have come across a common problem in SwiftUI when dealing with protocols, like Shape.

You're telling Swift that the shape property is some type that conforms to Shape, but the problem is that Ellipse and RoundedRectangle are two different concrete types — even though they both conform to Shape.

Swift needs to know the exact concrete type at compile time, so you can't return two different shape types from the same computed property unless you erase the type.

Swift suggests using any Shape, like this:

var shape: any Shape {
    if isEllipsis {
        Ellipse()
    } else {
        RoundedRectangle(cornerRadius: 8)
    }
}

But this approach doesn't work directly with modifiers like background(_:in:), because SwiftUI's view builder needs a specific Shape type, not a generic any Shape.



The solution is to use AnyShape – a type-erased shape value – and can be used by wrapping each Shape type like this:

var shape: some Shape {
    if isEllipsis {
        AnyShape(Ellipse())
    } else {
        AnyShape(RoundedRectangle(cornerRadius: 8))
    }
}

Now Swift can infer the concrete type as AnyShape, whilst also preserving the underlying shape's behaviour (like how it draws itself). That gives you flexibility without breaking Swift's strict type system.


You could also move the logic into the body and inline the condition using a ternary operator instead, like this:

.background(color, in: isEllipsis ? AnyShape(Ellipse()) : AnyShape(RoundedRectangle(cornerRadius: 8)))

// You can use the shorter syntax for creating shapes if you want to
.background(color, in: isEllipsis ? AnyShape(.ellipse) : AnyShape(.rect(cornerRadius: 8)))


I hope this explanation helps and also fixes your problem.

Thank you very much @BabyJ!

I see now. It's actually a little bit confusing (the part that Ellipse and RoundedRectangle both confirm to Shape but could not be easily used in the .background modifier). But what do you think about the performance of the approach which used AnyShape?

I also figured out, that what I want to achieve can be done with a @ViewBuilder like this:

@ViewBuilder
var shape: some View {
    if isEllipsis {
        color.clipShape(Ellipse())
    } else {
        color.clipShape(RoundedRectangle(cornerRadius: 8))
    }
}

and then the following usage of it in the .background:

.background(shape)

I'm not sure that this will perform better then AnyShape nor not sure that I used @ViewBuilder in a right way (I still don't understand how it works).

But I would be very appreciate if you give your opinion on this.

Thanks again!

Computed property for Shape within the SwiftUI's views
 
 
Q