Full code following thread 826485. Feedback: FB25055820.
Earlier calibrated Lab recordings on an iPhone 17 Pro Simulator (iOS 27.2 Beta 1, 24B5084k; Xcode 27.2, 27B5019j) showed a round press highlight inside the L, including on its empty cutout where the Button action did not fire. Interactive off removed it; a wider host widened it without stretching the L. These are Simulator observations. This extracted English project compiled, but has not been newly run or optically recorded.
The baseline already uses the SAME L contentShape after glassEffect. Other matching-L placements are not established failures. Which public API controls the highlight boundary, and where should contentShape go? Are nonconvex shapes supported?
Hold each arm, then the cutout; compare the highlight and action counter. Default uses the recorded baseline order.
import SwiftUI
@main
struct CustomGlassHighlightApp: App {
var body: some Scene {
WindowGroup { GlassHighlightStudy() }
}
}
private enum Experiment: String, CaseIterable, Identifiable {
case baseline = "Baseline — L after glass"
case interactiveOff = "Interactive off"
case wideHost = "Wide host (180 pt)"
case labelAndAfter = "L on label + after glass"
case rectangleBefore = "Rectangle before glass"
case rectangleAfter = "Rectangle after glass"
var id: String { rawValue }
var hostWidth: CGFloat { self == .wideHost ? 180 : 120 }
var isInteractive: Bool { self != .interactiveOff }
var explanation: String {
switch self {
case .baseline:
return "120 × 120 pt host. The same L Shape is used by glassEffect and by contentShape after glassEffect."
case .interactiveOff:
return "Same L, frame, and modifier order as baseline; only interactive is false."
case .wideHost:
return "180 × 120 pt host. The L remains 120 × 120 pt, centered in the wider host."
case .labelAndAfter:
return "Baseline plus the same L contentShape on the Button label, before the Button is constructed."
case .rectangleBefore:
return "Diagnostic mismatch: Rectangle contentShape before glassEffect; the glass remains an L."
case .rectangleAfter:
return "Diagnostic mismatch: Rectangle contentShape after glassEffect; the glass remains an L."
}
}
}
// A fixed-size nonconvex path. Widening its host does not stretch the L.
private struct Fixed120GlassL: Shape {
func path(in rect: CGRect) -> Path {
let origin = CGPoint(x: rect.midX - 60, y: rect.midY - 60)
let points: [CGPoint] = [
.zero, CGPoint(x: 120, y: 0), CGPoint(x: 120, y: 42),
CGPoint(x: 42, y: 42), CGPoint(x: 42, y: 120), CGPoint(x: 0, y: 120)
]
var path = Path()
for (index, point) in points.enumerated() {
let target = CGPoint(x: origin.x + point.x, y: origin.y + point.y)
if index == 0 { path.move(to: target) }
else { path.addLine(to: target) }
}
path.closeSubpath()
return path
}
}
private enum HintPosition { case label, before, after }
private struct HitShapeHint: ViewModifier {
let experiment: Experiment
let position: HintPosition
@ViewBuilder
func body(content: Content) -> some View {
switch (experiment, position) {
case (.labelAndAfter, .label):
content.contentShape(.interaction, Fixed120GlassL())
case (.rectangleBefore, .before), (.rectangleAfter, .after):
content.contentShape(.interaction, Rectangle())
case (.baseline, .after), (.interactiveOff, .after),
(.wideHost, .after), (.labelAndAfter, .after):
content.contentShape(.interaction, Fixed120GlassL())
default:
content
}
}
}
private struct GlassHighlightStudy: View {
@State private var experiment: Experiment = .baseline
@State private var actionCount = 0
var body: some View {
ZStack {
LinearGradient(
colors: [Color(red: 0.06, green: 0.14, blue: 0.26),
Color(red: 0.38, green: 0.00, blue: 0.25), .black],
startPoint: .topLeading, endPoint: .bottomTrailing
).ignoresSafeArea()
VStack(spacing: 24) {
Text("Custom Glass Highlight")
.font(.title2.bold())
Picker("Experiment", selection: $experiment) {
ForEach(Experiment.allCases) { item in
Text(item.rawValue).tag(item)
}
}
.pickerStyle(.menu)
Text(experiment.explanation)
.font(.callout)
.frame(minHeight: 76, alignment: .topLeading)
.frame(maxWidth: .infinity, alignment: .leading)
// This modifier order is extracted from the earlier causal probe.
Button { actionCount += 1 } label: {
Fixed120GlassL().fill(Color.white.opacity(0.001))
.frame(width: experiment.hostWidth, height: 120)
.modifier(HitShapeHint(experiment: experiment, position: .label))
}
.buttonStyle(.plain)
.modifier(HitShapeHint(experiment: experiment, position: .before))
.glassEffect(.regular.interactive(experiment.isInteractive), in: Fixed120GlassL())
.modifier(HitShapeHint(experiment: experiment, position: .after))
.overlay {
Fixed120GlassL()
.stroke(Color.white.opacity(0.35), lineWidth: 0.5)
.allowsHitTesting(false)
.accessibilityHidden(true)
}
.frame(width: experiment.hostWidth, height: 120)
.frame(width: 220, height: 220)
.id(experiment)
.accessibilityLabel("L-shaped glass button")
Text("Button actions: \(actionCount)")
.monospacedDigit()
Text("Press and hold the top arm, then the lower arm. Try the empty lower-right cutout. Compare the highlight with the L contour and the action counter.")
.font(.callout)
.frame(maxWidth: .infinity, alignment: .leading)
Spacer(minLength: 0)
}
.foregroundStyle(.white)
.padding(24)
}
.preferredColorScheme(.dark)
.onChange(of: experiment) { _, _ in actionCount = 0 }
}
}