diff --git a/KisaniCal.xcodeproj/project.pbxproj b/KisaniCal.xcodeproj/project.pbxproj index c04e6a0..fcdfe0f 100644 --- a/KisaniCal.xcodeproj/project.pbxproj +++ b/KisaniCal.xcodeproj/project.pbxproj @@ -79,7 +79,7 @@ 0506183945D16EC443A69651 /* LiveActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityManager.swift; sourceTree = ""; }; 0513EB8C7F6B6853B9E93E09 /* TaskLiveActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskLiveActivity.swift; sourceTree = ""; }; 0C96EC00F6B021DBA3CC1A79 /* ProfileSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSetupView.swift; sourceTree = ""; }; - 0D6D6740721F01A74E404EB9 /* KisaniCalWidgets.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = KisaniCalWidgets.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 0D6D6740721F01A74E404EB9 /* KisaniCalWidgets.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = KisaniCalWidgets.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 106EEF572C6F8990408329F0 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; }; 1AA7498EFED7692022F3E7E1 /* KisaniCal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = KisaniCal.entitlements; sourceTree = ""; }; 208F82348DEBD9FF7B0DCF17 /* MyTasksWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyTasksWidget.swift; sourceTree = ""; }; @@ -103,7 +103,7 @@ 9100804DB1E61EA882CC54DA /* FloatingTabState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingTabState.swift; sourceTree = ""; }; 92824ED40ECD41EFD4F78BEC /* ISSUES.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = ISSUES.md; sourceTree = ""; }; 93D045FE3DEB1D22D908A29F /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; - 9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = KisaniCal.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KisaniCal.app; sourceTree = BUILT_PRODUCTS_DIR; }; ADF6CCD95A587E26E30F5712 /* CalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarView.swift; sourceTree = ""; }; AF905C574F34B4EE51A8D21E /* AppGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroup.swift; sourceTree = ""; }; BC5E179A9189B0A8C3F856F6 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -286,7 +286,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1500; + LastUpgradeCheck = 2620; TargetAttributes = { 28B43516AD88946E21D9BFE0 = { DevelopmentTeam = K8BLMMR883; @@ -465,6 +465,7 @@ DEVELOPMENT_TEAM = K8BLMMR883; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -479,6 +480,7 @@ MTL_FAST_MATH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; @@ -494,7 +496,11 @@ GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = KisaniCalWidgets/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 18.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal.KisaniCalWidgets; SDKROOT = iphoneos; SWIFT_VERSION = 5.9; @@ -541,6 +547,7 @@ DEVELOPMENT_TEAM = K8BLMMR883; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -562,6 +569,7 @@ ONLY_ACTIVE_ARCH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -577,7 +585,11 @@ GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = KisaniCalWidgets/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 18.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal.KisaniCalWidgets; SDKROOT = iphoneos; SWIFT_VERSION = 5.9; diff --git a/KisaniCal.xcodeproj/project.xcworkspace/xcuserdata/kutesir.xcuserdatad/UserInterfaceState.xcuserstate b/KisaniCal.xcodeproj/project.xcworkspace/xcuserdata/kutesir.xcuserdatad/UserInterfaceState.xcuserstate index 4edff7d..869dee0 100644 Binary files a/KisaniCal.xcodeproj/project.xcworkspace/xcuserdata/kutesir.xcuserdatad/UserInterfaceState.xcuserstate and b/KisaniCal.xcodeproj/project.xcworkspace/xcuserdata/kutesir.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/KisaniCal.xcodeproj/xcshareddata/xcschemes/KisaniCal.xcscheme b/KisaniCal.xcodeproj/xcshareddata/xcschemes/KisaniCal.xcscheme index 9db359c..675309e 100644 --- a/KisaniCal.xcodeproj/xcshareddata/xcschemes/KisaniCal.xcscheme +++ b/KisaniCal.xcodeproj/xcshareddata/xcschemes/KisaniCal.xcscheme @@ -1,11 +1,10 @@ + LastUpgradeVersion = "2620" + version = "1.3"> + buildImplicitDependencies = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES"> - - - - - - diff --git a/KisaniCal/Views/CalendarView.swift b/KisaniCal/Views/CalendarView.swift index 651e4b6..bb9f5d4 100644 --- a/KisaniCal/Views/CalendarView.swift +++ b/KisaniCal/Views/CalendarView.swift @@ -5,7 +5,12 @@ import EventKitUI // MARK: - Calendar Store @MainActor final class CalendarStore: ObservableObject { + /// Single shared source of truth so every screen (Today, Calendar, Onboarding, + /// Settings) sees the same permission state and event cache. + static let shared = CalendarStore() + @Published var authStatus: EKAuthorizationStatus = .notDetermined + @Published private(set) var isRequesting = false @Published var monthEvents: [EKEvent] = [] private let ekStore = EKEventStore() @@ -63,20 +68,32 @@ final class CalendarStore: ObservableObject { // MARK: - Permission func requestAccess() { + // Debounce rapid taps; only the system prompt can move us off .notDetermined. + guard !isRequesting else { return } + guard authStatus == .notDetermined else { + // Already decided (granted/denied/write-only) — re-sync from the system. + refreshStatus() + return + } + isRequesting = true + + // Trust the system's authoritative status, not just the `granted` bool. + let finish: () -> Void = { [weak self] in + Task { @MainActor [weak self] in + guard let self else { return } + self.authStatus = EKEventStore.authorizationStatus(for: .event) + self.isRequesting = false + if self.isAuthorized { + self.fetchMonth(containing: self.fetchedMonth ?? Date()) + self.loadLocalCalendars() + } + } + } + if #available(iOS 17.0, *) { - ekStore.requestFullAccessToEvents { [weak self] granted, _ in - Task { @MainActor [weak self] in - self?.authStatus = granted ? .fullAccess : .denied - if granted { self?.fetchMonth(containing: Date()); self?.loadLocalCalendars() } - } - } + ekStore.requestFullAccessToEvents { _, _ in finish() } } else { - ekStore.requestAccess(to: .event) { [weak self] granted, _ in - Task { @MainActor [weak self] in - self?.authStatus = granted ? .authorized : .denied - if granted { self?.fetchMonth(containing: Date()); self?.loadLocalCalendars() } - } - } + ekStore.requestAccess(to: .event) { _, _ in finish() } } } @@ -158,6 +175,8 @@ final class CalendarStore: ObservableObject { func setLocalCalendarsEnabled(_ enabled: Bool) { localCalendarsEnabled = enabled saveCalPrefs() + // Turning it on with no access yet should prompt, not silently no-op. + if enabled && !isAuthorized { requestAccess() } if let m = fetchedMonth { fetchMonth(containing: m) } } @@ -231,7 +250,7 @@ struct CalendarView: View { @Environment(\.scenePhase) private var scenePhase @EnvironmentObject var taskVM: TaskViewModel @EnvironmentObject var workoutVM: WorkoutViewModel - @StateObject private var calStore = CalendarStore() + @StateObject private var calStore = CalendarStore.shared @State private var selectedDate: Date = Date() @State private var displayMonth: Date = { let c = Calendar.current diff --git a/KisaniCal/Views/OnboardingView.swift b/KisaniCal/Views/OnboardingView.swift index 418fba8..f277ef0 100644 --- a/KisaniCal/Views/OnboardingView.swift +++ b/KisaniCal/Views/OnboardingView.swift @@ -26,12 +26,10 @@ struct OnboardingView: View { @State private var weightStr = "" @State private var heightStr = "" @State private var selectedDays: Set = [] - @State private var calAuthStatus: EKAuthorizationStatus = EKEventStore.authorizationStatus(for: .event) + @ObservedObject private var calStore = CalendarStore.shared @ObservedObject private var hk = HealthKitManager.shared @FocusState private var focusedField: Field? - private let ekStore = EKEventStore() - private let timeSlots: [(Int, String, String)] = [ (0, "Morning", "sunrise.fill"), (1, "Midday", "sun.max.fill"), @@ -41,10 +39,7 @@ struct OnboardingView: View { fileprivate enum Field { case weight, height } private let weekdays: [(Int, String)] = [(2,"M"),(3,"T"),(4,"W"),(5,"T"),(6,"F"),(7,"S"),(1,"S")] - private var calIsGranted: Bool { - if #available(iOS 17.0, *) { return calAuthStatus == .fullAccess } - return calAuthStatus == .authorized - } + private var calIsGranted: Bool { calStore.isAuthorized } private var liveScheme: ColorScheme? { switch localAppearance { @@ -118,7 +113,7 @@ struct OnboardingView: View { .font(.system(size: 22)) .foregroundColor(AppColors.green) .transition(.scale.combined(with: .opacity)) - } else if calAuthStatus == .denied { + } else if calStore.isDenied { Text("Denied") .font(AppFonts.mono(10, weight: .bold)) .foregroundColor(AppColors.accent) @@ -137,7 +132,7 @@ struct OnboardingView: View { .buttonStyle(PressButtonStyle(scale: 0.92)) } } - .animation(KisaniSpring.micro, value: calAuthStatus.rawValue) + .animation(KisaniSpring.micro, value: calStore.authStatus.rawValue) } .padding(.horizontal, 14).padding(.vertical, 14) .cardStyle() @@ -551,29 +546,17 @@ struct OnboardingView: View { } } .onAppear { - calAuthStatus = EKEventStore.authorizationStatus(for: .event) + calStore.refreshStatus() } } // MARK: - Actions private func requestCalendar() { - if #available(iOS 17.0, *) { - ekStore.requestFullAccessToEvents { granted, _ in - Task { @MainActor in - withAnimation(KisaniSpring.snappy) { - self.calAuthStatus = granted ? .fullAccess : .denied - } - } - } + if calStore.isDenied, let url = URL(string: UIApplication.openSettingsURLString) { + UIApplication.shared.open(url) } else { - ekStore.requestAccess(to: .event) { granted, _ in - Task { @MainActor in - withAnimation(KisaniSpring.snappy) { - self.calAuthStatus = granted ? .authorized : .denied - } - } - } + calStore.requestAccess() } } diff --git a/KisaniCal/Views/TodayView.swift b/KisaniCal/Views/TodayView.swift index fbafd5e..1ecdd26 100644 --- a/KisaniCal/Views/TodayView.swift +++ b/KisaniCal/Views/TodayView.swift @@ -6,7 +6,7 @@ struct TodayView: View { @Environment(\.scenePhase) private var scenePhase @EnvironmentObject var taskVM: TaskViewModel @EnvironmentObject var workoutVM: WorkoutViewModel - @StateObject private var calStore = CalendarStore() + @StateObject private var calStore = CalendarStore.shared @State private var showAddTask = false @State private var showWorkoutSession = false @State private var editingTask: TaskItem? = nil