From 7ccb37479d3de49ffb3cd892be5fd329fc2abb36 Mon Sep 17 00:00:00 2001 From: Laurent Date: Wed, 1 Feb 2023 15:06:47 +0100 Subject: [PATCH] Adds stopwatch and alarm data types --- LeCountdown.xcodeproj/project.pbxproj | 42 ++- .../.xccurrentversion | 2 +- .../LeCountdown.0.3.xcdatamodel/contents | 33 +++ LeCountdown/Views/ContentView.swift | 33 ++- LeCountdown/Views/Data/AlarmFormView.swift | 32 +++ .../Views/{ => Data}/CountdownFormView.swift | 40 +-- .../Views/{ => Data}/ImageSelectionView.swift | 0 LeCountdown/Views/Data/NewAlarmView.swift | 247 ++++++++++++++++++ .../Views/{ => Data}/NewCountdownView.swift | 0 LeCountdown/Views/Data/NewStopwatchView.swift | 235 +++++++++++++++++ .../Views/Data/SoundImageFormView.swift | 89 +++++++ .../Views/Data/StopwatchFormView.swift | 44 ++++ LeCountdown/Views/DialView.swift | 43 --- LeCountdown/Widget/IntentDataProvider.swift | 3 +- 14 files changed, 751 insertions(+), 92 deletions(-) create mode 100644 LeCountdown/Model/LeCountdown.xcdatamodeld/LeCountdown.0.3.xcdatamodel/contents create mode 100644 LeCountdown/Views/Data/AlarmFormView.swift rename LeCountdown/Views/{ => Data}/CountdownFormView.swift (51%) rename LeCountdown/Views/{ => Data}/ImageSelectionView.swift (100%) create mode 100644 LeCountdown/Views/Data/NewAlarmView.swift rename LeCountdown/Views/{ => Data}/NewCountdownView.swift (100%) create mode 100644 LeCountdown/Views/Data/NewStopwatchView.swift create mode 100644 LeCountdown/Views/Data/SoundImageFormView.swift create mode 100644 LeCountdown/Views/Data/StopwatchFormView.swift delete mode 100644 LeCountdown/Views/DialView.swift diff --git a/LeCountdown.xcodeproj/project.pbxproj b/LeCountdown.xcodeproj/project.pbxproj index e49492d..a5ce0db 100644 --- a/LeCountdown.xcodeproj/project.pbxproj +++ b/LeCountdown.xcodeproj/project.pbxproj @@ -63,11 +63,15 @@ C4742B5B298414B000D5D950 /* ImageSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4742B5A298414B000D5D950 /* ImageSelectionView.swift */; }; C4742B5F2984205000D5D950 /* ViewModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4742B5E2984205000D5D950 /* ViewModifiers.swift */; }; C4F8B1532987FE6F005C86A5 /* LaunchWidgetLiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C7D72981216200BF3EF9 /* LaunchWidgetLiveActivity.swift */; }; - C4F8B1552988751B005C86A5 /* DialView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8B1542988751B005C86A5 /* DialView.swift */; }; C4F8B15729891271005C86A5 /* Conductor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8B15629891271005C86A5 /* Conductor.swift */; }; C4F8B15929891528005C86A5 /* forest_stream.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = C4F8B15829891528005C86A5 /* forest_stream.mp3 */; }; C4F8B15B29892D40005C86A5 /* SoundPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C445FA8E2987B83B0054D761 /* SoundPlayer.swift */; }; C4F8B15F298961A7005C86A5 /* ReorderableForEach.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8B15E298961A7005C86A5 /* ReorderableForEach.swift */; }; + C4F8B162298A9A1F005C86A5 /* NewAlarmView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8B161298A9A1F005C86A5 /* NewAlarmView.swift */; }; + C4F8B164298A9A92005C86A5 /* AlarmFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8B163298A9A92005C86A5 /* AlarmFormView.swift */; }; + C4F8B166298A9ABB005C86A5 /* SoundImageFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8B165298A9ABB005C86A5 /* SoundImageFormView.swift */; }; + C4F8B169298AA236005C86A5 /* StopwatchFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8B168298AA236005C86A5 /* StopwatchFormView.swift */; }; + C4F8B16B298AA240005C86A5 /* NewStopwatchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8B16A298AA240005C86A5 /* NewStopwatchView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -165,10 +169,15 @@ C4742B58298411E800D5D950 /* CountdownFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountdownFormView.swift; sourceTree = ""; }; C4742B5A298414B000D5D950 /* ImageSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSelectionView.swift; sourceTree = ""; }; C4742B5E2984205000D5D950 /* ViewModifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModifiers.swift; sourceTree = ""; }; - C4F8B1542988751B005C86A5 /* DialView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialView.swift; sourceTree = ""; }; C4F8B15629891271005C86A5 /* Conductor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Conductor.swift; sourceTree = ""; }; C4F8B15829891528005C86A5 /* forest_stream.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = forest_stream.mp3; sourceTree = ""; }; C4F8B15E298961A7005C86A5 /* ReorderableForEach.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReorderableForEach.swift; sourceTree = ""; }; + C4F8B160298A90E8005C86A5 /* LeCountdown.0.3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = LeCountdown.0.3.xcdatamodel; sourceTree = ""; }; + C4F8B161298A9A1F005C86A5 /* NewAlarmView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewAlarmView.swift; sourceTree = ""; }; + C4F8B163298A9A92005C86A5 /* AlarmFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmFormView.swift; sourceTree = ""; }; + C4F8B165298A9ABB005C86A5 /* SoundImageFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundImageFormView.swift; sourceTree = ""; }; + C4F8B168298AA236005C86A5 /* StopwatchFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StopwatchFormView.swift; sourceTree = ""; }; + C4F8B16A298AA240005C86A5 /* NewStopwatchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewStopwatchView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -352,12 +361,9 @@ C438C80B2981DE2E00BF3EF9 /* Views */ = { isa = PBXGroup; children = ( + C4F8B167298A9D91005C86A5 /* Data */, C4060DC1297AE73B003FAB80 /* ContentView.swift */, - C4742B58298411E800D5D950 /* CountdownFormView.swift */, - C4742B5A298414B000D5D950 /* ImageSelectionView.swift */, - C4060DF6297AFEF2003FAB80 /* NewCountdownView.swift */, C438C80E29828B8600BF3EF9 /* RecordsView.swift */, - C4F8B1542988751B005C86A5 /* DialView.swift */, C4F8B15E298961A7005C86A5 /* ReorderableForEach.swift */, ); path = Views; @@ -381,6 +387,21 @@ path = Sound_Assets; sourceTree = ""; }; + C4F8B167298A9D91005C86A5 /* Data */ = { + isa = PBXGroup; + children = ( + C4742B58298411E800D5D950 /* CountdownFormView.swift */, + C4742B5A298414B000D5D950 /* ImageSelectionView.swift */, + C4060DF6297AFEF2003FAB80 /* NewCountdownView.swift */, + C4F8B161298A9A1F005C86A5 /* NewAlarmView.swift */, + C4F8B163298A9A92005C86A5 /* AlarmFormView.swift */, + C4F8B165298A9ABB005C86A5 /* SoundImageFormView.swift */, + C4F8B168298AA236005C86A5 /* StopwatchFormView.swift */, + C4F8B16A298AA240005C86A5 /* NewStopwatchView.swift */, + ); + path = Data; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -577,7 +598,6 @@ C4060DC9297AE73D003FAB80 /* Persistence.swift in Sources */, C438C80F29828B8600BF3EF9 /* RecordsView.swift in Sources */, C438C80D2982847300BF3EF9 /* CoreDataRequests.swift in Sources */, - C4F8B1552988751B005C86A5 /* DialView.swift in Sources */, C4742B5F2984205000D5D950 /* ViewModifiers.swift in Sources */, C4F8B15729891271005C86A5 /* Conductor.swift in Sources */, C438C81129829EAF00BF3EF9 /* PropertyWrappers.swift in Sources */, @@ -589,10 +609,15 @@ C438C7C12980228B00BF3EF9 /* CountdownScheduler.swift in Sources */, C445FA8F2987B83B0054D761 /* SoundPlayer.swift in Sources */, C438C7C929803CA000BF3EF9 /* AppDelegate.swift in Sources */, + C4F8B169298AA236005C86A5 /* StopwatchFormView.swift in Sources */, C4F8B1532987FE6F005C86A5 /* LaunchWidgetLiveActivity.swift in Sources */, + C4F8B16B298AA240005C86A5 /* NewStopwatchView.swift in Sources */, C4060DF5297AE9A7003FAB80 /* TimeInterval+Extensions.swift in Sources */, + C4F8B166298A9ABB005C86A5 /* SoundImageFormView.swift in Sources */, + C4F8B164298A9A92005C86A5 /* AlarmFormView.swift in Sources */, C4060DF7297AFEF2003FAB80 /* NewCountdownView.swift in Sources */, C4060DCC297AE73D003FAB80 /* LeCountdown.xcdatamodeld in Sources */, + C4F8B162298A9A1F005C86A5 /* NewAlarmView.swift in Sources */, C4742B5B298414B000D5D950 /* ImageSelectionView.swift in Sources */, C438C7C5298024E900BF3EF9 /* NSManagedContext+Extensions.swift in Sources */, C4F8B15F298961A7005C86A5 /* ReorderableForEach.swift in Sources */, @@ -1106,11 +1131,12 @@ C4060DCA297AE73D003FAB80 /* LeCountdown.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + C4F8B160298A90E8005C86A5 /* LeCountdown.0.3.xcdatamodel */, C445FA902987C0CF0054D761 /* LeCountdown.0.2.xcdatamodel */, C418A14F298428CB00C22230 /* LeCountdown.0.1.xcdatamodel */, C4060DCB297AE73D003FAB80 /* LeCountdown.xcdatamodel */, ); - currentVersion = C445FA902987C0CF0054D761 /* LeCountdown.0.2.xcdatamodel */; + currentVersion = C4F8B160298A90E8005C86A5 /* LeCountdown.0.3.xcdatamodel */; path = LeCountdown.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/LeCountdown/Model/LeCountdown.xcdatamodeld/.xccurrentversion b/LeCountdown/Model/LeCountdown.xcdatamodeld/.xccurrentversion index 1dcfe8d..538b004 100644 --- a/LeCountdown/Model/LeCountdown.xcdatamodeld/.xccurrentversion +++ b/LeCountdown/Model/LeCountdown.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - LeCountdown.0.2.xcdatamodel + LeCountdown.0.3.xcdatamodel diff --git a/LeCountdown/Model/LeCountdown.xcdatamodeld/LeCountdown.0.3.xcdatamodel/contents b/LeCountdown/Model/LeCountdown.xcdatamodeld/LeCountdown.0.3.xcdatamodel/contents new file mode 100644 index 0000000..37e2357 --- /dev/null +++ b/LeCountdown/Model/LeCountdown.xcdatamodeld/LeCountdown.0.3.xcdatamodel/contents @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LeCountdown/Views/ContentView.swift b/LeCountdown/Views/ContentView.swift index 5d671e2..2fe37c1 100644 --- a/LeCountdown/Views/ContentView.swift +++ b/LeCountdown/Views/ContentView.swift @@ -62,7 +62,9 @@ struct ContentView: View { fileprivate let itemSpacing: CGFloat = 10.0 @State private var isShowingNewCountdown = false - + @State private var isShowingNewStopwatch = false + @State private var isShowingNewAlarm = false + @State var error: Error? @State var showDefaultAlert: Bool = false @State var showPermissionAlert: Bool = false @@ -135,13 +137,40 @@ struct ContentView: View { .sheet(isPresented: self.$isShowingNewCountdown, content: { NewCountdownView(isPresented: $isShowingNewCountdown) .environment(\.managedObjectContext, viewContext) }) + .sheet(isPresented: self.$isShowingNewStopwatch, content: { + NewStopwatchView(isPresented: $isShowingNewStopwatch) .environment(\.managedObjectContext, viewContext) + }) + .sheet(isPresented: self.$isShowingNewAlarm, content: { + NewAlarmView(isPresented: $isShowingNewAlarm) .environment(\.managedObjectContext, viewContext) + }) + .toolbar { ToolbarItemGroup(placement: .bottomBar) { Button { self.isShowingNewCountdown = true } label: { - Image(systemName: "plus") + HStack { + Image(systemName: "timer") + Text("countdown") + } } + Button { + self.isShowingNewStopwatch = true + } label: { + HStack { + Image(systemName: "stopwatch") + Text("stopwatch") + } + } + Button { + self.isShowingNewAlarm = true + } label: { + HStack { + Image(systemName: "alarm") + Text("alarm") + } + } + } ToolbarItem(placement: .navigationBarTrailing) { NavigationLink { diff --git a/LeCountdown/Views/Data/AlarmFormView.swift b/LeCountdown/Views/Data/AlarmFormView.swift new file mode 100644 index 0000000..3120153 --- /dev/null +++ b/LeCountdown/Views/Data/AlarmFormView.swift @@ -0,0 +1,32 @@ +// +// AlarmFormView.swift +// LeCountdown +// +// Created by Laurent Morvillier on 01/02/2023. +// + +import SwiftUI + +struct AlarmFormView: View { + + var dateBinding: Binding + + var imageBinding: Binding + var soundBinding: Binding + var repeatsBinding: Binding + + var body: some View { + + Form { + DatePicker("Time", selection: dateBinding, displayedComponents: .hourAndMinute) + SoundImageFormView(imageBinding: imageBinding, soundBinding: soundBinding, repeatsBinding: nil) + } + } +} + +struct AlarmFormView_Previews: PreviewProvider { + static var previews: some View { + AlarmFormView(dateBinding: .constant(Date()), + imageBinding: .constant(.pic1), soundBinding: .constant(.trainhorn), repeatsBinding: .constant(true)) + } +} diff --git a/LeCountdown/Views/CountdownFormView.swift b/LeCountdown/Views/Data/CountdownFormView.swift similarity index 51% rename from LeCountdown/Views/CountdownFormView.swift rename to LeCountdown/Views/Data/CountdownFormView.swift index c60492f..120e58c 100644 --- a/LeCountdown/Views/CountdownFormView.swift +++ b/LeCountdown/Views/Data/CountdownFormView.swift @@ -20,8 +20,6 @@ struct CountdownFormView : View { var textFieldIsFocused: FocusState.Binding - @State var imageSelectionSheetShown: Bool = false - var body: some View { Form { Section(header: Text("Duration")) { @@ -36,42 +34,10 @@ struct CountdownFormView : View { TextField("name", text: nameBinding) .focused(textFieldIsFocused) } - - Section(header: Text("Properties")) { - - Picker(selection: soundBinding) { - ForEach(Sound.allCases) { sound in - Text(sound.localizedString).tag(sound) - } - } label: { - Text("Sound") - } - Toggle("Sound repeats", isOn: repeatsBinding) - } - - Section(header: Text("Background")) { - - Button { - self.imageSelectionSheetShown = true - } label: { - Group { - if let image = self.imageBinding.wrappedValue { - Image(image.rawValue).resizable() - } else { - Image(imageBinding.wrappedValue.rawValue).resizable() - } - } - .font(Font.system(size: 90.0)) - .aspectRatio(1, contentMode: .fit) - .frame(width: 100.0, height: 100.0) - .cornerRadius(20.0) - } - - } - - }.sheet(isPresented: self.$imageSelectionSheetShown) { - ImageSelectionView(showBinding: self.$imageSelectionSheetShown, imageBinding: self.imageBinding) + SoundImageFormView(imageBinding: imageBinding, + soundBinding: soundBinding, + repeatsBinding: repeatsBinding) } } diff --git a/LeCountdown/Views/ImageSelectionView.swift b/LeCountdown/Views/Data/ImageSelectionView.swift similarity index 100% rename from LeCountdown/Views/ImageSelectionView.swift rename to LeCountdown/Views/Data/ImageSelectionView.swift diff --git a/LeCountdown/Views/Data/NewAlarmView.swift b/LeCountdown/Views/Data/NewAlarmView.swift new file mode 100644 index 0000000..47a93bb --- /dev/null +++ b/LeCountdown/Views/Data/NewAlarmView.swift @@ -0,0 +1,247 @@ +// +// NewAlarmView.swift +// LeCountdown +// +// Created by Laurent Morvillier on 01/02/2023. +// + +import SwiftUI +import CoreData +import WidgetKit + +struct NewAlarmView: View { + + @Environment(\.managedObjectContext) private var viewContext + + @Binding var isPresented: Bool + + var body: some View { + EditAlarmView(isPresented: $isPresented) + .environment(\.managedObjectContext, viewContext) + .navigationTitle("New countdown") + } + +} + +struct EditAlarmView: View { + + @Environment(\.managedObjectContext) private var viewContext + @Environment(\.dismiss) private var dismiss + + var alarm: Alarm? = nil + + @Binding var isPresented: Bool + + @State var time: Date = Date() + + @State var nameString: String = "" + + @State var sound: Sound = .trainhorn + @State var soundRepeats: Bool = true + @State var image: CoolPic = .pic1 + + @State var deleteConfirmationShown: Bool = false + @State var activityNameConfirmationShown: Bool = false + @State fileprivate var _rename: Bool? = nil + + @State var errorShown: Bool = false + @State var error: Error? = nil + + @FocusState private var textFieldIsFocused: Bool + +// @FetchRequest(sortDescriptors: []) +// private var countdowns: FetchedResults + + @State var _isAdding: Bool = false + + @Environment(\.isPresented) var envIsPresented + + var body: some View { + NavigationStack { + Rectangle() + .frame(width: 0.0, height: 0.0) + .onChange(of: envIsPresented) { newValue in + if !newValue && !self._isAdding { + self._save() + } + } + AlarmFormView(dateBinding: self.$time, + imageBinding: self.$image, + soundBinding: self.$sound, + repeatsBinding: self.$soundRepeats) + .onAppear { + self._onAppear() + } + .confirmationDialog("", isPresented: $deleteConfirmationShown, actions: { + Button("Yes", role: .destructive) { + withAnimation { + self._delete() + } + }.keyboardShortcut(.defaultAction) + Button("No", role: .cancel) {} + }, message: { + Text("Do you really want to delete?") + }) + .confirmationDialog("", isPresented: $activityNameConfirmationShown, actions: { + Button("Rename") { + self._rename = true + self._save() + } + Button("New activity") { + self._rename = false + self._save() + } + }, message: { + Text("Do you wish to rename or create a new activity") + }) + + .alert("", isPresented: $errorShown, actions: { }, message: { + Text(error?.localizedDescription ?? "error") + }) + .toolbar { + if self._isAdding { + ToolbarItem(placement: .navigationBarLeading) { + Button("Cancel") { + self._cancel() + } + } + ToolbarItem(placement: .navigationBarTrailing) { + Button("Save") { + self._save() + } + } + } else { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + self.deleteConfirmationShown = true + } label: { + Image(systemName: "trash") + } + } + } + ToolbarItemGroup(placement: .keyboard) { + Button { + textFieldIsFocused = false + } label: { + Image(systemName: "checkmark") + } + } + } + .navigationTitle("Edit countdown") + } + + } + + fileprivate func _onAppear() { + + self._isAdding = (self.alarm == nil) + + if let alarm { + + if let fireDate = alarm.fireDate { + self.time = fireDate + } + + if let name = alarm.activity?.name, !name.isEmpty { + self.nameString = name + } + + if let sound = Sound(rawValue: Int(alarm.sound)) { + self.sound = sound + } + + if let image = alarm.image, let coolpic = CoolPic(rawValue: image) { + self.image = coolpic + } + } + + } + fileprivate func _cancel() { + viewContext.rollback() + self.isPresented = false + } + + fileprivate func _save() { + + let a: Alarm + if let alarm { + a = alarm + } else { + a = Alarm(context: viewContext) + } + +// if self._isAdding { +// let max = self.countdowns.map { $0.order }.max() ?? 0 +// cd.order = max + 1 +// } + + a.fireDate = self.time + a.image = self.image.rawValue + a.sound = Int16(self.sound.rawValue) + a.repeats = true + + if !self.nameString.isEmpty { + + if let activity = a.activity, let currentActivityName = activity.name, self.nameString != currentActivityName { + + switch self._rename { + case .none: + self.activityNameConfirmationShown = true + return + case .some(let rename): + if rename { + activity.name = self.nameString + } else { + a.activity = CoreDataRequests.getOrCreateActivity(name: self.nameString) + } + } + } else { + a.activity = CoreDataRequests.getOrCreateActivity(name: self.nameString) + } + + } + + self._saveContext() + + WidgetCenter.shared.reloadAllTimelines() // refreshes the visual of existing widgets + + self._popOrDismiss() + } + + fileprivate func _popOrDismiss() { + if self._isAdding { + self.isPresented = false + } else { + dismiss() + } + } + + fileprivate func _delete() { + + guard let alarm else { + return + } + + viewContext.delete(alarm) + self._saveContext() + + WidgetCenter.shared.reloadAllTimelines() // refreshes the visual of existing widgets + + self._popOrDismiss() + } + + fileprivate func _saveContext() { + do { + try viewContext.save() + } catch { + self.errorShown = true + self.error = error + } + } +} + +struct NewAlarmView_Previews: PreviewProvider { + static var previews: some View { + NewAlarmView(isPresented: .constant(true)) + } +} diff --git a/LeCountdown/Views/NewCountdownView.swift b/LeCountdown/Views/Data/NewCountdownView.swift similarity index 100% rename from LeCountdown/Views/NewCountdownView.swift rename to LeCountdown/Views/Data/NewCountdownView.swift diff --git a/LeCountdown/Views/Data/NewStopwatchView.swift b/LeCountdown/Views/Data/NewStopwatchView.swift new file mode 100644 index 0000000..d36758b --- /dev/null +++ b/LeCountdown/Views/Data/NewStopwatchView.swift @@ -0,0 +1,235 @@ +// +// NewStopwatchView.swift +// LeCountdown +// +// Created by Laurent Morvillier on 01/02/2023. +// + +import SwiftUI +import CoreData +import WidgetKit + +struct NewStopwatchView: View { + + @Environment(\.managedObjectContext) private var viewContext + @Environment(\.dismiss) private var dismiss + + var stopwatch: Stopwatch? = nil + + @Binding var isPresented: Bool + + @State var time: Date = Date() + + @State var nameString: String = "" + + @State var playSound: Bool = false + @State var sound: Sound = .trainhorn + @State var image: CoolPic = .pic1 + + @State var deleteConfirmationShown: Bool = false + @State var activityNameConfirmationShown: Bool = false + @State fileprivate var _rename: Bool? = nil + + @State var errorShown: Bool = false + @State var error: Error? = nil + + @FocusState private var textFieldIsFocused: Bool + +// @FetchRequest(sortDescriptors: []) +// private var countdowns: FetchedResults + + @State var _isAdding: Bool = false + + @Environment(\.isPresented) var envIsPresented + + var body: some View { + NavigationStack { + Rectangle() + .frame(width: 0.0, height: 0.0) + .onChange(of: envIsPresented) { newValue in + if !newValue && !self._isAdding { + self._save() + } + } + StopwatchFormView(nameBinding: self.$nameString, + imageBinding: self.$image, + soundBinding: self.$sound, playSoundBinding: self.$playSound, + textFieldIsFocused: $textFieldIsFocused) + .onAppear { + self._onAppear() + } + .confirmationDialog("", isPresented: $deleteConfirmationShown, actions: { + Button("Yes", role: .destructive) { + withAnimation { + self._delete() + } + }.keyboardShortcut(.defaultAction) + Button("No", role: .cancel) {} + }, message: { + Text("Do you really want to delete?") + }) + .confirmationDialog("", isPresented: $activityNameConfirmationShown, actions: { + Button("Rename") { + self._rename = true + self._save() + } + Button("New activity") { + self._rename = false + self._save() + } + }, message: { + Text("Do you wish to rename or create a new activity") + }) + + .alert("", isPresented: $errorShown, actions: { }, message: { + Text(error?.localizedDescription ?? "error") + }) + .toolbar { + if self._isAdding { + ToolbarItem(placement: .navigationBarLeading) { + Button("Cancel") { + self._cancel() + } + } + ToolbarItem(placement: .navigationBarTrailing) { + Button("Save") { + self._save() + } + } + } else { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + self.deleteConfirmationShown = true + } label: { + Image(systemName: "trash") + } + } + } + ToolbarItemGroup(placement: .keyboard) { + Button { + textFieldIsFocused = false + } label: { + Image(systemName: "checkmark") + } + } + } + .navigationTitle("Edit countdown") + } + + } + + fileprivate func _onAppear() { + + self._isAdding = (self.stopwatch == nil) + + if let stopwatch { + + if let name = stopwatch.activity?.name, !name.isEmpty { + self.nameString = name + } + + if let sound = Sound(rawValue: Int(stopwatch.sound)) { + self.sound = sound + } + + if let image = stopwatch.image, let coolpic = CoolPic(rawValue: image) { + self.image = coolpic + } + } + + } + + fileprivate func _cancel() { + viewContext.rollback() + self.isPresented = false + } + + fileprivate func _save() { + + let sw: Stopwatch + if let stopwatch { + sw = stopwatch + } else { + sw = Stopwatch(context: viewContext) + } + +// if self._isAdding { +// let max = self.countdowns.map { $0.order }.max() ?? 0 +// cd.order = max + 1 +// } + + sw.image = self.image.rawValue + + if self.playSound { + sw.sound = Int16(self.sound.rawValue) + } else { +// sw.sound = nil + } + + if !self.nameString.isEmpty { + + if let activity = sw.activity, let currentActivityName = activity.name, self.nameString != currentActivityName { + + switch self._rename { + case .none: + self.activityNameConfirmationShown = true + return + case .some(let rename): + if rename { + activity.name = self.nameString + } else { + sw.activity = CoreDataRequests.getOrCreateActivity(name: self.nameString) + } + } + } else { + sw.activity = CoreDataRequests.getOrCreateActivity(name: self.nameString) + } + + } + + self._saveContext() + + WidgetCenter.shared.reloadAllTimelines() // refreshes the visual of existing widgets + + self._popOrDismiss() + } + + fileprivate func _popOrDismiss() { + if self._isAdding { + self.isPresented = false + } else { + dismiss() + } + } + + fileprivate func _delete() { + + guard let stopwatch else { + return + } + + viewContext.delete(stopwatch) + self._saveContext() + + WidgetCenter.shared.reloadAllTimelines() // refreshes the visual of existing widgets + + self._popOrDismiss() + } + + fileprivate func _saveContext() { + do { + try viewContext.save() + } catch { + self.errorShown = true + self.error = error + } + } + +} + +struct NewStopwatchView_Previews: PreviewProvider { + static var previews: some View { + NewStopwatchView(isPresented: .constant(true)) + .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) + } +} diff --git a/LeCountdown/Views/Data/SoundImageFormView.swift b/LeCountdown/Views/Data/SoundImageFormView.swift new file mode 100644 index 0000000..0c964e3 --- /dev/null +++ b/LeCountdown/Views/Data/SoundImageFormView.swift @@ -0,0 +1,89 @@ +// +// SoundImageFormView.swift +// LeCountdown +// +// Created by Laurent Morvillier on 01/02/2023. +// + +import SwiftUI + +struct SoundImageFormView : View { + + var imageBinding: Binding + var soundBinding: Binding + var repeatsBinding: Binding? = nil + var optionalSound: Binding? = nil + + @State var imageSelectionSheetShown: Bool = false + + var body: some View { + + Group { + + Section(header: Text("Properties")) { + + if self.optionalSound != nil { + + Toggle("Play sound on end", isOn: optionalSound!) + + if self.optionalSound?.wrappedValue == true { + Picker(selection: soundBinding) { + ForEach(Sound.allCases) { sound in + Text(sound.localizedString).tag(sound) + } + } label: { + Text("Sound") + } + } + + } else { + Picker(selection: soundBinding) { + ForEach(Sound.allCases) { sound in + Text(sound.localizedString).tag(sound) + } + } label: { + Text("Sound") + } + } + + if self.repeatsBinding != nil { + Toggle("Sound repeats", isOn: repeatsBinding!) + } + } + + Section(header: Text("Background")) { + + Button { + self.imageSelectionSheetShown = true + } label: { + Group { + if let image = self.imageBinding.wrappedValue { + Image(image.rawValue).resizable() + } else { + Image(imageBinding.wrappedValue.rawValue).resizable() + } + } + .font(Font.system(size: 90.0)) + .aspectRatio(1, contentMode: .fit) + .frame(width: 100.0, height: 100.0) + .cornerRadius(20.0) + + } + + } + }.sheet(isPresented: self.$imageSelectionSheetShown) { + ImageSelectionView(showBinding: self.$imageSelectionSheetShown, imageBinding: self.imageBinding) + } + } + +} + +struct SoundImageFormView_Previews: PreviewProvider { + static var previews: some View { + Form { + SoundImageFormView(imageBinding: .constant(.pic1), + soundBinding: .constant(.trainhorn), + repeatsBinding: .constant(true)) + } + } +} diff --git a/LeCountdown/Views/Data/StopwatchFormView.swift b/LeCountdown/Views/Data/StopwatchFormView.swift new file mode 100644 index 0000000..ddfc404 --- /dev/null +++ b/LeCountdown/Views/Data/StopwatchFormView.swift @@ -0,0 +1,44 @@ +// +// StopwatchFormView.swift +// LeCountdown +// +// Created by Laurent Morvillier on 01/02/2023. +// + +import SwiftUI + +struct StopwatchFormView: View { + + var nameBinding: Binding + var imageBinding: Binding + var soundBinding: Binding + var playSoundBinding: Binding + + var textFieldIsFocused: FocusState.Binding + + var body: some View { + + Form { + Section(header: Text("Name for tracking the activity")) { + TextField("name", text: nameBinding) + .focused(textFieldIsFocused) + } + + SoundImageFormView(imageBinding: imageBinding, + soundBinding: soundBinding, + optionalSound: playSoundBinding) + + } + } +} + +struct StopwatchFormView_Previews: PreviewProvider { + + @FocusState static var textFieldIsFocused: Bool + + static var previews: some View { + StopwatchFormView(nameBinding: .constant(""), + imageBinding: .constant(.pic1), + soundBinding: .constant(.trainhorn), playSoundBinding: .constant(true), textFieldIsFocused: $textFieldIsFocused) + } +} diff --git a/LeCountdown/Views/DialView.swift b/LeCountdown/Views/DialView.swift deleted file mode 100644 index 3638a20..0000000 --- a/LeCountdown/Views/DialView.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// DialView.swift -// LeCountdown -// -// Created by Laurent Morvillier on 30/01/2023. -// - -import SwiftUI - -struct DialView: View { - - @EnvironmentObject var environment: Conductor - - var name: String - var duration: String - - var body: some View { - - VStack { - HStack { - Text(name.uppercased()).monospaced() - Spacer() - } - HStack { - Text(duration).monospaced() - Spacer() - } - Spacer() - }.padding() - .frame(width: 200, height: 200) - .foregroundColor(.white) - .background(Color.cyan) - .cornerRadius(32.0) - } - -} - -struct DialView_Previews: PreviewProvider { - - static var previews: some View { - DialView(name: "Running", duration: "2:00").environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) - } -} diff --git a/LeCountdown/Widget/IntentDataProvider.swift b/LeCountdown/Widget/IntentDataProvider.swift index 7e87b87..522a1a9 100644 --- a/LeCountdown/Widget/IntentDataProvider.swift +++ b/LeCountdown/Widget/IntentDataProvider.swift @@ -6,6 +6,7 @@ // import Foundation +import CoreData class IntentDataProvider { @@ -13,7 +14,7 @@ class IntentDataProvider { func countdowns() throws -> [Countdown] { let context = PersistenceController.shared.container.viewContext - let request = Countdown.fetchRequest() + let request: NSFetchRequest = Countdown.fetchRequest() request.sortDescriptors = [NSSortDescriptor(keyPath: (\Countdown.order), ascending: true)] return try context.fetch(request) }