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)
}
}
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)
}
}
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)
Overflow menu — explicitly put secondary actions into the overflow menu rather than letting the system decide:
ToolbarOverflowMenu {
ChoosePhotoButton()
ExportAsImageButton()
ClearAllStickersButton()
}
Pinned trailing item — guarantee a button always appears in the trailing position, never hidden:
ToolbarItem(placement: .topBarPinnedTrailing) {
ShareButton()
}
Minimize on scroll — hide the navigation bar when the user scrolls down, maximizing content space:
ScrollView {
StickerListView()
}
.toolbarMinimizeBehavior(.onScrollDown, for: .navigationBar)
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()
}
}
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)
}
}
}
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(/* ... */)
}
}
}
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)
}
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
}
}
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()
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)
}
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)
@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
}
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 {
// ...
}
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:
- Build your existing app in Xcode 27 and check the updated Liquid Glass appearance — it requires no code changes.
- If you have a document-based app, the new
WritableDocument/ReadableDocumentprotocols are worth evaluating for the performance and multi-format benefits. - Audit your toolbar setup, especially on compact screens, using the three new toolbar APIs.
- If you have complex views with compiler performance issues, upgrading to Xcode 27 alone should help due to
ContentBuilder. - Switch any
@State-managed@Observableclasses over and verify the lazy initialization behavior works as expected in your initialization paths.
Top comments (0)