Kalenderberechtigung: Anfrage aus dem Vordergrundfenster

TCC zeigt den Dialog nur, wenn eine App im Vordergrund ist, an die er gehören
kann. Bei einer App ohne Fenster verpufft die Anfrage kommentarlos: sie kommt
mit false zurück, der Status bleibt notDetermined, und in den
Systemeinstellungen taucht die App nie auf — dort lässt sich auch nichts von
Hand hinzufügen, Einträge entstehen ausschließlich durch eine echte Anfrage.

Deshalb fragt der Reiter Berechtigungen jetzt selbst, sobald er sichtbar ist,
mit 400 ms Vorlauf: Aktivierung wirkt asynchron, ohne die Pause fragt Onyx
während es formal noch im Hintergrund liegt.

CalendarAuthorization trennt undetermined von denied. Im ersten Fall lohnt eine
Anfrage, im zweiten zeigt macOS nie wieder einen Dialog und es hilft nur der
Weg über die Systemeinstellungen — dorthin führt der Knopf dann auch.

Korrektur einer eigenen Fehldiagnose: promptPolicy = 0 im tccd-Protokoll gehört
zu kTCCServiceReminders, nicht zum Kalender. Der bekommt durchgehend
promptPolicy = 4, Nachfragen war also die ganze Zeit erlaubt. Die tatsächlichen
Ursachen waren die ausgelagerte Debug-dylib und ein verklemmter TCC-Eintrag.
This commit is contained in:
Guido Schmit
2026-08-10 18:26:10 +02:00
parent 40bd83e49e
commit 873c763d50
5 changed files with 336 additions and 294 deletions

View File

@@ -80,12 +80,25 @@ public enum CalendarSourceState: Equatable, Sendable {
case unreadable(reason: String)
}
/// Der Berechtigungszustand einer Quelle.
///
/// `undetermined` und `denied` müssen getrennt bleiben: im ersten Fall lohnt
/// eine Anfrage, im zweiten zeigt macOS nie wieder einen Dialog und es hilft
/// nur der Weg über die Systemeinstellungen.
public enum CalendarAuthorization: Equatable, Sendable {
case granted
case denied
case undetermined
/// Die Quelle braucht gar keine Berechtigung (z. B. Calendarr).
case notRequired
}
/// Eine Quelle für Termine. Apple-Kalender und Calendarr liefern beide dies.
public protocol CalendarSource: Sendable {
var id: String { get }
/// Ob bereits gelesen werden darf **ohne** dabei nachzufragen.
/// Die Einstellungen brauchen das, um den Zustand anzuzeigen, ohne
/// nebenbei einen Systemdialog auszulösen.
/// Der Zustand **ohne** dabei nachzufragen. Die Einstellungen brauchen
/// das, um ihn anzuzeigen, ohne nebenbei einen Systemdialog auszulösen.
var authorization: CalendarAuthorization { get }
var isAuthorized: Bool { get }
func requestAccess() async -> Bool
func calendars() async -> [OnyxCalendar]

View File

@@ -25,6 +25,7 @@ public final class CalendarModel {
/// Ob gelesen werden darf. Fragt nicht nach nur zum Anzeigen.
public var hasAccess: Bool { source.isAuthorized }
public var authorization: CalendarAuthorization { source.authorization }
/// Fragt die Berechtigung an und lädt bei Erfolg gleich.
///

View File

@@ -16,10 +16,18 @@ public final class EventKitSource: CalendarSource, @unchecked Sendable {
public init() {}
public var isAuthorized: Bool {
EKEventStore.authorizationStatus(for: .event) == .fullAccess
public var authorization: CalendarAuthorization {
switch EKEventStore.authorizationStatus(for: .event) {
case .fullAccess: .granted
case .denied, .restricted: .denied
// `writeOnly` ist für uns so gut wie nichts Onyx zeigt nur an.
case .notDetermined, .writeOnly: .undetermined
@unknown default: .denied
}
}
public var isAuthorized: Bool { authorization == .granted }
public func requestAccess() async -> Bool {
switch EKEventStore.authorizationStatus(for: .event) {
case .fullAccess: