Berechtigungsdialog und Einstellungsfenster reparieren

Zwei Fehler, beide durch Messen gefunden statt durch Raten — und einer davon
war meine eigene fehlerhafte Diagnose: `log show` wurde von zsh abgefangen, ich
hatte stderr umgeleitet und die Fehlermeldung nie gesehen. Meine Aussage "keine
TCC-Einträge, also wurde nie gefragt" stützte sich damit auf einen Befehl, der
gar nicht lief.

Einstellungsfenster: SwiftUIs Settings-Szene wird über den privaten Selektor
showSettingsWindow: geöffnet, dessen Name sich zwischen macOS-Versionen schon
geändert hat und der bei .accessory-Apps ohne Menüleiste unzuverlässig ankommt.
Der Menüeintrag tat schlicht nichts. Jetzt ein eigenes NSWindow.

Kalenderberechtigung, zwei Ursachen übereinander:

Xcode lagert im Debug-Build den Programmcode in eine eigene dylib aus (für
SwiftUI-Vorschauen). Das Hauptprogramm ist dann nur ein Rumpf, und TCC ordnete
die Anfrage dem Rumpf zu — kTCCServiceCalendar erreichte tccd überhaupt nicht.
ENABLE_DEBUG_DYLIB = NO behebt das; danach erscheint die Anfrage im Protokoll.

Danach immer noch kein Dialog: promptType 1, aber promptPolicy 0. TCC bereitet
den Dialog vor und entscheidet sich dagegen, weil keine App im Vordergrund ist,
an die er gehören könnte. NSApp.activate davor hilft nicht — Aktivierung wirkt
asynchron und ist beim Aufruf noch nicht durch. Deshalb wird beim ersten Start
das Einstellungsfenster auf dem Reiter Berechtigungen geöffnet: dort steht ein
echtes Fenster im Vordergrund, und der Nutzer weiß, warum gefragt wird.

Folgefehler dabei: ohne Berechtigung zeigte das Widget "Nichts mehr für heute"
— eine Aussage, die es ohne Zugriff gar nicht treffen kann. refresh() fragt
jetzt nicht mehr selbst nach und meldet ohne Berechtigung ehrlich `denied`.

Fehler aus requestFullAccessToEvents werden nicht mehr mit try? verschluckt.
This commit is contained in:
Guido Schmit
2026-08-10 18:20:21 +02:00
parent 70d4d67cca
commit 40bd83e49e
11 changed files with 774 additions and 141 deletions

View File

@@ -1,7 +1,11 @@
import SwiftUI
import OSLog
import EventKit
import OnyxDesign
import OnyxWidgetKit
private let log = Logger(subsystem: "com.scarriffleservices.onyx", category: "Calendar")
/// Hält die Termine für das Widget und lädt sie nach.
@MainActor
@Observable
@@ -19,6 +23,24 @@ public final class CalendarModel {
self.calendar = calendar
}
/// Ob gelesen werden darf. Fragt nicht nach nur zum Anzeigen.
public var hasAccess: Bool { source.isAuthorized }
/// Fragt die Berechtigung an und lädt bei Erfolg gleich.
///
/// Wird beim Start gerufen, nicht erst wenn das Widget sichtbar wird. Das
/// Panel ist beim Start zu, also erscheint eine View-gebundene Anfrage erst,
/// wenn jemand die Notch berührt und bis dahin steht in den
/// Systemeinstellungen kein Onyx, das man freischalten könnte.
@discardableResult
public func requestAccessNow() async -> Bool {
log.notice("Kalenderzugriff: Status vor der Anfrage = \(EKEventStore.authorizationStatus(for: .event).rawValue, privacy: .public)")
let granted = await source.requestAccess()
log.notice("Kalenderzugriff: erteilt = \(granted, privacy: .public), Status danach = \(EKEventStore.authorizationStatus(for: .event).rawValue, privacy: .public)")
if granted { refresh() } else { state = .denied }
return granted
}
public var window: EventWindow? {
if case .ready(let window) = state { return window }
return nil
@@ -33,7 +55,12 @@ public final class CalendarModel {
isLoading = true
defer { isLoading = false }
guard await source.requestAccess() else {
// Hier wird NICHT nachgefragt. Ein Berechtigungsdialog aus einer
// App ohne Fenster heraus wird von TCC verworfen, ohne dass etwas
// passiert die Anfrage muss aus dem Einstellungsfenster kommen.
// Ohne Berechtigung ist der ehrliche Zustand `denied` und nicht
// eine leere Terminliste, die "nichts mehr heute" behaupten würde.
guard source.isAuthorized else {
state = .denied
return
}