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 a NavigationSplitView, with options like .balanced.
  • columnVisibility: A property of NavigationSplitView that controls which panes are displayed, configurable via a Binding or .constant.
  • Size Classes: A system used by Apple platforms to describe the available screen space (e.g., compact, regular) which influences how NavigationSplitView behaves.
  • @Environment: A property wrapper used to access environment values, such as size classes.
  • List Selection: Managing which item is currently selected in a List using a Binding.
  • .selection: Modifier: A modifier for List that 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 @Observable object.
  • @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.
  • .destructive Role: A role for Button that 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 @State or @Observable properties observable.
  • GameEditor View: A new view designed to allow users to edit game properties like name and peg choices.
  • GameList View: A new view encapsulating the list of games.
  • GameChooser View: The main view that orchestrates navigation and displays the GameList and CodeBreakerView.

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 NavigationStack is 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 NavigationStack with NavigationSplitView.
  • NavigationSplitView Structure: Unlike NavigationStack which takes a single root view, NavigationSplitView has three ViewBuilder parameters:
    • 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 GameChooser view, which previously contained the List of games, is now the content pane.
    • The CodeBreakerView (the game itself) is intended for the detail pane.
    • An error "Missing argument for parameter 'detail' in this call" arises because NavigationSplitView requires a detail argument.
  • Handling the detail Pane:
    • Initially, a Text("Choose a game!") is placed in the detail pane. This serves as a placeholder when no game is selected.
    • The lecture notes that NavigationLink and .navigationDestination() still function within NavigationSplitView, automatically populating the detail pane when a link is activated.
  • NavigationSplitViewStyle:
    • By default, NavigationSplitView might hide panes. To ensure both panes are visible, the .navigationSplitViewStyle(.balanced) modifier is applied. This balances the space between the panes.
    • The .balanced style allows the user to interact with both panes simultaneously, unlike the default behavior where one pane might obscure the other.
  • columnVisibility:
    • To make both panes appear by default upon app launch, the columnVisibility: parameter is used.
    • This parameter requires a Binding to NavigationSplitViewVisibility.
    • An @State private var columnVisibility is introduced and initialized to .all (or .both in some contexts) to ensure both panes are always shown.
    • The lecture highlights that even with .all, the user can still hide panes. The Binding allows 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 for columnVisibility, simplifying the state management.

UI Adjustments for iPad

  • maxHeight Modifier: To prevent the PegChooser within the CodeBreakerView from being excessively large on the iPad, the .frame(maxHeight:) modifier is used. This limits the maximum height of the PegChooser to 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 like maxHeight.
  • .navigationTitle(): Titles are added to the panes for better context.
    • The List in the content pane gets .navigationTitle("Code Breaker").
    • The CodeBreakerView in the detail pane gets .navigationTitle(game.name), dynamically setting the title to the selected game's name.
  • .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 NavigationSplitView is used in an iPhone project, it gracefully degrades to NavigationStack behavior. The app does not break; instead, it navigates to the detail view as it would have with a NavigationStack.
  • 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 ViewBuilder that typically contains Buttons.
    • Buttons within a context menu should ideally have both a Text label and a systemImage for 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 withAnimation to provide a smooth visual transition.
    • The Button for deletion is given role: .destructive to visually indicate its destructive nature (styled in red on iOS).

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 List internally manages its selection, making it inaccessible.
  • Solution: Use the selection: parameter of the List modifier.
  • selection: Parameter:
    • This parameter takes a Binding to 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 optional CodeBreaker).
    • When a NavigationLink's value: matches the selection type, clicking the link updates the selection. Conversely, setting the selection programmatically automatically triggers the corresponding NavigationLink.
  • Handling Different NavigationLink Values: If a NavigationLink has a value: that doesn't match the selection type (e.g., a cheat code link when expecting a CodeBreaker), the selection is set to nil.
  • 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 the if let selection logic in the detail pane handles displaying the CodeBreakerView.
  • Programmatic Selection:
    • To pre-select an item on launch, the selection variable can be initialized with a specific item (e.g., games.first or games.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:).

onChange(of:) for Data Integrity

  • Problem: If an item is deleted from the games list while it's selected, the selection variable might still hold a reference to a non-existent item, leading to issues.
  • Solution: Use the onChange(of: games) modifier to monitor changes in the games array.
  • Logic: If the games array changes and the current selection is no longer present in the games array, the selection is set to nil. This ensures that the selected item remains valid.
  • let vs. var for Selection: The lecture clarifies that selection = nil will fail if selection is declared as let. It must be a var (or self.selection if within a closure that shadows the outer selection).

Code Cleanup and Refactoring

Extracting Views

  • GameList View: The List of games is extracted into a separate GameList SwiftUI View. This improves modularity and organization.
    • The GameList needs to manage its own games data and potentially share the selection binding with its parent.
    • The addSampleGames() function is moved into GameList and modified to only add games if the list is empty (games.isEmpty), preventing duplicate additions on reappear.
  • Helper Functions: Small, reusable pieces of code like deleteButton(for: game:) and addSampleGames() are extracted into their own functions.

GameEditor View for Editing

  • Purpose: A new GameEditor view is introduced to allow users to modify game properties like name and peg choices.
  • @Bindable for @Observable:
    • When creating bindings to variables within an @Observable object (like game), the object itself must be marked with @Bindable var. This is a specific requirement for enabling the $ prefix for bindings to @Observable properties.
  • Form Container:
    • The GameEditor uses a Form to structure the input fields. Form provides automatic layout, scrolling above the keyboard, and visual separation of sections.
  • TextField for Name Editing:
    • A TextField is used to edit the game's name.
    • The first argument is a label (e.g., "Name"), and the second is a Binding to the string to be edited ($game.name).
    • The TextField's label appears as a placeholder that is replaced by the user's input.
  • ColorPicker for Peg Choices:
    • A List within the Form iterates over game.pegChoices.
    • For each peg choice, a ColorPicker is used.
    • The ColorPicker takes a Binding to the Color (e.g., $game.pegChoices[index]).
    • The pegChoices property in the CodeBreaker model must be a var (not let) to allow binding and modification.
    • A label like "Peg Choice (index + 1)" is provided for each ColorPicker.
  • @Previewable for Previews:
    • When using @Observable objects in #Preview, they need to be marked with @Previewable to enable real-time updates and interaction within the preview canvas. This allows onChange modifiers to trigger.
  • Section in Form: Forms can be organized into Sections, similar to List, for better visual grouping of related controls.

Next Steps and Assignment 4 Considerations

  • Adding Pegs/Deleting Pegs: The GameEditor currently 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 GameEditor view (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 GameEditor and 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 GameEditor functionality, 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-Powered

Load the transcript when you're ready to chat so the initial page stays lighter.

Ready to summarize another video?

Summarize YouTube Video