← All daysDay 18 of 49 · Part 5: Lists and navigation
Day 18

Split views and adapting to screen size · Sheets, alerts, and popovers

These are the lesson notes for this day. Its build-along tutorial and flash cards have not been written yet.

Lesson 20

Split views and adapting to screen size

NavigationSplitView shows two or three columns — sidebar, content, detail — on iPad and Mac, and collapses to a single stack on iPhone by itself. To adapt your own layouts, read the size class with @Environment(\.horizontalSizeClass), or use ViewThatFits, which tries its children in order and shows the first one that fits the space available.

Do not design for particular devices. As of iOS 27, iPhone apps can be resized — on iPad, and through iPhone Mirroring on a Mac — so an iPhone app may be shown at many different sizes.

Exercise

Build a three-column app with NavigationSplitView and run it in both an iPhone and an iPad simulator. Then make a row that shows its icon and text side by side when there is room, and stacked when there is not, using ViewThatFits.

After this lesson you should be able to answer
  1. What does NavigationSplitView do on an iPhone?
  2. What is a size class, and how do you read it?
  3. Why can you not assume that an iPhone app always runs at the size of an iPhone screen?
Resources
Lesson 21

Sheets, alerts, and popovers

Presentations are driven by state. .sheet(isPresented:) shows a sheet while a Boolean is true. .sheet(item:) shows a sheet while an optional value is not nil, and passes that value to the sheet — use this form whenever the sheet displays or edits one particular thing. You dismiss a sheet by setting the state back, or from inside the sheet with @Environment(\.dismiss). .presentationDetents([.medium, .large]) makes a sheet resizable. .alert, .confirmationDialog, .popover and .fullScreenCover follow the same pattern, and as of iOS 27 alerts and confirmation dialogs accept an item binding too.

Coming from UIKit: there is no present(_:animated:) call. You change state, and the presentation follows.

Example
struct TripRows: View {
    let trips: [Trip]
    @State private var editing: Trip?          // nil means no sheet

    var body: some View {
        List(trips) { trip in
            Button(trip.name) { editing = trip }    // setting it presents the sheet
        }
        .sheet(item: $editing) { trip in            // shown while `editing` is not nil
            Text("Editing \(trip.name)")
                .presentationDetents([.medium, .large])
        }
    }
}
Exercise

Make tapping a row open a sheet that edits that row's item, with medium and large detents and a Save button that dismisses it.

After this lesson you should be able to answer
  1. When should you use .sheet(item:) rather than .sheet(isPresented:)?
  2. How does a sheet dismiss itself?
  3. What do presentation detents control?
Resources