Navigation changes

main
Laurent 3 years ago
parent b4ef494280
commit d05483d117
  1. 19
      LeCountdown/Views/ContentView.swift
  2. 71
      LeCountdown/Views/Countdown/CountdownFormView.swift
  3. 193
      LeCountdown/Views/Countdown/NewCountdownView.swift
  4. 54
      LeCountdown/Views/PresetsView.swift
  5. 86
      LeCountdown/Views/Reusable/SoundFormView.swift
  6. 7
      LeCountdown/Views/Reusable/TimerModel.swift
  7. 4
      LeCountdown/Views/Reusable/View+Extension.swift
  8. 3
      LeCountdown/Views/TimersView.swift
  9. 2
      LeCountdown/en.lproj/Localizable.strings
  10. 2
      LeCountdown/fr.lproj/Localizable.strings

@ -129,7 +129,8 @@ struct ContentView<T : AbstractTimer>: 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<T : AbstractTimer>: 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<T : AbstractTimer>: View {
} label: {
Image(systemName: "plus")
}
Button {
withAnimation {
self.showStatsSheet.toggle()
}
} label: {
Image(systemName: "chart.bar.doc.horizontal")
}
}
}
.onAppear {

@ -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<String>
var minutesBinding: Binding<String>
@ -29,12 +27,19 @@ struct CountdownFormView : View {
var repeatCountBinding: Binding<Int16>
// var textFieldIsFocused: FocusState<Bool>.Binding
var intervalRepeatBinding: Binding<Int>? = 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())
}
}
}

@ -70,14 +70,18 @@ struct CountdownEditView : View {
// @FocusState private var textFieldIsFocused: Bool
@FetchRequest(sortDescriptors: [])
private var timers: FetchedResults<AbstractTimer>
// @FetchRequest(sortDescriptors: [])
// private var timers: FetchedResults<AbstractTimer>
@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<Bool>, countdown: Countdown? = nil, tabSelection: Binding<Int>? = 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)
}
}

@ -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..<self._columnCount()).map { _ in GridItem(spacing: 10.0) }
}
}
struct PresetsView: View {
@Environment(\.managedObjectContext) private var viewContext
@ -137,18 +171,18 @@ struct TimerItemView: View {
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(self.name.uppercased())
Text(self.name.uppercased()).foregroundColor(.black)
Text(self.duration)
.foregroundColor(Color.accentColor)
Text(self.sound.uppercased()).foregroundColor(Color(white: 0.7))
}.padding()
}//.padding()
.multilineTextAlignment(.leading)
Spacer()
}.background(Color(white: 0.1))
.cornerRadius(16.0)
}//.background(Color(white: 0.1))
//.cornerRadius(16.0)
.monospaced()
.font(Font.system(size: 16.0, weight: .semibold))
.foregroundColor(Color.white)
.font(Font.system(size: 16.0))
//.foregroundColor(Color.white)
}
}
@ -262,9 +296,9 @@ enum Preset: Int, Identifiable, CaseIterable {
}
}
var sound: Set<Sound> {
return Set(SoundCatalog.main.sounds(for: self.playlist))
}
// var sounds: Set<Sound> {
// return Set(SoundCatalog.main.sounds(for: self.playlist))
// }
var formattedDuration: String {
let group = self.intervalGroup

@ -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)
}

@ -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)

@ -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
}
}
}

@ -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)
}
}

@ -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";

@ -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";

Loading…
Cancel
Save