Quickie: Redesigning for Liquid Glass? ToolbarContent is your friend

One of the first things any trained eye will notice about the new UI design in iOS 26 is how different app toolbars look. They look very different.

To me that's one of the most exciting changes and adopting this on day one is a way to set your app apart in a noisy world.
My first attempt at adopting it looked like this:
import SwiftUI
struct SettingsView: View {
var body: some View {
if #available(iOS 26.0, *) {
form
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
NavigationLink(
destination: DiagnosticsView(
database: LocalDatabase.shared,
analyticsService: AnalyticsService()
)
) {
Image(systemName: "stethoscope")
}
}
ToolbarSpacer(.fixed, placement: .topBarTrailing)
ToolbarItem(placement: .topBarTrailing) {
NavigationLink(destination: HelpView()) {
Image(systemName: "questionmark")
}
}
}
} else {
form
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
NavigationLink(destination: HelpView()) {
Image(systemName: "questionmark.circle")
}
}
}
}
}
}
As you can see, I'm using ToolbarSpacer
, which is the new tool we get to achieve spacings we see fit. But you'll also notice I broke the view's Form into a subview so I could apply different .toolbar
modifiers depending on OS version. This makes it very cumbersome to adopt the new toolbar everywhere in an app. I thought we just had to live with this.
In other views I got subviews to handle the entire toolbar for the sake of breaking up views and making them easier to read. Here's an example:
extension ReactionDetailView {
struct ToolbarControls: View {
@Binding var contentSortOption: Int
let playStopAction: () -> Void
let startSelectingAction: () -> Void
let isPlayingPlaylist: Bool
let soundArrayIsEmpty: Bool
let isSelecting: Bool
private var playStopIsDisabled: Bool {
soundArrayIsEmpty || isSelecting
}
var body: some View {
if #available(iOS 26.0, *) {
ToolbarItem {
Button {
playStopAction()
} label: {
Image(systemName: isPlayingPlaylist ? "stop.fill" : "play.fill")
}
.disabled(playStopIsDisabled)
}
ToolbarSpacer(.fixed)
ToolbarItem {
Menu {
// Omitted for readability
} label: {
Image(systemName: "ellipsis")
}
}
} else {
HStack(spacing: 15) {
Button {
playStopAction()
} label: {
Image(systemName: isPlayingPlaylist ? "stop.fill" : "play.fill")
.opacity(playStopIsDisabled ? 0.5 : 1.0)
}
.disabled(playStopIsDisabled)
Menu {
// Omitted for readability
} label: {
Image(systemName: "ellipsis")
}
}
}
}
}
}
The compiler was not happy about this. It would throw the following error: Static method 'buildExpression' requires that 'ToolbarItem<(), some View>' conform to 'View'
.
You could say the issue is right here in my face but I could not see it yet. I went asking on the iOS Folks Slack and a noble soul pointed out that .toolbar
expects ToolbarContent
and not a View
. So I went fixing:
extension ReactionDetailView {
struct ToolbarControls: ToolbarContent {
// Vars omitted for readability
var body: some ToolbarContent {
if #available(iOS 26.0, *) {
ToolbarItem {
Button {
playStopAction()
} label: {
Image(systemName: isPlayingPlaylist ? "stop.fill" : "play.fill")
}
.disabled(playStopIsDisabled)
}
ToolbarSpacer(.fixed)
ToolbarItem {
Menu {
// Omitted for readability
} label: {
Image(systemName: "ellipsis")
}
}
} else {
ToolbarItem {
Button {
playStopAction()
} label: {
Image(systemName: isPlayingPlaylist ? "stop.fill" : "play.fill")
.opacity(playStopIsDisabled ? 0.5 : 1.0)
}
.disabled(playStopIsDisabled)
}
ToolbarItem {
Menu {
// Omitted for readability
} label: {
Image(systemName: "ellipsis")
}
}
}
}
}
}
Keep in mind that while ToolbarSpacer
is a new component (and will not compile in previous Xcode versions, for example) ToolbarItem is not so it's completely fine to use it for iOS versions all the way back to 14.0.
Now our views can be as simple as:
struct SomeView: View {
var body: some View {
VStack {
// Your stuff
}
.toolbar {
YourCustomToolbarThatAdaptsToOSVersion()
}
}
}
Thank you for reading. Be sure to subscribe and leave a comment. SwiftUI is neat, it just takes some digging to fully love it. :)