16.
Sheets & Alert Views
Written by Bill Morefield
In a previous chapter, you learned how to use the 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.
Open the starter project for this chapter; you’ll find the project from the end of the last chapter.
In this chapter, you’ll expand the app to use different conditional views in SwiftUI.
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 are useful when you want to 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 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: \(self.isPresented)")
},
// 5
content: {
FlightSearchDetails(flight: flight)
}
)
Here’s what the elements of the modal sheet do:
- You wrap the row inside a button. The action of the button toggles the state variable.
- 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. - Here, you pass the
isPresentedstate variable you added earlier, which tells SwiftUI to show the modal when the variable becomestrue. When the user dismisses the modal, SwiftUI sets the state back tofalse. - 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. Here, you print a message to the console and show that the state variable’s value is nowfalse. - 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 existingFlightSearchDetails(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:
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, but it makes 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") {
self.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.
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:
- The view only displays when the flight status is
.canceled. - The button sets
rebookAlerttotruewhen tapped. - You call
alert(isPresented:content:)on theButtonto create the alert. You also pass in the state variable telling SwiftUI to show the alert whenrebookAlertbecomestrue. - In the closure,
Alertdefines 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.
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.
As with a modal sheet, you can also trigger the alert by binding it to an optional variable. You’ll use this method and implement an action sheet in the next section.
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 that 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.
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 you create 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
self.checkInFlight =
CheckInInfo(
airline: self.flight.airline,
flight: self.flight.number
)
}
// 3
.actionSheet(item: $checkInFlight) { flight in
// 4
ActionSheet(
title: Text("Check In"),
message: Text("Check in for \(flight.airline)" +
"Flight \(flight.flight)"),
// 5
buttons: [
// 6
.cancel(Text("Not Now")),
// 7
.destructive(Text("Reschedule"), action: {
print("Reschedule flight.")
}),
// 8
.default(Text("Check In"), action: {
print(
"Check-in for \(flight.airline) \(flight.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:
- You only show this button for a flight that has check-in available
- The button’s action sets
checkInFlightto a new instance ofCheckInInfothat stores the airline and number of the flight. - As you did with the alert, you add the action sheet to the button. Here, you use
actionSheet(item:content:)and notactionSheet(isPresented:content:). You pass the optional variable as theitem:parameter. When the variable becomes non-nil, as it will when the button’s action executes, SwiftUI displays the action sheet. WhencheckInFlightbecomes 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. - 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.
- 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.Buttonitems to thebuttons:parameter for the ones you wish to use in this action sheet. - The first defined button is the Cancel button. Providing a cancel button gives the user a clear back-out option. You do nothing when the user selects this option, so you don’t need any parameter other than text for this button.
- You use the
.destructivetype 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. - 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.
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.
Closely related to the action sheet is the final type of modal view in SwiftUI: the popover. 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. For this example, you’ll use a Bool state variable, 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: self.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.
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.
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
Boolstate variable or an optional state variable that implements theIdentifiableprotocol 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.
- 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 the 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:
- iOS: https://developer.apple.com/design/human-interface-guidelines/ios/app-architecture/modality/
- macOS: https://developer.apple.com/design/human-interface-guidelines/macos/app-architecture/modality/
- watchOS: https://developer.apple.com/design/human-interface-guidelines/watchos/app-architecture/modal-sheets/
The WWDC 2019 SwiftUI Essentials video also provides an overview of Apple’s guidelines on how views, navigation and lists fit together: