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

New WatchOS 26 App & View Lifecycle
The WatchOS app and view lifecycles for WatchKit and SwiftUI are documented in https://developer.apple.com/documentation/watchkit/working-with-the-watchos-app-life-cycle and https://developer.apple.com/documentation/swiftui/migrating-to-the-swiftui-life-cycle. WatchOS 26 appears to change the app & view lifecycle from the behavior in WatchOS 11, and no longer matches the documented lifecycles. On WatchOS 11, with a @WKApplicationDelegateAdaptor set, the following sequence of events would occur on app launch: WKApplicationDelegate applicationDidFinishLaunching in WKApplicationState .inactive. WKApplicationDelegate applicationWillEnterForeground in WKApplicationState .inactive. View .onAppear @Environment(.scenePhase) .inactive App onChange(of: @Environment(.scenePhase)): .active WKApplicationDelegate applicationDidBecomeActive in WKApplicationState .active. App onReceive(.didBecomeActiveNotification): WKApplicationState(rawValue: 0) View .onChange of: .@Environment(.scenePhase) .active In WatchOS 26, this is now: WKApplicationDelegate applicationDidFinishLaunching in WKApplicationState .inactive. WKApplicationDelegate applicationWillEnterForeground in WKApplicationState .inactive. App onChange(of: @Environment(.scenePhase)): .active WKApplicationDelegate applicationDidBecomeActive in WKApplicationState .active. View .onAppear @Environment(.scenePhase) .active When resuming from the background in WatchOS 11: App onChange(of: @Environment(.scenePhase)): inactive WKApplicationDelegate applicationWillEnterForeground in WKApplicationState .background. App onReceive(.willEnterForegroundNotification): WKApplicationState(rawValue: 2) View .onChange of: .@Environment(.scenePhase) inactive App onChange(of: @Environment(.scenePhase)): active WKApplicationDelegate applicationDidBecomeActive in WKApplicationState .active. App onReceive(.didBecomeActiveNotification): WKApplicationState(rawValue: 0) View .onChange of: .@Environment(.scenePhase) active The resume from background process in WatchOS 26 is baffling and seems like it must be a bug: App onChange(of: @Environment(.scenePhase)): inactive WKApplicationDelegate applicationWillEnterForeground in WKApplicationState .background. App onReceive(.willEnterForegroundNotification): WKApplicationState(rawValue: 2) App onChange(of: @Environment(.scenePhase)): active WKApplicationDelegate applicationDidBecomeActive in WKApplicationState .active. App onReceive(.didBecomeActiveNotification): WKApplicationState(rawValue: 0) View .onChange of: @Environment(.scenePhase) active App onChange(of: @Environment(.scenePhase)): inactive WKApplicationDelegate applicationWillResignActive in WKApplicationState .active. App onReceive(.willResignActiveNotification): WKApplicationState(rawValue: 0) View .onChange of: @Environment(.scenePhase) inactive App onChange(of: @Environment(.scenePhase)): active WKApplicationDelegate applicationDidBecomeActive in WKApplicationState .active. App onReceive(.didBecomeActiveNotification): WKApplicationState(rawValue: 0) View .onChange of: @Environment(.scenePhase) active The app becomes active, then inactive, then active again. The issues with these undocumented changes are: It is undocumented. If you relied on the previous process, this change can break your app. A view no longer receives .onChange of: .@Environment(.scenePhase) .active state change during the launch process. This bizarre applicationWillEnterForeground - applicationDidBecomeActive - applicationWillResignActive - applicationDidBecomeActive process on app resume does not match the documented process and is just...strange. Is this new process what is intended? Is it a bug? Can an Apple engineer explain this new App resume from background process and why the View is created slightly later in the App launch process, so it does not receive the .onChange of @Environment(.scenePhase) message? In contrast, the iOS 26 app lifecycle has not changed, and the iOS 18/26 app lifecycle closely follows the watchOS 11 app lifecycle (or watchOS 11 closely mimics iOS 18/26 with the exception that watchOS does not have a SceneDelegate).
0
0
129
Nov ’25
TabView Background Color
Hello I'm trying to use a TabView inside of the Sidebar in a NavigationSplitView. I'm wanting to use .listStyle(.sidebar) in order to get the Liquid Glass effect. However I can not find a way to remove the background of the TabView without changing the behavior of the TabView itself to paging with .tabViewStyle(.page) NavigationSplitView { TabView( selection: .constant("List") ) { Tab(value: "List") { List { Text("List") } } } } detail: { } Note: I wanting change the background of the TabView container itself. Not the TabBar.
0
0
64
Nov ’25
Missing Context menu items in Xcode 26 on a fresh project
When following the official SwiftUI Tutorial "Landmarks", I should be able to Cmd-Ctrl-Click an element of a preview in Selectable mode to view a list of options. But Xcode shows only one option (Embed). Furthermore, if I try to type anything the popover breaks completely and grows in length infinitely. I am not sure whether this is a bug, or I missed something when installing Xcode, or both
2
0
197
Nov ’25
WebView makes website content unaccessible on the top/bottom edges
I'm being faced with an issue when using SwiftUI's WebView on iOS 26. In many websites, the top/bottom content is unaccessible due to being under the app's toolbars. It feels like the WebView doesn't really understand the safe areas where it's being shown, because the content should start right below the navigation bar, and only when the user scrolls down, the content should move under the bar (but it's always reachable if the users scroll back up). Here's a demo of the issue: Here's a 'fix' by ensuring that the content of the WebView never leaves its bounds. But as you can see, it feels out of place on iOS 26 (would be fine on previous OS versions if you had a fully opaque toolbar): Code: struct ContentView: View { var body: some View { NavigationStack { WebView(url: URL(string: "https://apple.com")).toolbar { ToolbarItem(placement: .primaryAction) { Button("Top content covered, unaccessible.") {} } } } } } Does anyone know if there's a way to fix it using some sort of view modifier combination or it's just broken as-is?
13
1
382
Nov ’25
gesture(LongPressGesture()) issue with scroll view
I've being playing aground with long press gesture in scroll view and noticed gesture(LongPressGesture()) doesn't seem to work with scroll view's scrolling which doesn't seem to be the intended behavior to me. Take the following example: the blue rectangle is modified with onLongPressGesture and the red rectangle is modified with LongPressGesture (_EndedGesture<LongPressGesture> to be specific). ScrollView { Rectangle() .fill(.blue) .frame(width: 200, height: 200) .onLongPressGesture { print("onLongPressGesture performed") } onPressingChanged: { _ in print("onLongPressGesture changed") } .overlay { Text("onLongPressGesture") } Rectangle() .fill(.red) .frame(width: 200, height: 200) .gesture(LongPressGesture() .onEnded { _ in print("gesture ended") }) .overlay { Text("gesture(LongPressGesture)") } } If you start scrolling from either of the rectangles (that is, start scrolling with your finger on either of the rectangles), the ScrollView will scroll. However, if LongPressGesture is modified with either onChanged or .updating, ScrollView won't respond to scroll if the scroll is started from red rectangle. Even setting the maximumDistance to 0 won't help. As for its counter part onLongPressGesture, even though onPressingChanged to onLongPressGesture, scrolling still works if it's started from onLongPressGesture modified view. ScrollView { Rectangle() .fill(.blue) .frame(width: 200, height: 200) .onLongPressGesture { print("onLongPressGesture performed") } onPressingChanged: { _ in print("onLongPressGesture changed") } .overlay { Text("onLongPressGesture") } Rectangle() .fill(.red) .frame(width: 200, height: 200) .gesture(LongPressGesture(maximumDistance: 0) // scroll from the red rectangle won't work if I add either `updating` or `onChanged` but I put both here just to demonstrate // you will need to add `@GestureState private var isPressing = false` to your view body .updating($isPressing) { value, state, transaction in state = value print("gesture updating") } .onChanged { value in print("gesture changed") } .onEnded { _ in print("gesture ended") }) .overlay { Text("gesture(LongPressGesture)") } } This doesn't seem right to me. I would expect the view modified by LongPressGesture(), no matter if the gesture has onChanged or updating, should be able to start scroll in a scroll view, just like onLongPressGesture. I observed this behavior in a physical device running iOS 26.1, and I do not know the behavior on other versions.
0
0
53
Nov ’25
Extra unwanted space in main window
Hi there! I'm having this issue with my main windows. I'm having a big space on top of that without any logic explanation (at least for my poor knowledge). Using the code below I'm getting this Windows layout: Does anybody have any guidance on how to get out that extra space at the beginning? Thanks a lot! import SwiftUI import SwiftData #if os(macOS) import AppKit #endif // Helper to access and control NSWindow for size/position persistence #if os(macOS) struct WindowAccessor: NSViewRepresentable { let onWindow: (NSWindow) -> Void func makeNSView(context: Context) -> NSView { let view = NSView() DispatchQueue.main.async { if let window = view.window { onWindow(window) } } return view } func updateNSView(_ nsView: NSView, context: Context) { DispatchQueue.main.async { if let window = nsView.window { onWindow(window) } } } } #endif @main struct KaraoPartyApp: App { @StateObject private var songsModel = SongsModel() @Environment(\.openWindow) private var openWindow var body: some Scene { Group { WindowGroup { #if os(macOS) WindowAccessor { window in window.minSize = NSSize(width: 900, height: 700) // Configure window to eliminate title bar space window.titleVisibility = .hidden window.titlebarAppearsTransparent = true window.styleMask.insert(.fullSizeContentView) } #endif ContentView() .environmentObject(songsModel) } .windowToolbarStyle(.unifiedCompact) .windowResizability(.contentSize) .defaultSize(width: 1200, height: 900) .windowStyle(.titleBar) #if os(macOS) .windowToolbarStyle(.unified) #endif WindowGroup("CDG Viewer", id: "cdg-viewer", for: CDGWindowParams.self) { $params in if let params = params { ZStack { #if os(macOS) WindowAccessor { window in window.minSize = NSSize(width: 600, height: 400) // Restore window frame if available let key = "cdgWindowFrame" let defaults = UserDefaults.standard if let frameString = defaults.string(forKey: key) { let frame = NSRectFromString(frameString) if window.frame != frame { window.setFrame(frame, display: true) } } else { // Open CDG window offset from main window if let mainWindow = NSApp.windows.first { let mainFrame = mainWindow.frame let offsetFrame = NSRect(x: mainFrame.origin.x + 60, y: mainFrame.origin.y - 60, width: 800, height: 600) window.setFrame(offsetFrame, display: true) } } // Observe frame changes and save NotificationCenter.default.addObserver(forName: NSWindow.didMoveNotification, object: window, queue: .main) { _ in let frameStr = NSStringFromRect(window.frame) defaults.set(frameStr, forKey: key) } NotificationCenter.default.addObserver(forName: NSWindow.didEndLiveResizeNotification, object: window, queue: .main) { _ in let frameStr = NSStringFromRect(window.frame) defaults.set(frameStr, forKey: key) } } #endif CDGView( cancion: Cancion( title: params.title ?? "", artist: params.artist ?? "", album: "", genre: "", year: "", bpm: "", playCount: 0, folderPath: params.cdgURL.deletingLastPathComponent().path, trackName: params.cdgURL.deletingPathExtension().lastPathComponent + ".mp3" ), backgroundType: params.backgroundType, videoURL: params.videoURL, cdfContent: params.cdfContent.flatMap { String(data: $0, encoding: .utf8) }, artist: params.artist, title: params.title ) } } else { Text("No se pudo abrir el archivo CDG.") } } .windowResizability(.contentSize) .defaultSize(width: 800, height: 600) WindowGroup("Metadata Editor", id: "metadata-editor") { MetadataEditorView() .environmentObject(songsModel) } .windowResizability(.contentSize) .defaultSize(width: 400, height: 400) WindowGroup("Canciones DB", id: "canciones-db") { CancionesDBView() } .windowResizability(.contentSize) .defaultSize(width: 800, height: 500) WindowGroup("Importar canciones desde carpeta", id: "folder-song-importer") { FolderSongImporterView() } .windowResizability(.contentSize) .defaultSize(width: 500, height: 350) } .modelContainer(for: Cancion.self) // Add menu command under Edit .commands { CommandGroup(replacing: .pasteboard) { } CommandMenu("Edit") { Button("Actualizar Metadatos") { openWindow(id: "metadata-editor") } .keyboardShortcut(",", modifiers: [.command, .shift]) } CommandMenu("Base de Datos") { Button("Ver Base de Datos de Canciones") { openWindow(id: "canciones-db") } .keyboardShortcut("D", modifiers: [.command, .shift]) } } } init() { print("\n==============================") print("[KaraoParty] Nueva ejecución iniciada: \(Date())") print("==============================\n") } }
0
0
41
Nov ’25
SwiftUI Map menu / chrome placement — three approaches (overlay, ZStack + safeAreaPadding, safeAreaInset): which one is best practice?
Hi everyone, I’m building a full-screen Map (MapKit + SwiftUI) with persistent top/bottom chrome (menu buttons on top, session stats + map controls on bottom). I have three working implementations and I’d like guidance on which pattern Apple recommends long-term (gesture correctness, safe areas, Dynamic Island/home indicator, and future compatibility). Version 1 — overlay(alignment:) on Map Idea: Draw chrome using .overlay(alignment:) directly on the map and manage padding manually. Map(position: $viewModel.previewMapCameraPosition, scope: mapScope) { UserAnnotation { UserLocationCourseMarkerView(angle: viewModel.userCourse - mapHeading) } } .mapStyle(viewModel.mapType.mapStyle) .mapControls { MapUserLocationButton().mapControlVisibility(.hidden) MapCompass().mapControlVisibility(.hidden) MapPitchToggle().mapControlVisibility(.hidden) MapScaleView().mapControlVisibility(.hidden) } .overlay(alignment: .top) { mapMenu } // manual padding inside .overlay(alignment: .bottom) { bottomChrome } // manual padding inside Version 2 — ZStack + .safeAreaPadding Idea: Place the map at the back, then lay out top/bottom chrome in a VStack inside a ZStack, and use .safeAreaPadding(.all) so content respects safe areas. ZStack(alignment: .top) { Map(...).ignoresSafeArea() VStack { mapMenu Spacer() bottomChrome } .safeAreaPadding(.all) } Version 3 — .safeAreaInset on the Map Idea: Make the map full-bleed and then reserve top/bottom space with safeAreaInset, letting SwiftUI manage insets Map(...).ignoresSafeArea() .mapStyle(viewModel.mapType.mapStyle) .mapControls { MapUserLocationButton().mapControlVisibility(.hidden) MapCompass().mapControlVisibility(.hidden) MapPitchToggle().mapControlVisibility(.hidden) MapScaleView().mapControlVisibility(.hidden) } .safeAreaInset(edge: .top) { mapMenu } // manual padding inside .safeAreaInset(edge: .bottom) { bottomChrome } // manual padding inside Question I noticed: Safe-area / padding behavior – Version 2 requires the least extra padding and seems to create a small but partial safe-area spacing automatically. – Version 3 still needs roughly the same manual padding as Version 1, even though it uses safeAreaInset. Why doesn’t safeAreaInset fully handle that spacing? Rotation crash (Metal) When using Version 3 (safeAreaInset + ignoresSafeArea), rotating the device portrait↔landscape several times triggers a Metal crash: failed assertion 'The following Metal object is being destroyed while still required… CAMetalLayer Display Drawable' The same crash can happen with Version 1, though less often. I haven’t tested it much with Version 2. Is this a known issue or race condition between Map’s internal Metal rendering and view layout changes? Expected behavior What’s the intended or supported interaction between safeAreaInset, safeAreaPadding, and overlay when embedding persistent chrome inside a SwiftUI Map? Should safeAreaInset normally remove the need for manual padding, or is that by design?
0
0
52
Nov ’25
tabViewBottomAccessory in 26.1: View's @State is lost when switching tabs
Any view that is content for the tabViewBottomAccessory API fails to retain its state as of the last couple of 26.1 betas (and RC). The loss of state happens (at least) when the currently selected tab is switched (filed as FB20901325). Here's code to reproduce the issue: struct ContentView: View { @State private var selectedTab = TabSelection.one enum TabSelection: Hashable { case one, two } var body: some View { TabView(selection: $selectedTab) { Tab("One", systemImage: "1.circle", value: .one) { BugExplanationView() } Tab("Two", systemImage: "2.circle", value: .two) { BugExplanationView() } } .tabViewBottomAccessory { AccessoryView() } } } struct AccessoryView: View { @State private var counter = 0 // This guy's state gets lost (as of iOS 26.1) var body: some View { Stepper("Counter: \(counter)", value: $counter) .padding(.horizontal) } } struct BugExplanationView: View { var body: some View { ScrollView { VStack(alignment: .leading, spacing: 16) { Text("(1) Manipulate the counter state") Text("(2) Then switch tabs") Text("BUG: The counter state gets unexpectedly reset!") } .multilineTextAlignment(.leading) } } }
1
3
266
Nov ’25
ARView ignores multi-touch events
Hi, How to enable multitouch on ARView? Touch functions (touchesBegan, touchesMoved, ...) seem to only handle one touch at a time. In order to handle multiple touches at a time with ARView, I have to either: Use SwiftUI .simultaneousGesture on top of an ARView representable Position a UIView on top of ARView to capture touches and do hit testing by passing a reference to ARView Expected behavior: ARView should capture all touches via touchesBegan/Moved/Ended/Cancelled. Here is what I tried, on iOS 26.1 and macOS 26.1: ARView Multitouch The setup below is a minimal ARView presented by SwiftUI, with touch events handled inside ARView. Multitouch doesn't work with this setup. Note that multitouch wouldn't work either if the ARView is presented with a UIViewController instead of SwiftUI. import RealityKit import SwiftUI struct ARViewMultiTouchView: View { var body: some View { ZStack { ARViewMultiTouchRepresentable() .ignoresSafeArea() } } } #Preview { ARViewMultiTouchView() } // MARK: Representable ARView struct ARViewMultiTouchRepresentable: UIViewRepresentable { func makeUIView(context: Context) -> ARView { let arView = ARViewMultiTouch(frame: .zero) let anchor = AnchorEntity() arView.scene.addAnchor(anchor) let boxWidth: Float = 0.4 let boxMaterial = SimpleMaterial(color: .red, isMetallic: false) let box = ModelEntity(mesh: .generateBox(size: boxWidth), materials: [boxMaterial]) box.name = "Box" box.components.set(CollisionComponent(shapes: [.generateBox(width: boxWidth, height: boxWidth, depth: boxWidth)])) anchor.addChild(box) return arView } func updateUIView(_ uiView: ARView, context: Context) { } } // MARK: ARView class ARViewMultiTouch: ARView { required init(frame: CGRect) { super.init(frame: frame) /// Enable multi-touch isMultipleTouchEnabled = true cameraMode = .nonAR automaticallyConfigureSession = false environment.background = .color(.gray) /// Disable gesture recognizers to not conflict with touch events /// But it doesn't fix the issue gestureRecognizers?.forEach { $0.isEnabled = false } } required dynamic init?(coder decoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { for touch in touches { /// # Problem /// This should print for every new touch, up to 5 simultaneously on an iPhone (multi-touch) /// But it only fires for one touch at a time (single-touch) print("Touch began at: \(touch.location(in: self))") } } } Multitouch with an Overlay This setup works, but it doesn't seem right. There must be a solution to make ARView handle multi touch directly, right? import SwiftUI import RealityKit struct MultiTouchOverlayView: View { var body: some View { ZStack { MultiTouchOverlayRepresentable() .ignoresSafeArea() Text("Multi touch with overlay view") .font(.system(size: 24, weight: .medium)) .foregroundStyle(.white) .offset(CGSize(width: 0, height: -150)) } } } #Preview { MultiTouchOverlayView() } // MARK: Representable Container struct MultiTouchOverlayRepresentable: UIViewRepresentable { func makeUIView(context: Context) -> UIView { /// The view that SwiftUI will present let container = UIView() /// ARView let arView = ARView(frame: container.bounds) arView.autoresizingMask = [.flexibleWidth, .flexibleHeight] arView.cameraMode = .nonAR arView.automaticallyConfigureSession = false arView.environment.background = .color(.gray) let anchor = AnchorEntity() arView.scene.addAnchor(anchor) let boxWidth: Float = 0.4 let boxMaterial = SimpleMaterial(color: .red, isMetallic: false) let box = ModelEntity(mesh: .generateBox(size: boxWidth), materials: [boxMaterial]) box.name = "Box" box.components.set(CollisionComponent(shapes: [.generateBox(width: boxWidth, height: boxWidth, depth: boxWidth)])) anchor.addChild(box) /// The view that will capture touches let touchOverlay = TouchOverlayView(frame: container.bounds) touchOverlay.autoresizingMask = [.flexibleWidth, .flexibleHeight] touchOverlay.backgroundColor = .clear /// Pass an arView reference to the overlay for hit testing touchOverlay.arView = arView /// Add views to the container. /// ARView goes in first, at the bottom. container.addSubview(arView) /// TouchOverlay goes in last, on top. container.addSubview(touchOverlay) return container } func updateUIView(_ uiView: UIView, context: Context) { } } // MARK: Touch Overlay View /// A UIView to handle multi-touch on top of ARView class TouchOverlayView: UIView { weak var arView: ARView? override init(frame: CGRect) { super.init(frame: frame) isMultipleTouchEnabled = true isUserInteractionEnabled = true } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { let totalTouches = event?.allTouches?.count ?? touches.count print("--- Touches Began --- (New: \(touches.count), Total: \(totalTouches))") for touch in touches { let location = touch.location(in: self) /// Hit testing. /// ARView and Touch View must be of the same size if let arView = arView { let entity = arView.entity(at: location) if let entity = entity { print("Touched entity: \(entity.name)") } else { print("Touched: none") } } } } override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) { let totalTouches = event?.allTouches?.count ?? touches.count print("--- Touches Cancelled --- (Cancelled: \(touches.count), Total: \(totalTouches))") } }
1
0
262
Nov ’25
Matching launch image with with background image
The document-based SwiftUI example app (https://developer.apple.com/documentation/swiftui/building-a-document-based-app-with-swiftui) doesn't specify a launch image. It would seem per the HIG that the "pinkJungle" background in the app would be a decent candidate for a launch image, since it will be in the background when the document browser comes up. However when specifying it as the UIImageName, it is not aligned the same as the background image. I'm having trouble figuring out how it should be aligned to match the image. The launch image seems to be scaled up a bit over scaledToFill. I suppose a launch storyboard might make this more explicit, but I still should be able to do it without one. This is the image when displayed as the launch image: and this is how it's rendered in the background right before the document browser comes up:
0
0
29
Nov ’25
MenuBarExtra with .window style: .onHover modifier doesn't work on macOS 26 Tahoe
List { Text("ITEM 1") .onHover(perform: { hovering in debugPrint("hovering: ", hovering) }) .help("ITEM 1") Text("ITEM 2") .onHover(perform: { hovering in debugPrint("hovering: ", hovering) }) .help("ITEM 2") Text("ITEM 3") .onHover(perform: { hovering in debugPrint("hovering: ", hovering) }) .help("ITEM 3") } .fixedSize(horizontal: false, vertical: true) .frame(maxHeight: 200) } Hello everyone!!! Considering the snippet above, seems like the onHover action, including help modifiers, doesn't work for the elements of a List, on macOS Tahoe. The situation changes using a ScrollView embedding a LazyVStack, or disabling Liquid Glass from the info plist, so my guess is that the new Liquid Glass style has something to do with this issue though I didn't find any clue about it. Does anyone have any idea? Maybe there's a layer above that doesn't allow to trigger the onHover modifier? Thanks in advance for your help!
1
0
167
Nov ’25
Navigation bar fade breaks when using .ignoresSafeArea() on inverted ScrollViews in iOS 26
I’m seeing a strange visual bug in iOS 26 when building chat-style UIs that use an inverted ScrollView or List (via .rotationEffect(.radians(.pi)) and .scaleEffect(x: -1, y: 1)) to anchor messages at the bottom. When I add .ignoresSafeArea() to let the chat bleed behind the navigation bar - the new navigation bar fade (that subtle top-to-bottom gradient Apple added in iOS 26) behaves incorrectly. Instead of fading from the top of the screen toward the nav bar, it fades upward from the bottom of the view, effectively covering the entire screen with the gradient. This only happens when the view is inverted. If I remove .ignoresSafeArea(), the fade looks correct — but then my chat no longer extends behind the nav bar. It looks like the fade effect is being applied in the transformed coordinate space of the inverted scroll view rather than in visual screen space. I haven’t found a reliable workaround besides disabling the fade (which isn’t really possible). Has anyone found a proper solution or a modifier that prevents the fade inversion when using flipped ScrollViews? Would love to know if Apple is aware of this or if there’s a hidden API for disabling that fade effect. I have made a report about this in Feedback Assistant: FB20540755
1
0
158
Nov ’25
UIViewRepresentable Coordinator @Binding returns stale value when accessed via context.coordinator but fresh value when accessed via self
I'm encountering unexpected behavior with @Binding in a UIViewRepresentable's Coordinator. The same Coordinator instance returns different values depending on how the property is accessed. Environment: iOS 17+ / Xcode 15+ SwiftUI with UIViewRepresentable Issue: When I access @Binding var test inside the Coordinator: ✅ Via self.test in Coordinator methods: Returns the updated value ❌ Via context.coordinator.test in updateUIView: Returns the stale/initial value Both access the same Coordinator instance (verified by memory address), yet return different values. Minimal Reproducible Example: struct ContentView: View { @State private var test: [Int] = [1, 2, 3, 4, 5] var body: some View { VStack { TestRepresentable(test: $test) Text("State: \(test.description)") } } } struct TestRepresentable: UIViewRepresentable { @Binding var test: [Int] func makeUIView(context: Context) -> UIButton { let button = UIButton(type: .system) button.setTitle("Toggle", for: .normal) button.addTarget( context.coordinator, action: #selector(Coordinator.buttonTapped), for: .touchUpInside ) return button } func updateUIView(_ uiView: UIButton, context: Context) { // Log coordinator instance address let coordAddr = String(describing: Unmanaged.passUnretained(context.coordinator).toOpaque()) print("[updateUIView] Coordinator address: \(coordAddr)") // Log values print("[updateUIView] self.test: \(self.test)") print("[updateUIView] context.coordinator.test: \(context.coordinator.test)") // These should be the same but they're not! // self.test shows updated value // context.coordinator.test shows stale value } func makeCoordinator() -> Coordinator { Coordinator(test: $test) } class Coordinator: NSObject { @Binding var test: [Int] var idx: Int = 0 init(test: Binding<[Int]>) { _test = test } @objc func buttonTapped() { idx += 1 if idx < test.count { test[idx] += 5 } // Log coordinator instance address let selfAddr = String(describing: Unmanaged.passUnretained(self).toOpaque()) print("[buttonTapped] Coordinator address: \(selfAddr)") // Log value - this shows the UPDATED value print("[buttonTapped] self.test: \(test)") } } } Actual Output: [Initial] [updateUIView] Coordinator address: 0x600001234567 [updateUIView] self.test: [1, 2, 3, 4, 5] [updateUIView] context.coordinator.test: [1, 2, 3, 4, 5] [After first tap] [buttonTapped] Coordinator address: 0x600001234567 [buttonTapped] self.test: [1, 7, 3, 4, 5] ✅ Updated! [updateUIView] Coordinator address: 0x600001234567 ← Same instance [updateUIView] self.test: [1, 7, 3, 4, 5] ✅ Updated! [updateUIView] context.coordinator.test: [1, 2, 3, 4, 5] ❌ Stale! [After second tap] [buttonTapped] Coordinator address: 0x600001234567 [buttonTapped] self.test: [1, 7, 8, 4, 5] ✅ Updated! [updateUIView] Coordinator address: 0x600001234567 ← Same instance [updateUIView] self.test: [1, 7, 8, 4, 5] ✅ Updated! [updateUIView] context.coordinator.test: [1, 2, 3, 4, 5] ❌ Still stale! Questions: Why does context.coordinator.test return a stale value when it's the same Coordinator instance? Is this intended behavior or a bug? What's the correct pattern to access Coordinator's @Binding properties in updateUIView? Workaround Found: Using self.test instead of context.coordinator.test in updateUIView works, but I'd like to understand why accessing the same property through context yields different results. Related: I've seen suggestions to update coordinator.parent = self in updateUIView, but this doesn't explain why the same object's property returns different values. Binding documentation states it's "a pair of get and set closures," but there's no explanation of how Context affects closure behavior. Any insights would be greatly appreciated! P.S - https://stackoverflow.com/questions/69552418/why-does-a-binding-in-uiviewrepresentables-coordinator-have-a-constant-read-valu This is the link that I found out online with same problem that I have but not answered well.
0
0
41
Nov ’25
Slow rendering List backed by SwiftData @Query
Hello, I've a question about performance when trying to render lots of items coming from SwiftData via a @Query on a SwiftUI List. Here's my setup: // Item.swift: @Model final class Item: Identifiable { var timestamp: Date var isOptionA: Bool init() { self.timestamp = Date() self.isOptionA = Bool.random() } } // Menu.swift enum Menu: String, CaseIterable, Hashable, Identifiable { var id: String { rawValue } case optionA case optionB case all var predicate: Predicate<Item> { switch self { case .optionA: return #Predicate { $0.isOptionA } case .optionB: return #Predicate { !$0.isOptionA } case .all: return #Predicate { _ in true } } } } // SlowData.swift @main struct SlowDataApp: App { var sharedModelContainer: ModelContainer = { let schema = Schema([Item.self]) let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false) return try! ModelContainer(for: schema, configurations: [modelConfiguration]) }() var body: some Scene { WindowGroup { ContentView() } .modelContainer(sharedModelContainer) } } // ContentView.swift struct ContentView: View { @Environment(\.modelContext) private var modelContext @State var selection: Menu? = .optionA var body: some View { NavigationSplitView { List(Menu.allCases, selection: $selection) { menu in Text(menu.rawValue).tag(menu) } } detail: { DemoListView(selectedMenu: $selection) }.onAppear { // Do this just once // (0..<15_000).forEach { index in // let item = Item() // modelContext.insert(item) // } } } } // DemoListView.swift struct DemoListView: View { @Binding var selectedMenu: Menu? @Query private var items: [Item] init(selectedMenu: Binding<Menu?>) { self._selectedMenu = selectedMenu self._items = Query(filter: selectedMenu.wrappedValue?.predicate, sort: \.timestamp) } var body: some View { // Option 1: touching `items` = slow! List(items) { item in Text(item.timestamp.description) } // Option 2: Not touching `items` = fast! // List { // Text("Not accessing `items` here") // } .navigationTitle(selectedMenu?.rawValue ?? "N/A") } } When I use Option 1 on DemoListView, there's a noticeable delay on the navigation. If I use Option 2, there's none. This happens both on Debug builds and Release builds, just FYI because on Xcode 16 Debug builds seem to be slower than expected: https://indieweb.social/@curtclifton/113273571392595819 I've profiled it and the SwiftData fetches seem blazing fast, the Hang occurs when accessing the items property from the List. Is there anything I'm overlooking or it's just as fast as it can be right now?
4
4
1.2k
Nov ’25
Keyframe animation crashes with +[_SwiftUILayerDelegate _screen]: unrecognized selector sent to class on iOS 26
We have an UIViewController called InfoPlayerViewController. Its main subview is from a child view controller backed by SwiftUI via UIHostingController. The InfoPlayerViewController conforms to UIViewControllerTransitioningDelegate. The animation controller for dismissing is DismissPlayerAnimationController. It runs UIKit keyframe animations via UIViewPropertyAnimator. When the keyframe animation is executed there’s an occasional crash for end users in production. It only happens on iOS 26. FB Radar: FB20871547 An example crash is below. Exception Type: EXC_CRASH (SIGABRT) Exception Codes: 0x0000000000000000, 0x0000000000000000 Exception Reason: +[_SwiftUILayerDelegate _screen]: unrecognized selector sent to class 0x20c95da08 Termination Reason: SIGNAL 6 Abort trap: 6 Triggered by Thread: 0 Last Exception Backtrace: 0 CoreFoundation 0x1a23828c8 __exceptionPreprocess + 164 (NSException.m:249) 1 libobjc.A.dylib 0x19f2f97c4 objc_exception_throw + 88 (objc-exception.mm:356) 2 CoreFoundation 0x1a241e6cc +[NSObject(NSObject) doesNotRecognizeSelector:] + 364 (NSObject.m:158) 3 CoreFoundation 0x1a22ff4f8 ___forwarding___ + 1472 (NSForwarding.m:3616) 4 CoreFoundation 0x1a23073a0 _CF_forwarding_prep_0 + 96 (:-1) 5 UIKitCore 0x1a948e880 __35-[UIViewKeyframeAnimationState pop]_block_invoke + 300 (UIView.m:2973) 6 CoreFoundation 0x1a22cb170 __NSDICTIONARY_IS_CALLING_OUT_TO_A_BLOCK__ + 24 (NSDictionaryHelpers.m:10) 7 CoreFoundation 0x1a245d7cc -[__NSDictionaryM enumerateKeysAndObjectsWithOptions:usingBlock:] + 288 (NSDictionaryM.m:271) 8 UIKitCore 0x1a948e6bc -[UIViewKeyframeAnimationState pop] + 376 (UIView.m:2955) 9 UIKitCore 0x1a7bc40e8 +[UIViewAnimationState popAnimationState] + 60 (UIView.m:1250) 10 UIKitCore 0x1a94acc44 +[UIView(UIViewAnimationWithBlocks) _setupAnimationWithDuration:delay:view:options:factory:animations:start:animationStateGenerator:completion:] + 684 (UIView.m:17669) 11 UIKitCore 0x1a94ae334 +[UIView(UIViewKeyframeAnimations) animateKeyframesWithDuration:delay:options:animations:completion:] + 224 (UIView.m:17945) 12 MyApp 0x102c78dec static UIView.animateNestedKeyframe(withRelativeStartTime:relativeDuration:animations:) + 208 (UIView+AnimateNestedKeyframe.swift:10) 13 MyApp 0x102aef3c0 closure #1 in DismissPlayerAnimationController.slideDownBelowTabBarTransitionAnimator(using:) + 156 (DismissPlayerAnimationController.swift:229) 14 MyApp 0x102a2d3d4 <deduplicated_symbol> + 28 15 UIKitCore 0x1a7d5ae5c -[UIViewPropertyAnimator _runAnimations] + 172 (UIViewPropertyAnimator.m:2123) 16 UIKitCore 0x1a83e1594 __49-[UIViewPropertyAnimator startAnimationAsPaused:]_block_invoke_3 + 92 (UIViewPropertyAnimator.m:3557) 17 UIKitCore 0x1a83e1464 __49-[UIViewPropertyAnimator startAnimationAsPaused:]_block_invoke + 96 (UIViewPropertyAnimator.m:3547) 18 UIKitCore 0x1a83e1518 __49-[UIViewPropertyAnimator startAnimationAsPaused:]_block_invoke_2 + 144 (UIViewPropertyAnimator.m:3553) 19 UIKitCore 0x1a83e0e64 -[UIViewPropertyAnimator _setupAnimationTracking:] + 100 (UIViewPropertyAnimator.m:3510) 20 UIKitCore 0x1a83e1264 -[UIViewPropertyAnimator startAnimationAsPaused:] + 728 (UIViewPropertyAnimator.m:3610) 21 UIKitCore 0x1a83de42c -[UIViewPropertyAnimator pauseAnimation] + 68 (UIViewPropertyAnimator.m:2753) 22 UIKitCore 0x1a87d5328 -[UIPercentDrivenInteractiveTransition _startInterruptibleTransition:] + 244 (UIViewControllerTransitioning.m:982) 23 UIKitCore 0x1a87d5514 -[UIPercentDrivenInteractiveTransition startInteractiveTransition:] + 184 (UIViewControllerTransitioning.m:1012) 24 UIKitCore 0x1a7c7931c ___UIViewControllerTransitioningRunCustomTransitionWithRequest_block_invoke_3 + 152 (UIViewControllerTransitioning.m:1579) 25 UIKitCore 0x1a892aefc +[UIKeyboardSceneDelegate _pinInputViewsForKeyboardSceneDelegate:onBehalfOfResponder:duringBlock:] + 96 (UIKeyboardSceneDelegate.m:3518) 26 UIKitCore 0x1a7c79238 ___UIViewControllerTransitioningRunCustomTransitionWithRequest_block_invoke_2 + 236 (UIViewControllerTransitioning.m:1571) 27 UIKitCore 0x1a94ab4b8 +[UIView(Animation) _setAlongsideAnimations:toRunByEndOfBlock:animated:] + 188 (UIView.m:17089) 28 UIKitCore 0x1a7c79070 _UIViewControllerTransitioningRunCustomTransitionWithRequest + 556 (UIViewControllerTransitioning.m:1560) 29 UIKitCore 0x1a86cb7cc __77-[UIPresentationController runTransitionForCurrentStateAnimated:handoffData:]_block_invoke_3 + 1784 (UIPresentationController.m:1504) 30 UIKitCore 0x1a7c43888 -[_UIAfterCACommitBlock run] + 72 (_UIAfterCACommitQueue.m:137) 31 UIKitCore 0x1a7c437c0 -[_UIAfterCACommitQueue flush] + 168 (_UIAfterCACommitQueue.m:228) 32 UIKitCore 0x1a7c436d0 _runAfterCACommitDeferredBlocks + 260 (UIApplication.m:3297) 33 UIKitCore 0x1a7c43c34 _cleanUpAfterCAFlushAndRunDeferredBlocks + 80 (UIApplication.m:3275) 34 UIKitCore 0x1a7c1f104 _UIApplicationFlushCATransaction + 72 (UIApplication.m:3338) 35 UIKitCore 0x1a7c1f024 __setupUpdateSequence_block_invoke_2 + 352 (_UIUpdateScheduler.m:1634) 36 UIKitCore 0x1a7c2cee8 _UIUpdateSequenceRunNext + 128 (_UIUpdateSequence.mm:189) 37 UIKitCore 0x1a7c2c378 schedulerStepScheduledMainSectionContinue + 60 (_UIUpdateScheduler.m:1185) 38 UpdateCycle 0x28c58f5f8 UC::DriverCore::continueProcessing() + 84 (UCDriver.cc:288) 39 CoreFoundation 0x1a2323230 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 28 (CFRunLoop.c:2021) 40 CoreFoundation 0x1a23231a4 __CFRunLoopDoSource0 + 172 (CFRunLoop.c:2065) 41 CoreFoundation 0x1a2300c6c __CFRunLoopDoSources0 + 232 (CFRunLoop.c:2102) 42 CoreFoundation 0x1a22d68b0 __CFRunLoopRun + 820 (CFRunLoop.c:2983) 43 CoreFoundation 0x1a22d5c44 _CFRunLoopRunSpecificWithOptions + 532 (CFRunLoop.c:3462) 44 GraphicsServices 0x2416a2498 GSEventRunModal + 120 (GSEvent.c:2049) 45 UIKitCore 0x1a7c50ddc -[UIApplication _run] + 792 (UIApplication.m:3899) 46 UIKitCore 0x1a7bf5b0c UIApplicationMain + 336 (UIApplication.m:5574) // ...
2
1
347
Nov ’25
TipKit popoverTip repeatedly showing when switching tabs on iOS 26 (regression from previous versions)
Hi everyone, I’m currently experimenting an issue with TipKit’s popoverTip in combination with a TabView. On iOS 26, the popover tip keeps showing every time I switch tabs, even though it should only appear once. The same code behaves as expected on older iOS versions (for example, iOS 17 or 18), where the tip is displayed only once, as documented. Here’s a simplified example reproducing the issue: import TipKit struct ContentView: View { init() { try? Tips.configure() } var body: some View { TabView { VStack { Image(systemName: "globe") .imageScale(.large) .foregroundStyle(.tint) .popoverTip(HomeTip(), arrowEdge: .bottom) Text("Hello, world!") } .tabItem { Image(systemName: "house") Text("Home") } .tag(0) NavigationStack { List { Label("Reset Data store", systemImage: "gear") .onTapGesture { try? Tips.resetDatastore() } } .navigationTitle("Explore") } .tabItem { Image(systemName: "sparkles") Text("Settings") } .tag(1) } } private struct HomeTip: Tip { let id = "HomeTip" let title = Text("Test Tool Tip") } } Expected behavior: The tip appears once and does not reappear when switching between tabs. Observed behavior on iOS 26: The tip keeps reappearing every time the user switches back to the tab.
2
0
198
Nov ’25
SwiftData Migration: Objects Created in Custom Migration Aren't Persisted or Queryable (Repost)
I'm experiencing a critical issue with SwiftData custom migrations where objects created during migration appear to be inserted successfully but aren't persisted or found by queries after migration completes. The migration logs show objects being created, but subsequent queries return zero results. I'm migrating from schema version V2 to V2_5, which involves: Renaming Person class to GroupData Keeping the same data structure but changing the class name while keeping the old class. Using a custom migration stage to copy data from old to new schema Below is an extract of my two schema and migration plan: Environment: Xcode 16.0, iOS 18.0, Swift 6.0 SchemaV2 enum LinkMapV2: VersionedSchema { static let versionIdentifier: Schema.Version = .init(2, 0, 0) static var models: [any PersistentModel.Type] { [AnnotationData.self, Person.self, History.self] } @Model final class Person { @Attribute(.unique) var id: UUID var name: String var photo: String var requirement: String var statue: Bool var annotationId: UUID? var number: Int = 0 init(id: UUID = UUID(), name: String = "", photo: String = "", requirement: String = "", status: Bool = false, annotationId: UUID? = nil, number: Int = 0) { self.id = id self.name = name self.photo = photo self.requirement = requirement self.statue = status self.annotationId = annotationId self.number = number } } } Schema V2_5 static let versionIdentifier: Schema.Version = .init(2, 5, 0) static var models: [any PersistentModel.Type] { [AnnotationData.self, Person.self, GroupData.self, History.self] } // Keep the old Person model for migration @Model final class Person { @Attribute(.unique) var id: UUID var name: String var photo: String var requirement: String var statue: Bool var annotationId: UUID? var number: Int = 0 init(id: UUID = UUID(), name: String = "", photo: String = "", requirement: String = "", status: Bool = false, annotationId: UUID? = nil, number: Int = 0) { self.id = id self.name = name self.photo = photo self.requirement = requirement self.statue = status self.annotationId = annotationId self.number = number } } // Add the new GroupData model that mirrors Person @Model final class GroupData { @Attribute(.unique) var id: UUID var name: String var photo: String var requirement: String var status: Bool var annotationId: UUID? var number: Int = 0 init(id: UUID = UUID(), name: String = "", photo: String = "", requirement: String = "", status: Bool = false, annotationId: UUID? = nil, number: Int = 0) { self.id = id self.name = name self.photo = photo self.requirement = requirement self.status = status self.annotationId = annotationId self.number = number } } } Migration Plan static let migrationV2toV2_5 = MigrationStage.custom( fromVersion: LinkMapV2.self, toVersion: LinkMapV2_5.self, willMigrate: { context in do { let persons = try context.fetch(FetchDescriptor<LinkMapV2.Person>()) print("=== MIGRATION STARTED ===") print("Found \(persons.count) Person objects to migrate") guard !persons.isEmpty else { print("No Person data requires migration") return } for person in persons { print("Migrating Person: '\(person.name)' with ID: \(person.id)") let newGroup = LinkMapV2_5.GroupData( id: person.id, // Keep the same ID name: person.name, photo: person.photo, requirement: person.requirement, status: person.statue, annotationId: person.annotationId, number: person.number ) context.insert(newGroup) print("Inserted new GroupData: '\(newGroup.name)'") // Don't delete the old Person yet to avoid issues // context.delete(person) } try context.save() print("=== MIGRATION COMPLETED ===") print("Successfully migrated \(persons.count) Person objects to GroupData") } catch { print("=== MIGRATION ERROR ===") print("Migration failed with error: \(error)") } }, didMigrate: { context in do { // Verify migration in didMigrate phase let groups = try context.fetch(FetchDescriptor<LinkMapV2_5.GroupData>()) let oldPersons = try context.fetch(FetchDescriptor<LinkMapV2_5.Person>()) print("=== MIGRATION VERIFICATION ===") print("New GroupData count: \(groups.count)") print("Remaining Person count: \(oldPersons.count)") // Now delete the old Person objects for person in oldPersons { context.delete(person) } if !oldPersons.isEmpty { try context.save() print("Cleaned up \(oldPersons.count) old Person objects") } // Print all migrated groups for debugging for group in groups { print("Migrated Group: '\(group.name)', Status: \(group.status), Number: \(group.number)") } } catch { print("Migration verification error: \(error)") } } ) And I've attached console output below: Console Output
1
0
111
Nov ’25