From 330479e647e6c40294b544d0c81e4eff37d7eb82 Mon Sep 17 00:00:00 2001 From: NA <80475242+npna@users.noreply.github.com> Date: Wed, 15 Mar 2023 16:27:33 +0000 Subject: [PATCH] Code refactoring --- KnotClock.xcodeproj/project.pbxproj | 30 +-- KnotClock/Classes/Alerts.swift | 20 ++ .../BackupRestore.swift => Backup.swift} | 26 ++- .../Classes/{Countdowns => }/Countdowns.swift | 175 ++---------------- KnotClock/Classes/DateHelper.swift | 53 ++++++ KnotClock/Classes/Notifications.swift | 109 +++++++++++ KnotClock/Constants.swift | 1 + KnotClock/Structs/Countdown.swift | 3 +- KnotClock/Views/MainView.swift | 6 +- KnotClock/Views/OverrideDay.swift | 24 +-- .../Settings/ApplicationSettingsView.swift | 6 +- .../Settings/IndicationsSettingsView.swift | 2 +- ...ewDailyCountdownsTableCellWithAction.swift | 3 +- .../OverviewSingleCountdownsWithAction.swift | 3 +- 14 files changed, 249 insertions(+), 212 deletions(-) create mode 100644 KnotClock/Classes/Alerts.swift rename KnotClock/Classes/{Countdowns/BackupRestore.swift => Backup.swift} (76%) rename KnotClock/Classes/{Countdowns => }/Countdowns.swift (64%) create mode 100644 KnotClock/Classes/DateHelper.swift create mode 100644 KnotClock/Classes/Notifications.swift diff --git a/KnotClock.xcodeproj/project.pbxproj b/KnotClock.xcodeproj/project.pbxproj index 60c5e1a..fe413d3 100644 --- a/KnotClock.xcodeproj/project.pbxproj +++ b/KnotClock.xcodeproj/project.pbxproj @@ -7,7 +7,10 @@ objects = { /* Begin PBXBuildFile section */ - 0D13C08229C13F07006AC521 /* BackupRestore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D13C08129C13F07006AC521 /* BackupRestore.swift */; }; + 0D13C08229C13F07006AC521 /* Backup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D13C08129C13F07006AC521 /* Backup.swift */; }; + 0D18A3A929C21D9F00D075D4 /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D18A3A829C21D9F00D075D4 /* Notifications.swift */; }; + 0D18A3AB29C21F1F00D075D4 /* Alerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D18A3AA29C21F1F00D075D4 /* Alerts.swift */; }; + 0D18A3AD29C2231000D075D4 /* DateHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D18A3AC29C2231000D075D4 /* DateHelper.swift */; }; 0D803C0229C0B62100C7F4BE /* ContextualMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D803C0129C0B62100C7F4BE /* ContextualMenu.swift */; }; 0D803C0429C0B7BA00C7F4BE /* HiddenCountdowns.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D803C0329C0B7BA00C7F4BE /* HiddenCountdowns.swift */; }; 0DA02AE529BD1DC200CEBB8F /* ConditionalModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DA02AE429BD1DC200CEBB8F /* ConditionalModifier.swift */; }; @@ -46,7 +49,10 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 0D13C08129C13F07006AC521 /* BackupRestore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupRestore.swift; sourceTree = ""; }; + 0D13C08129C13F07006AC521 /* Backup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backup.swift; sourceTree = ""; }; + 0D18A3A829C21D9F00D075D4 /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = ""; }; + 0D18A3AA29C21F1F00D075D4 /* Alerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alerts.swift; sourceTree = ""; }; + 0D18A3AC29C2231000D075D4 /* DateHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateHelper.swift; sourceTree = ""; }; 0D803C0129C0B62100C7F4BE /* ContextualMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextualMenu.swift; sourceTree = ""; }; 0D803C0329C0B7BA00C7F4BE /* HiddenCountdowns.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HiddenCountdowns.swift; sourceTree = ""; }; 0DA02AE429BD1DC200CEBB8F /* ConditionalModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalModifier.swift; sourceTree = ""; }; @@ -97,15 +103,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 0D13C08029C13EF6006AC521 /* Countdowns */ = { - isa = PBXGroup; - children = ( - 0DCA4C3E29BBABD400F78368 /* Countdowns.swift */, - 0D13C08129C13F07006AC521 /* BackupRestore.swift */, - ); - path = Countdowns; - sourceTree = ""; - }; 0DCA4BFB29BBAB3400F78368 = { isa = PBXGroup; children = ( @@ -237,10 +234,14 @@ 0DCA4C3A29BBABD400F78368 /* Classes */ = { isa = PBXGroup; children = ( - 0D13C08029C13EF6006AC521 /* Countdowns */, + 0DCA4C3E29BBABD400F78368 /* Countdowns.swift */, 0DCA4C3B29BBABD400F78368 /* DataController.swift */, 0DCA4C3C29BBABD400F78368 /* MacMenubar.swift */, 0DCA4C3D29BBABD400F78368 /* AppDelegate.swift */, + 0D18A3A829C21D9F00D075D4 /* Notifications.swift */, + 0D18A3AC29C2231000D075D4 /* DateHelper.swift */, + 0D18A3AA29C21F1F00D075D4 /* Alerts.swift */, + 0D13C08129C13F07006AC521 /* Backup.swift */, ); path = Classes; sourceTree = ""; @@ -331,6 +332,7 @@ 0DCA4C5429BBABD400F78368 /* DataController.swift in Sources */, 0DCA4C5029BBABD400F78368 /* Small.swift in Sources */, 0DCA4C4029BBABD400F78368 /* Preferences.swift in Sources */, + 0D18A3AD29C2231000D075D4 /* DateHelper.swift in Sources */, 0DCA4C4429BBABD400F78368 /* CountdownsSettingsView.swift in Sources */, 0DA02AE529BD1DC200CEBB8F /* ConditionalModifier.swift in Sources */, 0D803C0429C0B7BA00C7F4BE /* HiddenCountdowns.swift in Sources */, @@ -342,10 +344,12 @@ 0DCA4C3F29BBABD400F78368 /* Countdown.swift in Sources */, 0DCA4C4D29BBABD400F78368 /* OverviewDailyCountdownsTableCellWithAction.swift in Sources */, 0DCA4C5629BBABD400F78368 /* AppDelegate.swift in Sources */, + 0D18A3AB29C21F1F00D075D4 /* Alerts.swift in Sources */, 0DCA4C4129BBABD400F78368 /* HideDaily.swift in Sources */, - 0D13C08229C13F07006AC521 /* BackupRestore.swift in Sources */, + 0D13C08229C13F07006AC521 /* Backup.swift in Sources */, 0DCA4C4729BBABD400F78368 /* TimeSliced.swift in Sources */, 0DCA4C4F29BBABD400F78368 /* Tiny.swift in Sources */, + 0D18A3A929C21D9F00D075D4 /* Notifications.swift in Sources */, 0DCA4C4B29BBABD400F78368 /* FocusModeView.swift in Sources */, 0D803C0229C0B62100C7F4BE /* ContextualMenu.swift in Sources */, 0DCA4C4529BBABD400F78368 /* IndicationsSettingsView.swift in Sources */, diff --git a/KnotClock/Classes/Alerts.swift b/KnotClock/Classes/Alerts.swift new file mode 100644 index 0000000..90f4ab4 --- /dev/null +++ b/KnotClock/Classes/Alerts.swift @@ -0,0 +1,20 @@ +// +// Alerts.swift +// KnotClock +// +// Created by NA on 3/15/23. +// + +import Foundation + +class Alerts: ObservableObject { + static let shared = Alerts() + + @Published var message = "" + @Published var isPresented = false + + static func show(_ message: String) { + Alerts.shared.message = message + Alerts.shared.isPresented = true + } +} diff --git a/KnotClock/Classes/Countdowns/BackupRestore.swift b/KnotClock/Classes/Backup.swift similarity index 76% rename from KnotClock/Classes/Countdowns/BackupRestore.swift rename to KnotClock/Classes/Backup.swift index c34e4d1..a3f41ea 100644 --- a/KnotClock/Classes/Countdowns/BackupRestore.swift +++ b/KnotClock/Classes/Backup.swift @@ -1,18 +1,17 @@ // -// BackupRestore.swift +// Backup.swift // KnotClock // // Created by NA on 3/14/23. // import SwiftUI -import CoreData import UniformTypeIdentifiers -#if os(macOS) -extension Countdowns { - func backup() { - let backupFileName = "\(K.appName) Backup \(getCurrentDate()).sqlite" +class Backup { + #if os(macOS) + func save() { + let backupFileName = "\(K.appName) Backup \(DateHelper().getCurrent()).sqlite" let backupDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] let backupURL = backupDirectory.appendingPathComponent(backupFileName) @@ -21,7 +20,7 @@ extension Countdowns { // Delete older backup with same backupFileName try? FileManager.default.removeItem(at: backupURL) - if let store = coordinator.persistentStores.last { + if let store = coordinator.persistentStores.first { // Save new backup DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { do { @@ -31,14 +30,12 @@ extension Countdowns { // Open path in Finder NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: backupDirectory.path()) } catch { - self.alertMessage = "Failed to create/store backup file: \(error)" - self.showAlert = true + Alerts.show("Failed to create/store backup file: \(error)") } } } } - - func restoreBackup() { + func restore() { guard let backupFileType = UTType.init(filenameExtension: "sqlite") else { return } let coordinator = DataController.shared.container.persistentStoreCoordinator @@ -52,12 +49,11 @@ extension Countdowns { if panel.runModal() == .OK, let selectedBackupURL = panel.url, let storeURL = stores.first?.url { do { try coordinator.replacePersistentStore(at: storeURL, destinationOptions: nil, withPersistentStoreFrom: selectedBackupURL, sourceOptions: nil, ofType: NSSQLiteStoreType) - reset(level: .reloadContainerRefetchResetNotifs) + Countdowns.shared.reset(level: .reloadContainerRefetchResetNotifs) } catch { - self.alertMessage = "Failed to restore backup file: \(error)" - self.showAlert = true + Alerts.show("Failed to restore backup file: \(error)") } } } + #endif } -#endif diff --git a/KnotClock/Classes/Countdowns/Countdowns.swift b/KnotClock/Classes/Countdowns.swift similarity index 64% rename from KnotClock/Classes/Countdowns/Countdowns.swift rename to KnotClock/Classes/Countdowns.swift index eca2504..69fd179 100644 --- a/KnotClock/Classes/Countdowns/Countdowns.swift +++ b/KnotClock/Classes/Countdowns.swift @@ -7,7 +7,6 @@ import SwiftUI import CoreData -import UserNotifications class Countdowns: ObservableObject { static let shared = Countdowns() @@ -18,17 +17,16 @@ class Countdowns: ObservableObject { @Published private(set) var hidden: [Countdown] = [] @Published private(set) var fullList: [Countdown] = [] - @Published var alertMessage = "" - @Published var showAlert = false - @Published private(set) var notIncludingTomorrowTodayOverridden: Bool = false @Published private(set) var notificationsTotalCount = 0 @AppStorage(K.StorageKeys.userPreferences) private var preferences = Preferences(x: DefaultUserPreferences()) - @AppStorage(K.StorageKeys.overrideDay) private var overrideDay = "" @AppStorage(K.StorageKeys.hiddenDailies) private var hiddenDailies = HideDaily(list: [HiddenDailyItem()]) @AppStorage(K.StorageKeys.fetchedTomorrowDailies) private var fetchedTomorrowDailies: Bool = false + private let notifications = Notifications.shared + private let dateHelper = DateHelper() + private var timer: Timer? = nil private var oldTimerInterval: Double? = nil private var lastRefetchDay: Int = 0 @@ -117,9 +115,9 @@ class Countdowns: ObservableObject { } func updateViewShouldRefetch(_ dontRefetch: Bool) -> Bool { - let shouldRefetch = (dontRefetch == false && (lastRefetchDay != todayYMD() || shouldIncludeTomorrowDailies(inRangeOfRefreshTimerInterval: true))) + let shouldRefetch = (dontRefetch == false && (lastRefetchDay != dateHelper.todayYMD() || shouldIncludeTomorrowDailies(inRangeOfRefreshTimerInterval: true))) #if DEBUG - if lastRefetchDay != todayYMD() { + if lastRefetchDay != dateHelper.todayYMD() { print("Day has changed, refetched data.") } else if shouldRefetch { print("Time to include tomorrow's daily countdowns.") @@ -128,100 +126,6 @@ class Countdowns: ObservableObject { return shouldRefetch } - func resetNotifications() { - guard preferences.x.notificationCenterAuthorized == true else { return } - - UNUserNotificationCenter.current().removeAllPendingNotificationRequests() - notificationsTotalCount = 0 - - if countNotificationsForCurrentSettings(withAlert: true) >= K.notificationsLimit { - return - } - - DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 0.1) { - self.addAllNotificationsWithoutChecks() - } - } - - func addAllNotificationsWithoutChecks() { - for countdown in fullList { - if preferences.x.firstCIEnabled && preferences.x.notificationOnFirstIndication { - let firstIndicationSeconds = TimeInterval(countdown.remainingSeconds - preferences.x.firstCIRemainingSeconds) - addNotification(countdown: countdown, willReach: "First Indication", inSeconds: firstIndicationSeconds) - } - - if preferences.x.secondCIEnabled && preferences.x.notificationOnSecondIndication { - let secondIndicationSeconds = TimeInterval(countdown.remainingSeconds - preferences.x.secondCIRemainingSeconds) - addNotification(countdown: countdown, willReach: "Second Indication", inSeconds: secondIndicationSeconds) - } - - if preferences.x.notificationOnCountdownHitsZero { - addNotification(countdown: countdown, willReach: "Zero", inSeconds: TimeInterval(countdown.remainingSeconds)) - } - } - - #if DEBUG - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - UNUserNotificationCenter.current().getPendingNotificationRequests(completionHandler: { requests in - DispatchQueue.main.async { - print("Pending Notifications: \(requests.count)") - } - }) - } - #endif - } - - func countNotificationsForCurrentSettings(withAlert: Bool = false) -> Int { - var count = 0 - - for _ in fullList { - if preferences.x.firstCIEnabled && preferences.x.notificationOnFirstIndication { count += 1 } - if preferences.x.secondCIEnabled && preferences.x.notificationOnSecondIndication { count += 1 } - if preferences.x.notificationOnCountdownHitsZero { count += 1 } - } - - if withAlert && count >= K.notificationsLimit { - alertMessage = "With current settings there will be \(notificationsTotalCount) notifications which exceeds system limit, please adjust the settings. For now notifications are disabled." - showAlert = true - } - - notificationsTotalCount = count - - return count - } - - func addNotification(countdown: Countdown, willReach: String, inSeconds: TimeInterval, repeatDailyCountdown: Bool = false) { - guard inSeconds > 0 else { return } - - let repeating = (countdown.category == .daily && repeatDailyCountdown) ? true : false - - let uniqueString = "\(countdown.id.uuidString)-\(willReach.replacingOccurrences(of: " ", with: ""))" - let content = UNMutableNotificationContent() - content.title = countdown.title - content.body = "\(countdown.title) has reached \(willReach)" - content.sound = .default - - let date = Date(timeIntervalSince1970: Date().timeIntervalSince1970 + inSeconds) - - var attachedComponents: Set = [.weekday, .hour, .minute, .second] - if repeating { - attachedComponents = [.hour, .minute, .second] - } - let dateComponents = Calendar.current.dateComponents(attachedComponents, from: date) - - let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: repeating) - let request = UNNotificationRequest(identifier: uniqueString, content: content, trigger: trigger) - - let notificationCenter = UNUserNotificationCenter.current() - notificationCenter.add(request) { error in - if let error { - #if DEBUG - print(error.localizedDescription) - #endif - } - } - } - func refetchAll(resetNotifs: Bool = false) { fullList.removeAll() @@ -248,12 +152,12 @@ class Countdowns: ObservableObject { return lhs.remainingSeconds < rhs.remainingSeconds } - lastRefetchDay = todayYMD() + lastRefetchDay = dateHelper.todayYMD() updateViewTimes(dontRefetch: true) if resetNotifs { - resetNotifications() + notifications.reset(fullList: fullList) } #if DEBUG @@ -276,12 +180,19 @@ class Countdowns: ObservableObject { switch level { case .updateViewTimes: updateViewTimes(dontRefetch: true) + case .refetch: refetchAll() + + case .resetNotifs: + notifications.reset(fullList: fullList) + case .refetchResetNotifs: refetchAll(resetNotifs: true) + case .refetchWithDelayResetNotifs: refetchWithDelay() + case .reloadContainerRefetchResetNotifs: DataController.shared.reload() refetchWithDelay() @@ -330,7 +241,7 @@ class Countdowns: ObservableObject { let weekdays = K.weekdays notIncludingTomorrowTodayOverridden = false - if let _ = todayIsOverriddenAs() { + if let _ = dateHelper.todayIsOverriddenAs() { if shouldIncludeTomorrowDailies() { notIncludingTomorrowTodayOverridden = true } @@ -338,7 +249,7 @@ class Countdowns: ObservableObject { } guard shouldIncludeTomorrowDailies(), - let currentIndex = weekdays.firstIndex(of: todayName()) + let currentIndex = weekdays.firstIndex(of: dateHelper.weekdayName(allowOverride: false)) else { return nil } @@ -349,7 +260,7 @@ class Countdowns: ObservableObject { } func getDailies(for day: String? = nil) -> [Daily] { - var selectedDay = getDayName() + var selectedDay = dateHelper.weekdayName(allowOverride: true) if let day, K.weekdays.contains(day.lowercased()) { selectedDay = day @@ -423,56 +334,11 @@ class Countdowns: ObservableObject { } } - func getCurrentDate(withFomat: String = K.dateFormat) -> String { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = K.dateFormat - return dateFormatter.string(from: Date()) - } - - func overrideToday(as weekday: String) { - overrideDay = "\(getCurrentDate())=\(weekday.lowercased())" - - reset(level: .refetchResetNotifs) - } - - func todayIsOverriddenAs() -> String? { - let separated = overrideDay.components(separatedBy: "=") - - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = K.dateFormat - - if dateFormatter.string(from: Date()) == separated[0] && K.weekdays.contains(separated[1]) && todayName() != separated[1] { - return separated[1] - } - - return nil - } - - func getDayName(checkForOverride: Bool = true) -> String { - if checkForOverride, let overridden = todayIsOverriddenAs() { - return overridden - } else { - return todayName() - } - } - - func todayName() -> String { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "EEEE" - return dateFormatter.string(from: Date()).lowercased() - } - - func todayYMD() -> Int { - let dateFromatter = DateFormatter() - dateFromatter.dateFormat = "yyyyMMdd" - return Int(dateFromatter.string(from: Date())) ?? 0 - } - func hideDaily(_ id: UUID) { if Int.random(in: 1...10) == 1 { clearOldHiddenDaily() } - hiddenDailies.list.append(HiddenDailyItem(id: id, ymd: todayYMD())) + hiddenDailies.list.append(HiddenDailyItem(id: id, ymd: dateHelper.todayYMD())) refetchAll() } @@ -482,14 +348,13 @@ class Countdowns: ObservableObject { } func isDailyHidden(_ id: UUID?) -> Bool { - if let _ = hiddenDailies.list.firstIndex(where: { $0.id == id && $0.ymd == todayYMD() }) { + if let _ = hiddenDailies.list.firstIndex(where: { $0.id == id && $0.ymd == dateHelper.todayYMD() }) { return true } return false } func clearOldHiddenDaily() { - hiddenDailies.list.removeAll(where: { $0.ymd < todayYMD() }) + hiddenDailies.list.removeAll(where: { $0.ymd < dateHelper.todayYMD() }) } } - diff --git a/KnotClock/Classes/DateHelper.swift b/KnotClock/Classes/DateHelper.swift new file mode 100644 index 0000000..ca45e1c --- /dev/null +++ b/KnotClock/Classes/DateHelper.swift @@ -0,0 +1,53 @@ +// +// DateHelper.swift +// KnotClock +// +// Created by NA on 3/15/23. +// + +import SwiftUI + +class DateHelper { + @AppStorage(K.StorageKeys.overrideDay) private var overrideDay = "" + + func getCurrent(withFomat: String = K.dateFormat) -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = K.dateFormat + return dateFormatter.string(from: Date()) + } + + func overrideToday(as weekday: String) { + overrideDay = "\(getCurrent())=\(weekday.lowercased())" + + Countdowns.shared.reset(level: .refetchResetNotifs) + } + + func todayIsOverriddenAs() -> String? { + let separated = overrideDay.components(separatedBy: "=") + + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = K.dateFormat + + if dateFormatter.string(from: Date()) == separated[0] && K.weekdays.contains(separated[1]) && weekdayName(allowOverride: false) != separated[1] { + return separated[1] + } + + return nil + } + + func weekdayName(allowOverride: Bool) -> String { + if allowOverride, let overridden = todayIsOverriddenAs() { + return overridden + } else { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "EEEE" + return dateFormatter.string(from: Date()).lowercased() + } + } + + func todayYMD() -> Int { + let dateFromatter = DateFormatter() + dateFromatter.dateFormat = "yyyyMMdd" + return Int(dateFromatter.string(from: Date())) ?? 0 + } +} diff --git a/KnotClock/Classes/Notifications.swift b/KnotClock/Classes/Notifications.swift new file mode 100644 index 0000000..c48666f --- /dev/null +++ b/KnotClock/Classes/Notifications.swift @@ -0,0 +1,109 @@ +// +// Notifications.swift +// KnotClock +// +// Created by NA on 3/15/23. +// + +import SwiftUI +import UserNotifications + +class Notifications: ObservableObject { + static let shared = Notifications() + + @AppStorage(K.StorageKeys.userPreferences) private var preferences = Preferences(x: DefaultUserPreferences()) + @Published private(set) var notificationsTotalCount = 0 + + func reset(fullList: [Countdown]) { + guard preferences.x.notificationCenterAuthorized == true else { return } + + UNUserNotificationCenter.current().removeAllPendingNotificationRequests() + notificationsTotalCount = 0 + + if countForCurrentSettings(fullList: fullList, withAlert: true) >= K.notificationsLimit { + return + } + + DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 0.2) { + self.addAllWithoutChecks(fullList) + } + } + + func addAllWithoutChecks(_ countdowns: [Countdown]) { + for countdown in countdowns { + if preferences.x.firstCIEnabled && preferences.x.notificationOnFirstIndication { + let firstIndicationSeconds = TimeInterval(countdown.remainingSeconds - preferences.x.firstCIRemainingSeconds) + add(countdown: countdown, willReach: "First Indication", inSeconds: firstIndicationSeconds) + } + + if preferences.x.secondCIEnabled && preferences.x.notificationOnSecondIndication { + let secondIndicationSeconds = TimeInterval(countdown.remainingSeconds - preferences.x.secondCIRemainingSeconds) + add(countdown: countdown, willReach: "Second Indication", inSeconds: secondIndicationSeconds) + } + + if preferences.x.notificationOnCountdownHitsZero { + add(countdown: countdown, willReach: "Zero", inSeconds: TimeInterval(countdown.remainingSeconds)) + } + } + + #if DEBUG + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + UNUserNotificationCenter.current().getPendingNotificationRequests(completionHandler: { requests in + DispatchQueue.main.async { + print("Pending Notifications: \(requests.count)") + } + }) + } + #endif + } + + func countForCurrentSettings(fullList: [Countdown], withAlert: Bool = false) -> Int { + var count = 0 + + for _ in fullList { + if preferences.x.firstCIEnabled && preferences.x.notificationOnFirstIndication { count += 1 } + if preferences.x.secondCIEnabled && preferences.x.notificationOnSecondIndication { count += 1 } + if preferences.x.notificationOnCountdownHitsZero { count += 1 } + } + + if K.notificationsLimit > 0 && withAlert && count >= K.notificationsLimit { + Alerts.show("With current settings there will be \(notificationsTotalCount) notifications which exceeds system limit, please adjust the settings. For now notifications are disabled.") + } + + notificationsTotalCount = count + + return count + } + + func add(countdown: Countdown, willReach: String, inSeconds: TimeInterval, repeatDailyCountdown: Bool = false) { + guard inSeconds > 0 else { return } + + let repeating = (countdown.category == .daily && repeatDailyCountdown) ? true : false + + let uniqueString = "\(countdown.id.uuidString)-\(willReach.replacingOccurrences(of: " ", with: ""))" + let content = UNMutableNotificationContent() + content.title = countdown.title + content.body = "\(countdown.title) has reached \(willReach)" + content.sound = .default + + let date = Date(timeIntervalSince1970: Date().timeIntervalSince1970 + inSeconds) + + var attachedComponents: Set = [.weekday, .hour, .minute, .second] + if repeating { + attachedComponents = [.hour, .minute, .second] + } + let dateComponents = Calendar.current.dateComponents(attachedComponents, from: date) + + let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: repeating) + let request = UNNotificationRequest(identifier: uniqueString, content: content, trigger: trigger) + + let notificationCenter = UNUserNotificationCenter.current() + notificationCenter.add(request) { error in + if let error { + #if DEBUG + print(error.localizedDescription) + #endif + } + } + } +} diff --git a/KnotClock/Constants.swift b/KnotClock/Constants.swift index 527b825..f6f1e5e 100644 --- a/KnotClock/Constants.swift +++ b/KnotClock/Constants.swift @@ -173,6 +173,7 @@ enum WhichIndication { enum CountdownResetLevel { case updateViewTimes case refetch + case resetNotifs case refetchResetNotifs case refetchWithDelayResetNotifs case reloadContainerRefetchResetNotifs diff --git a/KnotClock/Structs/Countdown.swift b/KnotClock/Structs/Countdown.swift index f301f79..836600c 100644 --- a/KnotClock/Structs/Countdown.swift +++ b/KnotClock/Structs/Countdown.swift @@ -139,8 +139,7 @@ struct Countdown: Identifiable, Equatable { try fetchAndDelete() Countdowns.shared.refetchAll() } catch { - Countdowns.shared.alertMessage = "An error occurred while deleting countdown" - Countdowns.shared.showAlert = true + Alerts.show("An error occurred while deleting countdown") } } diff --git a/KnotClock/Views/MainView.swift b/KnotClock/Views/MainView.swift index 73fc6d2..a4f3c1d 100644 --- a/KnotClock/Views/MainView.swift +++ b/KnotClock/Views/MainView.swift @@ -11,11 +11,13 @@ import Combine struct MainView: View { @Environment(\.managedObjectContext) var moc @EnvironmentObject private var countdowns: Countdowns + @ObservedObject private var alerts = Alerts.shared @State private var showAddCountdown = false @State private var showWeeklyOverviewSheet = false @State private var showOverrideDaySheet = false @State private var showHiddenSheet = false private var isInMenubar: Bool + private let dateHelper = DateHelper() @AppStorage(K.StorageKeys.userPreferences) private var preferences = Preferences(x: DefaultUserPreferences()) @@ -39,7 +41,7 @@ struct MainView: View { .onAppear { countdowns.reset(level: .refetchResetNotifs) } - .alert(countdowns.alertMessage, isPresented: $countdowns.showAlert) { + .alert(alerts.message, isPresented: $alerts.isPresented) { Button("OK"){} } .conditionalMofidier(!isInMenubar) { view in @@ -61,7 +63,7 @@ struct MainView: View { } .padding() .toolbar { - if let overridenAs = countdowns.todayIsOverriddenAs() { + if let overridenAs = dateHelper.todayIsOverriddenAs() { ToolbarItem(placement: .status) { Text("Overridden as \(overridenAs.capitalized)").font(.footnote).foregroundColor(.secondary) } diff --git a/KnotClock/Views/OverrideDay.swift b/KnotClock/Views/OverrideDay.swift index 27e4f63..a25f204 100644 --- a/KnotClock/Views/OverrideDay.swift +++ b/KnotClock/Views/OverrideDay.swift @@ -14,13 +14,15 @@ struct OverrideDay: View { @State private var showConfirm = false @State private var confirmForDay = "" + private let dateHelper = DateHelper() + var body: some View { VStack { Text("Override Today As:") ForEach(K.weekdays, id: \.self) { day in overrideButton(for: day) - .disabled(todayName().lowercased() == day.lowercased()) + .disabled(dateHelper.weekdayName(allowOverride: true).lowercased() == day.lowercased()) } Text("This will only affect Daily Countdowns for Today").font(.footnote).padding(.top) @@ -32,7 +34,9 @@ struct OverrideDay: View { .padding() .confirmationDialog("Are you sure you want to override today as \(confirmForDay.capitalized)?", isPresented: $showConfirm) { Button("Yes, override") { - override(to: confirmForDay) + dateHelper.overrideToday(as: confirmForDay) + showConfirm = false + dismiss() } Button("Cancel", role: .cancel){ @@ -42,22 +46,6 @@ struct OverrideDay: View { } } - func override(to day: String) { - countdowns.overrideToday(as: day) - dismiss() - } - - func todayName() -> String { - if let overridenAs = countdowns.todayIsOverriddenAs() { - return overridenAs - } - - let date = Date() - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "EEEE" - return dateFormatter.string(from: date) - } - func overrideButton(for day: String) -> some View { Button { confirmForDay = day diff --git a/KnotClock/Views/Settings/ApplicationSettingsView.swift b/KnotClock/Views/Settings/ApplicationSettingsView.swift index a278130..d29ce93 100644 --- a/KnotClock/Views/Settings/ApplicationSettingsView.swift +++ b/KnotClock/Views/Settings/ApplicationSettingsView.swift @@ -16,6 +16,8 @@ struct ApplicationSettingsView: View { @State private var showResetSettingsConfirmation = false @State private var showAlert = false @State private var alertMessage = "" + + private let backup = Backup() var body: some View { scrollViewOnMac { @@ -112,13 +114,13 @@ struct ApplicationSettingsView: View { Section { HStack { Button { - Countdowns.shared.backup() + backup.save() } label: { Label("Backup Countdowns", systemImage: "square.and.arrow.down.fill") } Button { - Countdowns.shared.restoreBackup() + backup.restore() } label: { Label("Restore", systemImage: "square.and.arrow.up.fill") } diff --git a/KnotClock/Views/Settings/IndicationsSettingsView.swift b/KnotClock/Views/Settings/IndicationsSettingsView.swift index aad5ed6..4aacaf9 100644 --- a/KnotClock/Views/Settings/IndicationsSettingsView.swift +++ b/KnotClock/Views/Settings/IndicationsSettingsView.swift @@ -112,7 +112,7 @@ struct IndicationsSettingsView: View { #if DEBUG print("Resetting Notifications") #endif - countdowns.resetNotifications() + countdowns.reset(level: .resetNotifs) } }) .alert(alertMessage, isPresented: $showAlert) { diff --git a/KnotClock/Views/SubViews/OverviewDailyCountdownsTableCellWithAction.swift b/KnotClock/Views/SubViews/OverviewDailyCountdownsTableCellWithAction.swift index 8cb500a..3b7ac6f 100644 --- a/KnotClock/Views/SubViews/OverviewDailyCountdownsTableCellWithAction.swift +++ b/KnotClock/Views/SubViews/OverviewDailyCountdownsTableCellWithAction.swift @@ -144,8 +144,7 @@ struct OverviewDailyCountdownTableCellWithAction: View { try moc.save() Countdowns.shared.reset(level: .refetchResetNotifs) } catch { - Countdowns.shared.alertMessage = error.localizedDescription - Countdowns.shared.showAlert = true + Alerts.show(error.localizedDescription) } } } diff --git a/KnotClock/Views/SubViews/OverviewSingleCountdownsWithAction.swift b/KnotClock/Views/SubViews/OverviewSingleCountdownsWithAction.swift index 7b6d79c..4b1f7f0 100644 --- a/KnotClock/Views/SubViews/OverviewSingleCountdownsWithAction.swift +++ b/KnotClock/Views/SubViews/OverviewSingleCountdownsWithAction.swift @@ -103,8 +103,7 @@ struct OverviewSingleCountdownWithAction: View { try moc.save() Countdowns.shared.reset(level: .refetchResetNotifs) } catch { - Countdowns.shared.alertMessage = error.localizedDescription - Countdowns.shared.showAlert = true + Alerts.show(error.localizedDescription) } } }