toolbar(iOS): size Liquid Glass buttons explicitly for iOS 27

Building with the iOS 27 SDK broke the VM toolbar on both iOS 26.1 and
iOS 27. On iOS 27 `.buttonStyle(.glass)` with a circle border shape sizes
each button from its label, so narrow glyphs got smaller circles than the
menus and the implicit gap iOS 26 added between buttons disappeared. On
iOS 26.1 the glass adapts to the backdrop and turns almost black over the
VM display, leaving only a faint outline of each button.

Replace the system glass button style with a custom style that draws the
icon in the same fixed frame as the pre-iOS 26 style and applies
interactive glass in a circle. A neutral gray tint with white icons keeps
the buttons readable over both dark and light guest content on either
runtime. Stack spacing no longer depends on the OS version, and the
pre-iOS 26 path is unchanged.

Assisted-by: Claude:claude-fable-5-1
This commit is contained in:
osy
2026-09-16 23:58:41 +02:00
parent e0943608a5
commit 6fef92f151
+32 -23
View File
@@ -37,16 +37,10 @@ struct VMToolbarView: View {
@Namespace private var namespace
private var spacing: CGFloat {
let add: CGFloat
if #available(iOS 26, *) {
add = 0
} else {
add = 8
}
if horizontalSizeClass == .compact || verticalSizeClass == .compact {
return add + 0
return 8
} else {
return add + 8
return 16
}
}
@@ -304,7 +298,7 @@ protocol ToolbarButtonBaseStyle<Label, Content> {
}
extension ToolbarButtonBaseStyle {
private var size: CGFloat {
var size: CGFloat {
(horizontalSizeClass == .compact || verticalSizeClass == .compact) ? 32 : 48
}
@@ -353,6 +347,34 @@ struct ToolbarButtonStyle: ButtonStyle, ToolbarButtonBaseStyle {
}
}
/// Liquid Glass variant of `ToolbarButtonStyle`.
///
/// Sizes the button explicitly instead of relying on `.buttonStyle(.glass)`:
/// the system style sizes each circle from its label on iOS 27, which makes
/// the buttons uneven and removes the gap between them. The neutral tint keeps
/// the buttons readable over both dark and light guest content, since glass
/// otherwise adapts to whatever the VM is displaying underneath.
@available(iOS 26, *)
struct ToolbarGlassButtonStyle: ButtonStyle, ToolbarButtonBaseStyle {
typealias Label = Configuration.Label
@Environment(\.isEnabled) private var isEnabled
var horizontalSizeClass: UserInterfaceSizeClass?
var verticalSizeClass: UserInterfaceSizeClass?
func makeBody(configuration: Configuration) -> some View {
configuration.label
.labelStyle(.iconOnly)
.foregroundStyle(.white)
.frame(width: size, height: size)
.contentShape(Circle())
.glassEffect(.regular.tint(.gray.opacity(0.5)).interactive(), in: .circle)
.opacity(isEnabled ? 1 : 0.5)
.hoverEffect(.lift)
}
}
struct ToolbarMenuStyle: MenuStyle, ToolbarButtonBaseStyle {
typealias Label = Menu<Configuration.Label, Configuration.Content>
@@ -370,11 +392,7 @@ private extension View {
if #available(iOS 26, *) {
self
.menuStyle(.button)
.buttonStyle(.glass)
.buttonBorderShape(.circle)
.labelStyle(.iconOnly)
.foregroundStyle(.primary)
.controlSize(forHorizontalSizeClass: horizontalSizeClass)
.buttonStyle(ToolbarGlassButtonStyle(horizontalSizeClass: horizontalSizeClass, verticalSizeClass: verticalSizeClass))
} else {
self
.buttonStyle(.toolbar(horizontalSizeClass: horizontalSizeClass, verticalSizeClass: verticalSizeClass))
@@ -393,15 +411,6 @@ private extension View {
.matchedGeometryEffect(id: id, in: namespace)
}
}
@ViewBuilder
func controlSize(forHorizontalSizeClass horizontalSizeClass: UserInterfaceSizeClass?) -> some View {
if horizontalSizeClass == .regular {
self.controlSize(.large)
} else {
self
}
}
}
// https://www.objc.io/blog/2019/10/01/swiftui-shake-animation/