diff --git a/LeCountdown/CountdownView.swift b/LaunchWidget/CountdownView.swift similarity index 100% rename from LeCountdown/CountdownView.swift rename to LaunchWidget/CountdownView.swift diff --git a/LeCountdown.xcodeproj/project.pbxproj b/LeCountdown.xcodeproj/project.pbxproj index b028038..ec0efb0 100644 --- a/LeCountdown.xcodeproj/project.pbxproj +++ b/LeCountdown.xcodeproj/project.pbxproj @@ -31,7 +31,6 @@ C438C7E02981216300BF3EF9 /* LaunchWidget.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = C438C7DB2981216200BF3EF9 /* LaunchWidget.intentdefinition */; }; C438C7E32981216300BF3EF9 /* LaunchWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = C438C7CE2981216200BF3EF9 /* LaunchWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; C438C7E82981255D00BF3EF9 /* TimeInterval+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4060DF4297AE9A7003FAB80 /* TimeInterval+Extensions.swift */; }; - C438C7EA2981260D00BF3EF9 /* CountdownView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C7E92981260D00BF3EF9 /* CountdownView.swift */; }; C438C7EB2981266F00BF3EF9 /* CountdownView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C7E92981260D00BF3EF9 /* CountdownView.swift */; }; C438C7F229812BB200BF3EF9 /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C438C7F129812BB200BF3EF9 /* Intents.framework */; }; C438C7F529812BB200BF3EF9 /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C7F429812BB200BF3EF9 /* IntentHandler.swift */; }; @@ -200,7 +199,6 @@ children = ( C4060DBF297AE73B003FAB80 /* LeCountdownApp.swift */, C4060DC1297AE73B003FAB80 /* ContentView.swift */, - C438C7E92981260D00BF3EF9 /* CountdownView.swift */, C4060DF6297AFEF2003FAB80 /* NewCountdownView.swift */, C438C7C02980228B00BF3EF9 /* CountdownScheduler.swift */, C4060DC3297AE73D003FAB80 /* Assets.xcassets */, @@ -258,6 +256,7 @@ C438C7D52981216200BF3EF9 /* LaunchWidgetBundle.swift */, C438C7D72981216200BF3EF9 /* LaunchWidgetLiveActivity.swift */, C438C7D92981216200BF3EF9 /* LaunchWidget.swift */, + C438C7E92981260D00BF3EF9 /* CountdownView.swift */, C438C7DB2981216200BF3EF9 /* LaunchWidget.intentdefinition */, C438C7DC2981216300BF3EF9 /* Assets.xcassets */, C438C7DE2981216300BF3EF9 /* Info.plist */, @@ -465,7 +464,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - C438C7EA2981260D00BF3EF9 /* CountdownView.swift in Sources */, C4060DC9297AE73D003FAB80 /* Persistence.swift in Sources */, C438C7FF2981300500BF3EF9 /* IntentDataProvider.swift in Sources */, C4060DC2297AE73B003FAB80 /* ContentView.swift in Sources */, diff --git a/LeCountdown/AppDelegate.swift b/LeCountdown/AppDelegate.swift index 89a07e4..2c9f5e8 100644 --- a/LeCountdown/AppDelegate.swift +++ b/LeCountdown/AppDelegate.swift @@ -29,7 +29,8 @@ extension AppDelegate: UNUserNotificationCenterDelegate { func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { print("willPresent notification") completionHandler([.banner, .sound]) - AppEnvironment.sun.clearNotificationDate() + + AppEnvironment.sun.clearNotificationDate(countdownId: notification.request.identifier) } } diff --git a/LeCountdown/ContentView.swift b/LeCountdown/ContentView.swift index 19a4393..7779e17 100644 --- a/LeCountdown/ContentView.swift +++ b/LeCountdown/ContentView.swift @@ -8,7 +8,30 @@ import SwiftUI import CoreData +struct CountdownLiveView: View { + + @EnvironmentObject var environment: AppEnvironment + + @ObservedObject var countdown: Countdown + + var body: some View { + VStack { + Text(countdown.name ?? "") + if let date = environment.notificationDates[countdown.stringId] { + Text(date, style: .timer) + } else { + Text(countdown.duration.minuteSecond) + } + } + .font(.title2) + } + +} + struct ContentView: View { + + @EnvironmentObject var environment: AppEnvironment + @Environment(\.managedObjectContext) private var viewContext @FetchRequest( @@ -43,13 +66,15 @@ struct ContentView: View { Button { self._launchCountdown(countdown) } label: { - CountdownView(name: countdown.name, duration: countdown.duration) + CountdownLiveView(countdown: countdown) + .environmentObject(AppEnvironment.sun) .aspectRatio(contentMode: .fill) .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) .aspectRatio(1, contentMode: .fit) .background(Color(red: 0.9, green: 0.95, blue: 1.0)) .cornerRadius(40.0) } +// Text("ORder = \(countdown.order)") NavigationLink { CountdownEditView(countdown: countdown, isPresented: $isShowingNewCountdown) @@ -94,8 +119,7 @@ struct ContentView: View { fileprivate func _startCountdownIfPossible(url: URL) { - var urlString = url.absoluteString - urlString.trimPrefix("start://countdown/") + let urlString = url.absoluteString if let countdown = viewContext.object(stringId: urlString) as? Countdown { self._launchCountdown(countdown) } else { @@ -105,7 +129,7 @@ struct ContentView: View { } fileprivate func _launchCountdown(_ countdown: Countdown) { - CountdownScheduler.master.scheduleIfPossible(duration: countdown.duration) { result in + CountdownScheduler.master.scheduleIfPossible(countdown: countdown) { result in switch result { case .success(_): break diff --git a/LeCountdown/CountdownScheduler.swift b/LeCountdown/CountdownScheduler.swift index 86ab29b..e87a290 100644 --- a/LeCountdown/CountdownScheduler.swift +++ b/LeCountdown/CountdownScheduler.swift @@ -12,24 +12,21 @@ class CountdownScheduler { static let master = CountdownScheduler() - func scheduleIfPossible(duration: Double, handler: @escaping (Result) -> Void) { - - UNUserNotificationCenter.current().getPendingNotificationRequests { requests in - self.cancel() - self._scheduleCountdownNotification(duration: duration, handler: handler) - } - + func scheduleIfPossible(countdown: Countdown, handler: @escaping (Result) -> Void) { + self.cancelCurrentNotifications(countdown: countdown) + self._scheduleCountdownNotification(countdown: countdown, handler: handler) } - func cancel() { - UNUserNotificationCenter.current().removeAllPendingNotificationRequests() - AppEnvironment.sun.clearNotificationDate() + func cancelCurrentNotifications(countdown: Countdown) { + UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [countdown.stringId]) + AppEnvironment.sun.clearNotificationDate(countdownId: countdown.stringId) } - fileprivate func _scheduleCountdownNotification(duration: Double, handler: @escaping (Result) -> Void) { + fileprivate func _scheduleCountdownNotification(countdown: Countdown, handler: @escaping (Result) -> Void) { let content = UNMutableNotificationContent() content.title = NSLocalizedString("It's time!", comment: "") + let duration = countdown.duration let minutes = duration / 60.0 let minutesLabel = minutes > 1 ? NSLocalizedString("minutes", comment: "") : NSLocalizedString("minute", comment: "") @@ -39,18 +36,20 @@ class CountdownScheduler { content.sound = UNNotificationSound.defaultCritical let trigger = UNTimeIntervalNotificationTrigger(timeInterval: duration, repeats: false) - let request = UNNotificationRequest(identifier: "com.staxriver.countdown", content: content, trigger: trigger) + let request = UNNotificationRequest(identifier: countdown.objectID.uriRepresentation().absoluteString, content: content, trigger: trigger) UNUserNotificationCenter.current().add(request) { error in - if let error { - handler(.failure(error)) - print("Scheduling error = \(error)") - } else { - if let date = trigger.nextTriggerDate() { - AppEnvironment.sun.saveNotificationDate(date) - handler(.success(trigger.nextTriggerDate())) + DispatchQueue.main.async { + if let error { + handler(.failure(error)) + print("Scheduling error = \(error)") } else { - let backupDate = Date().addingTimeInterval(duration) - AppEnvironment.sun.saveNotificationDate(backupDate) + if let date = trigger.nextTriggerDate() { + AppEnvironment.sun.saveNotificationDate(date, countdown: countdown) + handler(.success(trigger.nextTriggerDate())) + } else { + let backupDate = Date().addingTimeInterval(duration) + AppEnvironment.sun.saveNotificationDate(backupDate, countdown: countdown) + } } } } @@ -67,29 +66,31 @@ class AppEnvironment : ObservableObject { static let sun: AppEnvironment = AppEnvironment() init() { - self.notificationDate = UserDefaults.standard.value(forKey: Key.date.rawValue) as? Date + if let dates = UserDefaults.standard.value(forKey: Key.dates.rawValue) as? [String : Date] { + self.notificationDates = dates + } } - @Published var notificationDate: Date? = nil { + @Published var notificationDates: [String : Date] = [:] { didSet { - UserDefaults.standard.set(notificationDate, forKey: Key.date.rawValue) + UserDefaults.standard.set(notificationDates, forKey: Key.dates.rawValue) } } enum Key : String { - case date + case dates } - var hasNotificationDate: Bool { - return self.notificationDate != nil - } +// var hasNotificationDate: Bool { +// return self.notificationDates != nil +// } - func saveNotificationDate(_ date: Date) { - self.notificationDate = date + func saveNotificationDate(_ date: Date, countdown: Countdown) { + self.notificationDates[countdown.stringId] = date } - func clearNotificationDate() { - self.notificationDate = nil + func clearNotificationDate(countdownId: String) { + self.notificationDates.removeValue(forKey: countdownId) } } diff --git a/LeCountdown/LeCountdown.xcdatamodeld/LeCountdown.xcdatamodel/contents b/LeCountdown/LeCountdown.xcdatamodeld/LeCountdown.xcdatamodel/contents index 0523d7f..22703c4 100644 --- a/LeCountdown/LeCountdown.xcdatamodeld/LeCountdown.xcdatamodel/contents +++ b/LeCountdown/LeCountdown.xcdatamodeld/LeCountdown.xcdatamodel/contents @@ -1,7 +1,7 @@ - + diff --git a/LeCountdown/LeCountdownApp.swift b/LeCountdown/LeCountdownApp.swift index 2516789..9b2655a 100644 --- a/LeCountdown/LeCountdownApp.swift +++ b/LeCountdown/LeCountdownApp.swift @@ -17,6 +17,7 @@ struct LeCountdownApp: App { var body: some Scene { WindowGroup { ContentView() + .environmentObject(AppEnvironment.sun) .environment(\.managedObjectContext, persistenceController.container.viewContext) } } diff --git a/LeCountdown/NSManagedContext+Extensions.swift b/LeCountdown/NSManagedContext+Extensions.swift index a1d02b1..8f19b6f 100644 --- a/LeCountdown/NSManagedContext+Extensions.swift +++ b/LeCountdown/NSManagedContext+Extensions.swift @@ -24,4 +24,8 @@ extension NSManagedObject { return self.objectID.isTemporaryID } + var stringId: String { + return self.objectID.uriRepresentation().absoluteString + } + } diff --git a/LeCountdown/NewCountdownView.swift b/LeCountdown/NewCountdownView.swift index 5c1eaa5..55b9bfa 100644 --- a/LeCountdown/NewCountdownView.swift +++ b/LeCountdown/NewCountdownView.swift @@ -7,6 +7,7 @@ import SwiftUI import CoreData +import WidgetKit struct NewCountdownView : View { @@ -15,8 +16,9 @@ struct NewCountdownView : View { @Binding var isPresented: Bool var body: some View { - CountdownEditView(countdown: Countdown(context: viewContext), isPresented: $isPresented) + CountdownEditView(isPresented: $isPresented) .environment(\.managedObjectContext, viewContext) + .navigationTitle("New countdown") } } @@ -26,7 +28,7 @@ struct CountdownEditView : View { @Environment(\.managedObjectContext) private var viewContext @Environment(\.dismiss) private var dismiss - var countdown: Countdown + var countdown: Countdown? = nil @Binding var isPresented: Bool @@ -44,6 +46,8 @@ struct CountdownEditView : View { @FetchRequest(sortDescriptors: []) private var countdowns: FetchedResults + @State var _isAdding: Bool = false + var body: some View { NavigationStack { @@ -66,7 +70,7 @@ struct CountdownEditView : View { Text("Sound") } }.onAppear { - self._initDuration() + self._onAppear() } .confirmationDialog("", isPresented: $deleteConfirmationShown, actions: { Button("Yes", role: .destructive) { @@ -82,7 +86,7 @@ struct CountdownEditView : View { Text(error?.localizedDescription ?? "error") }) .toolbar { - if self.countdown.isTemporary { + if self._isAdding { ToolbarItem(placement: .navigationBarLeading) { Button("Cancel") { self._cancel() @@ -94,11 +98,13 @@ struct CountdownEditView : View { self._save() } } - ToolbarItem(placement: .bottomBar) { - Button { - self.deleteConfirmationShown = true - } label: { - Image(systemName: "trash") + if !self._isAdding { + ToolbarItem(placement: .bottomBar) { + Button { + self.deleteConfirmationShown = true + } label: { + Image(systemName: "trash") + } } } ToolbarItemGroup(placement: .keyboard) { @@ -109,26 +115,31 @@ struct CountdownEditView : View { } } } - .navigationTitle("New countdown") + .navigationTitle("Edit countdown") } } - fileprivate func _initDuration() { + fileprivate func _onAppear() { - let minutes = Int(self.countdown.duration / 60.0) - let seconds = self.countdown.duration - Double(minutes * 60) + self._isAdding = (self.countdown == nil) - if minutes > 0 { - self.minutesString = self._numberFormatter.string(from: NSNumber(value: minutes)) ?? "" - } + if let countdown { + + let minutes = Int(countdown.duration / 60.0) + let seconds = countdown.duration - Double(minutes * 60) + + if minutes > 0 { + self.minutesString = self._numberFormatter.string(from: NSNumber(value: minutes)) ?? "" + } - if seconds > 0 { - self.secondsString = self._numberFormatter.string(from: NSNumber(value: seconds)) ?? "" - } - - if let name = self.countdown.name, !name.isEmpty { - self.nameString = name + if seconds > 0 { + self.secondsString = self._numberFormatter.string(from: NSNumber(value: seconds)) ?? "" + } + + if let name = countdown.name, !name.isEmpty { + self.nameString = name + } } } @@ -150,29 +161,45 @@ struct CountdownEditView : View { fileprivate func _save() { - let temporary = self.countdown.isTemporary + let cd: Countdown + if let countdown { + cd = countdown + } else { + cd = Countdown(context: viewContext) + } - countdown.duration = self._minutes * 60.0 + self._seconds - if temporary { - countdown.order = Int16(self.countdowns.count) + cd.duration = self._minutes * 60.0 + self._seconds + if self._isAdding { + cd.order = Int16(self.countdowns.count) } if !self.nameString.isEmpty { - countdown.name = self.nameString + cd.name = self.nameString } self._saveContext() - if temporary { + WidgetCenter.shared.reloadTimelines(ofKind: "com.staxriver.launch-widget") // refreshes the visual of existing widgets + + self._popOrDismiss() + } + + fileprivate func _popOrDismiss() { + if self._isAdding { self.isPresented = false } else { dismiss() } - } fileprivate func _delete() { - viewContext.delete(self.countdown) + + guard let countdown else { + return + } + + viewContext.delete(countdown) self._saveContext() + self._popOrDismiss() } fileprivate func _saveContext() { @@ -189,5 +216,6 @@ struct CountdownEditView : View { struct NewCountdownView_Previews: PreviewProvider { static var previews: some View { NewCountdownView(isPresented: .constant(true)) + .environment(\.managedObjectContext, PersistenceController.shared.container.viewContext) } }