The Swift ternary conditional operator: From obscure to essential with SwiftUI
Monday, December 11, 2023 10:17 AM
Is your SwiftUI animation not doing what you expect it to? This might be why.
Level: Intermediate
Ternary conditional operators (often called the ternary operator in Swift) are found in many programming languages.
They’re called “ternary” because they take three inputs: A Boolean condition to evaluate, a result to return if that condition is true, and result to return if that condition is false.
In C like languages, including Swift, they take the form:
condition ? value if true : value if false
The condition can be any Boolean expression (it must evaluate to true or false).
In Swift, we can use it like this:
let shouldShowItem = true
var itemOpacity: Double
itemOpacity = shouldShowItem ? 1.0 : 0.0
When this code runs, the value of itemOpacity will be set to 1.0. Why?
shouldShowItem is true, so the ternary operator in the third line returns a value of 1.0
If we change the value of shouldShowItem to false, the value of itemOpacity will be set to 0.0.
There’s a more conventional way to achieve the same result:
let shouldShowItem = true
var itemOpacity: Double
if shouldShowItem {
itemOpacity = 1.0
} else {
itemOpacity = 0.0
}
Because many find the second example easier to read, the ternary operator wasn’t used much prior to the introduction of SwiftUI. But if you’re using SwiftUI, the ternary operator is essential. Even in circumstances where you can substitute a more conventional if…else statement, it’s usually a bad idea. Let’s see why.
We’re going to build an app that does this:
When the user slides the “Enlarge the circle” toggle to the on position, the circle get’s bigger, with a nice bouncy animation.
This is remarkably easy to create using SwiftUI. It just takes a few lines of code, so let’s give it a try.
in Xcode, create a new project. Select the App template and click “Next”.
Enter a product name and organization identifier. Make sure the interface is SwiftUI, then click “Next”.
Select ContentView.swift and edit it to look like this:
import SwiftUI
struct ContentView: View {
@State private var bigCircle = false
var body: some View {
VStack {
Spacer()
if bigCircle {
circleView
.frame(width: 150.0, height: 150.0)
} else {
circleView
.frame(width: 50.0, height: 50.0)
}
Spacer()
Toggle("Enlarge the circle", isOn: $bigCircle.animation(.bouncy))
.padding([.top, .leading, .trailing])
}
}
var circleView: some View {
Circle()
.foregroundColor(.blue)
.shadow(color: .gray, radius: 5)
}
}
#Preview {
ContentView()
}
Voila! No warnings or errors.
Our toggle changes the value of the State boolean variable named bigCircle.
We define a circleView that’s a blue circle with a gray shadow.
We use this circleView in the body of our view. We use an if…else conditional statement to draw circleView in two different sizes depending on the value of bigCircle.
With just a few lines of code we’ve drawn a small blue circle at the center of the screen and given the user a toggle so they can change the size of the circle. Because everyone loves animation, we’ve added a bouncy animation modifier to our toggle.
Make sure your preview is in live mode and click the toggle.
Our code compiles and runs without errors, but something’s wrong.
Where’s our nice bouncy animation?
Animations in SwiftUI are powerful and simple to implement. So simple that we can just add the .animation() method to a binding that changes state and SwiftUI will render all the intermediate steps needed to create a smooth animation from one state to the other. But it only works as intended when the change is confined to a single view. In our code we’ve defined a view called circleView. We use it here:
if bigCircle {
circleView
.frame(width: 150.0, height: 150.0)
} else {
circleView
.frame(width: 50.0, height: 50.0)
}
It’s the only view we use, but in SwiftUI these two instances of circleView are two different views. Why? Because the Circle shape we use is a struct, and structs are value types, not reference types. Here’s the definition of Circle. @frozen public struct Circle : Shape {…}. It’s a struct that conforms to the Shape protocol.
In SwiftUI, all views are structs. So even if we write:
if bigCircle {
Circle()
} else {
Circle()
}
SwiftUI treats each Circle() as a different view, just as if we’ve written:
if bigCircle {
Circle()
} else {
Rectangle()
}
So why don’t we get an error message when we try to apply .animation(.bouncy)? It’d be nice if we did, but as far as the compiler is concerned, this is valid code. And at runtime it’s actually trying to do what we ask. If you remove .animation(.bouncy) from your code, you’ll see the difference. SwiftUI is trying to animate between two different views, but it’s not what we want.
To fix this we need to restrict our animation to a single view. You might be tempted to try this:
Circle()
.frame(width: 50.0, height: 50.0)
if bigCircle {
.frame(width: 150.0, height: 50.0)
}
But you’ll get an error that says, "Reference to member 'frame' cannot be resolved without a contextual type”, because the compiler has no way of knowing what view you want to apply .frame(width: 150.0, height: 50.0) to.
There are a couple of ways to solve this problem. Here’s one way:
import SwiftUI
struct ContentView: View {
@State private var bigCircle = false
var body: some View {
VStack {
Spacer()
circleView
.frame(width: mySize, height: mySize)
Spacer()
Toggle("Enlarge the circle", isOn: $bigCircle.animation(.bouncy))
.padding([.top, .leading, .trailing])
}
}
var circleView: some View {
Circle()
.foregroundColor(.blue)
.shadow(color: .gray, radius: 5)
}
var mySize: Double {
if bigCircle {
return 150
} else {
return 50
}
}
}
#Preview {
ContentView()
}
Here we’ve added a computed variable called mySize. Its value changes when the value of bigCircle changes. Because there’s a single circleView, this works as expected.
But there’s a simpler way to do this that doesn’t require a computed variable or an if…else statement. We can simply make use of Swift’s ternary operator to dynamically change the size of our circle when bigCircle changes.
import SwiftUI
struct ContentView: View {
@State private var bigCircle = false
var body: some View {
VStack {
Spacer()
circleView
.frame(width: bigCircle ? 150 : 50, height: bigCircle ? 150 : 50)
Spacer()
Toggle("Enlarge the circle", isOn: $bigCircle.animation(.bouncy))
.padding([.top, .leading, .trailing])
}
}
var circleView: some View {
Circle()
.foregroundColor(.blue)
.shadow(color: .gray, radius: 5)
}
}
#Preview {
ContentView()
}
Everything needed to make this work is in a single line of code: .frame(width: bigCircle ? 150 : 50, height: bigCircle ? 150 : 50)
We don’t need a computed variable. Thanks to the ternary operator, we can dynamically change the width and height of the circle when bigCircle changes.
At first, ternary operators may be more difficult to read than if…else conditionals. Just remember the they’re functionally identical. So you can read our single line as “If bigCircle, width = 150, else width = 50. If bigCircle, height = 150, else height = 50. “
So how do we animate a change from one view to another? That’s another topic for another day...