Stanford CS193p: iOS Development with SwiftUI | 2025 | L11: iPad and Mac
By Unknown Author
Share:
Key Concepts
- Cross-Platform Development: Adapting an app to work on different Apple platforms (iPad, Mac) from an iPhone base.
- NavigationSplitView: A SwiftUI container that divides the screen into multiple panes (typically two or three) for displaying content, ideal for larger screen sizes like iPad and Mac.
- NavigationStack: A SwiftUI container that manages navigation by stacking views like cards, suitable for linear navigation flows.
- ViewBuilders: Closures that define the content of a view, used extensively in navigation containers.
- NavigationLink: A control that allows users to navigate to a new view.
.navigationDestination(): A modifier that associates a specific data type with a destination view for navigation..navigationSplitViewStyle(): A modifier to control the behavior and appearance of aNavigationSplitView, with options like.balanced.columnVisibility: A property ofNavigationSplitViewthat controls which panes are displayed, configurable via aBindingor.constant.- Size Classes: A system used by Apple platforms to describe the available screen space (e.g., compact, regular) which influences how
NavigationSplitViewbehaves. @Environment: A property wrapper used to access environment values, such as size classes.ListSelection: Managing which item is currently selected in aListusing aBinding..selection:Modifier: A modifier forListthat binds to a selection variable.@State: A property wrapper for managing local view state.@Bindable: A property wrapper required when creating bindings to variables within an@Observableobject.@Observable: A macro for creating observable objects that automatically manage state changes.TextField: A SwiftUI view for editing text.Form: A SwiftUI container for collecting user input, providing structure and automatic scrolling above the keyboard.ColorPicker: A SwiftUI view for selecting colors..contextMenu(): A modifier to add a context menu (accessible via long-press or right-click) to a view.withAnimation: A function to animate changes to view properties..destructiveRole: A role forButtonthat indicates a destructive action, often styled in red.onChange(of:): A view modifier that performs an action when a specific value changes.- Refactoring: Restructuring code to improve readability and maintainability, often by extracting views into separate files.
@Previewable: A property wrapper used in previews to make@Stateor@Observableproperties observable.GameEditorView: A new view designed to allow users to edit game properties like name and peg choices.GameListView: A new view encapsulating the list of games.GameChooserView: The main view that orchestrates navigation and displays theGameListandCodeBreakerView.
Cross-Platform Adaptation with NavigationSplitView
The lecture begins by addressing the challenge of adapting an iPhone app for larger screens like the iPad. The current app, when run on an iPad, appears in a "gigantic mode" that doesn't effectively utilize the available screen real estate.
Transitioning from NavigationStack to NavigationSplitView
- Problem: The existing
NavigationStackis designed for linear navigation (like a stack of cards), which is not optimal for displaying multiple pieces of information simultaneously on an iPad. - Solution: Replace
NavigationStackwithNavigationSplitView. NavigationSplitViewStructure: UnlikeNavigationStackwhich takes a single root view,NavigationSplitViewhas threeViewBuilderparameters:content: The leftmost pane.detail: The rightmost pane.- (Implicitly, a third pane can exist, often for inspectors or ancillary information, though not used in this specific example).
- Initial Implementation:
- The
GameChooserview, which previously contained theListof games, is now thecontentpane. - The
CodeBreakerView(the game itself) is intended for thedetailpane. - An error "Missing argument for parameter 'detail' in this call" arises because
NavigationSplitViewrequires adetailargument.
- The
- Handling the
detailPane:- Initially, a
Text("Choose a game!")is placed in thedetailpane. This serves as a placeholder when no game is selected. - The lecture notes that
NavigationLinkand.navigationDestination()still function withinNavigationSplitView, automatically populating thedetailpane when a link is activated.
- Initially, a
NavigationSplitViewStyle:- By default,
NavigationSplitViewmight hide panes. To ensure both panes are visible, the.navigationSplitViewStyle(.balanced)modifier is applied. This balances the space between the panes. - The
.balancedstyle allows the user to interact with both panes simultaneously, unlike the default behavior where one pane might obscure the other.
- By default,
columnVisibility:- To make both panes appear by default upon app launch, the
columnVisibility:parameter is used. - This parameter requires a
BindingtoNavigationSplitViewVisibility. - An
@State private var columnVisibilityis introduced and initialized to.all(or.bothin some contexts) to ensure both panes are always shown. - The lecture highlights that even with
.all, the user can still hide panes. TheBindingallows communication between the system and the app regarding user preferences. - For cases where the developer always wants both panes visible and doesn't need to react to user changes,
.constant(.all)can be used forcolumnVisibility, simplifying the state management.
- To make both panes appear by default upon app launch, the
UI Adjustments for iPad
maxHeightModifier: To prevent thePegChooserwithin theCodeBreakerViewfrom being excessively large on the iPad, the.frame(maxHeight:)modifier is used. This limits the maximum height of thePegChooserto a specific value (e.g., 80 or 100), ensuring a better visual balance with the game itself. The lecture emphasizes avoiding.frame(height:)and.frame(width:)in favor of more adaptive modifiers likemaxHeight..navigationTitle(): Titles are added to the panes for better context.- The
Listin thecontentpane gets.navigationTitle("Code Breaker"). - The
CodeBreakerViewin thedetailpane gets.navigationTitle(game.name), dynamically setting the title to the selected game's name.
- The
.navigationBarTitleDisplayMode(.inline): To reduce the space taken by large titles, this modifier can be used to display the title inline within the navigation bar.
Cross-Platform Behavior on iPhone and Mac
iPhone Behavior
- When
NavigationSplitViewis used in an iPhone project, it gracefully degrades toNavigationStackbehavior. The app does not break; instead, it navigates to the detail view as it would have with aNavigationStack. - The lecture emphasizes avoiding platform-specific code (
if iPad...) whenever possible, as SwiftUI's adaptive components handle much of this automatically.
Mac Behavior
- "Mac (Designed for iPad)" Target: A Mac app can be created by selecting the "Mac (Designed for iPad)" destination in project settings. This allows the iOS/iPadOS code to be reused for a Mac application.
- Differences from iPad:
- No Swipe to Delete: Swiping gestures are not applicable with a mouse.
- Direct Manipulation: Items in a list can be directly dragged and dropped without needing an explicit "edit mode," unlike on iPad.
- Context Menus: Right-clicking on an item brings up a context menu.
.contextMenu()Modifier:- This modifier is used to add context menus.
- It takes a
ViewBuilderthat typically containsButtons. - Buttons within a context menu should ideally have both a
Textlabel and asystemImagefor better usability on iOS (where long-press triggers the menu). - A "Delete" button is implemented using
.contextMenu(), which removes the selected game from the data source.
- Animation and Destructive Actions:
- The deletion action is wrapped in
withAnimationto provide a smooth visual transition. - The
Buttonfor deletion is givenrole: .destructiveto visually indicate its destructive nature (styled in red on iOS).
- The deletion action is wrapped in
Managing List Selection and Data Persistence
List Selection with selection:
- Problem: When the app launches on iPad, it displays "Choose a game!" instead of a pre-selected game, which is not ideal. The
Listinternally manages its selection, making it inaccessible. - Solution: Use the
selection:parameter of theListmodifier. selection:Parameter:- This parameter takes a
Bindingto a variable that holds the selection. - The type of this selection variable should match the type of the items in the
List(e.g.,CodeBreaker?for an optionalCodeBreaker). - When a
NavigationLink'svalue:matches the selection type, clicking the link updates the selection. Conversely, setting the selection programmatically automatically triggers the correspondingNavigationLink.
- This parameter takes a
- Handling Different NavigationLink Values: If a
NavigationLinkhas avalue:that doesn't match the selection type (e.g., a cheat code link when expecting aCodeBreaker), the selection is set tonil. - Taking Ownership of Selection: When
selection:is used, the app takes over the management of the detail pane. The.navigationDestination()modifier is no longer needed for this specific navigation flow, as theif let selectionlogic in thedetailpane handles displaying theCodeBreakerView. - Programmatic Selection:
- To pre-select an item on launch, the
selectionvariable can be initialized with a specific item (e.g.,games.firstorgames.last). - The lecture demonstrates how to set the initial selection to the first or last game in the list.
- Random selection is also possible using
Int.random(in:).
- To pre-select an item on launch, the
onChange(of:) for Data Integrity
- Problem: If an item is deleted from the
gameslist while it's selected, theselectionvariable might still hold a reference to a non-existent item, leading to issues. - Solution: Use the
onChange(of: games)modifier to monitor changes in thegamesarray. - Logic: If the
gamesarray changes and the currentselectionis no longer present in thegamesarray, theselectionis set tonil. This ensures that the selected item remains valid. letvs.varfor Selection: The lecture clarifies thatselection = nilwill fail ifselectionis declared aslet. It must be avar(orself.selectionif within a closure that shadows the outerselection).
Code Cleanup and Refactoring
Extracting Views
GameListView: TheListof games is extracted into a separateGameListSwiftUI View. This improves modularity and organization.- The
GameListneeds to manage its owngamesdata and potentially share theselectionbinding with its parent. - The
addSampleGames()function is moved intoGameListand modified to only add games if the list is empty (games.isEmpty), preventing duplicate additions on reappear.
- The
- Helper Functions: Small, reusable pieces of code like
deleteButton(for: game:)andaddSampleGames()are extracted into their own functions.
GameEditor View for Editing
- Purpose: A new
GameEditorview is introduced to allow users to modify game properties like name and peg choices. @Bindablefor@Observable:- When creating bindings to variables within an
@Observableobject (likegame), the object itself must be marked with@Bindable var. This is a specific requirement for enabling the$prefix for bindings to@Observableproperties.
- When creating bindings to variables within an
FormContainer:- The
GameEditoruses aFormto structure the input fields.Formprovides automatic layout, scrolling above the keyboard, and visual separation of sections.
- The
TextFieldfor Name Editing:- A
TextFieldis used to edit the game's name. - The first argument is a label (e.g., "Name"), and the second is a
Bindingto the string to be edited ($game.name). - The
TextField's label appears as a placeholder that is replaced by the user's input.
- A
ColorPickerfor Peg Choices:- A
Listwithin theFormiterates overgame.pegChoices. - For each peg choice, a
ColorPickeris used. - The
ColorPickertakes aBindingto theColor(e.g.,$game.pegChoices[index]). - The
pegChoicesproperty in theCodeBreakermodel must be avar(notlet) to allow binding and modification. - A label like "Peg Choice (index + 1)" is provided for each
ColorPicker.
- A
@Previewablefor Previews:- When using
@Observableobjects in#Preview, they need to be marked with@Previewableto enable real-time updates and interaction within the preview canvas. This allowsonChangemodifiers to trigger.
- When using
SectioninForm: Forms can be organized intoSections, similar toList, for better visual grouping of related controls.
Next Steps and Assignment 4 Considerations
- Adding Pegs/Deleting Pegs: The
GameEditorcurrently allows editing existing peg choices but not adding or deleting them. This will be addressed in the next lecture. - Presenting the Editor: The mechanism for presenting the
GameEditorview (e.g., as a modal sheet or within a navigation flow) will be covered. This is crucial for Assignment 4, where users need to access settings. - SwiftData: The subsequent lecture will introduce SwiftData for persistent data storage.
- Assignment 4: The instructor acknowledges that Assignment 4 requires implementing settings functionality, which is directly related to the
GameEditorand presentation techniques being taught. Further clarification on Assignment 4 requirements will be provided on the course discussion forum. - Wednesday's Lecture: The plan is to complete the
GameEditorfunctionality, including adding/deleting pegs, and then focus on presenting the editor. The instructor hopes to have time for slides on SwiftData.
Chat with this Video
AI-PoweredLoad the transcript when you're ready to chat so the initial page stays lighter.