Various fixes

release
Laurent 3 years ago
parent 92671c60c9
commit 1db60171e1
  1. 0
      LaunchWidget/CountdownView.swift
  2. 4
      LeCountdown.xcodeproj/project.pbxproj
  3. 3
      LeCountdown/AppDelegate.swift
  4. 32
      LeCountdown/ContentView.swift
  5. 51
      LeCountdown/CountdownScheduler.swift
  6. 2
      LeCountdown/LeCountdown.xcdatamodeld/LeCountdown.xcdatamodel/contents
  7. 1
      LeCountdown/LeCountdownApp.swift
  8. 4
      LeCountdown/NSManagedContext+Extensions.swift
  9. 62
      LeCountdown/NewCountdownView.swift

@ -31,7 +31,6 @@
C438C7E02981216300BF3EF9 /* LaunchWidget.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = C438C7DB2981216200BF3EF9 /* LaunchWidget.intentdefinition */; }; 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, ); }; }; 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 */; }; 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 */; }; C438C7EB2981266F00BF3EF9 /* CountdownView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C7E92981260D00BF3EF9 /* CountdownView.swift */; };
C438C7F229812BB200BF3EF9 /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C438C7F129812BB200BF3EF9 /* Intents.framework */; }; C438C7F229812BB200BF3EF9 /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C438C7F129812BB200BF3EF9 /* Intents.framework */; };
C438C7F529812BB200BF3EF9 /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C7F429812BB200BF3EF9 /* IntentHandler.swift */; }; C438C7F529812BB200BF3EF9 /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C7F429812BB200BF3EF9 /* IntentHandler.swift */; };
@ -200,7 +199,6 @@
children = ( children = (
C4060DBF297AE73B003FAB80 /* LeCountdownApp.swift */, C4060DBF297AE73B003FAB80 /* LeCountdownApp.swift */,
C4060DC1297AE73B003FAB80 /* ContentView.swift */, C4060DC1297AE73B003FAB80 /* ContentView.swift */,
C438C7E92981260D00BF3EF9 /* CountdownView.swift */,
C4060DF6297AFEF2003FAB80 /* NewCountdownView.swift */, C4060DF6297AFEF2003FAB80 /* NewCountdownView.swift */,
C438C7C02980228B00BF3EF9 /* CountdownScheduler.swift */, C438C7C02980228B00BF3EF9 /* CountdownScheduler.swift */,
C4060DC3297AE73D003FAB80 /* Assets.xcassets */, C4060DC3297AE73D003FAB80 /* Assets.xcassets */,
@ -258,6 +256,7 @@
C438C7D52981216200BF3EF9 /* LaunchWidgetBundle.swift */, C438C7D52981216200BF3EF9 /* LaunchWidgetBundle.swift */,
C438C7D72981216200BF3EF9 /* LaunchWidgetLiveActivity.swift */, C438C7D72981216200BF3EF9 /* LaunchWidgetLiveActivity.swift */,
C438C7D92981216200BF3EF9 /* LaunchWidget.swift */, C438C7D92981216200BF3EF9 /* LaunchWidget.swift */,
C438C7E92981260D00BF3EF9 /* CountdownView.swift */,
C438C7DB2981216200BF3EF9 /* LaunchWidget.intentdefinition */, C438C7DB2981216200BF3EF9 /* LaunchWidget.intentdefinition */,
C438C7DC2981216300BF3EF9 /* Assets.xcassets */, C438C7DC2981216300BF3EF9 /* Assets.xcassets */,
C438C7DE2981216300BF3EF9 /* Info.plist */, C438C7DE2981216300BF3EF9 /* Info.plist */,
@ -465,7 +464,6 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
C438C7EA2981260D00BF3EF9 /* CountdownView.swift in Sources */,
C4060DC9297AE73D003FAB80 /* Persistence.swift in Sources */, C4060DC9297AE73D003FAB80 /* Persistence.swift in Sources */,
C438C7FF2981300500BF3EF9 /* IntentDataProvider.swift in Sources */, C438C7FF2981300500BF3EF9 /* IntentDataProvider.swift in Sources */,
C4060DC2297AE73B003FAB80 /* ContentView.swift in Sources */, C4060DC2297AE73B003FAB80 /* ContentView.swift in Sources */,

@ -29,7 +29,8 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
print("willPresent notification") print("willPresent notification")
completionHandler([.banner, .sound]) completionHandler([.banner, .sound])
AppEnvironment.sun.clearNotificationDate()
AppEnvironment.sun.clearNotificationDate(countdownId: notification.request.identifier)
} }
} }

@ -8,7 +8,30 @@
import SwiftUI import SwiftUI
import CoreData 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 { struct ContentView: View {
@EnvironmentObject var environment: AppEnvironment
@Environment(\.managedObjectContext) private var viewContext @Environment(\.managedObjectContext) private var viewContext
@FetchRequest( @FetchRequest(
@ -43,13 +66,15 @@ struct ContentView: View {
Button { Button {
self._launchCountdown(countdown) self._launchCountdown(countdown)
} label: { } label: {
CountdownView(name: countdown.name, duration: countdown.duration) CountdownLiveView(countdown: countdown)
.environmentObject(AppEnvironment.sun)
.aspectRatio(contentMode: .fill) .aspectRatio(contentMode: .fill)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.aspectRatio(1, contentMode: .fit) .aspectRatio(1, contentMode: .fit)
.background(Color(red: 0.9, green: 0.95, blue: 1.0)) .background(Color(red: 0.9, green: 0.95, blue: 1.0))
.cornerRadius(40.0) .cornerRadius(40.0)
} }
// Text("ORder = \(countdown.order)")
NavigationLink { NavigationLink {
CountdownEditView(countdown: countdown, isPresented: $isShowingNewCountdown) CountdownEditView(countdown: countdown, isPresented: $isShowingNewCountdown)
@ -94,8 +119,7 @@ struct ContentView: View {
fileprivate func _startCountdownIfPossible(url: URL) { fileprivate func _startCountdownIfPossible(url: URL) {
var urlString = url.absoluteString let urlString = url.absoluteString
urlString.trimPrefix("start://countdown/")
if let countdown = viewContext.object(stringId: urlString) as? Countdown { if let countdown = viewContext.object(stringId: urlString) as? Countdown {
self._launchCountdown(countdown) self._launchCountdown(countdown)
} else { } else {
@ -105,7 +129,7 @@ struct ContentView: View {
} }
fileprivate func _launchCountdown(_ countdown: Countdown) { fileprivate func _launchCountdown(_ countdown: Countdown) {
CountdownScheduler.master.scheduleIfPossible(duration: countdown.duration) { result in CountdownScheduler.master.scheduleIfPossible(countdown: countdown) { result in
switch result { switch result {
case .success(_): case .success(_):
break break

@ -12,24 +12,21 @@ class CountdownScheduler {
static let master = CountdownScheduler() static let master = CountdownScheduler()
func scheduleIfPossible(duration: Double, handler: @escaping (Result<Date?, Error>) -> Void) { func scheduleIfPossible(countdown: Countdown, handler: @escaping (Result<Date?, Error>) -> Void) {
self.cancelCurrentNotifications(countdown: countdown)
UNUserNotificationCenter.current().getPendingNotificationRequests { requests in self._scheduleCountdownNotification(countdown: countdown, handler: handler)
self.cancel()
self._scheduleCountdownNotification(duration: duration, handler: handler)
}
} }
func cancel() { func cancelCurrentNotifications(countdown: Countdown) {
UNUserNotificationCenter.current().removeAllPendingNotificationRequests() UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [countdown.stringId])
AppEnvironment.sun.clearNotificationDate() AppEnvironment.sun.clearNotificationDate(countdownId: countdown.stringId)
} }
fileprivate func _scheduleCountdownNotification(duration: Double, handler: @escaping (Result<Date?, Error>) -> Void) { fileprivate func _scheduleCountdownNotification(countdown: Countdown, handler: @escaping (Result<Date?, Error>) -> Void) {
let content = UNMutableNotificationContent() let content = UNMutableNotificationContent()
content.title = NSLocalizedString("It's time!", comment: "") content.title = NSLocalizedString("It's time!", comment: "")
let duration = countdown.duration
let minutes = duration / 60.0 let minutes = duration / 60.0
let minutesLabel = minutes > 1 ? NSLocalizedString("minutes", comment: "") : NSLocalizedString("minute", comment: "") let minutesLabel = minutes > 1 ? NSLocalizedString("minutes", comment: "") : NSLocalizedString("minute", comment: "")
@ -39,18 +36,20 @@ class CountdownScheduler {
content.sound = UNNotificationSound.defaultCritical content.sound = UNNotificationSound.defaultCritical
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: duration, repeats: false) 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 UNUserNotificationCenter.current().add(request) { error in
DispatchQueue.main.async {
if let error { if let error {
handler(.failure(error)) handler(.failure(error))
print("Scheduling error = \(error)") print("Scheduling error = \(error)")
} else { } else {
if let date = trigger.nextTriggerDate() { if let date = trigger.nextTriggerDate() {
AppEnvironment.sun.saveNotificationDate(date) AppEnvironment.sun.saveNotificationDate(date, countdown: countdown)
handler(.success(trigger.nextTriggerDate())) handler(.success(trigger.nextTriggerDate()))
} else { } else {
let backupDate = Date().addingTimeInterval(duration) let backupDate = Date().addingTimeInterval(duration)
AppEnvironment.sun.saveNotificationDate(backupDate) AppEnvironment.sun.saveNotificationDate(backupDate, countdown: countdown)
}
} }
} }
} }
@ -67,29 +66,31 @@ class AppEnvironment : ObservableObject {
static let sun: AppEnvironment = AppEnvironment() static let sun: AppEnvironment = AppEnvironment()
init() { 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 { didSet {
UserDefaults.standard.set(notificationDate, forKey: Key.date.rawValue) UserDefaults.standard.set(notificationDates, forKey: Key.dates.rawValue)
} }
} }
enum Key : String { enum Key : String {
case date case dates
} }
var hasNotificationDate: Bool { // var hasNotificationDate: Bool {
return self.notificationDate != nil // return self.notificationDates != nil
} // }
func saveNotificationDate(_ date: Date) { func saveNotificationDate(_ date: Date, countdown: Countdown) {
self.notificationDate = date self.notificationDates[countdown.stringId] = date
} }
func clearNotificationDate() { func clearNotificationDate(countdownId: String) {
self.notificationDate = nil self.notificationDates.removeValue(forKey: countdownId)
} }
} }

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="22A400" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier=""> <model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="22A400" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier="">
<entity name="Countdown" representedClassName="Countdown" syncable="YES" codeGenerationType="class"> <entity name="Countdown" representedClassName="Countdown" syncable="YES" codeGenerationType="class">
<attribute name="duration" attributeType="Double" defaultValueString="1" usesScalarValueType="YES"/> <attribute name="duration" attributeType="Double" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="image" optional="YES" attributeType="String"/> <attribute name="image" optional="YES" attributeType="String"/>
<attribute name="name" optional="YES" attributeType="String"/> <attribute name="name" optional="YES" attributeType="String"/>
<attribute name="order" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/> <attribute name="order" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>

@ -17,6 +17,7 @@ struct LeCountdownApp: App {
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
ContentView() ContentView()
.environmentObject(AppEnvironment.sun)
.environment(\.managedObjectContext, persistenceController.container.viewContext) .environment(\.managedObjectContext, persistenceController.container.viewContext)
} }
} }

@ -24,4 +24,8 @@ extension NSManagedObject {
return self.objectID.isTemporaryID return self.objectID.isTemporaryID
} }
var stringId: String {
return self.objectID.uriRepresentation().absoluteString
}
} }

@ -7,6 +7,7 @@
import SwiftUI import SwiftUI
import CoreData import CoreData
import WidgetKit
struct NewCountdownView : View { struct NewCountdownView : View {
@ -15,8 +16,9 @@ struct NewCountdownView : View {
@Binding var isPresented: Bool @Binding var isPresented: Bool
var body: some View { var body: some View {
CountdownEditView(countdown: Countdown(context: viewContext), isPresented: $isPresented) CountdownEditView(isPresented: $isPresented)
.environment(\.managedObjectContext, viewContext) .environment(\.managedObjectContext, viewContext)
.navigationTitle("New countdown")
} }
} }
@ -26,7 +28,7 @@ struct CountdownEditView : View {
@Environment(\.managedObjectContext) private var viewContext @Environment(\.managedObjectContext) private var viewContext
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
var countdown: Countdown var countdown: Countdown? = nil
@Binding var isPresented: Bool @Binding var isPresented: Bool
@ -44,6 +46,8 @@ struct CountdownEditView : View {
@FetchRequest(sortDescriptors: []) @FetchRequest(sortDescriptors: [])
private var countdowns: FetchedResults<Countdown> private var countdowns: FetchedResults<Countdown>
@State var _isAdding: Bool = false
var body: some View { var body: some View {
NavigationStack { NavigationStack {
@ -66,7 +70,7 @@ struct CountdownEditView : View {
Text("Sound") Text("Sound")
} }
}.onAppear { }.onAppear {
self._initDuration() self._onAppear()
} }
.confirmationDialog("", isPresented: $deleteConfirmationShown, actions: { .confirmationDialog("", isPresented: $deleteConfirmationShown, actions: {
Button("Yes", role: .destructive) { Button("Yes", role: .destructive) {
@ -82,7 +86,7 @@ struct CountdownEditView : View {
Text(error?.localizedDescription ?? "error") Text(error?.localizedDescription ?? "error")
}) })
.toolbar { .toolbar {
if self.countdown.isTemporary { if self._isAdding {
ToolbarItem(placement: .navigationBarLeading) { ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") { Button("Cancel") {
self._cancel() self._cancel()
@ -94,6 +98,7 @@ struct CountdownEditView : View {
self._save() self._save()
} }
} }
if !self._isAdding {
ToolbarItem(placement: .bottomBar) { ToolbarItem(placement: .bottomBar) {
Button { Button {
self.deleteConfirmationShown = true self.deleteConfirmationShown = true
@ -101,6 +106,7 @@ struct CountdownEditView : View {
Image(systemName: "trash") Image(systemName: "trash")
} }
} }
}
ToolbarItemGroup(placement: .keyboard) { ToolbarItemGroup(placement: .keyboard) {
Button { Button {
textFieldIsFocused = false textFieldIsFocused = false
@ -109,15 +115,19 @@ struct CountdownEditView : View {
} }
} }
} }
.navigationTitle("New countdown") .navigationTitle("Edit countdown")
} }
} }
fileprivate func _initDuration() { fileprivate func _onAppear() {
let minutes = Int(self.countdown.duration / 60.0) self._isAdding = (self.countdown == nil)
let seconds = self.countdown.duration - Double(minutes * 60)
if let countdown {
let minutes = Int(countdown.duration / 60.0)
let seconds = countdown.duration - Double(minutes * 60)
if minutes > 0 { if minutes > 0 {
self.minutesString = self._numberFormatter.string(from: NSNumber(value: minutes)) ?? "" self.minutesString = self._numberFormatter.string(from: NSNumber(value: minutes)) ?? ""
@ -127,9 +137,10 @@ struct CountdownEditView : View {
self.secondsString = self._numberFormatter.string(from: NSNumber(value: seconds)) ?? "" self.secondsString = self._numberFormatter.string(from: NSNumber(value: seconds)) ?? ""
} }
if let name = self.countdown.name, !name.isEmpty { if let name = countdown.name, !name.isEmpty {
self.nameString = name self.nameString = name
} }
}
} }
@ -150,29 +161,45 @@ struct CountdownEditView : View {
fileprivate func _save() { 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 cd.duration = self._minutes * 60.0 + self._seconds
if temporary { if self._isAdding {
countdown.order = Int16(self.countdowns.count) cd.order = Int16(self.countdowns.count)
} }
if !self.nameString.isEmpty { if !self.nameString.isEmpty {
countdown.name = self.nameString cd.name = self.nameString
} }
self._saveContext() 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 self.isPresented = false
} else { } else {
dismiss() dismiss()
} }
} }
fileprivate func _delete() { fileprivate func _delete() {
viewContext.delete(self.countdown)
guard let countdown else {
return
}
viewContext.delete(countdown)
self._saveContext() self._saveContext()
self._popOrDismiss()
} }
fileprivate func _saveContext() { fileprivate func _saveContext() {
@ -189,5 +216,6 @@ struct CountdownEditView : View {
struct NewCountdownView_Previews: PreviewProvider { struct NewCountdownView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
NewCountdownView(isPresented: .constant(true)) NewCountdownView(isPresented: .constant(true))
.environment(\.managedObjectContext, PersistenceController.shared.container.viewContext)
} }
} }

Loading…
Cancel
Save