SwiftUI Tip: Using AnyShapeStyle

A quick tip that took me too long to realize and easy for someone new to SwiftUI to get stuck on. Say you have a piece of UI where you want to change the background depending on the state. In this case, we want to have no background (using .clear) or relying on one of the built-in styles like .tertiary or .background or maybe a material like .thinMaterial . Try this out with a ternary operator and you’ll get any number of errors that aren’t immediately clear about the cause.

Let’s try .tertiary and we’ll get an error that it can’t be used on ShapeStyle even though we know it can be used fine without the ternary operator

Xcode error using .tertiary and .clear

Let’s try another, here’s using .thinMaterial and we get yet another confusing error

Xcode error using .thinMaterial and .clear

One more and I swear we’ll get to the point soon. Let’s try .background

Xcode error using .background and .clear

Ah, yet another error, but this one is actually helpful! If you’ve spent much time in Swift, it’s probably obvious all along, but this a type problem. The .background() modifier we’re using is expecting a ShapeStyle. We have .clear which a Color, .tertiary which is a HierarchicalShapeStyle, .background which is a BackgroundStyle and .thinMaterial is a Material. They all individually conform to ShapeStyle so they work correctly when used in isolation, or they will work when using the same concrete type (.e.g - two Colors or two Materials, etc), but they can’t be directly combined in a ternary.

You may have reached for a conditional like this or a modifier to do the same, but that’s not ideal as it complicates the view hierarchy and won’t work as expected for animations since those are two distinct views.

if isSelected {
    Rectangle()
        .background(.thinMaterial)
} else {
    Rectangle()
        .background(.clear)
}

The trivial fix here is to use AnyShapeStyle to type-erase the underlying concrete types so they can be combined. All of these work as expected.

Rectangle()
    .background(isSelected ? AnyShapeStyle(.thinMaterial) : AnyShapeStyle(.clear))

Rectangle()
    .background(isSelected ? AnyShapeStyle(.tertiary) : AnyShapeStyle(.clear))

Rectangle()
    .background(isSelected ? AnyShapeStyle(.background) : AnyShapeStyle(.clear))

That was a long-winded explanation for a one-line fix, but hopefully helpful if you’re new to SwiftUI when you see a similar problem in the future (and you will), you’ll know what tool to reach for. There are a number of other type-erasing wrappers you might need in other cases like AnyShape or AnyGesture when confronted with the same problem.