From d05483d117dc16ac76dd90aac991ef6c4d5bcad5 Mon Sep 17 00:00:00 2001 From: Laurent Date: Wed, 5 Apr 2023 12:29:53 +0200 Subject: [PATCH] Navigation changes --- LeCountdown/Views/ContentView.swift | 19 +- .../Views/Countdown/CountdownFormView.swift | 71 +++---- .../Views/Countdown/NewCountdownView.swift | 193 ++++++++++++------ LeCountdown/Views/PresetsView.swift | 54 ++++- .../Views/Reusable/SoundFormView.swift | 86 +++----- LeCountdown/Views/Reusable/TimerModel.swift | 7 +- .../Views/Reusable/View+Extension.swift | 4 + LeCountdown/Views/TimersView.swift | 3 +- LeCountdown/en.lproj/Localizable.strings | 2 +- LeCountdown/fr.lproj/Localizable.strings | 2 +- 10 files changed, 248 insertions(+), 193 deletions(-) diff --git a/LeCountdown/Views/ContentView.swift b/LeCountdown/Views/ContentView.swift index 7205ebf..38af741 100644 --- a/LeCountdown/Views/ContentView.swift +++ b/LeCountdown/Views/ContentView.swift @@ -129,7 +129,8 @@ struct ContentView: View { }) .sheet(isPresented: self.$showAddSheet, content: { NavigationStack { - PresetsView(tabSelection: .constant(0)) + NewCountdownView(isPresented: $showAddSheet, tabSelection: .constant(0)) + .environment(\.managedObjectContext, viewContext) } }) .toolbar { @@ -143,6 +144,15 @@ struct ContentView: View { } } ToolbarItemGroup(placement: .navigationBarTrailing) { + if self.viewContext.count(entityName: "Record") > 0 { + Button { + withAnimation { + self.showStatsSheet.toggle() + } + } label: { + Image(systemName: "chart.bar.doc.horizontal") + } + } Button { withAnimation { self.showSettingsSheet.toggle() @@ -157,13 +167,6 @@ struct ContentView: View { } label: { Image(systemName: "plus") } - Button { - withAnimation { - self.showStatsSheet.toggle() - } - } label: { - Image(systemName: "chart.bar.doc.horizontal") - } } } .onAppear { diff --git a/LeCountdown/Views/Countdown/CountdownFormView.swift b/LeCountdown/Views/Countdown/CountdownFormView.swift index 16d9138..e88124a 100644 --- a/LeCountdown/Views/Countdown/CountdownFormView.swift +++ b/LeCountdown/Views/Countdown/CountdownFormView.swift @@ -7,19 +7,17 @@ import SwiftUI +enum CountdownField: Int, Hashable { + case name + case minutes + case seconds +} struct CountdownFormView : View { - enum CountdownField: Int, Hashable { - case minutes - case seconds - case name - } - - @FocusState private var focusedField: CountdownField? + @FocusState var focusedField: CountdownField? @EnvironmentObject var model: TimerModel - @EnvironmentObject var confirmationModel: TimerModel var secondsBinding: Binding var minutesBinding: Binding @@ -29,12 +27,19 @@ struct CountdownFormView : View { var repeatCountBinding: Binding -// var textFieldIsFocused: FocusState.Binding - var intervalRepeatBinding: Binding? = nil var body: some View { - Form { + + Group { + Section("Name") { + TextField("Name", text: nameBinding) + .focused($focusedField, equals: .name) + .onSubmit { + self.focusNextField($focusedField) + + } + } Section("Duration") { TextField("Minutes", text: minutesBinding) .keyboardType(.numberPad) @@ -45,11 +50,6 @@ struct CountdownFormView : View { TextField("Seconds", text: secondsBinding) .keyboardType(.numberPad) .focused($focusedField, equals: .seconds) - .onSubmit { - self.focusNextField($focusedField) - } - TextField("Name (activates tracking)", text: nameBinding) - .focused($focusedField, equals: .name) .onSubmit { self.focusedField = nil } @@ -59,25 +59,7 @@ struct CountdownFormView : View { model: self.model, imageBinding: imageBinding, repeatCountBinding: repeatCountBinding) - }.toolbar { - ToolbarItemGroup(placement: .keyboard) { - Button { - self.focusedField = nil - } label: { - Image(systemName: "keyboard.chevron.compact.down") - } - Spacer() - Button { - self.focusPreviousField($focusedField) - } label: { - Image(systemName: "chevron.up") - } - Button { - self.focusNextField($focusedField) - } label: { - Image(systemName: "chevron.down") - } - } + } } @@ -89,13 +71,16 @@ struct CountdownFormView_Previews: PreviewProvider { @FocusState static var textFieldIsFocused: Bool static var previews: some View { - CountdownFormView( - secondsBinding: .constant(""), - minutesBinding: .constant(""), - nameBinding: .constant(""), - imageBinding: .constant(.pic3), - repeatCountBinding: .constant(2), - intervalRepeatBinding: .constant(2)) - .environmentObject(TimerModel()) + + Form { + CountdownFormView( + secondsBinding: .constant(""), + minutesBinding: .constant(""), + nameBinding: .constant(""), + imageBinding: .constant(.pic3), + repeatCountBinding: .constant(2), + intervalRepeatBinding: .constant(2)) + .environmentObject(TimerModel()) + } } } diff --git a/LeCountdown/Views/Countdown/NewCountdownView.swift b/LeCountdown/Views/Countdown/NewCountdownView.swift index 2f43ab4..0190545 100644 --- a/LeCountdown/Views/Countdown/NewCountdownView.swift +++ b/LeCountdown/Views/Countdown/NewCountdownView.swift @@ -70,14 +70,18 @@ struct CountdownEditView : View { // @FocusState private var textFieldIsFocused: Bool - @FetchRequest(sortDescriptors: []) - private var timers: FetchedResults +// @FetchRequest(sortDescriptors: []) +// private var timers: FetchedResults @State var _isNewCountdown: Bool = false // false if editing an existing countdown @State var _hasLoaded = false @Environment(\.isPresented) var envIsPresented + @State var shouldScrollToTop: Bool = false + + @FocusState private var focusedField: CountdownField? + init(isPresented: Binding, countdown: Countdown? = nil, tabSelection: Binding? = nil) { _isPresented = isPresented self.countdown = countdown @@ -90,75 +94,118 @@ struct CountdownEditView : View { self.tabSelection = tabSelection } + fileprivate var _formId = "formId" + var body: some View { + NavigationStack { - Rectangle() - .frame(width: 0.0, height: 0.0) - .onChange(of: envIsPresented) { newValue in - if !newValue && !self._isNewCountdown { - self._save() // save when leaving an edit screen - } - } - CountdownFormView( - secondsBinding: $secondsString, - minutesBinding: $minutesString, - nameBinding: $nameString, - imageBinding: $image, - repeatCountBinding: $soundRepeatCount) - .environmentObject(self.model) - .onAppear { - self._onAppear() - } - .confirmationDialog("", isPresented: $deleteConfirmationShown, actions: { - Button("Yes", role: .destructive) { - withAnimation { - self._delete() + ScrollViewReader { reader in + + Rectangle() + .frame(width: 0.0, height: 0.0) + .onChange(of: envIsPresented) { newValue in + if !newValue && !self._isNewCountdown { + self._save() // save when leaving an edit screen + } } - }.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._isNewCountdown { - ToolbarItem(placement: .navigationBarLeading) { - Button("Cancel") { - self._cancel() - } + Form { + + EmptyView().id("anchor") + + CountdownFormView( + focusedField: _focusedField, + secondsBinding: $secondsString, + minutesBinding: $minutesString, + nameBinding: $nameString, + imageBinding: $image, + repeatCountBinding: $soundRepeatCount) + .environmentObject(self.model) + + BasePresetsView { preset in + self._loadPreset(preset) } - ToolbarItem(placement: .navigationBarTrailing) { - Button("Save") { - self._save() + }.toolbar { + ToolbarItemGroup(placement: .keyboard) { + Button { + self.focusedField = nil + } label: { + Image(systemName: "keyboard.chevron.compact.down") + } + Spacer() + Button { + self.focusPreviousField($focusedField) + } label: { + Image(systemName: "chevron.up") } - } - } else { - ToolbarItem(placement: .navigationBarTrailing) { Button { - self.deleteConfirmationShown = true + self.focusNextField($focusedField) } label: { - Image(systemName: "trash") + Image(systemName: "chevron.down") } } } + .onChange(of: self.shouldScrollToTop) { newValue in + withAnimation { + reader.scrollTo("anchor") + } + } + } + .navigationTitle("Edit countdown") } + .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._isNewCountdown { + 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") + } + } + } + } } @@ -185,16 +232,20 @@ struct CountdownEditView : View { let minutes = Int(preset.duration / 60.0) if minutes > 0 { self.minutesString = nf.string(from: NSNumber(value: minutes)) ?? "" + } else { + self.minutesString = "" } let seconds = Int(preset.duration) - minutes * 60 if seconds > 0 { self.secondsString = nf.string(from: NSNumber(value: seconds)) ?? "" + } else { + self.secondsString = "" } self.model.group = preset.intervalGroup - self.model.soundModel.sounds = preset.sound - + self.model.soundModel.loadPreset(preset) + self.shouldScrollToTop.toggle() } fileprivate func _loadCountdown(_ countdown: Countdown) { @@ -235,7 +286,7 @@ struct CountdownEditView : View { } fileprivate func _cancel() { - viewContext.rollback() + self.viewContext.rollback() self.isPresented = false } @@ -251,9 +302,18 @@ struct CountdownEditView : View { cd.duration = self._minutes * 60.0 + self._seconds if self._isNewCountdown { let max: Int16 - if let maxOrder = self.timers.map({ $0.order }).max() { - max = maxOrder + 1 - } else { + + do { + let request = AbstractTimer.fetchRequest() + let timers = try viewContext.fetch(request) + + if let maxOrder = timers.map({ $0.order }).max() { + max = maxOrder + 1 + } else { + max = 0 + } + + } catch { max = 0 } cd.order = max @@ -303,9 +363,7 @@ struct CountdownEditView : View { dismiss() } -// if self.preset != nil { - self.tabSelection?.wrappedValue = 1 -// } + self.tabSelection?.wrappedValue = 1 } @@ -340,7 +398,8 @@ struct CountdownEditView : View { struct NewCountdownView_Previews: PreviewProvider { static var previews: some View { - NewCountdownView(isPresented: .constant(true), tabSelection: .constant(0)) + NewCountdownView(isPresented: .constant(true), + tabSelection: .constant(0)) .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) } } diff --git a/LeCountdown/Views/PresetsView.swift b/LeCountdown/Views/PresetsView.swift index af1c9d5..052b7e9 100644 --- a/LeCountdown/Views/PresetsView.swift +++ b/LeCountdown/Views/PresetsView.swift @@ -9,10 +9,44 @@ import SwiftUI class PresetModel : ObservableObject { - @Published var selectedPreset: Preset = Preset.hardBoiledEggs + @Published var selectedPreset: Preset = Preset.nap } +struct BasePresetsView: View { + + var handler: (Preset) -> () + + var body: some View { + + Section("Preselections") { + + ForEach(PresetSection.allCases) { section in + ForEach(section.presets) { preset in + + Button { + self.handler(preset) + } label: { + TimerItemView(name: preset.localizedName, + duration: preset.formattedDuration, + sound: preset.soundTitle) + } + } + } + } + + } + + fileprivate func _columnCount() -> Int { + return 2 + } + + fileprivate func _columns() -> [GridItem] { + return (0.. { - return Set(SoundCatalog.main.sounds(for: self.playlist)) - } +// var sounds: Set { +// return Set(SoundCatalog.main.sounds(for: self.playlist)) +// } var formattedDuration: String { let group = self.intervalGroup diff --git a/LeCountdown/Views/Reusable/SoundFormView.swift b/LeCountdown/Views/Reusable/SoundFormView.swift index d99c918..fe0f78b 100644 --- a/LeCountdown/Views/Reusable/SoundFormView.swift +++ b/LeCountdown/Views/Reusable/SoundFormView.swift @@ -20,73 +20,37 @@ struct SoundFormView : View { var body: some View { - Group { + Section("Properties") { - Section(header: Text("Properties")) { - - if self.optionalSound != nil { - Toggle("Play sound on end", isOn: optionalSound!) - if self.optionalSound?.wrappedValue == true { - NavigationLink { - PlaylistsView(model: self.model.soundModel, catalog: .ring) - } label: { - Text("Sound") - } + if self.optionalSound != nil { + Toggle("Play sound on end", isOn: optionalSound!) + if self.optionalSound?.wrappedValue == true { + NavigationLink { + PlaylistsView(model: self.model.soundModel, catalog: .ring) + } label: { + Text("Sound") } - - } else { -// Picker(selection: soundBinding) { -// ForEach(Sound.allCases) { sound in -// Text(sound.localizedString).tag(sound) -// } -// } label: { -// Text("Sound") -// } } - - SoundLinkView(soundModel: self.model.soundModel, - catalog: .ring, - title: "Sound") - -// NavigationLink { -// NavigationStack { -// PlaylistsView(model: self.model.soundModel, catalog: .ring) -// } -// } label: { -// HStack { -// Text("Sound") -// Spacer() -// Text(self.model.soundModel.soundSelection) -// } -// } - - if self.repeatCountBinding != nil { - Picker("Repeat Count", selection: self.repeatCountBinding!) { - ForEach(0..<6) { - let count = Int16($0) - Text("\(count)").tag(count) - } - + } + + SoundLinkView(soundModel: self.model.soundModel, + catalog: .ring, + title: "Sound") + + if self.repeatCountBinding != nil { + Picker("Repeat Count", selection: self.repeatCountBinding!) { + ForEach(0..<6) { + let count = Int16($0) + Text("\(count)").tag(count) } + } - - SoundLinkView(soundModel: self.model.confirmationSoundModel, - catalog: .confirmation, - title: "Confirmation Sound") - -// NavigationLink { -// NavigationStack { -// PlaylistsView(model: self.model.confirmationSoundModel, catalog: .confirmation) -// } -// } label: { -// HStack { -// Text("Confirmation Sound") -// Spacer() -// Text(self.confirmationModel.soundSelection) -// } -// } - } + + SoundLinkView(soundModel: self.model.confirmationSoundModel, + catalog: .confirmation, + title: "Confirmation Sound") + }.sheet(isPresented: self.$imageSelectionSheetShown) { ImageSelectionView(showBinding: self.$imageSelectionSheetShown, imageBinding: self.imageBinding) } diff --git a/LeCountdown/Views/Reusable/TimerModel.swift b/LeCountdown/Views/Reusable/TimerModel.swift index de974e4..5602654 100644 --- a/LeCountdown/Views/Reusable/TimerModel.swift +++ b/LeCountdown/Views/Reusable/TimerModel.swift @@ -32,6 +32,12 @@ class SoundModel: ObservableObject, SoundHolder { } } + func loadPreset(_ preset: Preset) { + self.playlists.removeAll() + self.sounds.removeAll() + self.selectPlaylist(preset.playlist, selected: true) + } + func setPlayables(_ playables: [any Playable]) { for playable in playables { switch playable { @@ -104,7 +110,6 @@ class SoundModel: ObservableObject, SoundHolder { } func selectPlaylist(_ playlist: Playlist, selected: Bool) { - let sounds = SoundCatalog.main.sounds(for: playlist) if selected { self.playlists.insert(playlist) diff --git a/LeCountdown/Views/Reusable/View+Extension.swift b/LeCountdown/Views/Reusable/View+Extension.swift index 601b963..6b01329 100644 --- a/LeCountdown/Views/Reusable/View+Extension.swift +++ b/LeCountdown/Views/Reusable/View+Extension.swift @@ -30,6 +30,8 @@ extension View { let nextValue = currentValue.rawValue + 1 if let newValue = F.init(rawValue: nextValue) { field.wrappedValue = newValue + } else { + field.wrappedValue = nil } } @@ -54,6 +56,8 @@ extension View { let nextValue = currentValue.rawValue - 1 if let newValue = F.init(rawValue: nextValue) { field.wrappedValue = newValue + } else { + field.wrappedValue = nil } } } diff --git a/LeCountdown/Views/TimersView.swift b/LeCountdown/Views/TimersView.swift index 44a0945..8873887 100644 --- a/LeCountdown/Views/TimersView.swift +++ b/LeCountdown/Views/TimersView.swift @@ -68,7 +68,8 @@ struct TimersView: View { } .padding(.horizontal, itemSpacing) } else { - Text("You'll find your timers here. Start by creating them on the left screen").multilineTextAlignment(.center) + Text("You'll find your timers here. Start by creating them on the left screen") + .multilineTextAlignment(.center) } } diff --git a/LeCountdown/en.lproj/Localizable.strings b/LeCountdown/en.lproj/Localizable.strings index 71157ac..4d12c33 100644 --- a/LeCountdown/en.lproj/Localizable.strings +++ b/LeCountdown/en.lproj/Localizable.strings @@ -1 +1 @@ -"You'll find your timers here. Start by creating them on the left screen" = "You'll find your timers here.\nStart by creating them on the left screen"; +"You'll find your timers here. Start by creating them on the left screen" = "You'll find your timers here.\nStart by creating using the top right button"; diff --git a/LeCountdown/fr.lproj/Localizable.strings b/LeCountdown/fr.lproj/Localizable.strings index 748947d..9dd86e7 100644 --- a/LeCountdown/fr.lproj/Localizable.strings +++ b/LeCountdown/fr.lproj/Localizable.strings @@ -247,7 +247,7 @@ "Timer %@ started" = "Le minuteur %@ a démarré"; "The timer has not been found in the app" = "Le minuteur n'a pas été trouvé dans l'app"; "In-app purchase disabled" = "Les achats in-app sont désactivés. Veuillez les activer si vous souhaitez vous abonner"; -"You'll find your timers here. Start by creating them on the left screen" = "Vous retrouverez vos minuteurs ici.\nCommencez par en créer dans l'écran de gauche"; +"You'll find your timers here. Start by creating them on the left screen" = "Vous retrouverez vos minuteurs ici.\nCommencez par en créer un avec le bouton en haut à gauche"; "Settings" = "Réglages"; "Pasta" = "Pasta"; "Rice" = "Riz";