More Videos
Streaming is available in most browsers,
and in the Developer app.
- Overview
- Code
Learn how you can use SwiftUI to build great apps for any Apple platform. Explore a fresh new look and feel for tabs and documents on iPadOS. Improve your window management with new windowing APIs, and gain more control over immersive spaces and volumes in your visionOS apps. We'll also take you through other exciting refinements that help you make expressive charts, customize and layout text, and so much more.
Chapters
- 0:00 - Introduction
- 0:51 - Fresh apps
- 1:04 - Fresh apps: TabView
- 2:22 - Fresh apps: Presentation sizing
- 2:39 - Fresh apps: Zoom transition
- 3:02 - Fresh apps: Custom controls
- 3:38 - Fresh apps: Vectorized and function plots
- 4:10 - Fresh apps: TableColumnForEach
- 4:25 - Fresh apps: MeshGradient
- 4:51 - Fresh apps: Document launch experience
- 5:33 - Fresh apps: SF Symbols 6
- 6:37 - Harnessing the platform
- 6:52 - Harnessing the platform: Windowing
- 8:28 - Harnessing the platform: Input methods
- 10:45 - Harnessing the platform: Widgets and Live Activities
- 12:25 - Intermezzo
- 12:55 - Framework foundations
- 13:09 - Framework foundations: Custom containers
- 13:48 - Framework foundations: Ease of use
- 16:18 - Framework foundations: Scrolling enhancements
- 17:18 - Framework foundations: Swift 6 language mode
- 18:01 - Framework foundations: Improved interoperability
- 19:18 - Crafting experiences
- 19:43 - Crafting experiences: Volumes
- 20:27 - Crafting experiences: Immersive spaces
- 21:27 - Crafting experiences: TextRenderer
- 22:12 - Next steps
Resources
- Forum: UI Frameworks
- SwiftUI updates
- HD Video
- SD Video
Related Videos
WWDC24
- Bring your Live Activity to Apple Watch
- Catch up on accessibility in SwiftUI
- Create custom hover effects in visionOS
- Create custom visual effects with SwiftUI
- Demystify SwiftUI containers
- Dive deep into volumes and immersive spaces
- Elevate your tab and sidebar experience in iPadOS
- Enhance your UI animations and transitions
- Evolve your document launch experience
- Extend your app’s controls across the system
- Migrate your app to Swift 6
- Squeeze the most out of Apple Pencil
- Swift Charts: Vectorized and function plots
- Tailor macOS windows with SwiftUI
- What’s new in SF Symbols 6
- Work with windows in SwiftUI
Download
Array1:38 - TabView
import SwiftUIstruct KaraokeTabView: View { @State var customization = TabViewCustomization() var body: some View { TabView { Tab("Parties", image: "party.popper") { PartiesView(parties: Party.all) } .customizationID("karaoke.tab.parties") Tab("Planning", image: "pencil.and.list.clipboard") { PlanningView() } .customizationID("karaoke.tab.planning") Tab("Attendance", image: "person.3") { AttendanceView() } .customizationID("karaoke.tab.attendance") Tab("Song List", image: "music.note.list") { SongListView() } .customizationID("karaoke.tab.songlist") } .tabViewStyle(.sidebarAdaptable) .tabViewCustomization($customization) }}struct PartiesView: View { var parties: [Party] var body: some View { Text("PartiesView") }}struct PlanningView: View { var body: some View { Text("PlanningView") }}struct AttendanceView: View { var body: some View { Text("AttendanceView") }}struct SongListView: View { var body: some View { Text("SongListView") }}struct Party { static var all: [Party] = []}#Preview { KaraokeTabView()}
2:28 - Presentation sizing
import SwiftUIstruct AllPartiesView: View { @State var showAddSheet: Bool = true var parties: [Party] = [] var body: some View { PartiesGridView(parties: parties, showAddSheet: $showAddSheet) .sheet(isPresented: $showAddSheet) { AddPartyView() .presentationSizing(.form) } }}struct PartiesGridView: View { var parties: [Party] @Binding var showAddSheet: Bool var body: some View { Text("PartiesGridView") }}struct AddPartyView: View { var body: some View { Text("AddPartyView") }}struct Party { static var all: [Party] = []}#Preview { AllPartiesView()}
2:39 - Zoom transition
import SwiftUIstruct PartyView: View { var party: Party @Namespace() var namespace var body: some View { NavigationLink { PartyDetailView(party: party) .navigationTransition(.zoom( sourceID: party.id, in: namespace)) } label: { Text("Party!") } .matchedTransitionSource(id: party.id, in: namespace) }}struct PartyDetailView: View { var party: Party var body: some View { Text("PartyDetailView") }}struct Party: Identifiable { var id = UUID() static var all: [Party] = []}#Preview { @Previewable var party: Party = Party() NavigationStack { PartyView(party: party) }}
3:18 - Controls API
import WidgetKitimport SwiftUIstruct StartPartyControl: ControlWidget { var body: some ControlWidgetConfiguration { StaticControlConfiguration( kind: "com.apple.karaoke_start_party" ) { ControlWidgetButton(action: StartPartyIntent()) { Label("Start the Party!", systemImage: "music.mic") Text(PartyManager.shared.nextParty.name) } } }}// Model codeclass PartyManager { static let shared = PartyManager() var nextParty: Party = Party(name: "WWDC Karaoke")}struct Party { var name: String}// AppIntentimport AppIntentsstruct StartPartyIntent: AppIntent { static let title: LocalizedStringResource = "Start the Party" func perform() async throws -> some IntentResult { return .result() }}
3:49 - Function plotting
See Also31 Best CSS Hover Effects 2024 - uiCookies15+ Best Microsoft Edge Extensions Inspired From Chrome (2020) - TechWiser25 Best Google Chrome Security Extensions and Top-Rated PluginsWebex App | Share content in a meetingimport SwiftUIimport Chartsstruct AttendanceView: View { var body: some View { Chart { LinePlot(x: "Parties", y: "Guests") { x in pow(x, 2) } .foregroundStyle(.purple) } .chartXScale(domain: 1...10) .chartYScale(domain: 1...100) }}#Preview { AttendanceView() .padding(40)}
4:18 - Dynamic table columns
import SwiftUIstruct SongCountsTable: View { var body: some View { Table(Self.guestData) { // A static column for the name TableColumn("Name", value: \.name) TableColumnForEach(Self.partyData) { party in TableColumn(party.name) { guest in Text(guest.songsSung[party.id] ?? 0, format: .number) } } } } private static func randSongsSung(low: Bool = false) -> [Int : Int] { var songs: [Int : Int] = [:] for party in partyData { songs[party.id] = low ? Int.random(in: 0...3) : Int.random(in: 3...12) } return songs } private static let guestData: [GuestData] = [ GuestData(name: "Sommer", songsSung: randSongsSung()), GuestData(name: "Sam", songsSung: randSongsSung()), GuestData(name: "Max", songsSung: randSongsSung()), GuestData(name: "Kyle", songsSung: randSongsSung(low: true)), GuestData(name: "Matt", songsSung: randSongsSung(low: true)), GuestData(name: "Apollo", songsSung: randSongsSung()), GuestData(name: "Anna", songsSung: randSongsSung()), GuestData(name: "Raj", songsSung: randSongsSung()), GuestData(name: "John", songsSung: randSongsSung(low: true)), GuestData(name: "Harry", songsSung: randSongsSung()), GuestData(name: "Luca", songsSung: randSongsSung()), GuestData(name: "Curt", songsSung: randSongsSung()), GuestData(name: "Betsy", songsSung: randSongsSung()) ] private static let partyData: [PartyData] = [ PartyData(partyNumber: 1, numberGuests: 5), PartyData(partyNumber: 2, numberGuests: 6), PartyData(partyNumber: 3, numberGuests: 7), PartyData(partyNumber: 4, numberGuests: 9), PartyData(partyNumber: 5, numberGuests: 9), PartyData(partyNumber: 6, numberGuests: 10), PartyData(partyNumber: 7, numberGuests: 11), PartyData(partyNumber: 8, numberGuests: 12), PartyData(partyNumber: 9, numberGuests: 11), PartyData(partyNumber: 10, numberGuests: 13), ] }struct GuestData: Identifiable { let name: String let songsSung: [Int : Int] let id = UUID()}struct PartyData: Identifiable { let partyNumber: Int let numberGuests: Int let symbolSize = 100 var id: Int { partyNumber } var name: String { "\(partyNumber)" }}#Preview { SongCountsTable() .padding(40)}
4:42 - Mesh gradients
import SwiftUIstruct MyMesh: View { var body: some View { MeshGradient( width: 3, height: 3, points: [ .init(0, 0), .init(0.5, 0), .init(1, 0), .init(0, 0.5), .init(0.3, 0.5), .init(1, 0.5), .init(0, 1), .init(0.5, 1), .init(1, 1) ], colors: [ .red, .purple, .indigo, .orange, .cyan, .blue, .yellow, .green, .mint ] ) }}#Preview { MyMesh() .statusBarHidden()}
5:14 - Document launch scene
DocumentGroupLaunchScene("Your Lyrics") { NewDocumentButton() Button("New Parody from Existing Song") { // Do something! }} background: { PinkPurpleGradient()} backgroundAccessoryView: { geometry in MusicNotesAccessoryView(geometry: geometry) .symbolEffect(.wiggle(.rotational.continuous()))} overlayAccessoryView: { geometry in MicrophoneAccessoryView(geometry: geometry)}
7:04 - Window styling and default placement
Window("Lyric Preview", id: "lyricPreview") { LyricPreview()} .windowStyle(.plain) .windowLevel(.floating) .defaultWindowPlacement { content, context in let displayBounds = context.defaultDisplay.visibleRect let contentSize = content.sizeThatFits(.unspecified) return topPreviewPlacement(size: contentSize, bounds: displayBounds) }}
7:30 - Window Drag Gesture
Text(currentLyric) .background(.thinMaterial, in: .capsule) .gesture(WindowDragGesture())
8:18 - Push window environment action
struct EditorView: View { @Environment(\.pushWindow) private var pushWindow var body: some View { Button("Play", systemImage: "play.fill") { pushWindow(id: "lyric-preview") } }}
8:47 - Hover effects
struct ProfileButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .background(.thinMaterial) .hoverEffect(.highlight) .clipShape(.capsule) .hoverEffect { effect, isActive, _ in effect.scaleEffect(isActive ? 1.05 : 1.0) } }}
9:14 - Modifier key alternates
Button("Preview Lyrics in Window") { // show preview in window}.modifierKeyAlternate(.option) { Button("Preview Lyrics in Full Screen") { // show preview in full screen }}.keyboardShortcut("p", modifiers: [.shift, .command])
9:32 - Responding to modifier keys
var body: some View { LyricLine() .overlay(alignment: .top) { if showBouncingBallAlignment { // Show bouncing ball alignment guide } } .onModifierKeysChanged(mask: .option) { showBouncingBallAlignment = !$1.isEmpty } }
9:55 - Pointer customization
ForEach(resizeAnchors) { anchor in ResizeHandle(anchor: anchor) .pointerStyle(.frameResize(position: anchor.position))}
10:23 - Pencil squeeze gesture
@Environment(\.preferredPencilSqueezeAction) var preferredAction var body: some View { LyricsEditorView() .onPencilSqueeze { phase in if preferredAction == .showContextualPalette, case let .ended(value) = phase { if let anchorPoint = value.hoverPose?.anchor { lyricDoodlePaletteAnchor = .point(anchorPoint) } lyricDoodlePalettePresented = true } }
13:13 - Custom containers
struct DisplayBoard<Content: View>: View { @ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { ForEach(subviewOf: content) { subview in CardView { subview } } } .background { BoardBackgroundView() } }}DisplayBoard { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) }}
13:35 - Custom containers with sectioning
DisplayBoard { Section("Matt's Favorites") { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") .displayBoardCardRejected(true) }Section("Sam's Favorites") { ForEach(songsFromSam) { song in Text(song.title) } }}
13:52 - Entry macro
extension EnvironmentValues { @Entry var karaokePartyColor: Color = .purple}extension FocusValues { @Entry var lyricNote: String? = nil}extension Transaction { @Entry var animatePartyIcons: Bool = false}extension ContainerValues { @Entry var displayBoardCardStyle: DisplayBoardCardStyle = .bordered}
14:12 - Default accessibility label augmentation
SongView(song) .accessibilityElement(children: .combine) .accessibilityLabel { label in if let rating = song.rating { Text(rating) } label }
14:52 - Previewable
#Preview { @Previewable @State var showAllSongs = true Toggle("Show All songs", isOn: $showAllSongs)}
15:06 - Programatic text selection
struct LyricView: View { @State private var selection: TextSelection? var body: some View { TextField("Line \(line.number)", text: $line.text, selection: $selection) // ... }}
15:19 - Getting selected ranges
InspectorContent(text: line.text, ranges: selection?.ranges)
15:29 - Binding to search field focus state
// Binding to search field focus statestruct SongSearchView: View { @FocusState private var isSearchFieldFocused: Bool @State private var searchText = "" @State private var isPresented = false var body: some View { NavigationSplitView { Text("Power Ballads") Text("Show Tunes") } detail: { // ... if !isSearchFieldFocused { Button("Find another song") { isSearchFieldFocused = true } } } .searchable(text: $searchText, isPresented: $isPresented) .searchFocused($isSearchFieldFocused) }}
15:41 - Text suggestions
TextField("Line \(line.number)", text: $line.text) .textInputSuggestions { ForEach(lyricCompletions) { Text($0.attributedCompletion) .textInputCompletion($0.text) } }
15:59 - Color mixing
Color.red.mix(with: .purple, by: 0.2)Color.red.mix(with: .purple, by: 0.5)Color.red.mix(with: .purple, by: 0.8)
16:13 - Custom shaders
ContentView() .task { let slimShader = ShaderLibrary.slim() try! await slimShader.compile(as: .layerEffect) }
16:23 - React to scroll geometry changes
struct ContentView: View { @State private var showBackButton = false ScrollView { // ... } .onScrollGeometryChange(for: Bool.self) { geometry in geometry.contentOffset.y < geometry.contentInsets.top } action: { wasScrolledToTop, isScrolledToTop in withAnimation { showBackButton = !isScrolledToTop } }}
16:42 - React to scroll visibility changes
struct AutoPlayingVideo: View { @State private var player: AVPlayer = makePlayer() var body: some View { VideoPlayer(player: player) .onScrollVisibilityChange(threshold: 0.2) { visible in if visible { player.play() } else { player.pause() } } }}
16:54 - New scroll positions
struct ContentView: View { @State private var position: ScrollPosition = .init(idType: Int.self) var body: some View { ScrollView { // ... } .scrollPosition($position) .overlay { FloatingButton("Back to Invitation") { position.scrollTo(edge: .top) } } }}
18:17 - Gesture interoperability
struct VideoThumbnailScrubGesture: UIGestureRecognizerRepresentable { @Binding var progress: Double func makeUIGestureRecognizer(context: Context) -> VideoThumbnailScrubGestureRecognizer { VideoThumbnailScrubGestureRecognizer() } func handleUIGestureRecognizerAction( _ recognizer: VideoThumbnailScrubGestureRecognizer, context: Context ) { progress = recognizer.progress }}struct VideoThumbnailTile: View { var body: some View { VideoThumbnail() .gesture(VideoThumbnailScrubGesture(progress: $progress)) }}
18:34 - SwiftUI animations in UIKit and AppKit
let animation = SwiftUI.Animation.spring(duration: 0.8)// UIKitUIView.animate(animation) { view.center = endOfBracelet}// AppKitNSAnimationContext.animate(animation) { view.center = endOfBracelet}
18:57 - Representable animation bridging
struct BeadBoxWrapper: UIViewRepresentable { @Binding var isOpen: Bool func updateUIView(_ box: BeadBox, context: Context) {context.animate { box.lid.center.y = isOpen ? -100 : 100} }}
19:59 - Volume baseplate visibility
struct KaraokePracticeApp: App { var body: some Scene { WindowGroup { ContentView() } .windowStyle(.volumetric) .defaultWorldScaling(.trueScale) .volumeBaseplateVisibility(.hidden) }}
20:15 - React to volume viewpoint changes
struct MicrophoneView: View { @State var micRotation: Rotation3D = .identity var body: some View { Model3D(named: "microphone") .onVolumeViewpointChange { _, new in micRotation = rotateToFace(new) } .rotation3DEffect(micRotation) .animation(.easeInOut, value: micRotation) } }
20:38 - Control allowed immersion levels
struct KaraokeApp: App { @State private var immersion: ImmersionStyle = .progressive( 0.4...1.0, initialAmount: 0.5) var body: some Scene { ImmersiveSpace(id: "Karaoke") { LoungeView() } .immersionStyle(selection: $immersion, in: immersion) }}
21:00 - Preferred surrounding effects
struct LoungeView: View { var body: some View { StageView() .preferredSurroundingsEffect(.colorMultiply(.purple)) }}
21:33 - Custom text renderers
struct KaraokeRenderer: TextRenderer { func draw( layout: Text.Layout, in context: inout GraphicsContext ) { for line in layout { for run in line { var glow = context glow.addFilter(.blur(radius: 8)) glow.addFilter(purpleColorFilter) glow.draw(run) context.draw(run) } } }}struct LyricsView: View { var body: some View { Text("A Whole View World") .textRenderer(KaraokeRenderer()) }}#Preview { LyricsView()}
Looking for something specific? Enter a topic above and jump straight to the good stuff.