I'm looking for clarification on a SwiftUI performance point mentioned in the recent Optimize your app's speed and efficiency | Meet with Apple video.
(YouTube link not allowed, but the video is available on the Apple Developer channel.)
At the 1:48:50 mark, the presenter says:
Writing a value to the Environment doesn't only affect the views that read the key you're updating. It updates any view that reads from any Environment key. [abbreviated quote]
That statement seems like a big deal if your app relies heavily on Environment values.
Context
I'm building a macOS application with a traditional three-panel layout. At any given time, there are many views on screen, plus others that exist in the hierarchy but are currently hidden (for example, views inside tab views or collapsed splitters).
Nearly every major view reads something from the environment—often an @Observable object that acts as a service or provider.
However, there are a few relatively small values that are written to the environment frequently, such as:
The selected tab index
The currently selected object on a canvas
The Question
Based on the presenter's statement, I’m wondering:
Does writing any value to the environment really cause all views in the entire SwiftUI view hierarchy that read any environment key to have their body re-evaluated?
Do environment writes only affect child views, or do they propagate through the entire SwiftUI hierarchy?
Example:
View A
└─ View B
├─ View C
└─ View D
If View B updates an environment value, does that affect only C and D, or does it also trigger updates in A and B (assuming each view has at least one @Environment property)?
Possible Alternative
If all views are indeed invalidated by environment writes, would it be more efficient to “wrap” frequently-changing values inside an @Observable object instead of updating the environment directly?
// Pseudocode
@Observable final class SelectedTab {
var index: Int
}
ContentView()
.environment(\.selectedTab, selectedTab)
struct TabView: View {
@Environment(\.selectedTab) private var selectedTab
var body: some View {
Button("Action") {
// Would this avoid invalidating all views using the environment?
selectedTab.index = 1
}
}
}
Summary
From what I understand, it sounds like the environment should primarily be used for stable, long-lived objects—not for rapidly changing values—since writes might cause far more view invalidations than most developers realize.
Is that an accurate interpretation?
Follow-Up
In Xcode 26 / Instruments, is there a way to monitor writes to @Environment?
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.
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Created
Since iPadOS 26.1 I notice a new annoying bug when changing the dark mode option of the system. The appearance of the UI changes, but no longer for view controllers which are presented as Popover. For these view controllers the method "traitCollectionDidChange()" is still called (though sometimes with a very large delay), but checking the traitCollection property of the view controller in there does no longer return the correct appearance (which is probably why the visual appearance of the popover doesn't change anymore). So if the dark mode was just switched on, traitCollectionDidChange() is called, but the "traitCollection.userInterfaceStyle" property still tells me that the system is in normal mode.
More concrete, traitCollection.userInterfaceStyle seems to be set correctly only(!) when opening the popover, and while the popover is open, it is never updated anymore when the dark mode changes.
This is also visible in the standard Apps of the iPad, like the Apple Maps App: just tap on the "map" icon at the top right to open the "Map mode" view. While the view is open, change the dark mode. All of the Maps App will change its appearance, with the exception of this "Map mode" view.
Does anyone know an easy workaround? Or do I really need to manually change the colors for all popup view controllers whenever the dark mode changes? Using dynamic UIColors won't help, because these rely on the "userInterfaceStyle" property, and this is no longer correct.
Bugreport: FB20928471
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.
In one of my apps, i am using .glassEffect(_:In) to add glass effect on various elements. The app always crushes when a UI element with glassEffect(_in:) modifier is being rendered. This only happens on device running iOS 26 public beta. I know this for certain because I connected the particular device to xcode and run the app on the device. When i comment out the glassEffect modifier, app doesn't crush.
Is it possible to check particular realeases with #available? If not, how should something like this be handled. Also how do i handle such os level erros without the app crushing. Thanks.
The same code that I have, runs fine on iOS 26.0, but on iOS 26.1, there's a delay in the Button with role .confirm to be shown properly and tinted.
Shown in the screen recording here ->
https://imgur.com/a/uALuW50
This is my code that shows slightly different button in iOS 18 vs iOS26.
var body: some View {
if #available(iOS 26.0, *) {
Button("Save", systemImage: "checkmark", role: .confirm) {
action()
}.labelStyle(.iconOnly)
} else {
Button("Save") {
action()
}
}
}
Topic:
UI Frameworks
SubTopic:
SwiftUI
I’m trying to figure out how to extend PaperKit beyond a single fixed-size canvas.
From what I understand, calling PaperMarkup(bounds:) creates one finite drawing region, and so far I have not figured out a reliable way to create multi-page or infinite canvases.
Are any of these correct?
Creating multiple PaperMarkup instances, each managed by its own PaperMarkupViewController, and arranging them in a ScrollView or similar paged container to represent multiple pages?
Overlaying multiple PaperMarkup instances on top of PDFKit pages for paged annotation workflows?
Or possibly another approach that works better with PaperKit’s design?
I mean it has to be possible, right? Apple's native Preview app almost certainly uses it, and there are so many other notes apps that get this behavior working correctly, even if it requires using a legacy thing other than PaperKit.
Curious if others have been able to find the right pattern for going beyond a single canvas.
Such a simple piece of code:
import SwiftUI
import WebKit
struct ContentView: View {
var body: some View {
WebView(url: URL(string: "https://www.apple.com"))
}
}
When I run this, the web content shows under the top notch’s safe area, and buttons inside that region aren’t tappable. I tried a bunch of things and the only “fix” that seems to work is .padding(.top, 1), but that leaves a noticeable white strip in non-portrait orientations.
What’s the proper way to solve this? Safari handles the safe area correctly and doesn’t render content there.
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")
}
}
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?
Starting with iOS 18, UITabBarController no longer updates tab bar item titles when localized strings are changed or reassigned at runtime.
This behavior worked correctly in iOS 17 and earlier, but in iOS 18 the tab bar titles remain unchanged until the app restarts or the view controller hierarchy is reset. This regression appears to be caused by internal UITabBarController optimizations introduced in iOS 18.
Steps to Reproduce
Create a UITabBarController with two or more tabs, each having a UITabBarItem with a title.
Localize the tab titles using NSLocalizedString():
tabBar.items?[0].title = NSLocalizedString("home_tab", comment: "")
tabBar.items?[1].title = NSLocalizedString("settings_tab", comment: "")
Run the app.
Change the app’s language at runtime (without restarting), or manually reassign the localized titles again:
tabBar.items?[0].title = NSLocalizedString("home_tab", comment: "")
tabBar.items?[1].title = NSLocalizedString("settings_tab", comment: "")
Observe that the tab bar titles do not update visually.
Hi all,
when I launch my macOS app from Xcode 16 on ARM64, appKit logs me this error on the debug console:
It's not legal to call -layoutSubtreeIfNeeded on a view which is already being laid out. If you are implementing the view's -layout method, you can call -[super layout] instead. Break on _NSDetectedLayoutRecursion(void) to debug. This will be logged only once. This may break in the future.
_NSDetectedLayoutRecursion doesn't help a lot, giving me these assembly codes from a call to a subclassed window method that looks like this:
-(void) setFrame:(NSRect)frameRect display:(BOOL)flag {
if (!_frameLocked) [super setFrame:frameRect display:flag];
}
I have no direct call to -layoutSubtreeIfNeeded from a
-layout implementation in my codes. I have a few calls to this method from update methods, however even if I comment all of them, the error is still logged...
Finally, apart from that log, I cannot observe any layout error when running the program. So I wonder if this error can be safely ignored?
Thanks!
This problem occurs only when using the Japanese keyboard.
After typing ""あい"" into a text field and pressing Enter, insert ""う"" between ""あ"" and ""い"".
As a result, the displayed text becomes ""あううい"".
Text field: ""あい"" → ""あううい""
The range values at this point:
range.location = 2
range.length = 2
・Behavior in iOS versions before 26
After typing ""あい"" into a text field and pressing Enter, insert ""う"" between ""あ"" and ""い"".
As a result, the displayed text becomes ""あうい"".
Text field: ""あい"" → ""あうい""
The range values at this point:
range.location = 1
range.length = 1
The following source code can be used to reproduce the issue.
mainApp (displaying a UIKit ViewController from SwiftUI)
import SwiftUI
import UIKit
struct ViewControllerWrapper: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> ViewController {
return ViewController()
}
func updateUIViewController(_ uiViewController: ViewController, context: Context) {}
}
@main
struct konnitihaApp: App {
var body: some Scene {
WindowGroup {
ViewControllerWrapper() // UIKitのViewControllerを表示
}
}
}
ViewController
import UIKit
class ViewController: UIViewController, UITextFieldDelegate {
let textField = UITextField()
let displayLabel = UILabel()
// 追加で管理するプロパティ
var replacementText: String = ""
var textInsertLocation: Int = 0
var textSelectedLength: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
// UITextFieldの設定
textField.borderStyle = .roundedRect
textField.placeholder = ""
textField.delegate = self
textField.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(textField)
// UILabelの設定
displayLabel.text = ""
displayLabel.layer.borderWidth = 2
displayLabel.layer.borderColor = UIColor.blue.cgColor
displayLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(displayLabel)
// レイアウト
NSLayoutConstraint.activate([
textField.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40),
textField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
textField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
textField.heightAnchor.constraint(equalToConstant: 40),
//label
displayLabel.topAnchor.constraint(equalTo: textField.bottomAnchor, constant: 500),
displayLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
displayLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
displayLabel.heightAnchor.constraint(equalToConstant: 40),
])
}
// UITextFieldDelegate
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
self.replacementText = string
self.textInsertLocation = range.location
self.textSelectedLength = range.length
print("rangeは",range)
// ラベルに最新の文字列を反映
if let text = textField.text, let textRange = Range(range, in: text) {
let updatedText = text.replacingCharacters(in: textRange, with: string)
if textField == self.textField {
displayLabel.text = updatedText
}
}
return true
}
}
Topic:
UI Frameworks
SubTopic:
UIKit
in swiftui how can I change the text color when use "menu"? I mean I need that each text in pippo will be .red color as you can see in the code, but .foregroundColor(.red) do not works
thanks
var pippo = ["AAA","BBB","CCC", "DDD"]
var body: some View {
Menu {
ForEach(pippo, id: \.self) { item in
Button {
//categoriaSelezionata = categoria
} label: {
Text(item)
.foregroundColor(.red)
}
}
} label: {
HStack {
Text("Select Theme")
.multilineTextAlignment(.center)
Image(systemName: "chevron.down")
.foregroundColor(.gray)
}
.frame(maxWidth: .infinity)
.padding(6)
}
Menu {
ForEach(pippo, id: \.self) { item in
Button(item) {
// azione
}
.foregroundColor(.red)
}
} label: {
HStack {
Text("Select Theme")
Image(systemName: "chevron.down")
.foregroundColor(.gray)
}
.frame(maxWidth: .infinity)
.padding(6)
}
.menuStyle(BorderlessButtonMenuStyle()) // Prova diversi stili
.menuIndicator(.hidden) // Nasconde l'indicatore di default
}
Topic:
UI Frameworks
SubTopic:
SwiftUI
NSVisualEffectView in AppKit has two main properties: material and blendingMode.
Material is well supported in SwiftUI, but I can't seem to find an equivalent for blendingMode.
What is the SwiftUI equivalent to NSVisualEffect.BlendingMode?
I’m running into an issue using .contactAccessPicker on a device running iOS 26.1 - a blank sheet appears instead of the expected Contact Access Picker.
It works on:
Simulator (iOS 26.0)
Device + Simulator (iOS 18.6)
The issue appears specific to real devices on iOS 26.0+.
Environment:
Device: iPhone 16 Pro
iOS Versions Tested: 26.0 and 26.1
Xcode 26.0.1
SwiftUI app, deployment target: iOS 17+
@available(iOS 18.0, *)
private var contactPicker: some View {
contactsSettingsButton("Title") {
showContactPicker = true
}
.contactAccessPicker(isPresented: $showContactPicker) { _ in
Task {
await contactManager.fetchContacts()
showSelectContacts = true
}
}
}
Filed a Feedback: FB20929400
Is there a known workaround?
I have an iPad app that supports multiple scenes.
I discovered some issues with my app's user interface that I would like to tweak based on whether the user has setup multitasking (in iPadOS 26) as "Full Screen Apps" or "Windowed Apps".
Is there any API or way to determine the current iPadOS 26 multitasking setting?
I've looked at UIDevice.current.isMultitaskingSupported and UIApplication.shared.supportsMultipleScenes. Both always return true no matter the user's chosen multitasking choice.
I also looked at UIWindowScene isFullScreen which was always false. I tried to look at UIWindowScene windowingBehaviors but that was always nil.
Hi everyone,
I’m working on a macOS SwiftUI app that integrates a global keyboard shortcut using the old Carbon API (RegisterEventHotKey, InstallEventHandler, etc.). Everything works fine initially, but I’m running into a consistent EXC_BAD_ACCESS (code=EXC_I386_GPFLT) crash when the app is reopened, or sometimes even while drawing on my SwiftUI canvas view.
Setup
Here’s the relevant setup (simplified):
private let hotKeySignature: FourCharCode = 0x626c6e6b // 'blnk'
private weak var hotKeyDelegate: AppDelegate?
private func overlayHotKeyHandler(
_ callRef: EventHandlerCallRef?,
_ event: EventRef?,
_ userData: UnsafeMutableRawPointer?
) -> OSStatus {
guard let appDelegate = hotKeyDelegate else { return noErr }
return appDelegate.handleHotKey(event: event)
}
final class AppDelegate: NSObject, NSApplicationDelegate {
private var hotKeyRef: EventHotKeyRef?
private var eventHandlerRef: EventHandlerRef?
override init() {
super.init()
hotKeyDelegate = self
}
func applicationDidFinishLaunching(_ notification: Notification) {
registerGlobalHotKey()
}
func applicationWillTerminate(_ notification: Notification) {
unregisterGlobalHotKey()
}
private func registerGlobalHotKey() {
var eventType = EventTypeSpec(eventClass: OSType(kEventClassKeyboard),
eventKind: UInt32(kEventHotKeyPressed))
InstallEventHandler(GetEventDispatcherTarget(),
overlayHotKeyHandler,
1,
&eventType,
nil,
&eventHandlerRef)
var hotKeyID = EventHotKeyID(signature: hotKeySignature, id: 1)
RegisterEventHotKey(UInt32(kVK_Space),
UInt32(cmdKey | shiftKey),
hotKeyID,
GetEventDispatcherTarget(),
0,
&hotKeyRef)
}
private func unregisterGlobalHotKey() {
if let ref = hotKeyRef {
UnregisterEventHotKey(ref)
}
if let handler = eventHandlerRef {
RemoveEventHandler(handler)
}
}
}
What happens
The first run works fine.
If I close the app window and reopen (SwiftUI recreates the AppDelegate), the next time the hot key triggers, I get:
Thread 8: EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
0x7ff824028ff0 <+240>: callq *%rax
The debugger stops at a ud2 instruction, which likely means the handler is calling through a freed pointer (hotKeyDelegate).
What I suspect
Because RegisterEventHotKey and InstallEventHandler store global callbacks, they may outlive my SwiftUI AppDelegate instance. When SwiftUI reinitializes the app scene or recreates AppDelegate, the old handler still points to a now-freed object.
What I’ve tried
Unregistering the hot key and removing the handler in applicationWillTerminate().
Setting the global delegate to nil in deinit.
Replacing the weak global with a static weak reference cleared on deallocation.
Confirmed no Combine or SwiftUI memory leaks; the crash still occurs exactly when the Carbon handler fires after reopening.
Question
How should a modern SwiftUI macOS app safely integrate a global keyboard shortcut using the Carbon Hot Key APIs?
Is there a recommended way to manage the callback lifetime so it doesn’t call into a deallocated AppDelegate or scene?
Any advice on a modern alternative (e.g., using NSEvent.addGlobalMonitorForEvents or another framework) that can replace this global hotkey mechanism would also be appreciated.
Environment
macOS 15.0 Sonoma
Xcode 16.0
Swift 5.10
AppKit + SwiftUI hybrid app
Topic:
UI Frameworks
SubTopic:
SwiftUI
Apparently now with iOS 26.1 if you have .tabViewBottomAccessory { } you get a pill shape floater all the time. That was not like that in 26.0.
I just updated to Xcode 26.1 and am using simulators for iOS 26.1. Previously I could have a hidden tabview accessory where nothing would be displayed but now whenever no view is placed in the closure or even an EmptyView, it is visible all the time. Does anyone else have this issue?
Topic:
UI Frameworks
SubTopic:
SwiftUI
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: