DEV Community

Cover image for WWDC 2026 - What's New in SwiftUI - A Developer's Breakdown
ArshTechPro
ArshTechPro

Posted on

WWDC 2026 - What's New in SwiftUI - A Developer's Breakdown

WWDC26 brought a substantial round of updates to SwiftUI — not a ground-up redesign, but a lot of small limitations removed, new APIs that were clearly driven by real-world pain points, and meaningful performance improvements. This post walks through every major announcement so you know exactly what's available and when to reach for it.


Look and Feel: Liquid Glass and the 2027 Releases

The most immediately visible change costs you zero code. Apps built with SwiftUI automatically pick up the updated Liquid Glass appearance on the 2027 OS releases. The glass tint responds to the new system-level Liquid Glass slider without any changes on your part.

On iPad, windows now dim when inactive, reinforcing which window has focus — again, automatic. On Mac, custom interactive Liquid Glass elements respond more fluidly to the mouse pointer.

There are a few opt-in refinements available when you want tighter control:

Responding to active state — use the appearsActive environment value to reduce opacity on custom elements when the window is inactive:

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

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

Menu bar icons — the menu bar now shows a minimal set of icons by default. Add .labelStyle(.titleAndIcon) to a specific menu item to make its icon visible:

CommandMenu("Stickers") {
    Button { openStore() } label: {
        Label("Store", systemImage: "bag.fill")
            .labelStyle(.titleAndIcon)
    }
}
Enter fullscreen mode Exit fullscreen mode

Resizability on iPhone

iPhone apps become resizable on iOS 27, which matters for iPhone Mirroring and running iPhone apps on iPad. Xcode 27's Live Previews now include resize handles so you can test this interactively without running on a device.

If your app mixes UIKit and SwiftUI, check the session "Modernize your UIKit app" for specifics around screen geometry, size classes, and orientation handling.


Toolbar APIs

The toolbar has been a source of friction on smaller screens for a while. Three new APIs address this directly.

Visibility priority — mark a toolbar group as high-priority so it stays visible when space is tight:

ToolbarItemGroup {
    UndoButton()
    RedoButton()
}
.visibilityPriority(.high)
Enter fullscreen mode Exit fullscreen mode

Overflow menu — explicitly put secondary actions into the overflow menu rather than letting the system decide:

ToolbarOverflowMenu {
    ChoosePhotoButton()
    ExportAsImageButton()
    ClearAllStickersButton()
}
Enter fullscreen mode Exit fullscreen mode

Pinned trailing item — guarantee a button always appears in the trailing position, never hidden:

ToolbarItem(placement: .topBarPinnedTrailing) {
    ShareButton()
}
Enter fullscreen mode Exit fullscreen mode

Minimize on scroll — hide the navigation bar when the user scrolls down, maximizing content space:

ScrollView {
    StickerListView()
}
.toolbarMinimizeBehavior(.onScrollDown, for: .navigationBar)
Enter fullscreen mode Exit fullscreen mode

Prominent Tab Role

Tabs can now be visually distinguished from the main tab row using the new .prominent role. This is useful for actions like a shopping cart or a creation button that should stand apart from content navigation tabs:

TabView {
    Tab { EventsTab() }
    Tab { HolidaysTab() }

    Tab(role: .prominent) {
        CartTab()
    }
}
Enter fullscreen mode Exit fullscreen mode

Document API Overhaul

This is the most substantial new API surface in this release. The existing FileDocument and ReferenceFileDocument protocols are still there, but the 2027 releases introduce a new, more capable layer on top.

Document creation context

You can now define named creation sources and branch your initialization logic based on which one was selected:

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

@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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

WritableDocument and DocumentWriter

The new WritableDocument protocol separates the document model from the disk-writing logic. You provide a snapshot() method that captures the current state, and a Writer that knows how to serialize it.

The writer's write method is nonisolated and async, so heavy I/O stays off the main actor. You can compare current and previous snapshots to write only what changed, and report progress using Foundation's Subprogress API:

struct Writer: DocumentWriter {
    typealias Snapshot = PageSnapshot

    let contentType: UTType

    nonisolated func write(
        snapshot: sending PageSnapshot, to destination: URL,
        previous: sending PageSnapshot?, progress: consuming Subprogress
    ) async throws {
        if contentType.conforms(to: .stickerDocument) {
            // write custom format
        } else if contentType.conforms(to: .png) {
            let context = CGContext(/* ... */)
            context.draw(/* ... */)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Supporting multiple export formats is a matter of adding types to writableContentTypes and adding branches in write. The ReadableDocument and DocumentReader protocols mirror this structure for reading.

This API works with the @Observable macro, so views update only when the specific properties they depend on actually change.


Reorderable Containers

Drag-to-reorder is now available on any container, not just List. Apply .reorderable() to your ForEach and .reorderContainer to the parent, and SwiftUI handles the drag interaction and animation:

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

The same code works on LazyVGrid — you don't need to rewrite anything when switching container types. Reordering also comes to watchOS for the first time this release.

For applying the ReorderDifference back to your array, the recommended approach uses the swift-collections package from swift.org:

import OrderedCollections

extension ReorderDifference where CollectionID == ReorderableSingleCollectionIdentifier {
    func apply(to values: inout [some Identifiable<ItemID>]) {
        var dictionary = OrderedDictionary(
            uniqueKeys: values.map { $0.id }, values: values)
        // resolve destination and move keys
        values = dictionary.values.elements
    }
}
Enter fullscreen mode Exit fullscreen mode

Swipe Actions on Any View

Previously, .swipeActions only worked inside List. Now it works on any view inside a scroll container. Add .swipeActionsContainer() to the scroll view to coordinate the interactions:

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

Confirmation Dialogs and Alerts with Item Binding

Both .confirmationDialog and .alert now accept the same item-binding pattern that sheets use. Pass a binding to an optional item; when the item is set, the dialog appears with that item in scope:

.confirmationDialog("Delete?", item: $stickerToDelete) { sticker in
    DeleteStickerButton(sticker)
}
Enter fullscreen mode Exit fullscreen mode

This replaces the pattern of managing a separate isPresented Bool alongside a selected-item variable.


AsyncImage Caching

AsyncImage now respects standard HTTP cache headers by default, so images loaded from the network are cached without any code changes. When scrolling back to previously loaded content, images appear immediately from cache.

When you need more control, you can provide a custom URLRequest per image and attach a custom URLSession with a configured URLCache:

@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)
Enter fullscreen mode Exit fullscreen mode

@State Is Now a Macro — With Lazy Initialization

This is a subtle but impactful change. @State has been converted from a Dynamic Property to a macro. The practical effect: @Observable classes stored in @State are now initialized lazily — only once for the lifetime of the view, not on every reinitialization of the parent.

Previously, if a parent view reinitializes StickerStoreView, a new StickerStore() would be created each time (and immediately discarded). Now, the class is only ever created once. This behavior is back-ported to iOS 17, macOS 14, and aligned releases.

One source-breaking edge case to watch for: if you assign a default value in the property declaration and assign a different value in init, Xcode 27 will show a "variable used before being initialized" error. The fix is to remove the default value from the declaration:

// Before — causes error
@State private var page = StickerPage()

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

// After — correct
@State private var page: StickerPage

init(title: String) {
    self.page = StickerPage(title: title)
    self.title = title
}
Enter fullscreen mode Exit fullscreen mode

ContentBuilder: Better Compiler Performance

If your app has complex, deeply nested SwiftUI views, you may have seen "The compiler is unable to type-check this expression in reasonable time." This happens because Section, Group, ForEach, and similar containers each have multiple overloads, and the compiler has to explore every combination.

The 2027 releases introduce ContentBuilder, a unified builder type that replaces the per-container overloads. This reduces type-checking to a single path through the decision tree, which translates to meaningfully faster compile times. ContentBuilder is available for any deployment target because it's built on top of the existing ViewBuilder.

You can use it directly when needed:

@ContentBuilder
func stickerLibraryView() -> some View {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

The improvement applies when building with Xcode 27 regardless of your minimum deployment target.


SwiftUI Agent Skills in Xcode 27

Xcode 27 ships two new agent skills for the Coding Assistant:

  • SwiftUI Specialist — enforces SwiftUI best practices as you write code
  • What's New in SwiftUI — guides adoption of the new 2027 APIs

Both are available inside Xcode 27's Coding Assistant. If you use other tools, you can export them as markdown files with xcrun agent skills export.


Where to Start

If you want to work through these APIs systematically:

  1. Build your existing app in Xcode 27 and check the updated Liquid Glass appearance — it requires no code changes.
  2. If you have a document-based app, the new WritableDocument / ReadableDocument protocols are worth evaluating for the performance and multi-format benefits.
  3. Audit your toolbar setup, especially on compact screens, using the three new toolbar APIs.
  4. If you have complex views with compiler performance issues, upgrading to Xcode 27 alone should help due to ContentBuilder.
  5. Switch any @State-managed @Observable classes over and verify the lazy initialization behavior works as expected in your initialization paths.

Top comments (0)