Explore the various UI frameworks available for building app interfaces. Discuss the use cases for different frameworks, share best practices, and get help with specific framework-related questions.

All subtopics
Posts under UI Frameworks topic

Post

Replies

Boosts

Views

Activity

A Summary of the WWDC25 Group Lab - UI Frameworks
At WWDC25 we launched a new type of Lab event for the developer community - Group Labs. A Group Lab is a panel Q&A designed for a large audience of developers. Group Labs are a unique opportunity for the community to submit questions directly to a panel of Apple engineers and designers. Here are the highlights from the WWDC25 Group Lab for UI Frameworks. How would you recommend developers start adopting the new design? Start by focusing on the foundational structural elements of your application, working from the "top down" or "bottom up" based on your application's hierarchy. These structural changes, like edge-to-edge content and updated navigation and controls, often require corresponding code modifications. As a first step, recompile your application with the new SDK to see what updates are automatically applied, especially if you've been using standard controls. Then, carefully analyze where the new design elements can be applied to your UI, paying particular attention to custom controls or UI that could benefit from a refresh. Address the large structural items first then focus on smaller details is recommended. Will we need to migrate our UI code to Swift and SwiftUI to adopt the new design? No, you will not need to migrate your UI code to Swift and SwiftUI to adopt the new design. The UI frameworks fully support the new design, allowing you to migrate your app with as little effort as possible, especially if you've been using standard controls. The goal is to make it easy to adopt the new design, regardless of your current UI framework, to achieve a cohesive look across the operating system. What was the reason for choosing Liquid Glass over frosted glass, as used in visionOS? The choice of Liquid Glass was driven by the desire to bring content to life. The see-through nature of Liquid Glass enhances this effect. The appearance of Liquid Glass adapts based on its size; larger glass elements look more frosted, which aligns with the design of visionOS, where everything feels larger and benefits from the frosted look. What are best practices for apps that use customized navigation bars? The new design emphasizes behavior and transitions as much as static appearance. Consider whether you truly need a custom navigation bar, or if the system-provided controls can meet your needs. Explore new APIs for subtitles and custom views in navigation bars, designed to support common use cases. If you still require a custom solution, ensure you're respecting safe areas using APIs like SwiftUI's safeAreaInset. When working with Liquid Glass, group related buttons in shared containers to maintain design consistency. Finally, mark glass containers as interactive. For branding, instead of coloring the navigation bar directly, consider incorporating branding colors into the content area behind the Liquid Glass controls. This creates a dynamic effect where the color is visible through the glass and moves with the content as the user scrolls. I want to know why new UI Framework APIs aren’t backward compatible, specifically in SwiftUI? It leads to code with lots of if-else statements. Existing APIs have been updated to work with the new design where possible, ensuring that apps using those APIs will adopt the new design and function on both older and newer operating systems. However, new APIs often depend on deep integration across the framework and graphics stack, making backward compatibility impractical. When using these new APIs, it's important to consider how they fit within the context of the latest OS. The use of if-else statements allows you to maintain compatibility with older systems while taking full advantage of the new APIs and design features on newer systems. If you are using new APIs, it likely means you are implementing something very specific to the new design language. Using conditional code allows you to intentionally create different code paths for the new design versus older operating systems. Prefer to use if #available where appropriate to intentionally adopt new design elements. Are there any Liquid Glass materials in iOS or macOS that are only available as part of dedicated components? Or are all those materials available through new UIKit and AppKit views? Yes, some variations of the Liquid Glass material are exclusively available through dedicated components like sliders, segmented controls, and tab bars. However, the "regular" and "clear" glass materials should satisfy most application requirements. If you encounter situations where these options are insufficient, please file feedback. If I were to create an app today, how should I design it to make it future proof using Liquid Glass? The best approach to future-proof your app is to utilize standard system controls and design your UI to align with the standard system look and feel. Using the framework-provided declarative API generally leads to easier adoption of future design changes, as you're expressing intent rather than specifying pixel-perfect visuals. Pay close attention to the design sessions offered this year, which cover the design motivation behind the Liquid Glass material and best practices for its use. Is it possible to implement your own sidebar on macOS without NSSplitViewController, but still provide the Liquid Glass appearance? While technically possible to create a custom sidebar that approximates the Liquid Glass appearance without using NSSplitViewController, it is not recommended. The system implementation of the sidebar involves significant unseen complexity, including interlayering with scroll edge effects and fullscreen behaviors. NSSplitViewController provides the necessary level of abstraction for the framework to handle these details correctly. Regarding the SceneDelagate and scene based life-cycle, I would like to confirm that AppDelegate is not going away. Also if the above is a correct understanding, is there any advice as to what should, and should not, be moved to the SceneDelegate? UIApplicationDelegate is not going away and still serves a purpose for application-level interactions with the system and managing scenes at a higher level. Move code related to your app's scene or UI into the UISceneDelegate. Remember that adopting scenes doesn't necessarily mean supporting multiple scenes; an app can be scene-based but still support only one scene. Refer to the tech note Migrating to the UIKit scene-based life cycle and the Make your UIKit app more flexible WWDC25 session for more information.
Topic: UI Frameworks SubTopic: General
0
0
597
Jun ’25
List jumps back to the top
Following on from this thread: https://developer.apple.com/forums/thread/805037 my list of items is now correctly maintaining state (no more disappearing rows), but I'm now hitting a really annoying issue: Every time something changes - even just changing the dark mode of the device - the entire list of items is refreshed, and the list jumps back to the top. A simple representation: // modelData.filteredItems is either all items or some items, depending on whether the user is searching List { ForEach(modelData.filteredItems) { item in ItemRow(item: item) } } When the user isn't searching, filteredItems has everything in it. When they turn on search, I filter and sort the data in place: // Called when the user turns on search, or when the searchString or searchType changes func sortAndFilterItemsInModelData() { modelData.filteredItems.removeAll() // Remove all items from the filtered array modelData.filteredItems.append(contentsOf: modelData.allItems) // Add all items back in let searchString: String = modelData.searchString.lowercased() switch(modelData.searchType) { case 1: // Remove all items from the filtered array that don't match the search string modelData.filteredItems.removeAll(where: { !$0.name.lowercased().contains(searchString) }) ... } // Sorting switch(modelData.sortKey) { case sortKeyDate: modelData.sortAscending ? modelData.filteredItems.sort { $0.date < $1.date } : modelData.filteredItems.sort { $0.date > $1.date } // Sorts in place ... } } The method doesn't return anything because all the actions are done in place on the data, and the view should display the contents of modelData.filteredItems. If you're searching and there are, say 10 items in the list and you're at the bottom of the list, then you change the search so there are now 11 items, it jumps back to the top rather than just adding the extra ItemRow to the bottom. Yes, the data is different, but it hasn't been replaced; it has been altered in place. The biggest issue here is that you can simply change the device to/from Dark Mode - which can happen automatically at a certain time of day - and you're thrown back to the top of the list. The array of data hasn't changed, but SwiftUI treats it as though it has. There's also a section in the List that can be expanded and contracted. It shows or hides items of a certain type. When I expand it, I expect the list to stay in the same place and just show the extra rows, but again, it jumps to the top. It's a really poor user experience. Am I doing something wrong (probably, yes), or is there some other way to retain the scroll position in a List? The internet suggests switching to a LazyVStack, but I lose left/right swipe buttons and the platform-specific styling. Thanks.
4
0
79
3h
Scrolling UICollectionView inside a container view scrolls navigation controller title
I am trying to understand if what I am seeing is expected behavior or not with the following UIKit components. 1.I have a view controller "A" embedded in a navigation controller (part of a multi-step flow). Large titles are active on this navigation controller. In this view controller "A", I have a container view that contains another view controller "B" (I want to reuse the contents of B in other flows) Inside view controller "B" I have a UICollectionView using a diffable data source. When you load view controller "A" it appears to work fine. My collection view loads data, I see a nice list and when I scroll it... ... the expectation is it scrolls inside it's container and has no impact on the parent controller "B" However, the navigation bar and title in "A" reflect the content offset of the collection view. Scroll a couple lines, the large title turns small and centered on top. If I turn off large title, I still see the background color of the navigation bar change as it would if you were scrolling a view directly inside controller "A" without the container view. Am I supposed to be manually capturing the gesture recognizer in B and somehow preventing the gesture to bubble up to A? It seems like strange behavior to have to correct. Any suggestions? Thanks!
Topic: UI Frameworks SubTopic: UIKit Tags:
4
0
1.4k
6h
Navigation Bar Occupies Too Much Space in iOS 26 Landscape Orientation
I’m really frustrated with iOS 26. It was supposed to make better use of screen space, but when you combine the navigation bar, tab bar, and search bar, they eat up way too much room. Apple actually did a great job with the new tab bar — it’s smaller, smooth, and looks great when expanding or collapsing while scrolling. The way the search bar appears above the keyboard is also really nice. But why did they keep the navigation bar the same height in both portrait and landscape? In landscape it takes up too much space and just looks bad. It was way better in iOS 18.
0
0
47
7h
NSStagedMigrationManager shortcomings
It seems to me that NSStagedMigrationManager has algorithmic issues. It doesn't perform staged migration, if all its stages are NSLightweightMigrationStage. You can try it yourself. There is a test project with three model versions V1, V2, V3, V4. Migrating V1->V2 is compatible with lightweight migration, V2->V3, V3->V4 is also compatible, but V1->V3 is not. I have following output: Migrating V1->V2, error: nil Migrating V2->V3, error: nil Migrating V3->V4, error: nil Migrating V1->V3, no manager, error: Optional("Persistent store migration failed, missing mapping model.") Migrating V1->V3, lightweight[1, 2, 3], error: Optional("Persistent store migration failed, missing mapping model.") Migrating V1->V3, lightweight[1]->lightweight[2]->lightweight[3], error: Optional("Persistent store migration failed, missing mapping model.") Migrating V1->V3, custom[1->2]->lightweight[3], error: nil Migrating V1->V3, lightweight[1]->custom[2->3], error: nil Migrating V1->V3, custom[1->2]->custom[2->3], error: nil Migrating V1->V4, error: Optional("Persistent store migration failed, missing mapping model.") Migrating V2->V4, error: nil Migrating V1->V4, custom[1->2]->lightweight[3, 4], error: nil Migrating V1->V4, lightweight[3, 4]->custom[1->2], error: Optional("A store file cannot be migrated backwards with staged migration.") Migrating V1->V4, lightweight[1, 2]->lightweight[3, 4], error: Optional("Persistent store migration failed, missing mapping model.") Migrating V1->V4, lightweight[1]->custom[2->3]->lightweight[4], error: nil Migrating V1->V4, lightweight[1,4]->custom[2->3], error: nil Migrating V1->V4, custom[2->3]->lightweight[1,4], error: Optional("Persistent store migration failed, missing mapping model.") I think that staged migration should satisfy the following rules for two consecutive stages: Any version of lightweight stage to any version of lightweight stage; Any version of lightweight stage to current version of custom stage; Next version of custom stage to any version of lightweight stage; Next version of custom stage to current version of custom stage. However, rule 1 doesn't work, because migration manager skips intermediate versions if they are inside lightweight stages, even different ones. Note that lightweight[3, 4]->custom[1->2] doesn't work, lightweight[1,4]->custom[2->3] works, but custom[2->3]->lightweight[1,4] doesn't work again. Would like to hear your opinion on that, especially, from Core Data team, if possible. Thanks!
2
0
91
8h
iOS 26 stops receiving push notifications
I a using the current RC version of iOS on both my iPhone and iPad. I and developing an iCloud based app and it works correctly on iOS 18. When I upgraded to iOS 26 the iCloud functions work correctly but the push notifications do not work. The issue appears to be creating subscriptions. The following code should create a subscription and does not get an error, but it did to create a subscription under iOS 26. func subscribeToNotifications(recordType: String, subscriptionID: String, notification: CKSubscription.NotificationInfo) { let subscriptionIDForType = "\(subscriptionID)-\(recordType)" let predicate = NSPredicate(value: true) let subscription = CKQuerySubscription(recordType: recordType, predicate: predicate, subscriptionID: subscriptionIDForType, options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion]) let notification = CKSubscription.NotificationInfo() subscription.notificationInfo = notification CKContainer.default().publicCloudDatabase.save(subscription) { (returnedSubscription, error) in if let error = error { print("Error saving subscription: \(error)") } else { print("Successfully saved subscription: recordType: " + recordType + " subscriptionID: " + subscriptionIDForType) } } } Print results: Successfully saved subscription: recordType: folder subscriptionID: folderName-folder
0
0
105
1d
How to prevent VoiceOver from reading text INSIDE an image?
In the example below, VoiceOver (in both iOS 18 and 26) reads the text contained within the image after the .accessibilityLabel, introduced by a “beep.” VoiceOver: Purple rounded square with the word 'Foo' in white letters. Image [beep] foo. I’d like it to only read the accessibility label. As a developer focused on accessibility, I make sure every image already has an appropriate label, so having iOS read the image text is redundant. Sample Code import SwiftUI struct ContentView: View { var body: some View { Image("TextInImage") .resizable() .scaledToFit() .frame(width: 64, height: 64) .accessibilityLabel("Purple rounded square with the word 'Foo' in white letters.") } } Sample Image Drop this image in to Assets.xcassets and confirm it's named TextInImage.
4
0
157
1d
Menu List Error
Hi, I'm trying to have a menu at the trailing edge of a list but when I do so I am getting UIKit errors. I pasted my sample code below. Not exactly sure how to fix this. ForEach(0..<100) { i in HStack { Text("\(i)") Spacer() Menu { Text("Test") } label: { Image(systemName: "ellipsis") } } } } Adding '_UIReparentingView' as a subview of UIHostingController.view is not supported and may result in a broken view hierarchy. Add your view above UIHostingController.view in a common superview or insert it into your SwiftUI content in a UIViewRepresentable instead.
Topic: UI Frameworks SubTopic: SwiftUI
0
0
34
1d
Complications not showing up after WatchOS 26
We are having an issue with our app after upgrading to WatchOS 26. After some time our complication disappears from the watch face. If you go to add it back, our app shows up with an empty icon placeholder and you are unable to tap it to add it back. Sometimes restarting the watch will bring it back, sometimes it does not. Has anyone experienced this? What should we be looking at to figure out why this is happening? Or could this be a bug in WatchOS 26?
2
0
116
1d
List rows disappearing when scrolling
I have a List containing ItemRow views based on an ItemDetails object. The content is provided by a model which pulls it from Core Data. When I scroll through the list one or two of the rows will disappear and reappear when I scroll back up. I have a feeling it's because the state is being lost? Here's some relevant info (only necessary parts of the files are provided): -- ModelData.swift: @Observable class ModelData { var allItems: [ItemDetails] = coreData.getAllItems() ... } -- ItemDetails.swift: struct ItemDetails: Identifiable, Hashable, Equatable { public let id: UUID = UUID() public var itemId: String // Also unique, but used for a different reason ... } -- MainApp.swift: let modelData: ModelData = ModelData() // Created as a global class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Methods in here (and in lots of other places) use `modelData`, which is why it's a global } @main struct MainApp: App { var body: some Scene { WindowGroup { MainView() } } ... } -- MainView.swift: struct MainView: View { var body: some View { List { ForEach(modelData.allItems, id: \.id) { item in ItemRow(item) } } } } struct ItemRow: View, Equatable { var item: ItemDetails var body: some View { ... } static func == (lhs: Self, rhs: Self) -> Bool { lhs.item == rhs.item } } There's obviously more code in the app than that, but it's not relevant to the issue. I've tried: ItemRow(item).equatable() Wrapping ItemRow in an EquatableView Giving the List a unique id Using class ModelData: ObservableObject and @StateObject for modelData None made any difference. I'm using iOS/iPadOS 26.0.1, and I see it on my physical iPhone 17 Pro Max and iPad Pro 11-inch M4, but I don't see it in the equivalent simulators on those versions. The Simulator also doesn't exhibit this for versions 17.5 and 18.5, and I have no physical devices on 17.5/18.5 to check. Should I be doing as I currently am, where I create modelData as a global let so I can access it everywhere, or should I pass it through the view hierarchy as an Environment variable, like @Environment(ModelData.self) var modelData: ModelData? Bear in mind that some functions are outside of the view hierarchy and cannot access modelData if I do this. Various things like controllers that need access to values in modelData cannot get to it. Any ideas? Thanks.
2
0
83
1d
Symbol Effects Cause Crash When Switching Between Conditional `.fill` SF Symbols
I am reporting a consistent EXC_BAD_ACCESS (SIGSEGV) crash when using the .contentTransition(.symbolEffect(.replace.byLayer)) modifier on a conditional $SF$ Symbol, but only under very specific conditions: Both Symbols must be .fill variants (e.g., arrow.up.circle.fill $\leftrightarrow$ plus.circle.fill). The crash only occurs when the state changes and the view attempts to transition back to the original symbol (the one present when the view was first rendered). For example, if the initial state displays arrow.up.circle.fill, the app successfully transitions to plus.circle.fill. However, the app crashes when the state changes again, attempting to transition from plus.circle.fill back to arrow.up.circle.fill.This crash is fully reproducible in the iOS Simulator and is not limited to Xcode Previews. Commenting out the .contentTransition modifier resolves the crash immediately. The crash is a KERN_INVALID_ADDRESS at 0x0000000000000060, pointing to a null pointer dereference deep within the graphics rendering pipeline in the RenderBox framework. It appears to be a failure in handling the layer data during the reverse transition of two filled-variant symbols. Exception Type: EXC_BAD_ACCESS (SIGSEGV) Exception Subtype: KERN_INVALID_ADDRESS at 0x0000000000000060 Thread 0 Crashed: 0 RenderBox 0x1cc692a0c RB::Symbol::Glyph::Layer::resolve_draw_transforms(...) + 292 1 RenderBox 0x1cc692730 RB::Symbol::Glyph::Layer::append_path(...) + 392 ... 7 SwiftUICore 0x1d9d5f824 specialized _ShapeStyle_RenderedShape.renderVectorGlyph(...) + 1444 The code below crashes on the second tap (transitioning back to arrow.up.circle.fill): struct QuickAddButtonTest: View { @State private var isTextFieldFocused: Bool = false // Both symbols use the .fill variant var body: some View { Button(action: { isTextFieldFocused.toggle() }) { Image(systemName: isTextFieldFocused ? "arrow.up.circle.fill" : "plus.circle.fill") .foregroundColor(.primary) .font(.system(size: 30)) .contentTransition(.symbolEffect(.replace.byLayer)) // <-- CRASH SOURCE } } } The crash can be avoided by: Removing the .contentTransition(.symbolEffect(.replace.byLayer)) modifier, or Switching one or both symbols to a non-filled variant (e.g., plus.circle instead of plus.circle.fill).
0
0
37
1d
Navigation title in master splitview panel wrong colour with dark mode and hard scrollEdgeEffect
The navigation title color is wrong (dark on dark background) when applying hard scrolledgeeffectstyle. Illustrated by code example. In simulator or physical device. Changing scrollEdgeEffectStyle to soft resolves the issue. As does changing the listStyle. @main struct AppView: App { var body: some Scene { WindowGroup { NavigationSplitView { NavigationStack { Section { NavigationLink(destination: OptionsView()) { Label("More Options", systemImage: "gearshape") } .isDetailLink(false) } header: { Text("WITH OPTIONS").bold() } .navigationTitle("TESTING") } } detail: { Text("DETAIL") } .preferredColorScheme(.dark) } } } struct OptionsView: View { var body: some View { List { Text("List Item") } .listStyle(.plain) .navigationTitle("MORE OPTIONS TITLE") .scrollEdgeEffectStyle(.hard, for: .all) } } Submittted FB20811402 Bug appears in Landscape mode for iPhone only. Any orientation for iPad.
Topic: UI Frameworks SubTopic: SwiftUI
4
0
124
1d
Focusable doesn't work on iPad with external keyboard
I have a custom input view in my app which is .focusable(). It behaves similar to a TextField, where it must be focused in order to be used. This works fine on all platforms including iPad, except when when an external keyboard is connected (magic keyboard), in which case it can't be focused anymore and becomes unusable. Is there a solution to this, or a workaround? My view is very complex, so simple solutions like replacing it with a native view isn't possible, and I must be able to pragmatically force it to focus. Here's a very basic example replicating my issue. Non of the functionality works when a keyboard is connected: struct FocusableTestView: View { @FocusState private var isRectFocused: Bool var body: some View { VStack { // This text field should focus the custom input when pressing return: TextField("Enter text", text: .constant("")) .textFieldStyle(.roundedBorder) .onSubmit { isRectFocused = true } .onKeyPress(.return) { isRectFocused = true return .handled } // This custom "input" should focus itself when tapped: Rectangle() .fill(isRectFocused ? Color.accentColor : Color.gray.opacity(0.3)) .frame(width: 100, height: 100) .overlay( Text(isRectFocused ? "Focused" : "Tap me") ) .focusable(true, interactions: .edit) .focused($isRectFocused) .onTapGesture { isRectFocused = true print("Focused rectangle") } // The focus should be able to be controlled externally: Button("Toggle Focus") { isRectFocused.toggle() } .buttonStyle(.bordered) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) } }
0
0
98
1d
Add a value to the Photos Caption field
In the iOS Photos app there is a caption field the user can write to. How can you write to this value from Swift when creating a photo? I see apps that do this, but there doesn't seem to be any official way to do this using the Photo library through PHAssetCreationRequest or PHAssetResourceCreationOptions or setting EXIF values, I tried settings a bunch of values there including IPTC values but nothing appears in the caption field in the iOS photos app. There must be some way to do it since I see other apps setting that value somehow after capturing a photo.
1
0
77
1d
Automating pickerWheels in VisionOS
Hi! I am learning Swift and UIKit for work. I am trying to automate using a pickerWheel in VisionOS, but since .adjust(toValue: ) was removed in VisionOS's API, I am absolutely struggling to find a way to set a pickerWheel to a specific value. Currently, my solution is to calculate the amount of times I would need to increment/decrement the wheel to get from the current value to the desired value, then do so one at a time. However, this currently does not work, as .accessibilityIncrement() and .accessibilityDecrement() do not work, and .swipeUp() and .swipeDown() go too far. What can I do? Note: I am not a frontend engineer, so while solutions may exist that involve changes to the frontend, I would much rather try and get the frontend we do have to work as is.
1
0
39
2d
Prevent default file selector in a SwiftUI DocumentGroup app and show a custom welcome window on launch
I’m building a macOS document based app using SwiftUI’s DocumentGroup API. By default, when a document based app launches, macOS automatically shows a file open panel or creates a new untitled document window. However, I want to suppress this default behavior and instead show a custom welcome window when the app starts — something similar to how Xcode or Final Cut Pro shows a “Welcome” or “Start Project” screen first. So basically, when the user opens the app normally, it should not open the document selector or create a document automatically. Instead, it should show my custom SwiftUI or AppKit window. Here is my Code :- //MyApp.swift import SwiftUI import AppKit @main struct PhiaApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var body: some Scene { DocumentGroup(newDocument: MyDocumentModel()) { file in EditorView(document: file.document, filePath: file.fileURL) } Settings { EmptyView() } } } Current I have this code setup for my MainApp.swift, where I am using the AppDelegate to create a custom recording window using appkit and also defining the DocumentGroup to handle the custom .myapp file opens. However, when I launch the app, its showing my appkit window as well as the macOs native file Selector to select the file I want to open. I want when the user opens the app normally, it should not open the document selector or create a document automatically. Instead, it should show my custom SwiftUI or AppKit window. However, the app should still fully support opening .myapp documents by double clicking from Finder, using the standard File → Open and File → New menu options, also having multiple document windows open at once. This is my AppDelegate.swift file :- import AppKit import SwiftUI class AppDelegate: NSObject, NSApplicationDelegate { var panel: Panel? private var statusItem: NSStatusItem? func applicationDidFinishLaunching(_ notification: Notification) { showWindow() } // MARK: - Window control func showWindow() { if panel == nil { let root = RecordingViewMain() let newPanel = Panel(rootView: root) if let screen = NSScreen.main { let size = NSSize(width: 360, height: 240) let origin = NSPoint( x: screen.visibleFrame.midX - size.width / 2, y: screen.visibleFrame.midY - size.height / 2 ) newPanel.setFrame(NSRect(origin: origin, size: size), display: true) } panel = newPanel } panel?.makeKeyAndOrderFront(nil) } func hideWindow() { panel?.orderOut(nil) } @objc private func showPanelAction() { showWindow() } @objc private func quitAction() { NSApp.terminate(nil) } }
2
0
142
2d
SpriteKit/RealityKit + SwiftUI Performance Regression on iOS 26
Hi, Toggling a SwiftUI menu in iOS 26 significantly reduces the framerate of an underlying SKView or ARView. Below are test cases for SpriteKit and RealityKit. I ran these tests on iOS 26.1 Beta using an iPhone 13 (A15 chip). Results were similar on iOS 26.0.1. Both scenes consist of circles and balls bouncing on the ground. The restitution of the physics bodies is set for near-perfect elasticity, so they keep bouncing indefinitely. In both SKView and ARView, the framerate drops significantly whenever the SwiftUI menu is toggled. The menu itself is simple and uses standard SwiftUI animations and styling. SpriteKit import SpriteKit import SwiftUI class SKRestitutionScene: SKScene { override func didMove(to view: SKView) { view.contentMode = .center size = view.bounds.size scaleMode = .resizeFill backgroundColor = .darkGray anchorPoint = CGPoint(x: 0.5, y: 0.5) let groundWidth: CGFloat = 300 let ground = SKSpriteNode(color: .gray, size: CGSize(width: groundWidth, height: 10)) ground.physicsBody = SKPhysicsBody(rectangleOf: ground.size) ground.physicsBody?.isDynamic = false addChild(ground) let circleCount = 5 let spacing: CGFloat = 60 let totalWidth = CGFloat(circleCount - 1) * spacing let startX = -totalWidth / 2 for i in 0..<circleCount { let circle = SKShapeNode(circleOfRadius: 18) circle.fillColor = .systemOrange circle.lineWidth = 0 circle.physicsBody = SKPhysicsBody(circleOfRadius: 18) circle.physicsBody?.restitution = 1 circle.physicsBody?.linearDamping = 0 let x = startX + CGFloat(i) * spacing circle.position = CGPoint(x: x, y: 150) addChild(circle) } } override func willMove(from view: SKView) { self.removeAllChildren() } } struct SKRestitutionView: View { var body: some View { ZStack { SpriteView(scene: SKRestitutionScene(), preferredFramesPerSecond: 120) .ignoresSafeArea() VStack { Spacer() Menu { Button("Edit", systemImage: "pencil") {} Button("Share", systemImage: "square.and.arrow.up") {} Button("Delete", systemImage: "trash") {} } label: { Text("Menu") } .buttonStyle(.glass) } .padding() } } } #Preview { SKRestitutionView() } RealityKit import RealityKit import SwiftUI struct ARViewPhysicsRestitution: UIViewRepresentable { let arView = ARView() func makeUIView(context: Context) -> some ARView { arView.contentMode = .center arView.cameraMode = .nonAR arView.automaticallyConfigureSession = false arView.environment.background = .color(.gray) // MARK: Root let anchor = AnchorEntity() arView.scene.addAnchor(anchor) // MARK: Camera let camera = Entity() camera.components.set(PerspectiveCameraComponent()) camera.position = [0, 1, 4] camera.look(at: .zero, from: camera.position, relativeTo: nil) anchor.addChild(camera) // MARK: Ground let groundWidth: Float = 3.0 let ground = Entity() let groundMesh = MeshResource.generateBox(width: groundWidth, height: 0.1, depth: groundWidth) let groundModel = ModelComponent(mesh: groundMesh, materials: [SimpleMaterial(color: .white, roughness: 1, isMetallic: false)]) ground.components.set(groundModel) let groundShape = ShapeResource.generateBox(width: groundWidth, height: 0.1, depth: groundWidth) let groundCollision = CollisionComponent(shapes: [groundShape]) ground.components.set(groundCollision) let groundPhysicsBody = PhysicsBodyComponent( material: PhysicsMaterialResource.generate(friction: 0, restitution: 0.97), mode: .static ) ground.components.set(groundPhysicsBody) anchor.addChild(ground) // MARK: Balls let ballCount = 5 let spacing: Float = 0.4 let totalWidth = Float(ballCount - 1) * spacing let startX = -totalWidth / 2 let radius: Float = 0.12 let ballMesh = MeshResource.generateSphere(radius: radius) let ballMaterial = SimpleMaterial(color: .systemOrange, roughness: 1, isMetallic: false) let ballShape = ShapeResource.generateSphere(radius: radius) for i in 0..<ballCount { let ball = Entity() let ballModel = ModelComponent(mesh: ballMesh, materials: [ballMaterial]) ball.components.set(ballModel) let ballCollision = CollisionComponent(shapes: [ballShape]) ball.components.set(ballCollision) var ballPhysicsBody = PhysicsBodyComponent( material: PhysicsMaterialResource.generate(friction: 0, restitution: 0.97), /// 0.97 for near perfect elasticity mode: .dynamic ) ballPhysicsBody.linearDamping = 0 ballPhysicsBody.angularDamping = 0 ball.components.set(ballPhysicsBody) let shadow = GroundingShadowComponent(castsShadow: true) ball.components.set(shadow) let x = startX + Float(i) * spacing ball.position = [x, 1, 0] anchor.addChild(ball) } return arView } func updateUIView(_ uiView: UIViewType, context: Context) { } } struct PhysicsRestitutionView: View { var body: some View { ZStack { ARViewPhysicsRestitution() .ignoresSafeArea() .background(.black) VStack { Spacer() Menu { Button("Edit", systemImage: "pencil") {} Button("Share", systemImage: "square.and.arrow.up") {} Button("Delete", systemImage: "trash") {} } label: { Text("Menu") } .buttonStyle(.glass) } .padding() } } } #Preview { PhysicsRestitutionView() }
1
0
100
2d