Modern SwiftUI · advanced

Liquid Glass and modern SwiftUI

What actually changed in SwiftUI for iOS 26 and iOS 27, what to adopt now, and what to leave alone.

13 min read12 min practice0/1 exercises4 recall cards

By the end you will be able to

  • Apply Liquid Glass correctly, and say which layer of an interface it belongs to
  • Use the current toolbar, tab and scroll APIs
  • Decide what to adopt against a given deployment target
Guess firstAnswering before you read makes the explanation stick — even when you get it wrong.

Liquid Glass is the material introduced across Apple's platforms in iOS 26. Where should you apply it?

The current landscape

Two releases matter right now, and knowing which is which decides your deployment target.

iOS 26 (shipped Sept 2025)iOS 27 (WWDC 2026, shipping autumn 2026)
DesignLiquid Glass across the systemAutomatic adoption; refinements
Toolbars.toolbar placementsvisibilityPriority, overflow menus, pinned trailing, minimize-on-scroll
ListsList, ForEachDrag-to-reorder in any container; swipe actions in any scroll view
ImagesAsyncImageHTTP caching by default
State@State as a property wrapper@State as a macro (Xcode 27 — back-deploys to older OSes), lazy initialisation of stored classes
Buildersper-container ViewBuilder overloadsunified ContentBuilder, materially faster type-checking

Everything below is marked with the version it needs.

Liquid Glass (iOS 26+)

Text("Now playing")
    .padding()
    .glassEffect()

The default is .regular. .clear is more transparent, for use over imagery.

.glassEffect(.regular, in: .capsule)
.glassEffect(.clear.interactive(), in: .rect(cornerRadius: 20))

.interactive() adds the scale and shimmer response on touch. Apply the effect after layout and styling modifiers — it works on the final shape of the view. Order it wrong and you can see why: .glassEffect().padding() pours the material over the bare label and then pads outside it — a tight glass pill floating in empty inset — while .padding().glassEffect() pours it over the padded control, the full-size bar you meant. Same modifiers, two different pieces of glass.

Glass over a coloured backdrop. Try .clear instead of .regular.
Run to see the preview.

GlassEffectContainer

When several glass elements sit near each other, wrap them so they sample the same backdrop and blend as one material rather than stacking:

GlassEffectContainer(spacing: 12) {
    HStack(spacing: 12) {
        Button { } label: { Image(systemName: "backward.fill") }
            .glassEffect()
        Button { } label: { Image(systemName: "play.fill") }
            .glassEffect()
        Button { } label: { Image(systemName: "forward.fill") }
            .glassEffect()
    }
}

Without the container, three separate glass layers overlap and the result looks muddy. With it, they merge — and morph into one another when they move close.

Toolbars (iOS 27)

The 2026 additions exist because iPhone apps became resizable, so a toolbar has to survive widths it was never designed for.

.toolbar {
    ToolbarItemGroup(placement: .topBarTrailing) {
        Button("Share") { }
            .visibilityPriority(.high)      // keep this one visible longest
        Button("Duplicate") { }
        Button("Info") { }
    }
    ToolbarOverflowMenu {                   // always in the "…" menu
        Button("Export") { }
        Button("Print") { }
    }
}
.toolbarMinimizeBehavior(.onScrollDown, for: .navigationBar)
  • visibilityPriority(_:) — which items survive as space shrinks.
  • ToolbarOverflowMenu — items that live in the overflow menu permanently.
  • .topBarPinnedTrailing — a placement that is never collapsed.
  • .toolbarMinimizeBehavior(.onScrollDown,for:) — collapse the bar while scrolling down.

Tabs

TabView {
    Tab("Home", systemImage: "house") { HomeView() }
    Tab("Search", systemImage: "magnifyingglass", role: .search) { SearchView() }
    Tab("Create", systemImage: "plus", role: .prominent) { CreateView() }   // iOS 27
}
.tabViewStyle(.sidebarAdaptable)

The Tab builder replaced .tabItem in iOS 18. role: .search gets the system search treatment; role: .prominent (iOS 27) visually elevates a primary action.

Reorderable containers (iOS 27)

Drag-to-reorder used to require List and onMove. Now any container can do it:

LazyVGrid(columns: columns) {
    ForEach(items) { item in
        ItemCell(item: item)
    }
    .reorderable()
}
.reorderContainer(for: Item.self) { difference in
    difference.apply(to: &items)
}

AsyncImage caching (iOS 27)

AsyncImage now respects standard HTTP cache headers with no code change — a real fix for the long-standing complaint that it re-downloaded on every appearance. For finer control:

.asyncImageURLSession(URLSession(configuration: myConfiguration))

Scroll effects (iOS 17–18 — older than they look)

ScrollView {
    ForEach(items) { item in
        Row(item: item)
            .scrollTransition { content, phase in
                content
                    .opacity(phase.isIdentity ? 1 : 0.4)
                    .scaleEffect(phase.isIdentity ? 1 : 0.9)
            }
    }
}
.scrollTargetBehavior(.viewAligned)
.onScrollGeometryChange(for: CGFloat.self) { geometry in
    geometry.contentOffset.y
} action: { _, offset in
    isScrolled = offset > 0
}

scrollTransition gives per-item entry and exit effects; onScrollGeometryChange replaces the GeometryReader-in-a-ScrollView hacks people used to write. Availability worth knowing for target decisions: scrollTransition and .scrollTargetBehavior(.viewAligned) are iOS 17, onScrollGeometryChange is iOS 18 — none of this needs a modern target, it is just underused.

@State becomes a macro (Xcode 27)

In Xcode 27, @State is a macro rather than a dynamic property. This is a compiler change, not an OS one — it back-deploys to the iOS 17 era, so it does not raise your deployment target. The practical effect: a class stored in @State is initialised lazily, once per view lifetime, instead of on every body evaluation.

@State private var model = DataModel()   // now constructed once, on first use

There is one migration trap. Giving a property a default value and assigning it again in init behaves differently under the macro — keep one or the other: either the inline default, or the assignment in init, not both.

Check yourself

Your deployment target is iOS 26. Which of these can you use today?

Glass control bar

Build the view

Build a horizontal control bar over a gradient: three circles in an HStack, padded, with a capsule-shaped regular glass effect.

Solution unlocks after 3 attempts
Design itSaved on this device. Never graded.

Pick an app on your phone that has adopted Liquid Glass. Write down which elements use it and which do not, and whether that matches the functional-layer rule. Note one place where you think they got it wrong, and why.

Checkpoint

You can now:

  • Apply .glassEffect and GlassEffectContainer in the right layer
  • Use the iOS 27 toolbar, tab and reordering APIs
  • Say which APIs need iOS 26 and which need iOS 27
  • Name the @State macro migration trap

Next up: keep building. The curriculum's remaining tracks cover lists and navigation, animation, SwiftData, architecture, testing, and shipping.

How well do you know this now? Rating yourself honestly, then being tested on it, is how you find out where your intuition is wrong.