Chapters

Hide chapters

SwiftUI by Tutorials

Fourth Edition · iOS 15, macOS 12 · Swift 5.5 · Xcode 13.1

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

17. Sheets & Alert Views
Written by Bill Morefield

In a previous chapter, you learned how to use standard navigation to switch between views in your app. However, sometimes you need to display a view to the user only under certain conditions. You’ll often use these views when showing important messages that interrupt the user’s current context and need direct feedback or response before continuing.

Presenting a view outside the navigation stack lets the user’s focus remain on the task they initiated. It also provides a way for your app to provide critical information or request essential feedback.

Starting in SwiftUI 3.0, Apple appears to be shifting the approach to these views. The initial versions of SwiftUI focused on the type of view to display. The changes to APIs and new modifiers in SwiftUI 3.0 indicate a shift to the view’s purpose instead of the kind of view. In this chapter, you’ll expand the app to use different conditional views in SwiftUI. Along the way, you’ll explore the new SwiftUI 3.0 APIs to prepare your app for the future.

Displaying a modal sheet

In Chapter 14: Lists, you built a view allowing users to search for a flight. One element deferred then was the ability to view details or interact with those results. You’re going to add that ability in this chapter. Modal sheets help focus the user’s attention on the current view without building through the overall navigation hierarchy. The modal sheet slides a new view over the current view.

SwiftUI provides two ways to display a modal, both based on a @State variable in the view. The first method uses a Bool variable that you set to true when the sheet should display. The second method uses an optional state variable that shows the modal when the variable becomes non-nil. You’ll use the Bool method for this modal.

All modals provide these two options; you’ll see an example using an optional variable later in this chapter.

Open the starter project for this chapter; you’ll find the project from the end of the last chapter. Go to SearchResultRow.swift. Notice the view for each row now resides in a separate view. That will make the code changes for this chapter a little cleaner. Add the following new variable after flight:

@State private var isPresented = false

This line defines a @State variable that indicates when to show the modal sheet. Change the view to:

// 1
Button(
  action: {
    isPresented.toggle()
  }, label: {
    FlightSearchSummary(flight: flight)
  })
  // 2
  .sheet(
    // 3
    isPresented: $isPresented,
    // 4
    onDismiss: {
      print("Modal dismissed. State now: \(isPresented)")
    },
    // 5
    content: {
      FlightSearchDetails(flight: flight)
    }
  )

Here’s what the elements of the modal sheet do:

  1. You wrap the row inside a button. The action of the button toggles the state variable.
  2. To tell SwiftUI you want to display a modal, you call sheet(isPresented:onDismiss:content:). This call must attach to an element of the view.
  3. Here, you pass the isPresented state variable you added earlier, which tells SwiftUI to show the modal when the variable becomes true. When the user dismisses the modal, SwiftUI sets the state back to false.
  4. The optional onDismiss: is a closure you can use to execute code after the user dismisses the modal. In an app, this would be the place to react to user actions in the modal. You print a message to the console and show that the state variable’s value is now false.
  5. You provide the view to show on the modal sheet as the closure for sheet(isPresented:onDismiss:content:). For the moment, you’ll use the existing FlightSearchDetails(flight:) view.

Build and run, navigate to Search Flights and tap any row to see the modal appear. Swipe down on the modal to dismiss it. In the debug console, you’ll see the state variable become false after you dismiss the modal:

Initial Modal view
Initial Modal view

Programmatically dismissing a modal

You probably noticed that the navigation view disappears in the modal sheet. That’s because a modal sheet takes over the whole screen and no longer wraps the view in any existing navigation view. You can create a new navigation view on the modal, making an entirely new navigation view stack.

You should also add a button to dismiss the modal, primarily since some platforms, such as Catalyst apps, don’t support the swipe gesture.

Open FlightSearchDetails.swift. First, you’ll need a variable to store a @Binding to the passed display flag from FlightRow. So add the following code after flight:

@Binding var showModal: Bool

You’ll add the button next to the header at the top of the modal. Replace FlightDetailHeader with this:

HStack {
  FlightDetailHeader(flight: flight)
  Spacer()
  Button("Close") {
    showModal = false
  }
}

You are adding a Button with an action to set the binding to false. Assigning false to the Binding programmatically from the button tells SwiftUI to close the modal.

Since the view now expects the caller to pass in the state, you need to update the preview to do so. Change the preview to read:

FlightSearchDetails(
  flight: FlightData.generateTestFlight(date: Date()),
  showModal: .constant(true)
).environmentObject(AppEnvironment())

Using .constant(true) provides a pseudo-state that lets the preview behave correctly.

Now, go back to SearchResultRow.swift and change the call to FlightSearchDetails in the closure to sheet(isPresented:onDismiss:content:) to pass in the state:

FlightSearchDetails(
  flight: flight,
  showModal: $isPresented
)

Run the app again. Tapping on the row now brings up the modal with a Close button in the navigation bar. Tapping the button dismisses the modal, just as swiping down does.

Modal done
Modal done

SwiftUI allows you to prevent the swipe action from dismissing a view. Go back to FlightSearchDetails.swift. Add the following code to the end of the ZStack view (after the onAppear(perform:) modifier):

.interactiveDismissDisabled()

Run the app, and you’ll see swiping down no longer dismisses the view. The view does dip, but it returns to the displayed state when you stop your gesture. You now have to use the Close button to dismiss the modal.

A modal is an excellent choice when your view needs the user’s full attention. Used correctly, they help your user focus on relevant information and improve the app experience. However, modal views interrupt the app experience, so you should use them sparingly. SwiftUI provides three more specialized modal views to help you capture the user’s attention: alerts, action sheets and popovers. You’ll learn how to use each of those now.

Creating an alert

Alerts bring something important to the user’s attention, such as a warning about a problem or a request to confirm an action that could have severe consequences.

You’re going to add a button to help the user rebook a canceled flight. It won’t do anything yet — you’re waiting on the back-end team to finish that API. Instead, you’ll display an alert telling the user to contact the airline much as you would in the event of an error.

Open FlightSearchDetails.swift. You can set alerts, like modals, to display based on a state variable. Add the following state after the showModal Binding:

@State private var rebookAlert = false

Add the following after the FlightDetailHeader HStack and before FlightInfoPanel:

// 1
if flight.status == .canceled {
  // 2
  Button("Rebook Flight") {
    rebookAlert = true
  }
  // 3
  .alert(isPresented: $rebookAlert) {
    // 4
    Alert(
      title: Text("Contact Your Airline"),
      message: Text(
        "We cannot rebook this flight. Please contact the airline to reschedule this flight."
      )
    )
  }
}

Here’s what you’re doing with this code:

  1. The view only displays when the flight status is .canceled.
  2. The button sets rebookAlert to true when tapped.
  3. You call alert(isPresented:content:) on the Button to create the alert. You also pass in the state variable telling SwiftUI to show the alert when rebookAlert becomes true.
  4. In the closure, Alert defines the alert message to show the user. You don’t provide any additional buttons, so the user’s only option is to tap the OK button to dismiss the alert.

Build and run. Tap Search Flights, then tap any Canceled flight (look for Pacific 228 From Dallas/Ft. Worth). Tap on the Rebook Flight button, and the alert appears.

Alert Dialog
Alert Dialog

If you’re familiar with iOS and iPadOS development, you’ll see that the Alert method in SwiftUI has some limitations. The current SwiftUI alert doesn’t support adding a text field for feedback like what’s supported in UIAlertController for iOS. You’ll need to create a modal sheet to perform that task.

SwiftUI 3.0 introduced a new API for alerts that works more like an action sheet that you’ll work with later in this chapter. While you will need to use the Alert structure for compatibility with older versions of SwiftUI, you’ll now convert it to use the new format.

Replace the current .alert modifier with:

// 1
.alert("Contact Your Airline", isPresented: $rebookAlert) {
  // 2
  Button("OK", role: .cancel) {
  }
  // 3
} message: {
  Text(
    "We cannot rebook this flight. Please contact the airline to reschedule this flight."
  )
}

This code should all look familiar since you’re doing the same task with different formatting.

  1. The alert now includes the title inside the alert(_:isPresented:actions:message:) modifier. You still use the same rebookAlert boolean to trigger the alert when it becomes true.
  2. Instead of an Alert struct, you provide a button for the options you want to show in the alert. Note the use of the .cancel role on the button. You tell SwiftUI this button cancels the alert, and therefore SwiftUI will automatically set rebookAlert to false. If you do not include a cancel button, then SwiftUI will add one for you.
  3. You now pass the message for the alert as an additional parameter of the alert(_:isPresented:actions:message:) modifier.

Run the app, and you’ll see this works as the previous version did. Unless you need backward compatibility, you should use this new format for alerts.

Alert Dialog
Alert Dialog

You can also trigger the alert with a modal sheet by binding it to an optional variable. In the next section, you’ll use this method and implement an action sheet.

Adding an action sheet

An action sheet should appear in response to a user action, and the user should expect it to appear. For example, you might want to use an action sheet to confirm an action or let the user select between multiple options.

In this section, you’ll add a button to let the user check in for a flight and display an action sheet to confirm the request.

Instead of the Boolean state variable you used for the modal sheet and alert, you’ll use an optional variable. You can use either of these methods with any of the modal views in this chapter.

There are a couple of reasons you would use this method over the Boolean variable. First, none of the views discussed in this chapter can be used more than once for a view. If you try to attach two alert views, for example, only the last one will work. You can attach an alert, modal, or action sheet to sibling views in a view hierarchy, but you can’t attach more than one to the same view (or to a child and parent). Using an optional enum, you can use just one, but specify which content you need to display based on the enum.

The second reason to use the optional variable over the Boolean is to access the variable’s data inside the closure. The variable must implement the Identifiable protocol discussed in the previous chapter.

You’ll create a simple struct that implements Identifiable for this action sheet for your next step. Create a new Swift file named CheckInInfo.swift under the Models group. Change the contents of the file to read:

import SwiftUI

struct CheckInInfo: Identifiable {
  let id = UUID()
  let airline: String
  let flight: String
}

Here, you define a new CheckInInfo struct that implements Identifiable. To meet the protocol requirements, you include an id member of type UUID.

By definition, a UUID provides a unique value and implements the Hashable protocol, making it a perfect unique identifier when you don’t care about anything other than it is unique. You then add airline and flight strings, which you’ll provide when creating the message.

Now, inside FlightSearchDetails, add the following state variable to hold CheckInInfo after your current state variable at the top of the view:

@State private var checkInFlight: CheckInInfo?

Next, add the following code after the alert you added in the last section and before the FlightInfoPanel view:

// 1
if flight.isCheckInAvailable {
  Button("Check In for Flight") {
    // 2
    checkInFlight =
      CheckInInfo(
        airline: flight.airline,
        flight: flight.number
      )
  }
  // 3
  .actionSheet(item: $checkInFlight) { checkIn in
    // 4
    ActionSheet(
      title: Text("Check In"),
      message: Text("Check in for \(checkIn.airline)" +
        "Flight \(checkIn.flight)"),
      // 5
      buttons: [
        // 6
        .cancel(Text("Not Now")),
        // 7
        .destructive(Text("Reschedule")) {
          print("Reschedule flight.")
        },
        // 8
        .default(Text("Check In")) {
          print(
            "Check-in for \(checkIn.airline) \(checkIn.flight)."
          )
        }
      ]
    )
  }
}

This code looks similar to the code you used to create the modal sheet and the alert, except that the action sheet uses the optional variable in place of a Bool. It also needs information about the buttons to display.

Here’s how the new elements in this code work:

  1. You only show this button for a flight that has check-in available
  2. The button’s action sets checkInFlight to a new instance of CheckInInfo that stores the airline and number of the flight.
  3. As you did with the alert, you add the action sheet to the button. Here, you use actionSheet(item:content:) and not actionSheet(isPresented:content:). You pass the optional variable as the item: parameter. When the variable becomes non-nil, as it will when the button’s action executes, SwiftUI displays the action sheet. When checkInFlight becomes non-nil, it triggers the same way the alert’s Boolean binding told SwiftUI to display the alert. You also provide a parameter inside the closure. When SwiftUI shows the sheet, this parameter contains the contents of the bindable value that triggered it.
  4. You create an action sheet using the passed-in variable’s contents to display the name of the flight to the user on the action sheet.
  5. An alert provides a limited ability to gather feedback. You have many more options with an action sheet, though all must be buttons. Here, you pass an array of ActionSheet.Button items to the buttons: parameter for those you wish to use in this action sheet.
  6. The first defined button is the Cancel button. Providing a cancel button gives the user a clear back-out option. When the user selects this option, you do nothing, so you don’t need any parameter other than text for this button.
  7. You use the .destructive type method for actions that have destructive or dangerous results. SwiftUI displays the text in red to highlight this action’s seriousness. action: provides code that SwiftUI executes when the user selects this option. Here, you display a message to the debug console.
  8. The default button for the action sheet uses action: to display a message to the debug console.

Build and run. Select Search Flights and then tap any outgoing flight that’s not yet departed. Next, tap the Check In for Flight button, and the action sheet will appear.

Action Sheet
Action Sheet

If you tap the Not Now button, nothing happens since you provided no action parameter. Tap either the Check In or Reschedule button, and the appropriate message appears in the console window in the debug area of Xcode.

Action sheet console
Action sheet console

In the next section, you’ll explore how the new SwiftUI 3.0 alert can work in place on an action sheet.

Using alerts as action sheets

The new SwiftUI alert format allows it to work very similarly to an action sheet. In this section, you’ll implement the action sheet from the last section using the new alert.

Add the following state to the top of the view after showFlightHistory:

@State private var showCheckIn = false

The new alert API doesn’t work with an optional parameter. Instead, you must use a boolean to indicate when SwiftUI should show the alert. Replace the current button labeled Check In for Flight with:

Button("Check In for Flight") {
  checkInFlight =
    CheckInInfo(
      airline: flight.airline,
      flight: flight.number
    )
  showCheckIn = true
}

The only change is that you set the showCheckIn true and set the checkInFlight property with information on the flight. Last, replace the current .actionSheet modifier (starting at comment three in the earlier code) with:

// 1
.alert("Check In", isPresented: $showCheckIn, presenting: checkInFlight) { checkIn in
  // 2
  Button("Check In") {
    print(
      "Check-in for \(checkIn.airline) \(checkIn.flight)."
    )
  }
  // 3
  Button("Reschedule", role: .destructive) {
    print("Reschedule flight.")
  }
  // 4
  Button("Not Now", role: .cancel) { }
  // 5
} message: { checkIn in
  Text("Check in for \(checkIn.airline)" +
    "Flight \(checkIn.flight)")
}

You’re replacing the action sheet with the impressive length alert(_:isPresented:presenting:actions:message:) modifier. As when you changed the alert to this new API earlier, you’ll see a lot of the same code. Here’s what’s changed:

  1. The title moves to a parameter to the alert(_:isPresented:presenting:actions:message:) modifier. You pass the new boolean to the isPresented parameter to indicate when SwiftUI should show the view. The new presenting parameter provides the function that binding to a nullable object previously did. You pass in the checkInFlight property here to make it available inside the alert as flight.
  2. You provide each option for the alert as a standard SwiftUI button view. You can use flight to access the object passed in through the presenting parameter.
  3. You mark this button as destructive, letting SwiftUI format it appropriately.
  4. As earlier, if you do not provide a button with the cancel role, SwiftUI will add one for you. Notice that you do not need to set showCheckIn to false as the framework assumes this from the button role.
  5. The message for the action sheet before now becomes another parameter. The object passed to the presenting parameter is available inside the closure as with the alert.

Run the app, and you’ll see the new alert doing the same role as the action sheet, though with a different user interface. Which to use will likely depend on the use of the view in your app.

Action Sheet using Alert
Action Sheet using Alert

As of SwiftUI 3.0, SwiftUI has not deprecated the actionSheet modifier as with the older Alert struct, but as you can see, the new alert API can handle both roles. In keeping with the new focus on purpose, SwiftUI 3.0 also added another new modifier named confirmationDialog. It works almost exactly like the alert dialog you just implemented. You can replace alert with confirmationDialog in the code you did in this section, and it will work with no other changes.

Replace the alert modifier with confirmationDialog and run the app. You’ll see the view now looks like the action sheet.

Confirmation Dialog
Confirmation Dialog

You’ll notice the result looks much like the original action sheet. The confirmationDialog displays as an action sheet on smaller devices and a popover on larger devices. You can also specify a popover directly in SwiftUI. In the next section, you’ll add a popover to the app.

Showing a popover

Like the action sheet, you usually display a popover in response to a user action. Popovers work best on larger-screen devices, such as iPads and Macs. On devices with smaller screens, a full-screen view, such as a modal sheet, better serves your needs. If the screen is too tiny, SwiftUI renders the popover as a modal sheet instead.

Your popover should save state changes immediately when it displays because the user can dismiss it at any time.

Creating and using a popover works much like an alert and action sheet. You can use a Boolean or optional type as with the other modal views. You’ll use a Bool state variable for this example, as you did with the alert.

You’ll add a button that shows a popover with a new FlightTimeHistory view that shows the flight’s recent history in a list.

Start by opening FlightSearchDetails.swift and adding the code for a new state variable after the existing ones:

@State private var showFlightHistory = false

Now, add the following code after the alert you added in the last section and before the FlightInfoPanel view:

Button("On-Time History") {
  showFlightHistory.toggle()
}
.popover(
  isPresented: $showFlightHistory,
  arrowEdge: .top) {
  FlightTimeHistory(flight: flight)
}

Again the code resembles that used to add an alert to the view earlier. Alerts, action sheets and popovers all perform the same task — providing a temporary view to inform the user and, optionally, gather a response. As a result, they operate in similar ways. popover(isPresented:attachmentAnchor:arrowEdge:content:) watches the showFlightHistory state variable to see if it should show the pop-up.

Popovers traditionally show an arrow pointing back to the control that initiated the popover. arrowEdge defines the arrow’s direction. Here, .top instructs the popover sheet to display an arrow at its top, pointing to the control. That means the popover shows below the control.

Otherwise, this code should look familiar. The button toggles showFlightHistory to true, causing the popover to appear.

If you’re using an iPhone device or simulator, you’ll see that the popover renders as a modal due to the screen size. Also, note how the new modal nicely stacks on top of your existing modal. You can dismiss it by swiping down, as you would with a modal view.

Popover phone
Popover phone

Now, build and run with an iPad target and follow the same steps to display the on-time history. You’ll now see the view render as a pop-up that includes a small arrow back to the button you tapped to display the view. You can dismiss it by tapping anywhere outside the view. Note that it also is stacked nicely on top of the existing modal view.

Popover display
Popover display

As you can see, Apple provides you with different options to grab the users’ attention. Try using the best choice for each situation and scenario.

Key points

  • Modal sheets display on top of the view. You can use either a Bool state variable or an optional state variable that implements the Identifiable protocol to tell SwiftUI to display them.
  • The alert, action sheet and popover views provide a standard way to display information to the user and collect feedback.
  • SwiftUI 3.0 introduced a new API for alerts that provides more flexibility and an easier to understand implementation.
  • Alerts generally display information about unexpected situations or confirm actions that have severe consequences.
  • Action sheets and popovers display in response to a user action. You use action sheets for smaller screen devices and popovers on larger screens.

Where to go from here?

As mentioned in a previous chapter, the first stop for information on user interfaces on Apple platforms should be the Human Interface Guidelines on Modality for the appropriate SwiftUI operating systems:

The WWDC 2019 SwiftUI Essentials video also provides an overview of Apple’s guidelines on how views, navigation and lists fit together:

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.