What’s new in SwiftUI - WWDC24 - Videos - Apple Developer (2024)

Table of Contents
Chapters Resources Related Videos

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

    Array
    • 1: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

      import 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.

    What’s new in SwiftUI - WWDC24 - Videos - Apple Developer (2024)
    Top Articles
    Latest Posts
    Article information

    Author: Tyson Zemlak

    Last Updated:

    Views: 6016

    Rating: 4.2 / 5 (43 voted)

    Reviews: 82% of readers found this page helpful

    Author information

    Name: Tyson Zemlak

    Birthday: 1992-03-17

    Address: Apt. 662 96191 Quigley Dam, Kubview, MA 42013

    Phone: +441678032891

    Job: Community-Services Orchestrator

    Hobby: Coffee roasting, Calligraphy, Metalworking, Fashion, Vehicle restoration, Shopping, Photography

    Introduction: My name is Tyson Zemlak, I am a excited, light, sparkling, super, open, fair, magnificent person who loves writing and wants to share my knowledge and understanding with you.