diff --git a/LeCountdown.xcodeproj/project.pbxproj b/LeCountdown.xcodeproj/project.pbxproj index 030aae5..8e152be 100644 --- a/LeCountdown.xcodeproj/project.pbxproj +++ b/LeCountdown.xcodeproj/project.pbxproj @@ -109,6 +109,7 @@ C4BA2B23299BE82E00CB4FBA /* AbstractSoundTimer+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2AEF2996A11900CB4FBA /* AbstractSoundTimer+CoreDataProperties.swift */; }; C4BA2B25299D35C100CB4FBA /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B24299D35C100CB4FBA /* HomeView.swift */; }; C4BA2B2D299E2DEE00CB4FBA /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B2C299E2DEE00CB4FBA /* Preferences.swift */; }; + C4BA2B2F299E69A000CB4FBA /* View+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B2E299E69A000CB4FBA /* View+Extension.swift */; }; C4F8B1532987FE6F005C86A5 /* LaunchWidgetLiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C7D72981216200BF3EF9 /* LaunchWidgetLiveActivity.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 */; }; @@ -291,6 +292,7 @@ C4BA2B0E299BE61E00CB4FBA /* Countdown+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Countdown+CoreDataProperties.swift"; sourceTree = ""; }; C4BA2B24299D35C100CB4FBA /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; C4BA2B2C299E2DEE00CB4FBA /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; + C4BA2B2E299E69A000CB4FBA /* View+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extension.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 = ""; }; @@ -623,6 +625,7 @@ C4F8B165298A9ABB005C86A5 /* SoundImageFormView.swift */, C4BA2AD52993F62700CB4FBA /* SoundSelectionView.swift */, C4BA2ADA299549BC00CB4FBA /* TimerModel.swift */, + C4BA2B2E299E69A000CB4FBA /* View+Extension.swift */, ); path = Components; sourceTree = ""; @@ -881,6 +884,7 @@ C4BA2B0F299BE61E00CB4FBA /* Interval+CoreDataClass.swift in Sources */, C4F8B164298A9A92005C86A5 /* AlarmFormView.swift in Sources */, C40FDB622992985C0042A390 /* TextToSpeechRecorder.swift in Sources */, + C4BA2B2F299E69A000CB4FBA /* View+Extension.swift in Sources */, C4060DF7297AFEF2003FAB80 /* NewCountdownView.swift in Sources */, C4060DCC297AE73D003FAB80 /* LeCountdown.xcdatamodeld in Sources */, C4F8B17C298AC234005C86A5 /* Record+CoreDataClass.swift in Sources */, diff --git a/LeCountdown/Views/Components/View+Extension.swift b/LeCountdown/Views/Components/View+Extension.swift new file mode 100644 index 0000000..601b963 --- /dev/null +++ b/LeCountdown/Views/Components/View+Extension.swift @@ -0,0 +1,59 @@ +// +// View+Extension.swift +// LeCountdown +// +// Created by Laurent Morvillier on 16/02/2023. +// + +import Foundation +import SwiftUI + +extension View { + /// Focuses next field in sequence, from the given `FocusState`. + /// Requires a currently active focus state and a next field available in the sequence. + /// + /// Example usage: + /// ``` + /// .onSubmit { self.focusNextField($focusedField) } + /// ``` + /// Given that `focusField` is an enum that represents the focusable fields. For example: + /// ``` + /// @FocusState private var focusedField: Field? + /// enum Field: Int, Hashable { + /// case name + /// case country + /// case city + /// } + /// ``` + func focusNextField(_ field: FocusState.Binding) where F.RawValue == Int { + guard let currentValue = field.wrappedValue else { return } + let nextValue = currentValue.rawValue + 1 + if let newValue = F.init(rawValue: nextValue) { + field.wrappedValue = newValue + } + } + + /// Focuses previous field in sequence, from the given `FocusState`. + /// Requires a currently active focus state and a previous field available in the sequence. + /// + /// Example usage: + /// ``` + /// .onSubmit { self.focusNextField($focusedField) } + /// ``` + /// Given that `focusField` is an enum that represents the focusable fields. For example: + /// ``` + /// @FocusState private var focusedField: Field? + /// enum Field: Int, Hashable { + /// case name + /// case country + /// case city + /// } + /// ``` + func focusPreviousField(_ field: FocusState.Binding) where F.RawValue == Int { + guard let currentValue = field.wrappedValue else { return } + let nextValue = currentValue.rawValue - 1 + if let newValue = F.init(rawValue: nextValue) { + field.wrappedValue = newValue + } + } +} diff --git a/LeCountdown/Views/ContentView.swift b/LeCountdown/Views/ContentView.swift index c42c842..c2dfc30 100644 --- a/LeCountdown/Views/ContentView.swift +++ b/LeCountdown/Views/ContentView.swift @@ -42,11 +42,11 @@ class TimerSpot : Identifiable, Equatable { } -class TimersModel : ObservableObject { - - @Published var spots: [TimerSpot] = [] - -} +//class TimersModel : ObservableObject { +// +// @Published var spots: [TimerSpot] = [] +// +//} struct ContentView: View { @@ -55,30 +55,17 @@ struct ContentView: View { @Environment(\.managedObjectContext) private var viewContext - @StateObject fileprivate var model: TimersModel = TimersModel() +// @StateObject fileprivate var model: TimersModel = TimersModel() @FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \T.order, ascending: true)], animation: .default) - private var timers: FetchedResults { - didSet { - self._buildItemsList() - } - } + private var timers: FetchedResults - var coreDataPublisher: NotificationCenter.Publisher { NotificationCenter.default - .publisher(for: .NSManagedObjectContextDidSave, object: viewContext) } - var cloudkitPublisher: NotificationCenter.Publisher { NotificationCenter.default - .publisher(for: Notification.Name(rawValue: "NSPersistentStoreRemoteChangeNotificationOptionKey"), object: viewContext) } +// var coreDataPublisher: NotificationCenter.Publisher { NotificationCenter.default +// .publisher(for: .NSManagedObjectContextDidSave, object: viewContext) } - @State private var isEditing: Bool = false { - didSet { - if self.isEditing == false { - self._saveOrder() - } - self._buildItemsList() - } - } + @State private var isEditing: Bool = false fileprivate let itemSpacing: CGFloat = 10.0 @@ -101,43 +88,41 @@ struct ContentView: View { spacing: itemSpacing ) { - if !self.isEditing { - - ForEach(self.model.spots) { spot in - if let timer = spot.timer { - DialView(timer: timer, isEditingBinding: self.$isEditing, frameSize: width) - .environment(\.managedObjectContext, viewContext) - .environmentObject(Conductor.maestro) - .environmentObject(boringContext) - - } else { - Color.clear - .frame(width: width, height: 80.0) - .cornerRadius(20.0) - } - } + ReorderableForEach(items: Array(timers)) { timer in - } else { - - ReorderableForEach(items: self.model.spots) { spot in - - if let timer = spot.timer { - DialView(timer: timer, isEditingBinding: self.$isEditing, frameSize: width) - .environment(\.managedObjectContext, viewContext) - .environmentObject(Conductor.maestro) - .environmentObject(boringContext) - - } else { - - Color(white: 0.9) - .frame(width: width, height: 80.0) - .cornerRadius(20.0) - } - - } moveAction: { from, to in - self._reorderSpots(from: from, to: to) - } + DialView(timer: timer, isEditingBinding: self.$isEditing, frameSize: width) + .environment(\.managedObjectContext, viewContext) + .environmentObject(Conductor.maestro) + .environmentObject(boringContext) + } moveAction: { from, to in + self._reorder(from: from, to: to) } + +// if !self.isEditing { +// +// +// } +// else { +// +// ReorderableForEach(items: self.model.spots) { spot in +// +// if let timer = spot.timer { +// DialView(timer: timer, isEditingBinding: self.$isEditing, frameSize: width) +// .environment(\.managedObjectContext, viewContext) +// .environmentObject(Conductor.maestro) +// .environmentObject(boringContext) +// +// } else { +// +// Color(white: 0.9) +// .frame(width: width, height: 80.0) +// .cornerRadius(20.0) +// } +// +// } moveAction: { from, to in +// self._reorderSpots(from: from, to: to) +// } +// } } }.padding(.horizontal, itemSpacing) @@ -188,13 +173,13 @@ struct ContentView: View { // self._buildItemsList() // } // }) - .onReceive(coreDataPublisher, perform: { _ in - withAnimation { - self._buildItemsList() - } - }) +// .onReceive(coreDataPublisher, perform: { _ in +// withAnimation { +// self._buildItemsList() +// } +// }) .onAppear { - self._buildItemsList() +// self._buildItemsList() self._askPermissions() } .onOpenURL { url in @@ -203,32 +188,32 @@ struct ContentView: View { } - fileprivate func _buildItemsList() { - - var spots: [TimerSpot] = [] - - let more: Int = self.isEditing ? 20 : 0 // add 20 empty spots when editing - - let count = max(self.timers.count, Int(self.timers.last?.order ?? 0) + 1) + more - - for i in 0..: View { // MARK: - Business - fileprivate func _reorderSpots(from: IndexSet, to: Int) { - var spots: [TimerSpot] = self.model.spots - spots.move(fromOffsets: from, toOffset: to) - self.model.spots = spots - } - -// fileprivate func _reorder(from: IndexSet, to: Int) { -// var timers: [AbstractTimer] = Array(self.timers) -// timers.move(fromOffsets: from, toOffset: to) -// for (i, countdown) in timers.enumerated() { -// countdown.order = Int16(i) -// } -// do { -// try viewContext.save() -// } catch { -// self.boringContext.error = error -// } +// fileprivate func _reorderSpots(from: IndexSet, to: Int) { +// var spots: [TimerSpot] = self.model.spots +// spots.move(fromOffsets: from, toOffset: to) +// self.model.spots = spots // } + fileprivate func _reorder(from: IndexSet, to: Int) { + var timers: [AbstractTimer] = Array(self.timers) + timers.move(fromOffsets: from, toOffset: to) + for (i, countdown) in timers.enumerated() { + countdown.order = Int16(i) + } + do { + try viewContext.save() + } catch { + self.boringContext.error = error + } + } + fileprivate func _askPermissions() { UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .criticalAlert]) { success, error in print("requestAuthorization > success = \(success), error = \(String(describing: error))") diff --git a/LeCountdown/Views/Countdown/CountdownFormView.swift b/LeCountdown/Views/Countdown/CountdownFormView.swift index 1e7e103..1077ffd 100644 --- a/LeCountdown/Views/Countdown/CountdownFormView.swift +++ b/LeCountdown/Views/Countdown/CountdownFormView.swift @@ -7,7 +7,16 @@ import SwiftUI + struct CountdownFormView : View { + + enum CountdownField: Int, Hashable { + case minutes + case seconds + case name + } + + @FocusState private var focusedField: CountdownField? @EnvironmentObject var model: TimerModel @@ -19,56 +28,59 @@ struct CountdownFormView : View { var repeatCountBinding: Binding - var textFieldIsFocused: FocusState.Binding +// var textFieldIsFocused: FocusState.Binding var intervalRepeatBinding: Binding? = nil var body: some View { Form { - Section { - TextField("minutes", text: minutesBinding) + Section("Duration") { + TextField("Minutes", text: minutesBinding) .keyboardType(.numberPad) - .focused(textFieldIsFocused) - TextField("seconds", text: secondsBinding) + .focused($focusedField, equals: .minutes) + .onSubmit { + self.focusNextField($focusedField) + } + TextField("Seconds", text: secondsBinding) .keyboardType(.numberPad) - .focused(textFieldIsFocused) - } header: { - HStack { - Text("Duration") - Spacer() - Button { - self._addInterval() - } label: { - Label("add interval", image: "plus") -// Image(systemName: "plus") + .focused($focusedField, equals: .seconds) + .onSubmit { + self.focusNextField($focusedField) } - - } - } footer: { - if intervalRepeatBinding != nil { - HStack { - Stepper("Repeat", value: intervalRepeatBinding!, in: 0...20) - Text(self.intervalRepeatBinding!.wrappedValue.formatted()) + TextField("Name (activates tracking)", text: nameBinding) + .focused($focusedField, equals: .name) + .onSubmit { + self.focusNextField($focusedField) } - } - } - Section(header: Text("Name for tracking the activity")) { - TextField("name", text: nameBinding) - .focused(textFieldIsFocused) } SoundImageFormView( imageBinding: imageBinding, repeatCountBinding: repeatCountBinding) .environmentObject(self.model) + }.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") + } + } } } - fileprivate func _addInterval() { - - } - } struct CountdownFormView_Previews: PreviewProvider { @@ -82,7 +94,6 @@ struct CountdownFormView_Previews: PreviewProvider { nameBinding: .constant(""), imageBinding: .constant(.pic3), repeatCountBinding: .constant(2), - textFieldIsFocused: $textFieldIsFocused, intervalRepeatBinding: .constant(2)) .environmentObject(TimerModel()) } diff --git a/LeCountdown/Views/Countdown/NewCountdownView.swift b/LeCountdown/Views/Countdown/NewCountdownView.swift index 507eda9..fe7ffae 100644 --- a/LeCountdown/Views/Countdown/NewCountdownView.swift +++ b/LeCountdown/Views/Countdown/NewCountdownView.swift @@ -51,7 +51,7 @@ struct CountdownEditView : View { var tabSelection: Binding? = nil - @FocusState private var textFieldIsFocused: Bool +// @FocusState private var textFieldIsFocused: Bool @FetchRequest(sortDescriptors: []) private var timers: FetchedResults @@ -86,8 +86,7 @@ struct CountdownEditView : View { minutesBinding: $minutesString, nameBinding: $nameString, imageBinding: $image, - repeatCountBinding: $soundRepeatCount, - textFieldIsFocused: $textFieldIsFocused) + repeatCountBinding: $soundRepeatCount) .environmentObject(self.model) .onAppear { self._onAppear() @@ -139,13 +138,6 @@ struct CountdownEditView : View { } } } - ToolbarItemGroup(placement: .keyboard) { - Button { - textFieldIsFocused = false - } label: { - Image(systemName: "keyboard.chevron.compact.down") - } - } } .navigationTitle("Edit countdown") } diff --git a/LeCountdown/Views/DialView.swift b/LeCountdown/Views/DialView.swift index 8a0d10e..c7953ca 100644 --- a/LeCountdown/Views/DialView.swift +++ b/LeCountdown/Views/DialView.swift @@ -24,7 +24,7 @@ struct DialView: View { var body: some View { ZStack { - Image(timer.imageName).resizable().saturation(self.isEditingBinding.wrappedValue ? 0.0 : 1.0) + Image(self.timer.imageName).resizable().saturation(self.isEditingBinding.wrappedValue ? 0.0 : 1.0) switch self.isEditingBinding.wrappedValue { case false: @@ -42,23 +42,34 @@ struct DialView: View { } } case true: - VStack { - Spacer() - self._dialView().padding(.horizontal) - Spacer() - } - - HStack { - Spacer() - NavigationLink { - self._editView(timer: timer, isPresented: $boringContext.isShowingNewData) - } label: { - Image(systemName: "gearshape.fill") - .font(.system(size: 30.0)) - .padding(.horizontal) - .foregroundColor(Color.accentColor) + + NavigationLink { + self._editView(timer: timer, isPresented: $boringContext.isShowingNewData) + } label: { + VStack { + Spacer() + self._dialView().padding(.horizontal) + Spacer() } } + +// VStack { +// Spacer() +// self._dialView().padding(.horizontal) +// Spacer() +// } +// +// HStack { +// Spacer() +// NavigationLink { +// self._editView(timer: timer, isPresented: $boringContext.isShowingNewData) +// } label: { +// Image(systemName: "gearshape.fill") +// .font(.system(size: 30.0)) +// .padding(.horizontal) +// .foregroundColor(Color.accentColor) +// } +// } } } diff --git a/LeCountdown/Views/NewDataView.swift b/LeCountdown/Views/NewDataView.swift index a7b2852..c1def1c 100644 --- a/LeCountdown/Views/NewDataView.swift +++ b/LeCountdown/Views/NewDataView.swift @@ -43,7 +43,7 @@ struct NewDataView: View { } } .pickerStyle(.segmented) - .padding() + .padding(.horizontal) TabView(selection: $selection) { NewCountdownView(isPresented: $isPresented) diff --git a/LeCountdown/Views/PresetsView.swift b/LeCountdown/Views/PresetsView.swift index e542a35..803eba0 100644 --- a/LeCountdown/Views/PresetsView.swift +++ b/LeCountdown/Views/PresetsView.swift @@ -10,7 +10,7 @@ import SwiftUI enum PresetSection: Int, Identifiable, CaseIterable { var id: Int { return self.rawValue } - case workout +// case workout case chill case cooking case tea @@ -20,7 +20,7 @@ enum PresetSection: Int, Identifiable, CaseIterable { switch self { case .cooking: return [.softBoiled, .mediumBoiledEggs, .hardBoiledEggs] case .tea: return [.greenTea, .blackTea] - case .workout: return [.runningSplits] +// case .workout: return [.runningSplits] case .chill: return [.nap, .meditation] case .other: return [.toothbrushing] } @@ -29,7 +29,7 @@ enum PresetSection: Int, Identifiable, CaseIterable { var localizedName: String { switch self { case .cooking: return NSLocalizedString("Cooking", comment: "") - case .workout: return NSLocalizedString("Workout", comment: "") +// case .workout: return NSLocalizedString("Workout", comment: "") case .chill: return NSLocalizedString("Chill", comment: "") case .other: return NSLocalizedString("Other", comment: "") case .tea: return NSLocalizedString("Tea", comment: "")