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

What's new in SwiftUI

Watch original video

Highlight

SwiftUI in the 2027 platform releases introduces a new Document API with direct disk access and snapshot-based differential writes, reorderable containers, toolbar visibility priority and automatic minimization, swipe gestures for any view, plus compile-time improvements from the @State macro and ContentBuilder.

Core Content

Liquid Glass and Responding to Window State

In the 2027 releases, SwiftUI apps automatically receive the updated Liquid Glass appearance without any code changes. (02:17) Custom Liquid Glass elements on macOS can be marked as interactive, making them respond more smoothly to mouse clicks. On iPad, when an app window is inactive, icons and text automatically dim so users can identify the active window.

Developers can use the appearsActive environment value to precisely control how custom views appear when a window is inactive:

struct SidebarFooterView: View {
    @Environment(\.appearsActive) private var appearsActive

    var body: some View {
        MyAccountView()
            .opacity(appearsActive ? 1 : 0.5)
    }
}

Key points:

  • appearsActive is a new environment value in 2027
  • It returns false when the window is inactive and true when it is active
  • Use it for visual feedback in custom sidebars, toolbars, and similar elements

On iPad and Mac, menu bars show only the icons for key actions by default. You can use .labelStyle(.titleAndIcon) to show both the icon and title for specific menu items, making them stand out more. (03:34)

Adaptive Toolbars and Visibility Control

As features grow, toolbars accumulate more buttons. On iPhone or in smaller windows, the system automatically hides buttons that do not fit. The 2027 release provides three new APIs to address this.

Visibility priority: Use visibilityPriority(.high) to keep important buttons visible first when space is constrained.

StickerPageView()
    .toolbar {
        ToolbarItemGroup {
            UndoButton()
            RedoButton()
        }
        .visibilityPriority(.high)
        ToolbarOverflowMenu {
            ChoosePhotoButton()
            ExportAsImageButton()
            ClearAllStickersButton()
        }
        ToolbarItem(placement: .topBarPinnedTrailing) {
            ShareButton()
        }
    }

Key points:

  • visibilityPriority(.high) marks Undo and Redo as important actions and keeps them visible first
  • ToolbarOverflowMenu places less frequently used buttons permanently in an overflow menu
  • .topBarPinnedTrailing keeps the Share button visible on the trailing side so it is never hidden

Automatic minimization: When the user scrolls down, the navigation bar automatically collapses to leave more room for content. (07:37)

ScrollView {
    StickerListView()
}
.toolbarMinimizeBehavior(.onScrollDown, for: .navigationBar)

The New Document API

SwiftUI’s existing FileDocument and ReferenceFileDocument protocols are substantially expanded in the 2027 releases. The new API provides document creation context, optimized disk reads and writes, and direct access to a document URL. (08:12)

Document creation sources: An app can offer multiple ways to create a document at launch.

@main
struct Stickers: App {
    var body: some Scene {
        DocumentGroupLaunchScene("Create a Sticker Page") {
            NewDocumentButton("New Sticker Page", source: .blank)
            NewDocumentButton("Sticker Page from Photo…", source: .photo)
        }

        DocumentGroup { document in
            StickerPageDocumentView(document)
        } { configuration, context in
            StickerPageDocument(configuration: configuration, context: context)
        }
    }
}

extension DocumentCreationSource {
    static let blank = Self(id: "blank")
    static let photo = Self(id: "photo")
}

Key points:

  • DocumentGroupLaunchScene defines new-document buttons in the launch scene
  • DocumentCreationSource declares different creation sources, such as blank or from a photo
  • The creation closure receives source information through the context parameter, letting the initializer decide what to do

Write optimization: The new WritableDocument protocol requires three pieces: writable formats, a snapshot method, and a writer. (11:25)

@Observable
final class StickerDocument {
    static let writableDocumentTypes: [UTType] = [.stickerDocument]

    @MainActor
    func snapshot(contentType: UTType) async throws -> sending PageSnapshot {
        makeSnapshot()
    }

    func writer(configuration: sending WriteConfiguration) -> sending Writer {
        Writer(contentType: configuration.contentType)
    }
}

struct Writer<Snapshot>: DocumentWriter {
    typealias Snapshot = PageSnapshot
    let contentType: UTType

    nonisolated func write(
        snapshot: sending PageSnapshot, to destination: URL,
        previous: sending PageSnapshot?, progress: consuming Subprogress
    ) async throws {
        // Compare the current and previous snapshots, then write only the changed parts.
        // Use Subprogress to report write progress.
    }
}

Key points:

  • snapshot() returns the document’s complete state at a moment in time, with sending ensuring thread safety
  • Writer’s write method is nonisolated and asynchronous, so disk writes run in the background
  • Comparing snapshot and previous enables incremental writes
  • Subprogress reports save progress to the system

Multi-format export: A single writer can support multiple output formats. (14:35)

@Observable
final class StickerDocument: WritableDocument {
    static let writableContentTypes: [UTType] = [.stickerDocument, .png]
}

// Dispatch based on contentType inside the write method.
if contentType.conforms(to: .stickerDocument) {
    // Write the custom package format.
} else if contentType.conforms(to: .png) {
    let context = CGContext(/* ... */)
    context.draw(/* ... */)
}

Read protocol: ReadableDocument mirrors WritableDocument and uses DocumentReader to perform disk reads. (13:27)

Reorderable Containers

The 2027 release introduces the Reorderable API, which supports drag-based reordering in any container, including List and Grid. (15:58)

List {
    ForEach(stickers) { sticker in
        StickerListItemView(sticker: sticker)
    }
    .reorderable()
}
.reorderContainer(for: Sticker.self) { difference in
    difference.apply(to: &stickers)
}

The same code can be used directly with LazyVGrid without changing the reordering logic:

LazyVGrid {
    ForEach(stickers) { sticker in
        StickerListItemView(sticker: sticker)
    }
    .reorderable()
}
.reorderContainer(for: Sticker.self) { difference in
    difference.apply(to: &stickers)
}

Key points:

  • .reorderable() modifies ForEach to enable drag interaction
  • .reorderContainer(for:) modifies the outer container and coordinates the reordering gesture
  • difference.apply uses OrderedDictionary from swift-collections to commit ordering changes
  • watchOS is supported for the first time

Swipe Gestures on Any View

Swipe gestures are no longer limited to List; they can now be applied to any view. (18:15)

ScrollView {
    LazyVStack {
        ForEach(stickers) { sticker in
            StickerListItemView(sticker: sticker)
                .swipeActions {
                    DeleteButton(sticker: sticker)
                }
        }
    }
}
.swipeActionsContainer()

Key points:

  • .swipeActions() modifies an individual list item
  • .swipeActionsContainer() modifies the outer ScrollView and coordinates swipe gestures for all child items

Item Bindings for Confirmation Dialog and Alert

confirmationDialog and alert now support the same item-binding pattern as sheet. (18:54)

struct StickerCanvasView: View {
    var stickers: [Sticker]
    @State private var stickerToDelete: Sticker?

    var body: some View {
        ZStack {
            ForEach(stickers) { sticker in
                PlacedStickerView(sticker: sticker)
                    .contextMenu {
                        Button("Delete") { stickerToDelete = sticker }
                    }
            }
        }
        .confirmationDialog("Delete?", item: $stickerToDelete) { sticker in
            DeleteStickerButton(sticker)
        }
    }
}

Key points:

  • item: $stickerToDelete binds an optional value
  • When the value is non-nil, the dialog appears automatically; when it is nil, the dialog disappears automatically
  • alert supports the same item-binding pattern

Details

AsyncImage Cache Improvements

Previously, AsyncImage did not keep loaded images in memory. If you scrolled up and then back down, images were loaded again. In the 2027 release, standard HTTP caching is enabled by default without any code changes. (20:57)

For finer control, you can pass a custom URLRequest and URLSession:

@Observable class StickerStore {
    static let imageSession: URLSession = {
        let config = URLSessionConfiguration.default
        config.urlCache = URLCache(
            memoryCapacity: 64 * 1024 * 1024,
            diskCapacity: 256 * 1024 * 1024)
        return URLSession(configuration: config)
    }()
}

ForEach(pets) { pet in
    AsyncImage(request: URLRequest(
        url: pet.imageURL,
        cachePolicy: .returnCacheDataElseLoad)
    )
}
.asyncImageURLSession(StickerStore.imageSession)

Key points:

  • URLRequest lets you set cache policy, headers, and other request details
  • The asyncImageURLSession modifier sets a global URLSession, which is useful for long-lived configuration
  • Default caching works automatically without extra code

@State as a Macro and Lazy Initialization

In the 2027 release, @State changes from a property wrapper into a macro. The biggest change is that Observable classes modified by @State now initialize only once. (23:08)

@Observable class StickerStore { }

struct StickerStoreView: View {
    @State private var store = StickerStore()

    var body: some View {
        // store is created only during the first initialization. Parent view updates do not create it again.
    }
}

This behavior has been back-deployed to iOS 17, macOS 14, and corresponding releases.

Migration note: If an @State variable has a default value and is also assigned in init, it produces a compile error. (23:48)

// Error: Variable 'self.title' used before being initialized
struct StickerPageView: View {
    @State private var page = StickerPage()
    let title: String

    init(title: String) {
        self.page = StickerPage(title: title)
        self.title = title
    }
}

// Fix: remove the default value.
struct StickerPageView: View {
    @State private var page: StickerPage
    let title: String

    init(title: String) {
        self.page = StickerPage(title: title)
        self.title = title
    }
}

ContentBuilder Compile-Time Improvements

Deeply nested SwiftUI views often trigger the compiler error: “The compiler is unable to type-check this expression in reasonable time.” (24:31)

The reason is that containers such as Section, Group, and ForEach each have multiple initializer overloads, and the compiler has to try all combinations before it can determine the type. In the 2027 release, these common builders are unified as ContentBuilder, eliminating most type-checking branches. (26:07)

@ContentBuilder
func stickerLibraryView() -> some View {
    // ...
}

Key points:

  • ContentBuilder is the evolution of ViewBuilder and unifies several builder types
  • Compile-time improvements apply to all minimum deployment targets
  • They take effect when building with Xcode 27, regardless of whether the target platform is 2027 or an earlier release

Key Takeaways

1. Refactor existing document apps with the new Document API

If you maintain an app based on FileDocument, migrating to the new WritableDocument/ReadableDocument protocols gives you incremental writes, asynchronous save progress, multi-format export, and more. The entry points are DocumentGroup and DocumentGroupLaunchScene.

2. Add adaptive toolbars to iPad and Mac apps

Use visibilityPriority(.high) for core actions, ToolbarOverflowMenu for secondary buttons, and .topBarPinnedTrailing to pin the Share button. Pair that with .toolbarMinimizeBehavior(.onScrollDown) to collapse the navigation bar during scrolling and improve the reading experience.

3. Replace custom drag reordering with the Reorderable API

If you previously used onMove or a custom DragGesture for list reordering, you can now replace it with .reorderable() plus .reorderContainer(). The code is simpler, animations and interactions are automatic, and it supports multiple container types such as List, Grid, and LazyVGrid.

4. Add swipe gestures to any scroll view

Swipe gestures are no longer limited to List. Any child view inside LazyVStack or ScrollView can add .swipeActions(). Add .swipeActionsContainer() to the outer container to coordinate the gestures.

5. Check compile errors caused by the @State macro change

After upgrading to Xcode 27, check whether any @State variables in your project are assigned again in init. The fix is to remove the default value from the property declaration and assign it only in init.

Comments

GitHub Issues · utterances