WWDC Quick Look 💓 By SwiftGGTeam
What’s new in SwiftUI

What’s new in SwiftUI

Watch original video

Highlight

In 2025, SwiftUI gets the Liquid Glass design system, a 6× speedup for macOS list loading, the @Animatable macro, WebView, Chart3D, rich-text bindings for TextEditor, and the ability to bridge SwiftUI scenes into UIKit/AppKit apps.


Core content

Every developer who has shipped a list has hit these walls: a List of 100,000 rows on macOS stutters on refresh and drops frames while scrolling; animating a custom Shape forces you to write a long AnimatableData that unpacks every property by hand; you want to use a SwiftUI-only scene like MenuBarExtra inside an existing UIKit app and find no way in. SwiftUI 2025 answers all of these head on.

Anna and Peter use a hike-planning app to thread the updates together. After a recompile, the app picks up the Liquid Glass look automatically: the sidebar gains a glass material, toolbar items morph during navigation transitions, and on iPhone the search bar moves to the bottom where it sits better under the thumb. Underneath, list loading is 6× faster and updates 16× faster, the scroll scheduler is rewritten to cut frame drops at high refresh rates, and the new SwiftUI Performance Instrument adds dedicated lanes that pinpoint slow body updates and slow platform view updates. The @Animatable macro lets SwiftUI synthesize animatable properties for you. Scene Bridging lets a UIKit/AppKit app consume SwiftUI-only scenes such as MenuBarExtra and ImmersiveSpace directly. WebView, Chart3D, and the AttributedString binding on TextEditor fill in view capabilities that have been missing for years.


Detailed content

Liquid Glass toolbar grouping (02:27). The new ToolbarSpacer API gives you explicit control over how toolbar items are grouped:

NavigationStack {
    TripList()
        .toolbar {
            ToolbarItemGroup(placement: .primaryAction) {
                UpButton()
                DownButton()
            }

            ToolbarSpacer(.fixed, placement: .primaryAction)

            ToolbarItem(placement: .primaryAction) {
                SettingsButton()
            }
        }
}

Key points:

  • ToolbarItemGroup collapses Up and Down into a single capsule.
  • ToolbarSpacer(.fixed, placement: .primaryAction) inserts a fixed gap so SettingsButton becomes its own Liquid Glass capsule.
  • During navigation transitions, capsules morph along this group structure.

Search as its own Tab (04:12). Declare search as a Tab with role: .search and the system separates it visually from regular tabs and morphs it directly into a search field on tap:

TabView {
    Tab("Summary", systemImage: "heart") {
        NavigationStack { Text("Summary") }
    }
    Tab("Sharing", systemImage: "person.2") {
        NavigationStack { Text("Sharing") }
    }
    Tab(role: .search) {
        NavigationStack { Text("Search") }
    }
}
.searchable(text: $text)

Key points:

  • Tab(role: .search) marks this as the search destination.
  • .searchable still hangs off the TabView and binds text.
  • On iPhone, tapping the tab morphs the search field out from the tab’s position rather than pushing a new layer.

The @Animatable macro (11:24). Animating a few properties on a custom Shape used to mean hand-writing AnimatableData that nested values into AnimatablePair. Now hand it to the macro:

@Animatable
struct LoadingArc: Shape {
    var center: CGPoint
    var radius: CGFloat
    var startAngle: Angle
    var endAngle: Angle
    @AnimatableIgnored var drawPathClockwise: Bool

    func path(in rect: CGRect) -> Path {
        return Path()
    }
}

Key points:

  • @Animatable synthesizes animatableData and threads every animatable property through it.
  • @AnimatableIgnored marks boolean properties like drawPathClockwise as not interpolated.
  • No more nested AnimatablePair<AnimatablePair<...>, ...> boilerplate.

WebView + WebPage (20:44). WebKit finally has a native SwiftUI view. The simplest form takes a URL, but for programmatic navigation use WebPage:

struct InAppBrowser: View {
    @State private var page = WebPage()

    var body: some View {
        WebView(page)
            .ignoresSafeArea()
            .onAppear {
                page.load(URLRequest(url: sunshineMountainURL))
            }
    }
}

Key points:

  • WebPage is an Observable model designed for Swift, not a thin wrapper over WKWebView.
  • WebView(page) takes a WebPage instance and renders the page it drives.
  • page.load(_:) triggers navigation; the same API also reads page properties, calls JavaScript, and serves custom URL schemes.

3D charts (21:35). Swift Charts adds Chart3D, which can draw surfaces:

Chart3D {
    SurfacePlot(x: "x", y: "y", z: "z") { x, y in
        sin(x) * cos(y)
    }
    .foregroundStyle(Gradient(colors: [.orange, .pink]))
}
.chartXScale(domain: -3 ... 3)
.chartYScale(domain: -3 ... 3)
.chartZScale(domain: -3 ... 3)

Key points:

  • Chart3D is a new container; pair it with SurfacePlot to render a surface.
  • The closure (x, y) -> z hands SwiftUI a math function directly; no pre-sampling needed.
  • The three chart*Scale modifiers bound the visible domain.

Drag and Drop with multi-select and delete (22:18). Drag and drop on the Mac has long been the worst offender for UIKit/AppKit bridging. This year fills the gap:

ScrollView {
    LazyVGrid(columns: gridColumns) {
        ForEach(model.photos) { photo in
            view(photo: photo)
                .draggable(containerItemID: photo.id)
        }
    }
}
.dragContainer(for: Photo.self, selection: selectedPhotos) { draggedIDs in
    photos(ids: draggedIDs)
}
.dragConfiguration(DragConfiguration(allowMove: false, allowDelete: true))
.onDragSessionUpdated { session in
    let ids = session.draggedItemIDs(for: Photo.ID.self)
    if session.phase == .ended(.delete) {
        trash(ids)
        deletePhotos(ids)
    }
}
.dragPreviewsFormation(.stack)

Key points:

  • draggable(containerItemID:) marks each cell as a draggable item inside the container, passing only the ID.
  • dragContainer(for:selection:) runs only when a drop actually happens, lazily loading the real Photo data.
  • DragConfiguration(allowDelete: true) lets a drag onto the trash trigger a delete.
  • onDragSessionUpdated watches for session.phase == .ended(.delete) and does the real cleanup.
  • .dragPreviewsFormation(.stack) stacks the dragged images into a single pile as the drag preview.

Rich text in TextEditor (23:55). When TextEditor takes an AttributedString binding, the toolbar comes with bold, italic, list, and other format controls built in:

struct CommentEditor: View {
    @Binding var commentText: AttributedString

    var body: some View {
        TextEditor(text: $commentText)
    }
}

Key points:

  • Switch the binding from String to AttributedString and rich text works out of the box.
  • The system supplies formatting controls; developers can also customize paragraph styles or restrict the allowed attribute set.

Core takeaways

  • What to do: remove UIRequiresFullscreen from Info.plist and adapt your app to free window resizing.

    • Why it’s worth doing: this key is deprecated in iPadOS 26 and leaving it in restricts how your app behaves in iPad multitasking windows.
    • How to start: delete the key, run the app, and check how NavigationSplitView columns collapse at narrow widths and whether custom layouts break at extreme sizes.
  • What to do: use the SwiftUI Performance Instrument as the first pass for performance work, instead of Time Profiler.

    • Why it’s worth doing: the generic Time Profiler shows you a CPU flame graph; the new Instrument tells you directly which view’s body ran too many times and which platform view updates were slowest.
    • How to start: open Instruments in Xcode 26, pick the SwiftUI template, run your most complex list or animation scenario, and read the long body update lane first.
  • What to do: replace animatableData on existing custom Shapes with @Animatable.

    • Why it’s worth doing: nested AnimatablePair is universally recognized boilerplate; the macro removes it and readability jumps a level.
    • How to start: add @Animatable to the Shape, mark non-animating properties with @AnimatableIgnored, delete the hand-written animatableData, and re-run your existing animation tests.
  • What to do: evaluate whether Scene Bridging can let your existing UIKit/AppKit app open MenuBarExtra / ImmersiveSpace / RemoteImmersiveSpace.

    • Why it’s worth doing: for older projects that don’t plan to migrate to the SwiftUI lifecycle wholesale, this is the cheapest path to wire Vision Pro stereoscopic content and Mac menu bar features into the app.
    • How to start: declare a SwiftUI scene from the UIKit/AppKit entry point, start with a minimal MenuBarExtra for verification, then consider RemoteImmersiveSpace together with CompositorServices.
  • What to do: replace your old wrappers around embedded H5 pages with WebView + WebPage.

    • Why it’s worth doing: old WKWebView bridges typically use a JavaScript bridge for interaction, which is type-unsafe and hard to debug; WebPage is an Observable model, so navigation and page properties are reachable from pure Swift.
    • How to start: move display-only pages (FAQ, terms) to WebView(url:) first; migrate interactive pages to WebView(page) + page.load(_:) and use WebPage to listen for navigation events.

Comments

GitHub Issues · utterances