Your First iOS & SwiftUI App: Polishing the App

Mar 1 2022 · Swift 5.5, iOS 15, Xcode 13

Part 3: A Custom Alert

27. Display a Custom Alert

Episode complete

Play next episode

Next
About this episode

Leave a rating/review

See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 26. Challenge: Create a Custom Alert Next episode: 28. Intro to SwiftUI Animation
Transcript: 27. Display a Custom Alert

Now that we’ve created a view for our custom alert, we need to modify Bullseye to display this view when the user taps the “Hit Me” button, instead of the default alert that it currently shows.

To understand how we can do this, let’s review how the default alert currently works.

Inside ContentView, we have have created a @State property called alertIsVisible. Remember that whenever you change the value of a @State property in SwiftUI,SwiftUI automatically refreshes your view by re-calculating it’s body.

And when you re-calculate the body of a view, it also re-calculates any of its child views. In the case of ContentView, HitMeButton is one of its child views.

Let’s take a look at HitMeButton.

Inside HitMeButton, we have created bindings to our @State variables so we can access them inside this view.

When the button is tapped, we set alertIsVisible to true. Again, since this is a state variable owned by ContentView, this means that by setting this to true, SwiftUI will re-calculate ContentView’s body, and hence also HitMeButton’s body, since it’s a child view.

The body of the HitMeButton currently calls the .alert() method on the button, which makes it so that when alertIsVisible is true, it presents an alert in the default style.

OK - that’s how things currently work - now how can we change things to display our custom alert instead?

Well first things first, we can remove the .alert() method we were calling on the button, as we won’t be using the default alert anymore.

But we’ll still use the alertIsShowing property on ContentView. Since we know that whenever this value changes, SwiftUI will recalculate the view’s body, we’ll simply add an if statement inside ContentView’s body method.

The if statement will check ot see if alartIsShowing is true. If alertIsShowing is true, we’ll include the PointsView inside our VStack. If alertIsShowing is false, we’ll include the HitMeButton inside our VStack.

We also need to make a few other adjustments as well, but I’ll show you those as code this up. Let’s give this a try in Xcode.

Delete from HitMeButton:

.alert(isPresented: $alertIsVisible) {
  let roundedValue = Int(sliderValue.rounded())
  let points = game.points(sliderValue: roundedValue)
  return Alert(
    title: Text("Hello there!"),
    message: Text("The slider's value is \(roundedValue).\n" +
                    "You scored \(points) points this round."),
    dismissButton: .default(Text("Awesome!")) {
      game.startNewRound(points: points)
    })
}

In ContentView.swift, temporarily set alertIsVisible to true:

@State private var alertIsVisible = true

Add display of alert:

if alertIsVisible {
  PointsView()
} else {
  HitMeButton(alertIsVisible: $alertIsVisible, sliderValue: $sliderValue, game: $game)
}

Hide slider:

if !alertIsVisible {
  SliderView(sliderValue: $sliderValue)
}

Adjust padding:

.padding(.bottom, alertIsVisible ? 0 : 100)

Set alertIsVisible back to false:

@State private var alertIsVisible = false

Our new PointsView now appears in our view when you tap the Hit Me Button, and it looks great.

Now we need to modify PointsView so that when you tap the Start New Round button, it starts a new round on the game, and dismisses the alert. We also need to modify PointsView to display the actual slider value and points, rather than hard-coded numbers.

To do this, we need to pass bindings to the three State variables to PointsView, like we’ve done a few times earlier in this course. Let’s give it a try.

Add to PointsView.swift (copy from HitMeButton):

@Binding var alertIsVisible: Bool
@Binding var sliderValue: Double
@Binding var game: Game

Modify call to PointsView (copy from HitMeButton):

PointsView(alertIsVisible: $alertIsVisible, sliderValue: $sliderValue, game: $game)

Modify PointsView_Previews:

struct PointsView_Previews: PreviewProvider {

  static private var alertIsVisible = Binding.constant(false)
  static private var sliderValue = Binding.constant(50.0)
  static private var game = Binding.constant(Game())

  static var previews: some View {
    PointsView(alertIsVisible: alertIsVisible, sliderValue: sliderValue, game: game)
    PointsView(alertIsVisible: alertIsVisible, sliderValue: sliderValue, game: game)
      .previewLayout(.fixed(width: 568, height: 320))
    PointsView(alertIsVisible: alertIsVisible, sliderValue: sliderValue, game: game)
      .preferredColorScheme(.dark)
    PointsView(alertIsVisible: alertIsVisible, sliderValue: sliderValue, game: game)
      .previewLayout(.fixed(width: 568, height: 320))
      .preferredColorScheme(.dark)
  }
}

In PointsView, add to top of body:

let roundedValue = Int(sliderValue.rounded())
let points = game.points(for: roundedValue)

Modify BigNumberText and BodyText:

BigNumberText(text: String(roundedValue))
BodyText(text: "You scored \(points) Points\n🎉🎉🎉")

Add inside Button:

alertIsVisible = false
game.startNewRound(points: points)

Build & run, and show it working!