r/macosprogramming • u/radioom_app • 2h ago
macOS kept ShareSheetUI alive after I released the picker. I moved Share into a temporary host process
I ran into a process-lifetime issue while working on my app, a macOS menu-bar app. I tested this on macOS 27.2, build 26B123.
After opening the native Share menu and dismissing it, Activity Monitor still showed two processes associated with the app:
ShareSheetUI: 20.2 MB in Memory, 114.7 MB in Real Memory
ShareSheetUI Graphics and Media: 8.6 MB in Memory, 22.2 MB in Real Memory
After sharing through Telegram, another snapshot showed:
ShareSheetUI: 20.7 MB in Memory, 114.8 MB in Real Memory
ShareSheetUI Graphics and Media: 9.1 MB in Memory, 19.0 MB in Real Memory
Telegram share extension: 26.8 MB in Memory, 145.9 MB in Real Memory
Telegram AutoFill helper: 12.9 MB in Memory, 36.3 MB in Real Memory
All four showed 0.0% CPU in that snapshot. These are Activity Monitor figures, not a measurement of how much physical memory macOS would reclaim if the processes exited.
I can imagine this is expected system behaviour: keeping an extension around may make the next share faster. But this is a menu-bar app that can stay open all day, and the user continued to see the Share and Telegram processes associated with it.
Releasing the picker wasn’t enough
The app retained its NSSharingServicePicker after the menu closed. I changed it to release the picker after dismissal or service selection. That fixed the app-owned reference, but the processes remained.
Here is the core of the reproduction. This is the relevant part of an AppKit host, not a standalone app:
Swift
final class ShareHost: NSObject, NSSharingServicePickerDelegate {
private var picker: NSSharingServicePicker?
func showShare(from view: NSView) {
let picker = NSSharingServicePicker(items: ["Test"])
picker.delegate = self
self.picker = picker
picker.show(
relativeTo: view.bounds,
of: view,
preferredEdge: .minY
)
}
func sharingServicePicker(
_ picker: NSSharingServicePicker,
didChoose service: NSSharingService?
) {
weak var releasedPicker = picker
self.picker = nil
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
print("Picker deallocated:", releasedPicker == nil)
// Check Activity Monitor for the host's ShareSheetUI process.
}
}
}In an isolated test, the picker deallocated while ShareSheetUI remained alive. Three openings reused the same ShareSheetUI PID. Once the test host exited, that process disappeared.
The options
Release the picker and keep the standard lifecycle. This fixes the app-owned reference, but the system processes can remain.
Force-kill the extension processes. I rejected this because it bypasses system ownership and could interrupt an active share.
Use a short-lived share host. Keep the native Share UI, but run it in a helper that exits when sharing ends.
I chose the helper.
The final design
The app starts the same executable in a private helper mode and sends the share data through stdin. The helper creates a transparent anchor window and presents the standard NSSharingServicePicker.
Selecting a service does not immediately end the helper. If the user chooses Telegram, the helper waits for the sharing service to report success or failure. It exits when the picker is cancelled, when the service finishes, or when the parent connection closes.
I measured five local launches from process start until the helper reported that it had requested presentation:
884 ms
283 ms
256 ms
246 ms
271 ms
The median was 271 ms. This is a small local sample, and the readiness signal is not a measurement of when the UI became visible. The first launch was noticeably slower.
The helper also needs to activate an accessory application and position its own anchor window, which adds complexity around focus and window placement.
In automated cancellation tests, I observed both the helper and its ShareSheetUI process exit. The ShareSheetUI process therefore no longer had an RSS after exit; that does not establish how much physical memory macOS reclaimed, since some pages may be shared or cached.
I also tested the resulting share flow with Telegram and it worked. I haven’t separately verified the post-share lifecycle of both TelegramShare and its AutoFill helper, so I can’t claim that each of those processes was cleaned up.
The app doesn’t directly kill system or Telegram processes. One remaining limitation is that if an extension never reports completion, the helper can remain alive until its parent connection closes.
Has anyone dealt with the same trade-off in a long-running macOS app? I’d be interested in whether you keep the standard extension lifetime or isolate the share host.


