Highlight
This SwiftUI update covers a lot of ground, but if you remember one thing, remember this: for the past five years, any SwiftUI app that needed rich text editing had to bridge
UITextViewand write piles ofUIViewRepresentableboilerplate. NowTextEditornatively acceptsAttributedString, selection management, and find navigation. That bridge layer can finally come down.
Core content
How painful was it to write a notes-style app with bold, italic, and colored highlights in SwiftUI? You had to give up SwiftUI’s TextEditor, wrap a UITextView in UIViewRepresentable, handle selection sync, the keyboard toolbar, and the undo stack yourself, and then wire NSAttributedString to SwiftUI state. A simple-looking feature often took two or three hundred lines of glue code. WWDC25’s SwiftUI cuts that path straight: TextEditor now takes an AttributedString binding directly, and selection management is a first-class citizen.
Liquid Glass is the other main thread of the same release. It reaches across the whole visual system, far beyond any single API. The system’s TabBar, Toolbar, and buttons get the new look automatically under the new SDK. But any hand-rolled glass blur, custom background color, or ZStack overlay you wrote in the past may clash with the new material. Apple ships a glassEffect() modifier so custom views can join this material language too, plus a new ToolbarSpacer for grouping toolbar buttons. Add the @Animatable macro that synthesizes the Animatable protocol for you, and manipulable() on visionOS that lets any 3D view be grabbed and scaled with hands — this update lands across iOS, iPadOS, macOS, and visionOS at the same time.
Details
Toolbar grouping and Liquid Glass buttons. The new ToolbarSpacer splits toolbar buttons into separate groups. With Liquid Glass, each group becomes its own capsule.
import SwiftUI
struct TripDetailView: View {
var body: some View {
NavigationStack {
TripList()
.toolbar {
ToolbarItemGroup(placement: .primaryAction) {
UpButton()
DownButton()
}
ToolbarSpacer(.fixed, placement: .primaryAction)
ToolbarItem(placement: .primaryAction) {
SettingsButton()
}
}
}
}
}
Key points:
ToolbarItemGroupputs the up and down arrow buttons inside one capsule, visually a single group.ToolbarSpacer(.fixed, placement: .primaryAction)inserts a fixed gap after them; Liquid Glass uses that gap to render the next button as a separate capsule.SettingsButtonis therefore split off from the previous two, so not every action is crammed into the same pill.
Adding glass material to a custom view. System controls adopt Liquid Glass automatically, but a floating button or hint card you draw yourself has to opt in.
import SwiftUI
struct ToTopButton: View {
var body: some View {
Button("To Top", systemImage: "chevron.up") {
scrollToTop()
}
.padding()
.glassEffect()
}
func scrollToTop() {
// Scroll to top of view
}
}
Key points:
.glassEffect()is a new view modifier that renders the view as Liquid Glass material.- It replaces the old hand-rolled combo of
.background(.ultraThinMaterial)+.clipShape(Capsule()). padding()comes first so the glass region includes the button’s inner padding instead of hugging the text.
The @Animatable macro kills the boilerplate. Animating a custom Shape used to mean writing animatableData by hand and packing several properties into nested AnimatablePairs. One macro now does the job.
import SwiftUI
@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 {
// Creates a `Path` arc using properties
return Path()
}
}
Key points:
@Animatablegoes on aShapeand synthesizesanimatableDataat compile time, threading every interpolatable property together.@AnimatableIgnoredmarks properties that don’t animate — discrete values likeBoolcan’t be interpolated and must be excluded.- One line of code replaces a dozen lines of nested
AnimatablePair<AnimatablePair<…>, …>boilerplate.
visionOS: 3D views you can grab and rest on a table. manipulable() lets users grab, rotate, and scale a view with gestures; surfaceSnappingInfo tells you whether the view has snapped to a real surface.
import ARKit
import RealityKit
import SwiftUI
struct BackpackWaterBottle: View {
@Environment(\.surfaceSnappingInfo) var snappingInfo: SurfaceSnappingInfo
var body: some View {
VStackLayout().depthAlignment(.center) {
waterBottleView
.manipulable()
Pedestal()
.opacity(
snappingInfo.classification == .table ? 1.0 : 0.0)
}
}
var waterBottleView: some View {
Model3D(named: "waterBottle")
}
}
Key points:
.manipulable()gives aModel3Dview system gestures for grabbing, rotating, and scaling, in one line.@Environment(\.surfaceSnappingInfo)reads the current snapping info — the classification has values like.table,.wall, and.floor.- When the user sets the water bottle on a table, the
Pedestalunderneath shows up; while the bottle floats in midair it stays hidden, which reads better visually.
Takeaways
-
Do this: replace every
UIViewRepresentable<UITextView>rich text bridge in your project with nativeTextEditor+AttributedString.- Why it pays off: each bridge averages two to three hundred lines of glue code, with three error-prone spots — selection sync, keyboard toolbar, undo stack. The native API pulls that work back inside SwiftUI, and long-term maintenance cost drops sharply.
- How to start: list every
UIViewRepresentablesubclass in your repo, pick a smallest case that only uses bold, italic, and color as a pilot, then migrate harder cases one by one.
-
Do this: audit your toolbar background customizations and move to
ToolbarSpacerplus the default Liquid Glass look.- Why it pays off: the custom
.toolbarBackgroundcolors you set for visual alignment break the new material’s refraction under Liquid Glass, and most likely look ugly once compiled. - How to start: use Xcode’s project-wide search for
toolbarBackgroundandultraThinMaterial, strip the custom values one at a time, regroup buttons withToolbarSpacer(.fixed, ...), then run a visual regression.
- Why it pays off: the custom
-
Do this: change every hand-written
animatableDataShapeto use the@Animatablemacro.- Why it pays off: hand-written nested
AnimatablePairis highly templated code — change one property and you change three places. The macro removes this low-value labor at the source. - How to start: search for the keyword
var animatableDatato find candidates, add@Animatableto each, mark discrete properties with@AnimatableIgnored, and run UI tests to confirm the animation curves match.
- Why it pays off: hand-written nested
-
Do this: in your visionOS project, evaluate adding
manipulable()to everyModel3Dview.- Why it pays off: in a spatial environment, the user’s first instinct is to grab things with their hands. You used to write a
DragGesture+RotateGesture3Dcombination yourself; one modifier now does it. - How to start: drop
.manipulable()into a demo scene first, then read the snap state withsurfaceSnappingInfoand decide whether to show a pedestal, shadow, or other helper visuals once the view snaps.
- Why it pays off: in a spatial environment, the user’s first instinct is to grab things with their hands. You used to write a
Related sessions
- Meet Liquid Glass — an overview of the Liquid Glass design language; read this first, then the SwiftUI implementation.
- Build a SwiftUI app with the new design — a complete app that ties Liquid Glass adoption together in SwiftUI.
- Cook up a rich text experience in SwiftUI with AttributedString — how
TextEditorandAttributedStringwork together for rich text editing. - Explore concurrency in SwiftUI — how SwiftUI and Swift concurrency interact, the execution model behind this update.
- Optimize SwiftUI performance with Instruments — the new SwiftUI Instrument, paired with this release for performance verification.
Comments
GitHub Issues · utterances