Provide views, controls, and layout structures for declaring your app's user interface using SwiftUI.

Posts under SwiftUI tag

200 Posts

Post

Replies

Boosts

Views

Activity

Toolbar background behaviour
Say i have a scrollview: I find it’s dumb how if you have the safe area inset or any kind of element below the toolbar, the background of those will extend to the top edge of the screen (by default), yet if the scrollview scrolls to the toolbar the toolbar background just “appears”, covering the existing background— its even worse cuz the toolbar background is there by default even when you didn’t set it in code. It seems more like a bug; I feel like this is something the SwiftUI team should improve upon. How about disabling toolbar background by default? Just wanted to get some more perspectives.
0
0
4
10m
iOS 27 beta 1: .scrollEdgeEffectStyle(.soft) renders fully transparent above safeAreaBar
Feedback ID: FB23086400 On iOS 27 beta 1, .scrollEdgeEffectStyle(.soft, for: .top) on a List underneath a custom .safeAreaBar(edge: .top) no longer renders the progressive fade-blur. The top edge is fully transparent — scrolled rows pass under the bar with no visual treatment at all, as if scrollEdgeEffectDisabled() had been applied. What I've verified so far: .hard renders correctly in the exact same hierarchy; only .soft is affected. The same binary works correctly on iOS 26.x Xcode preview. I'm building with Xcode 26.3 (iOS 26 SDK). Minimal reproduction: import SwiftUI struct EdgeEffectRepro: View { enum Style: String, CaseIterable, Identifiable { case automatic, soft, hard var id: Self { self } var value: ScrollEdgeEffectStyle { switch self { case .automatic: .automatic case .soft: .soft case .hard: .hard } } } @State private var style: Style = .soft @State private var useSystemBarOnly = false var body: some View { NavigationStack { List(0..<60, id: \.self) { i in Text("Row \(i)") .frame(maxWidth: .infinity, alignment: .leading) .listRowBackground( i.isMultiple(of: 2) ? Color.orange.opacity(0.45) : Color.teal.opacity(0.45) ) } .scrollIndicators(.hidden) .scrollEdgeEffectStyle(style.value, for: .top) .safeAreaBar(edge: .top) { if !useSystemBarOnly { VStack(spacing: 8) { HStack { Text("Custom Top Bar") .font(.system(size: 28, weight: .bold)) Spacer() } HStack { Text("Second row (e.g. date range picker)") .font(.caption) .foregroundStyle(.secondary) Spacer() } } .padding(.horizontal) } } .safeAreaInset(edge: .bottom) { VStack(spacing: 8) { Picker("Edge effect style", selection: $style) { ForEach(Style.allCases) { Text($0.rawValue).tag($0) } } .pickerStyle(.segmented) Toggle("System bar only (control group)", isOn: $useSystemBarOnly) .font(.caption) } .padding() .background(.regularMaterial) } .navigationTitle("EdgeEffect Repro") .navigationBarTitleDisplayMode(.inline) } } } Steps: run on iOS 27 beta 1, set the picker to soft, scroll rows under the bar. Expected: fade-blur as on iOS 26. Actual: fully transparent. Switch to hard: renders fine.
24
13
4.7k
8h
Hard scroll edge effect
The example they gave for the hard scroll edge effect is the calendar app, but Idk if this is a bad example or if i am missing something, because the calendar app theres a date picker row below the toolbar, and from my testing, whenever theres some element between the toolbar and the scrollview, the fade and blur effect does not happen -- so in fact, the hard scroll edge effect is completely unnecessary.
0
0
56
11h
NSToolbarItemViewer's min/max size is nan
When attempting to use Toolbar(id: "") this crash occurs immediately upon showing the window. FAULT: NSInternalInconsistencyException: NSToolbarItemViewer's min/max size is nan. This indicates an item's size has been changed during layout which is illegal; { NSAssertFile = "NSToolbarItemViewer.m"; NSAssertLine = 942; } If I comment out the toolbars, re-run show the window once, then re-add id: the window properly displays, allows customization and does not error unless I clear data within ~/Library/Containers.
1
0
338
22h
Button or onTapGesture grows its hit region by a fixed amount past the view’s bounds.
SwiftUI Button hit area extends ~16pt beyond its visual frame — expected OS behavior? Hi everyone, I’m working on a custom SwiftUI segmented control and noticed some unexpected hit-testing behavior with Button. I have a button structured roughly like this: Button { selectItem(at: index) } label: { ZStack { if selectedIndex == index { selectedBackground } SegmentCellView( tab: tab, isSelected: selectedIndex == index ) } .frame(maxWidth: .infinity) .contentShape(Rectangle()) } .accessibilityLabel(Text(tab.labelText)) The visual frame of the button has a certain height, but when testing on a real device, I can tap approximately 16pt outside the button's visible frame and the button still receives the tap. This seems to happen even though I explicitly use: .contentShape(Rectangle()) to define the tappable shape. My questions are: Is this ~16pt expansion of the interactive area expected SwiftUI/OS behavior? Is this related to Apple's minimum hit-target/accessibility guidelines being automatically applied to controls such as Button? If so, is there an official API/documentation describing this behavior? Is there a recommended way to make the Button's hit-testing area exactly match the bounds defined by .contentShape(Rectangle()), if that is actually possible? I'm particularly interested in understanding whether this is intentional system behavior or something caused by the ButtonStyle / layout hierarchy. Thanks!
0
0
28
1d
How can I set per-window titles in the Window menu for an iPad app running on Apple silicon Mac?
I’m developing a SwiftUI iPad app that runs on an Apple silicon Mac as an iPad app—not as a Mac Catalyst app. The app supports multiple windows, with each Live Screen window associated with a different virtual machine. In the macOS Window menu, all child windows appear as “VirtualProg” rather than showing their content or VM name. The Live Screen window is created from a data-driven  WindowGroup , roughly like this: WindowGroup(id: "vm-screen", for: String.self) { $vmName in VMScreenWindow(vmName: vmName) .navigationTitle(vmName.map { "Live ($0)" } ?? "Live Screen") } I’ve tried setting the title with  .navigationTitle  both on the  WindowGroup  content and inside the window’s  NavigationStack . I also tried setting  windowScene.title  on the specific scene obtained from  view.window?.windowScene  after the view controller appeared. Neither changed the Window-menu entries. Is there a supported way to set a per-window title for the macOS Window menu when running an iPad app on Mac? Does  UIScene.title  affect that menu, or only the window title bar and app switcher? Is this a limitation of the iPad-app-on-Mac runtime, or is there a recommended SwiftUI/UIKit lifecycle hook for this? I’m looking for a public API solution rather than private AppKit/window introspection.
0
0
48
1d
toolbarMinimizationBehavior(_:for:) crashes on some iOS 27.0.0 devices: "missing weak symbol" despite #available(iOS 27.0, *)
We are seeing a production crash that only affects devices reporting iOS 27.0.0. Devices on 27.0.1 and later, and on iOS 26, are unaffected. The app is built with Xcode 27.0 (the same binary shape appears with the Xcode 27.1 RC toolchain), deployment target iOS 26.0. The crashing code is a plain availability-gated call: extension View { @ViewBuilder func disableNavigationBarMinimization() -> some View { if #available(iOS 27.0, *) { toolbarMinimizationBehavior(.never, for: .navigationBar) } else { self } } } Crash report excerpt (addresses and app symbols removed): crash_info_entry_0: Failed to look up symbolic reference at <addr> - offset <n> - symbol <nearest stripped symbol> in <app binary> - pointer at <addr> is likely a reference to a missing weak symbol Crashed: com.apple.main-thread 0 libsystem_kernel.dylib __pthread_kill 2 libsystem_c.dylib abort 3 libswiftCore.dylib <redacted> 9 libswiftCore.dylib swift_getTypeByMangledName + 908 10 libswiftCore.dylib swift_getTypeByMangledNameInContext2 + 248 11 <app> __swift_instantiateConcreteTypeFromMangledNameV2 12 <app> specialized closure #1 in View.disableNavigationBarMinimization() 13 <app> closure #1 in SomeView.body.getter What we established: Because the deployment target is 26.0 and the API is @available(iOS 27.0, *), the app weak-imports both the function and its opaque result type descriptor. dyld_info -imports on the binary shows: _$s7SwiftUI4ViewPAAE27toolbarMinimizationBehavior_3forQrAA07ToolbareF0V_AA0H9PlacementVdtF [weak-import] (from SwiftUI) _$s7SwiftUI4ViewPAAE27toolbarMinimizationBehavior_3forQrAA07ToolbareF0V_AA0H9PlacementVdtFQOMQ [weak-import] (from SwiftUI) The call site has to instantiate the opaque return type's metadata before calling the function. That is the frame that aborts. So when dyld binds the descriptor to null, #available(iOS 27.0, *) passes and the process still dies. The symbol is present and exported on the iOS 27.0 build we can inspect locally (24A437, via Xcode's DeviceSupport symbols), and the same code runs fine on the iOS 27.0 simulator runtime (24A434). We cannot reproduce the crash on any device we own. The SwiftUI binary in 24A437 exports both toolbarMinimizeBehavior(_:for:) / ToolbarMinimizeBehavior and toolbarMinimizationBehavior(_:for:) / ToolbarMinimizationBehavior. The iOS 27.1 SDK only declares the latter, still annotated @available(iOS 27.0, *). This looks like a rename late in the 27.0 cycle. Our working theory is that some iOS 27.0.0 builds in the field (earlier 27.0 builds, possibly the ones preinstalled on new devices) predate the rename and only contain toolbarMinimizeBehavior, so the renamed symbol is absent and the weak import resolves to null. Questions: Is the @available(iOS 27.0, *) annotation on toolbarMinimizationBehavior(_:for:) accurate for every shipped 27.0.0 build? Should it be 27.0.1 or 27.1? Is there a supported way to guard against a missing opaque type descriptor at runtime, short of raising the availability check? Feedback filed as FB25095054.
0
0
63
1d
SwiftUI views lock up after background and sleep for “Designed for iPad” apps
There's an easily reproducible SwiftUI bug on macOS where an app's UI state no longer updates/re-renders for "Designed for iPad" apps (i.e. ProcessInfo.processInfo.isiOSAppOnMac == true). The bug occurs in Xcode and also if the app is running independent of Xcode. The bug occurs when: the user Hides the app (i.e. it goes into the background) the user puts the Mac to sleep (e.g. Apple menu > Sleep) a total of ~60 seconds transpires (i.e. macOS puts the app into the "suspended state") when the app is brought back into the foreground the UI no longer updates properly The only way I have found to fix this is to manually open a new actual full app window via File > New, in which case the app works fine again in the new window. The following extremely simple code in a default Xcode project illustrates the issue: import SwiftUI @main struct staleApp: App { @State private var isBright = true var body: some Scene { WindowGroup() { ZStack { (isBright ? Color.white : Color.black).ignoresSafeArea() Button("TOGGLE") { isBright.toggle(); print("TAPPED") } } .onAppear { print("\(isBright ? "light" : "dark") view appeared") } } } } For the code above, after Hiding the app and putting the computer to sleep for 60 seconds or more, the button no longer swaps views, although the print statements still appear in the console upon tapping the button. Also, while in this buggy state, i can get the view to update to the current state (i.e. the view triggered by the last tap) by manually dragging the corner of the app window to resize the window. But after resizing, the view again does not update upon button tapping until I resize the window again. so it appears the diff engine is mucked or that the Scene or WindowGroup are no longer correctly running on the main thread I have tried rebuilding the entire view hierarchy by updating .id() on views but this approach does NOT work. I have tried many other options/hacks but have not been able to reset the 'view engine' other than opening a new window manually or by using: @Environment(.openWindow) private var openWindow openWindow could be a viable solution except there's no way to programmatically close the old window for isiOSAppOnMac (@Environment(.dismissWindow) private var dismissWindow doesn't work for iOS)
2
1
287
2d
iOS 27: SwiftUI keyboard safe area is not restored after returning to a UIHostingController
I have a SwiftUI view embedded in a UIHostingController. It contains a numeric text field at the top and a .borderedProminent button at the bottom. The text field is automatically focused when the view appears. The bottom button pushes another instance of the same hosting controller. On the initial appearance, SwiftUI correctly positions the button above the keyboard. However, after navigating back from VC2 to VC1, the text field is focused and the keyboard is visible, but the button remains behind the keyboard. This occurs regardless of whether VC2 is closed using the navigation bar back button or the interactive back gesture. This appears to be an iOS 27 regression involving SwiftUI keyboard avoidance inside a UIHostingController. The issue is especially problematic with a numeric keyboard because it has no Return or Done key. If the covered button is the primary way to continue or dismiss the keyboard, the user can no longer access it. Interestingly, the button is repositioned correctly as soon as I begin moving the app into the background. This suggests that SwiftUI still knows about the keyboard safe area but does not update the layout correctly when the hosting controller reappears after being popped to. Minimal reproducible example import SwiftUI import UIKit final class NumericInputHostingController: UIHostingController<NumericInputView> { init() { super.init( rootView: NumericInputView(onContinue: {}) ) rootView = NumericInputView { [weak self] in self?.navigationController?.pushViewController( NumericInputHostingController(), animated: true ) } } @available(*, unavailable) @MainActor required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } struct NumericInputView: View { @State private var value = "" @FocusState private var isTextFieldFocused: Bool let onContinue: () -> Void var body: some View { VStack(spacing: 24) { TextField("Enter a number", text: $value) .keyboardType(.numberPad) .focused($isTextFieldFocused) .padding(.horizontal, 16) .frame(height: 52) .background { RoundedRectangle(cornerRadius: 10) .stroke( Color.accentColor, lineWidth: 1.5 ) } Spacer() Button("Open Another", action: onContinue) .buttonStyle(.borderedProminent) .controlSize(.large) .frame(maxWidth: .infinity) } .padding(20) .background(Color(.systemBackground)) .onAppear { DispatchQueue.main.async { isTextFieldFocused = true } } .onDisappear { isTextFieldFocused = false } } } The initial controller is embedded in a navigation controller: let controller = NumericInputHostingController() let navigationController = UINavigationController( rootViewController: controller ) Steps to reproduce Present VC1. VC1 automatically focuses the numeric text field. Confirm that the bottom button is above the keyboard. Tap the button to push VC2. Navigate back to VC1. VC1 focuses its text field and displays the numeric keyboard. Observe that the bottom button is now behind the keyboard. Begin putting the app into the background. Observe that the button suddenly moves to the correct position above the keyboard. UIKit’s keyboardLayoutGuide handles the equivalent UIKit layout correctly. Is this a known issue with keyboard safe-area updates when a UIHostingController reappears after navigation? Is there a supported SwiftUI solution that does not require observing keyboard notifications manually?
3
1
303
2d
.contactAccessPicker shows blank sheet on iOS 26.2.1 on device
Calling contactAccessPicker results in a blank sheet and a jetsam error, rather than the expected contact picker, using Apple’s sample code, only on device with iOS 26.2.1. This is happening on a iPhone 17 Pro Max running 26.2.1, and not on a simulator. I’m running Apple's sample project Accessing a person’s contact data using Contacts and ContactsUI Steps: Run the sample app on device running iOS 26.2.1. Use the flow to authorize .limited access with 1 contact: Tap request access, Continue, Select Contacts. Select a contact, Continue, Allow Selected Contact. This all works as expected. Tap the add contact button in the toolbar to add a second contact. Expected: This should show the Contact Access Picker UI. Actual: Sheet is shown with no contents. See screenshot of actual results on iOS device running 26.2.1. Reported as FB21812568 I see a similar (same?) error reported for 26.1. It seems strange that the feature is completely broken for multiple point releases. Is anyone else seeing this or are the two of us running into the same rare edge case? Expected Outcome, seen on simulator running 26.2 Actual outcome, seen on device running 26.2.1
10
3
1.5k
2d
Dismiss/Close button - leading or trailing edge?
What is the recommended position of the "X" button dismissing a modal sheet on iOS? When we have "X" and "✓" the case is easy, but what if we just have "X" button? For example, on a "What's New" screen. I found guidelines here: https://developer.apple.com/design/human-interface-guidelines/sheets Based on resizable sheets presented there, the "X" button should be on leading edge. However, iOS is inconsistent and sometimes it presents the button on the leading edge and sometimes on the trailing edge. How do you approach it? Do you prefer having it on the leading or trailing edge? SwiftUI provides ToolbarItem(placement: .cancellationAction) , but it doesn't feel like the right placement. cancellationAction sounds like something for actionable sheets when we have something to be cancelled. I'm missing placement: .dismiss :).
1
0
875
2d
NavigationSplitView without detail information
Hi, I have a working program with categories, lists and detail views. All connected via NavigationLink inside a NavigationStack. It looks a bit strange on an iPad, so I replaced the NavigationStack with a NavigationSplitView. The categories went into the sidebar and the lists into the content section. The detail section of the NavigationSplitView is empty and it still works. Is that the correct behavior? I don't mind it, because it works. example code: var body: some View { NavigationSplitView { List(selection: $listSelection) { Section("Section 1") { NavigationLink(value: EnumValue.V1) { CardView("Card 1") } NavigationLink(value: EnumValue.V2) { CardView("Card 2") } NavigationLink(value: EnumValue.V3) { CardView("Card 3") } } Section("Section 2") { NavigationLink(value: EnumValue.V4) { CardView("Card 4") } NavigationLink(value: EnumValue.V5) { CardView("Card 5") } } } .scrollIndicators(.hidden) .navigationTitle("Detail Empty") } content: { if let mt = listSelection { switch mt { case .V1: ListView1() case .V2: ListView2() case .V3: ListView3() case .V4: ListView4() case .V5: ListView5() } } } detail: { // empty } } In each ListView, I assign the ID to the EditorView so that the EditorView is updated. .navigationDestination(for: ViewListTransfer.self) { vl in switch vl { case .trend(bp: let bp): EditorView(value: bp) .id(bp.Id) case .trendNew: EditorView(val: nil) } } It seems to work because everything is linked via NavigationLinks, but it does look strange that the detail section is empty. The details section is mandatory; therefore, it cannot be omitted. Is this behavior something for a bug report? Christian
2
0
352
2d
Using .sidebarAdaptable style breaks NavigationStack's transition animation in tvOS
When a NavigationStack is nested inside a TabView that's using the .sidebarAdaptable style, it completely overwrites the transition animations of the NavigationStack. In tvOS 26, the transition for push and pop instantly snaps in place. in tvOS 27, it uses its own transition animation, which has strange behaviors, such as slow crossfade, and items rearranging if the frame of the child view is different. Here is a sample code that shows what I'm talking about. struct ContentView: View { @State private var selectedTab = 0 var body: some View { TabView(selection: $selectedTab) { Tab("Home", systemImage: "house", value: 0) { NavigationStack { VStack(spacing: 30) { Text("Root Home View") .font(.title) NavigationLink("Push Detail View") { Text("Child View") .font(.largeTitle) } } } } } .tabViewStyle(.sidebarAdaptable) } } Note: Placing NavigationStack over a TabView will retain the NavigationStack's transition animations, but you lose navigation paths per tab, and child views a rendered over the tabview.
0
0
271
2d
Question: Task modifier with id called on initial
Hello, I have a question regarding the task(id:) modifier. It is possible, of course, that I am using the modifier for the wrong purpose. Consider a multi-field search scenario where any change to a field triggers a loading process via the task(id:) modifier. The problem is that every task(id:) modifier executes when the view is first loaded, causing the same data to be loaded multiple times—which makes no sense. I do not want to add a separate search button or implement debouncing; those would merely be workarounds to bypass the actual issue. The onChange modifier has an initial parameter that defaults to false, preventing the action from running when the view is first loaded. However, the task(id:) modifier does not have this parameter. How can I prevent all task(id:) modifiers from executing when the view loads? After all, the standard task modifier should handle the initial load. struct ContentView: View { @State private var field1: String = "" @State private var field2: String = "" @State private var field3: String = "" var body: some View { VStack { TextField("val1", text: $field1) TextField("val2", text: $field2) TextField("val3", text: $field3) } .padding() .task { print("task") } .task(id: field1) { print("task 1") } .task(id: field2) { print("task 2") } .task(id: field3) { print("task 3") } } } Christian
1
0
259
3d
Can we opt out of the Horizontal Bar on the iPhone Duo?
We are aware that we can opt out of the vertical bar to make it horizontal for folded and unfolded landscape: https://developer.apple.com/documentation/uikit/uiviewcontroller/preferredverticalbarbehavior As per the title, we would like to know if if the reverse is possible (i.e. can we have the unfolded portrait display a vertical bar instead of a horizontal one)
1
0
108
3d
[VoiceOver] Dynamic fractions (e.g., ½, 1/2) are completely omitted/skipped in iOS 27
Platform & OS Version: iOS 27 Screen Reader: VoiceOver Framework: SwiftUI / UIKit / WebKit (WKWebView) Issue Summary: When updating dynamic text or UI elements containing fraction values—whether using Unicode fraction glyphs (e.g., ½, ¾) VoiceOver in iOS 27 completely omits and skips reading the fraction value entirely. Steps to Reproduce: Enable VoiceOver on iOS 27 (Settings > Accessibility > VoiceOver). Open an app or web view where fraction values, tap to focus on the element or trigger voice over Expected Result: VoiceOver should speak the accessible label or text content (e.g., "one half") Actual Result: VoiceOver skips the fraction entirely, reading only surrounding text or remaining silent over the fraction value.
0
0
463
3d
SwiftUI interactive Liquid Glass highlight boundary for a custom L shape
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 } } }
0
0
102
4d
Toolbar background behaviour
Say i have a scrollview: I find it’s dumb how if you have the safe area inset or any kind of element below the toolbar, the background of those will extend to the top edge of the screen (by default), yet if the scrollview scrolls to the toolbar the toolbar background just “appears”, covering the existing background— its even worse cuz the toolbar background is there by default even when you didn’t set it in code. It seems more like a bug; I feel like this is something the SwiftUI team should improve upon. How about disabling toolbar background by default? Just wanted to get some more perspectives.
Replies
0
Boosts
0
Views
4
Activity
10m
iOS 27 beta 1: .scrollEdgeEffectStyle(.soft) renders fully transparent above safeAreaBar
Feedback ID: FB23086400 On iOS 27 beta 1, .scrollEdgeEffectStyle(.soft, for: .top) on a List underneath a custom .safeAreaBar(edge: .top) no longer renders the progressive fade-blur. The top edge is fully transparent — scrolled rows pass under the bar with no visual treatment at all, as if scrollEdgeEffectDisabled() had been applied. What I've verified so far: .hard renders correctly in the exact same hierarchy; only .soft is affected. The same binary works correctly on iOS 26.x Xcode preview. I'm building with Xcode 26.3 (iOS 26 SDK). Minimal reproduction: import SwiftUI struct EdgeEffectRepro: View { enum Style: String, CaseIterable, Identifiable { case automatic, soft, hard var id: Self { self } var value: ScrollEdgeEffectStyle { switch self { case .automatic: .automatic case .soft: .soft case .hard: .hard } } } @State private var style: Style = .soft @State private var useSystemBarOnly = false var body: some View { NavigationStack { List(0..<60, id: \.self) { i in Text("Row \(i)") .frame(maxWidth: .infinity, alignment: .leading) .listRowBackground( i.isMultiple(of: 2) ? Color.orange.opacity(0.45) : Color.teal.opacity(0.45) ) } .scrollIndicators(.hidden) .scrollEdgeEffectStyle(style.value, for: .top) .safeAreaBar(edge: .top) { if !useSystemBarOnly { VStack(spacing: 8) { HStack { Text("Custom Top Bar") .font(.system(size: 28, weight: .bold)) Spacer() } HStack { Text("Second row (e.g. date range picker)") .font(.caption) .foregroundStyle(.secondary) Spacer() } } .padding(.horizontal) } } .safeAreaInset(edge: .bottom) { VStack(spacing: 8) { Picker("Edge effect style", selection: $style) { ForEach(Style.allCases) { Text($0.rawValue).tag($0) } } .pickerStyle(.segmented) Toggle("System bar only (control group)", isOn: $useSystemBarOnly) .font(.caption) } .padding() .background(.regularMaterial) } .navigationTitle("EdgeEffect Repro") .navigationBarTitleDisplayMode(.inline) } } } Steps: run on iOS 27 beta 1, set the picker to soft, scroll rows under the bar. Expected: fade-blur as on iOS 26. Actual: fully transparent. Switch to hard: renders fine.
Replies
24
Boosts
13
Views
4.7k
Activity
8h
Hard scroll edge effect
The example they gave for the hard scroll edge effect is the calendar app, but Idk if this is a bad example or if i am missing something, because the calendar app theres a date picker row below the toolbar, and from my testing, whenever theres some element between the toolbar and the scrollview, the fade and blur effect does not happen -- so in fact, the hard scroll edge effect is completely unnecessary.
Replies
0
Boosts
0
Views
56
Activity
11h
NSToolbarItemViewer's min/max size is nan
When attempting to use Toolbar(id: "") this crash occurs immediately upon showing the window. FAULT: NSInternalInconsistencyException: NSToolbarItemViewer's min/max size is nan. This indicates an item's size has been changed during layout which is illegal; { NSAssertFile = "NSToolbarItemViewer.m"; NSAssertLine = 942; } If I comment out the toolbars, re-run show the window once, then re-add id: the window properly displays, allows customization and does not error unless I clear data within ~/Library/Containers.
Replies
1
Boosts
0
Views
338
Activity
22h
Ghost Padding on NavigationBarPlatterContainer
Hello, I have a SwiftUI View sitting in the UIHosting view controller. On rotation to landscape, the system would add padding to the toolbar. I presume it's the nav bar. Anyone experienced this? What would be this space? padding? content margin?
Replies
4
Boosts
0
Views
457
Activity
1d
Button or onTapGesture grows its hit region by a fixed amount past the view’s bounds.
SwiftUI Button hit area extends ~16pt beyond its visual frame — expected OS behavior? Hi everyone, I’m working on a custom SwiftUI segmented control and noticed some unexpected hit-testing behavior with Button. I have a button structured roughly like this: Button { selectItem(at: index) } label: { ZStack { if selectedIndex == index { selectedBackground } SegmentCellView( tab: tab, isSelected: selectedIndex == index ) } .frame(maxWidth: .infinity) .contentShape(Rectangle()) } .accessibilityLabel(Text(tab.labelText)) The visual frame of the button has a certain height, but when testing on a real device, I can tap approximately 16pt outside the button's visible frame and the button still receives the tap. This seems to happen even though I explicitly use: .contentShape(Rectangle()) to define the tappable shape. My questions are: Is this ~16pt expansion of the interactive area expected SwiftUI/OS behavior? Is this related to Apple's minimum hit-target/accessibility guidelines being automatically applied to controls such as Button? If so, is there an official API/documentation describing this behavior? Is there a recommended way to make the Button's hit-testing area exactly match the bounds defined by .contentShape(Rectangle()), if that is actually possible? I'm particularly interested in understanding whether this is intentional system behavior or something caused by the ButtonStyle / layout hierarchy. Thanks!
Replies
0
Boosts
0
Views
28
Activity
1d
How can I set per-window titles in the Window menu for an iPad app running on Apple silicon Mac?
I’m developing a SwiftUI iPad app that runs on an Apple silicon Mac as an iPad app—not as a Mac Catalyst app. The app supports multiple windows, with each Live Screen window associated with a different virtual machine. In the macOS Window menu, all child windows appear as “VirtualProg” rather than showing their content or VM name. The Live Screen window is created from a data-driven  WindowGroup , roughly like this: WindowGroup(id: "vm-screen", for: String.self) { $vmName in VMScreenWindow(vmName: vmName) .navigationTitle(vmName.map { "Live ($0)" } ?? "Live Screen") } I’ve tried setting the title with  .navigationTitle  both on the  WindowGroup  content and inside the window’s  NavigationStack . I also tried setting  windowScene.title  on the specific scene obtained from  view.window?.windowScene  after the view controller appeared. Neither changed the Window-menu entries. Is there a supported way to set a per-window title for the macOS Window menu when running an iPad app on Mac? Does  UIScene.title  affect that menu, or only the window title bar and app switcher? Is this a limitation of the iPad-app-on-Mac runtime, or is there a recommended SwiftUI/UIKit lifecycle hook for this? I’m looking for a public API solution rather than private AppKit/window introspection.
Replies
0
Boosts
0
Views
48
Activity
1d
toolbarMinimizationBehavior(_:for:) crashes on some iOS 27.0.0 devices: "missing weak symbol" despite #available(iOS 27.0, *)
We are seeing a production crash that only affects devices reporting iOS 27.0.0. Devices on 27.0.1 and later, and on iOS 26, are unaffected. The app is built with Xcode 27.0 (the same binary shape appears with the Xcode 27.1 RC toolchain), deployment target iOS 26.0. The crashing code is a plain availability-gated call: extension View { @ViewBuilder func disableNavigationBarMinimization() -> some View { if #available(iOS 27.0, *) { toolbarMinimizationBehavior(.never, for: .navigationBar) } else { self } } } Crash report excerpt (addresses and app symbols removed): crash_info_entry_0: Failed to look up symbolic reference at <addr> - offset <n> - symbol <nearest stripped symbol> in <app binary> - pointer at <addr> is likely a reference to a missing weak symbol Crashed: com.apple.main-thread 0 libsystem_kernel.dylib __pthread_kill 2 libsystem_c.dylib abort 3 libswiftCore.dylib <redacted> 9 libswiftCore.dylib swift_getTypeByMangledName + 908 10 libswiftCore.dylib swift_getTypeByMangledNameInContext2 + 248 11 <app> __swift_instantiateConcreteTypeFromMangledNameV2 12 <app> specialized closure #1 in View.disableNavigationBarMinimization() 13 <app> closure #1 in SomeView.body.getter What we established: Because the deployment target is 26.0 and the API is @available(iOS 27.0, *), the app weak-imports both the function and its opaque result type descriptor. dyld_info -imports on the binary shows: _$s7SwiftUI4ViewPAAE27toolbarMinimizationBehavior_3forQrAA07ToolbareF0V_AA0H9PlacementVdtF [weak-import] (from SwiftUI) _$s7SwiftUI4ViewPAAE27toolbarMinimizationBehavior_3forQrAA07ToolbareF0V_AA0H9PlacementVdtFQOMQ [weak-import] (from SwiftUI) The call site has to instantiate the opaque return type's metadata before calling the function. That is the frame that aborts. So when dyld binds the descriptor to null, #available(iOS 27.0, *) passes and the process still dies. The symbol is present and exported on the iOS 27.0 build we can inspect locally (24A437, via Xcode's DeviceSupport symbols), and the same code runs fine on the iOS 27.0 simulator runtime (24A434). We cannot reproduce the crash on any device we own. The SwiftUI binary in 24A437 exports both toolbarMinimizeBehavior(_:for:) / ToolbarMinimizeBehavior and toolbarMinimizationBehavior(_:for:) / ToolbarMinimizationBehavior. The iOS 27.1 SDK only declares the latter, still annotated @available(iOS 27.0, *). This looks like a rename late in the 27.0 cycle. Our working theory is that some iOS 27.0.0 builds in the field (earlier 27.0 builds, possibly the ones preinstalled on new devices) predate the rename and only contain toolbarMinimizeBehavior, so the renamed symbol is absent and the weak import resolves to null. Questions: Is the @available(iOS 27.0, *) annotation on toolbarMinimizationBehavior(_:for:) accurate for every shipped 27.0.0 build? Should it be 27.0.1 or 27.1? Is there a supported way to guard against a missing opaque type descriptor at runtime, short of raising the availability check? Feedback filed as FB25095054.
Replies
0
Boosts
0
Views
63
Activity
1d
Accent Color is broken in Xcode 27.1 RC 1
In Xcode 27.1 RC 1 there is a regression bug displaying incorrect accent color across the app. It worked correctly in Xcode 27.1 Beta 1. It affects borderPromintent top bar buttons and tab bars: https://x.com/kulik_wojciech/status/2107252543709692166?s=20 I already reported it as a bug: FB25074768
Replies
4
Boosts
0
Views
124
Activity
2d
SwiftUI views lock up after background and sleep for “Designed for iPad” apps
There's an easily reproducible SwiftUI bug on macOS where an app's UI state no longer updates/re-renders for "Designed for iPad" apps (i.e. ProcessInfo.processInfo.isiOSAppOnMac == true). The bug occurs in Xcode and also if the app is running independent of Xcode. The bug occurs when: the user Hides the app (i.e. it goes into the background) the user puts the Mac to sleep (e.g. Apple menu > Sleep) a total of ~60 seconds transpires (i.e. macOS puts the app into the "suspended state") when the app is brought back into the foreground the UI no longer updates properly The only way I have found to fix this is to manually open a new actual full app window via File > New, in which case the app works fine again in the new window. The following extremely simple code in a default Xcode project illustrates the issue: import SwiftUI @main struct staleApp: App { @State private var isBright = true var body: some Scene { WindowGroup() { ZStack { (isBright ? Color.white : Color.black).ignoresSafeArea() Button("TOGGLE") { isBright.toggle(); print("TAPPED") } } .onAppear { print("\(isBright ? "light" : "dark") view appeared") } } } } For the code above, after Hiding the app and putting the computer to sleep for 60 seconds or more, the button no longer swaps views, although the print statements still appear in the console upon tapping the button. Also, while in this buggy state, i can get the view to update to the current state (i.e. the view triggered by the last tap) by manually dragging the corner of the app window to resize the window. But after resizing, the view again does not update upon button tapping until I resize the window again. so it appears the diff engine is mucked or that the Scene or WindowGroup are no longer correctly running on the main thread I have tried rebuilding the entire view hierarchy by updating .id() on views but this approach does NOT work. I have tried many other options/hacks but have not been able to reset the 'view engine' other than opening a new window manually or by using: @Environment(.openWindow) private var openWindow openWindow could be a viable solution except there's no way to programmatically close the old window for isiOSAppOnMac (@Environment(.dismissWindow) private var dismissWindow doesn't work for iOS)
Replies
2
Boosts
1
Views
287
Activity
2d
iOS 27: SwiftUI keyboard safe area is not restored after returning to a UIHostingController
I have a SwiftUI view embedded in a UIHostingController. It contains a numeric text field at the top and a .borderedProminent button at the bottom. The text field is automatically focused when the view appears. The bottom button pushes another instance of the same hosting controller. On the initial appearance, SwiftUI correctly positions the button above the keyboard. However, after navigating back from VC2 to VC1, the text field is focused and the keyboard is visible, but the button remains behind the keyboard. This occurs regardless of whether VC2 is closed using the navigation bar back button or the interactive back gesture. This appears to be an iOS 27 regression involving SwiftUI keyboard avoidance inside a UIHostingController. The issue is especially problematic with a numeric keyboard because it has no Return or Done key. If the covered button is the primary way to continue or dismiss the keyboard, the user can no longer access it. Interestingly, the button is repositioned correctly as soon as I begin moving the app into the background. This suggests that SwiftUI still knows about the keyboard safe area but does not update the layout correctly when the hosting controller reappears after being popped to. Minimal reproducible example import SwiftUI import UIKit final class NumericInputHostingController: UIHostingController<NumericInputView> { init() { super.init( rootView: NumericInputView(onContinue: {}) ) rootView = NumericInputView { [weak self] in self?.navigationController?.pushViewController( NumericInputHostingController(), animated: true ) } } @available(*, unavailable) @MainActor required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } struct NumericInputView: View { @State private var value = "" @FocusState private var isTextFieldFocused: Bool let onContinue: () -> Void var body: some View { VStack(spacing: 24) { TextField("Enter a number", text: $value) .keyboardType(.numberPad) .focused($isTextFieldFocused) .padding(.horizontal, 16) .frame(height: 52) .background { RoundedRectangle(cornerRadius: 10) .stroke( Color.accentColor, lineWidth: 1.5 ) } Spacer() Button("Open Another", action: onContinue) .buttonStyle(.borderedProminent) .controlSize(.large) .frame(maxWidth: .infinity) } .padding(20) .background(Color(.systemBackground)) .onAppear { DispatchQueue.main.async { isTextFieldFocused = true } } .onDisappear { isTextFieldFocused = false } } } The initial controller is embedded in a navigation controller: let controller = NumericInputHostingController() let navigationController = UINavigationController( rootViewController: controller ) Steps to reproduce Present VC1. VC1 automatically focuses the numeric text field. Confirm that the bottom button is above the keyboard. Tap the button to push VC2. Navigate back to VC1. VC1 focuses its text field and displays the numeric keyboard. Observe that the bottom button is now behind the keyboard. Begin putting the app into the background. Observe that the button suddenly moves to the correct position above the keyboard. UIKit’s keyboardLayoutGuide handles the equivalent UIKit layout correctly. Is this a known issue with keyboard safe-area updates when a UIHostingController reappears after navigation? Is there a supported SwiftUI solution that does not require observing keyboard notifications manually?
Replies
3
Boosts
1
Views
303
Activity
2d
.contactAccessPicker shows blank sheet on iOS 26.2.1 on device
Calling contactAccessPicker results in a blank sheet and a jetsam error, rather than the expected contact picker, using Apple’s sample code, only on device with iOS 26.2.1. This is happening on a iPhone 17 Pro Max running 26.2.1, and not on a simulator. I’m running Apple's sample project Accessing a person’s contact data using Contacts and ContactsUI Steps: Run the sample app on device running iOS 26.2.1. Use the flow to authorize .limited access with 1 contact: Tap request access, Continue, Select Contacts. Select a contact, Continue, Allow Selected Contact. This all works as expected. Tap the add contact button in the toolbar to add a second contact. Expected: This should show the Contact Access Picker UI. Actual: Sheet is shown with no contents. See screenshot of actual results on iOS device running 26.2.1. Reported as FB21812568 I see a similar (same?) error reported for 26.1. It seems strange that the feature is completely broken for multiple point releases. Is anyone else seeing this or are the two of us running into the same rare edge case? Expected Outcome, seen on simulator running 26.2 Actual outcome, seen on device running 26.2.1
Replies
10
Boosts
3
Views
1.5k
Activity
2d
Dismiss/Close button - leading or trailing edge?
What is the recommended position of the "X" button dismissing a modal sheet on iOS? When we have "X" and "✓" the case is easy, but what if we just have "X" button? For example, on a "What's New" screen. I found guidelines here: https://developer.apple.com/design/human-interface-guidelines/sheets Based on resizable sheets presented there, the "X" button should be on leading edge. However, iOS is inconsistent and sometimes it presents the button on the leading edge and sometimes on the trailing edge. How do you approach it? Do you prefer having it on the leading or trailing edge? SwiftUI provides ToolbarItem(placement: .cancellationAction) , but it doesn't feel like the right placement. cancellationAction sounds like something for actionable sheets when we have something to be cancelled. I'm missing placement: .dismiss :).
Replies
1
Boosts
0
Views
875
Activity
2d
NavigationSplitView without detail information
Hi, I have a working program with categories, lists and detail views. All connected via NavigationLink inside a NavigationStack. It looks a bit strange on an iPad, so I replaced the NavigationStack with a NavigationSplitView. The categories went into the sidebar and the lists into the content section. The detail section of the NavigationSplitView is empty and it still works. Is that the correct behavior? I don't mind it, because it works. example code: var body: some View { NavigationSplitView { List(selection: $listSelection) { Section("Section 1") { NavigationLink(value: EnumValue.V1) { CardView("Card 1") } NavigationLink(value: EnumValue.V2) { CardView("Card 2") } NavigationLink(value: EnumValue.V3) { CardView("Card 3") } } Section("Section 2") { NavigationLink(value: EnumValue.V4) { CardView("Card 4") } NavigationLink(value: EnumValue.V5) { CardView("Card 5") } } } .scrollIndicators(.hidden) .navigationTitle("Detail Empty") } content: { if let mt = listSelection { switch mt { case .V1: ListView1() case .V2: ListView2() case .V3: ListView3() case .V4: ListView4() case .V5: ListView5() } } } detail: { // empty } } In each ListView, I assign the ID to the EditorView so that the EditorView is updated. .navigationDestination(for: ViewListTransfer.self) { vl in switch vl { case .trend(bp: let bp): EditorView(value: bp) .id(bp.Id) case .trendNew: EditorView(val: nil) } } It seems to work because everything is linked via NavigationLinks, but it does look strange that the detail section is empty. The details section is mandatory; therefore, it cannot be omitted. Is this behavior something for a bug report? Christian
Replies
2
Boosts
0
Views
352
Activity
2d
Using .sidebarAdaptable style breaks NavigationStack's transition animation in tvOS
When a NavigationStack is nested inside a TabView that's using the .sidebarAdaptable style, it completely overwrites the transition animations of the NavigationStack. In tvOS 26, the transition for push and pop instantly snaps in place. in tvOS 27, it uses its own transition animation, which has strange behaviors, such as slow crossfade, and items rearranging if the frame of the child view is different. Here is a sample code that shows what I'm talking about. struct ContentView: View { @State private var selectedTab = 0 var body: some View { TabView(selection: $selectedTab) { Tab("Home", systemImage: "house", value: 0) { NavigationStack { VStack(spacing: 30) { Text("Root Home View") .font(.title) NavigationLink("Push Detail View") { Text("Child View") .font(.largeTitle) } } } } } .tabViewStyle(.sidebarAdaptable) } } Note: Placing NavigationStack over a TabView will retain the NavigationStack's transition animations, but you lose navigation paths per tab, and child views a rendered over the tabview.
Replies
0
Boosts
0
Views
271
Activity
2d
Question: Task modifier with id called on initial
Hello, I have a question regarding the task(id:) modifier. It is possible, of course, that I am using the modifier for the wrong purpose. Consider a multi-field search scenario where any change to a field triggers a loading process via the task(id:) modifier. The problem is that every task(id:) modifier executes when the view is first loaded, causing the same data to be loaded multiple times—which makes no sense. I do not want to add a separate search button or implement debouncing; those would merely be workarounds to bypass the actual issue. The onChange modifier has an initial parameter that defaults to false, preventing the action from running when the view is first loaded. However, the task(id:) modifier does not have this parameter. How can I prevent all task(id:) modifiers from executing when the view loads? After all, the standard task modifier should handle the initial load. struct ContentView: View { @State private var field1: String = "" @State private var field2: String = "" @State private var field3: String = "" var body: some View { VStack { TextField("val1", text: $field1) TextField("val2", text: $field2) TextField("val3", text: $field3) } .padding() .task { print("task") } .task(id: field1) { print("task 1") } .task(id: field2) { print("task 2") } .task(id: field3) { print("task 3") } } } Christian
Replies
1
Boosts
0
Views
259
Activity
3d
Is it possible to apply the glass effect to the top toolbar?
when I tried ToolbarItemGroup(placement: .title) { VStack { header() }.glassEffect() } the glass effect is only applied to the element itself. And the ToolbarItemGroup does seem to have the glass effect apis either. What I want is for the overall toolbar to have the glass effect, not just the individual elements in the toolbar.
Replies
0
Boosts
0
Views
68
Activity
3d
Can we opt out of the Horizontal Bar on the iPhone Duo?
We are aware that we can opt out of the vertical bar to make it horizontal for folded and unfolded landscape: https://developer.apple.com/documentation/uikit/uiviewcontroller/preferredverticalbarbehavior As per the title, we would like to know if if the reverse is possible (i.e. can we have the unfolded portrait display a vertical bar instead of a horizontal one)
Replies
1
Boosts
0
Views
108
Activity
3d
[VoiceOver] Dynamic fractions (e.g., ½, 1/2) are completely omitted/skipped in iOS 27
Platform & OS Version: iOS 27 Screen Reader: VoiceOver Framework: SwiftUI / UIKit / WebKit (WKWebView) Issue Summary: When updating dynamic text or UI elements containing fraction values—whether using Unicode fraction glyphs (e.g., ½, ¾) VoiceOver in iOS 27 completely omits and skips reading the fraction value entirely. Steps to Reproduce: Enable VoiceOver on iOS 27 (Settings > Accessibility > VoiceOver). Open an app or web view where fraction values, tap to focus on the element or trigger voice over Expected Result: VoiceOver should speak the accessible label or text content (e.g., "one half") Actual Result: VoiceOver skips the fraction entirely, reading only surrounding text or remaining silent over the fraction value.
Replies
0
Boosts
0
Views
463
Activity
3d
SwiftUI interactive Liquid Glass highlight boundary for a custom L shape
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 } } }
Replies
0
Boosts
0
Views
102
Activity
4d