Files
onyx/Onyx/OnboardingWindowController.swift
Scarriffle 667fb61c96 Absturz beim XPC-Rückruf, Menü zurück, Fenster weg von der Notch
Der Absturzbericht war eindeutig:

  Thread 8: com.apple.NSXPCConnection.m-user…onyx.helper
    swift_task_isCurrentExecutorWithFlags
    closure #3 in FanControl.proxy()
    EXC_BREAKPOINT

Die Rückrufe der XPC-Verbindung erben die MainActor-Isolation der Methode,
in der sie stehen. XPC ruft sie aber auf seiner eigenen Warteschlange auf,
Swift 6 prüft das zur Laufzeit und beendet den Prozess. Das
`Task { @MainActor in … }` im Rumpf half nicht: die Prüfung geschieht beim
Betreten des Abschlusses, nicht beim Zugriff.

Alle sieben Rückrufe sind jetzt `@Sendable`. Und weil das eine Fehlerklasse
ist und kein Einzelfall, dieselbe Behandlung für die übrigen Stellen, an
denen ein MainActor-Typ einen Abschluss an eine Systemschnittstelle gibt:
Papierkorb, Vorschaubilder, Adapter-Ende, Darwin-Nachricht.

Dazu drei Dinge aus dem Bericht von eben:

Der Linksklick aufs Menüleistensymbol fuhr das Panel aus — und ging dabei
als Fixieren durch. Danach stand das Panel offen und reagierte auf nichts
mehr. Das war schlechter als das Problem, das es lösen sollte. Ein
Statuselement zeigt bei einem Klick sein Menü; alles andere überrascht.
„Panel öffnen" bleibt draußen.

Ein Klick daneben schließt jetzt auch ein fixiertes Panel. „Klick fixiert"
ist eine gute Regel, aber wer sie nicht kennt, sitzt sonst vor etwas, das
offen steht und nicht reagiert — und sucht den Fehler in der App.

Einstellungs- und Einrichtungsfenster gehen nicht mehr direkt unter der
Notch auf. `center()` setzt oberhalb der Mitte; der Schließknopf landete
damit so weit oben, dass man auf dem Weg dorthin die Notch auslöste.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 12:54:32 +02:00

95 lines
3.6 KiB
Swift

import AppKit
import SwiftUI
import CalendarProvider
import WeatherProvider
import AudioProvider
/// Zeigt das Onboarding einmal, beim ersten Start.
///
/// Als eigenes Fenster und nicht im Panel: TCC blendet Berechtigungsdialoge
/// stillschweigend aus, wenn keine App im Vordergrund steht, und eine
/// `.accessory`-App mit ausgefahrenem Panel steht nicht im Vordergrund. Genau
/// daran ist die Kalenderberechtigung schon einmal gescheitert.
@MainActor
final class OnboardingWindowController: NSObject, NSWindowDelegate {
private static let doneKey = "onboarding.completed"
private var window: NSWindow?
private let defaults: UserDefaults
init(defaults: UserDefaults = .standard) {
self.defaults = defaults
}
var isDone: Bool { defaults.bool(forKey: Self.doneKey) }
func showIfNeeded(calendarModel: CalendarModel, weatherModel: WeatherModel,
mixer: AudioMixer, fanControl: FanControl,
launchAtLogin: LaunchAtLogin) {
guard !isDone else { return }
show(calendarModel: calendarModel, weatherModel: weatherModel, mixer: mixer,
fanControl: fanControl, launchAtLogin: launchAtLogin)
}
func show(calendarModel: CalendarModel, weatherModel: WeatherModel,
mixer: AudioMixer, fanControl: FanControl, launchAtLogin: LaunchAtLogin) {
// Solange dieses Fenster steht, gehört Onyx in den Dock und in den
// Vordergrund sonst erscheinen die Systemdialoge gar nicht.
NSApp.setActivationPolicy(.regular)
if window == nil {
let hosting = NSHostingController(
rootView: OnboardingView(
calendarModel: calendarModel, weatherModel: weatherModel,
mixer: mixer, fanControl: fanControl, launchAtLogin: launchAtLogin,
onFinish: { [weak self] in self?.finish() }))
let window = NSWindow(contentViewController: hosting)
window.title = "Onyx"
window.styleMask = [.titled, .closable]
window.isReleasedWhenClosed = false
window.delegate = self
// Nicht direkt unter die Notch: wer den Schließknopf trifft,
// fährt sonst durch die Auslösefläche.
if let screen = NSScreen.main {
let visible = screen.visibleFrame
window.setFrameOrigin(NSPoint(x: visible.midX - window.frame.width / 2,
y: visible.maxY - 60 - window.frame.height))
} else {
window.center()
}
self.window = window
}
NSApp.activate(ignoringOtherApps: true)
window?.makeKeyAndOrderFront(nil)
}
/// Auch das Schließen über den roten Knopf gilt als erledigt. Ein Fenster,
/// das beim nächsten Start wiederkommt, weil man es weggeklickt hat, ist
/// eine Zumutung.
private func finish() {
defaults.set(true, forKey: Self.doneKey)
hide()
}
/// Wie bei den Einstellungen: ausblenden statt schließen, damit ein
/// Hilfsprogramm mit letztes Fenster zu = App beenden" Onyx nicht mit
/// wegräumt.
func windowShouldClose(_ sender: NSWindow) -> Bool {
defaults.set(true, forKey: Self.doneKey)
hide()
return false
}
private func hide() {
window?.orderOut(nil)
// Zurück in den Hintergrund es sei denn, ein anderes Fenster steht
// noch offen.
if !NSApp.windows.contains(where: { $0 !== window && $0.isVisible }) {
NSApp.setActivationPolicy(.accessory)
}
}
}