Left cloud filled with code
Right cloud filled iwth code
A napping developer

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 itemOpacityDouble

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 itemOpacityDouble

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 ContentViewView {

 @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 circleViewsome View {

    Circle()

      .foregroundColor(.blue)

      .shadow(color: .grayradius5)

  }

}


#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)? Itd be nice if we did, but as far as the compiler is concerned, this is valid code. And at runtime its actually trying to do what we ask. If you remove .animation(.bouncy) from your code, youll see the difference. SwiftUI is trying to animate between two different views, but its 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(width50.0height50.0)

    if bigCircle {

      .frame(width150.0height50.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(width150.0height50.0) to.


There are a couple of ways to solve this problem. Heres one way:


import SwiftUI


struct ContentViewView {

  @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 circleViewsome View {

    Circle()

      .foregroundColor(.blue)

      .shadow(color: .grayradius5)

  }

  

  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 ContentViewView {

  @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 circleViewsome View {

    Circle()

      .foregroundColor(.blue)

      .shadow(color: .grayradius5)

  }

}


#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 dont 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...



•••

If you find any of these posts useful, please make a charitable donation

Link to the Epilepsy Foundation of Kentuckiana
Link to The Asclepius Initiative