Adds stopwatch and alarm data types

release
Laurent 3 years ago
parent 16fbfa1d65
commit 7ccb37479d
  1. 42
      LeCountdown.xcodeproj/project.pbxproj
  2. 2
      LeCountdown/Model/LeCountdown.xcdatamodeld/.xccurrentversion
  3. 33
      LeCountdown/Model/LeCountdown.xcdatamodeld/LeCountdown.0.3.xcdatamodel/contents
  4. 33
      LeCountdown/Views/ContentView.swift
  5. 32
      LeCountdown/Views/Data/AlarmFormView.swift
  6. 40
      LeCountdown/Views/Data/CountdownFormView.swift
  7. 0
      LeCountdown/Views/Data/ImageSelectionView.swift
  8. 247
      LeCountdown/Views/Data/NewAlarmView.swift
  9. 0
      LeCountdown/Views/Data/NewCountdownView.swift
  10. 235
      LeCountdown/Views/Data/NewStopwatchView.swift
  11. 89
      LeCountdown/Views/Data/SoundImageFormView.swift
  12. 44
      LeCountdown/Views/Data/StopwatchFormView.swift
  13. 43
      LeCountdown/Views/DialView.swift
  14. 3
      LeCountdown/Widget/IntentDataProvider.swift

@ -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 = "<group>"; };
C4742B5A298414B000D5D950 /* ImageSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSelectionView.swift; sourceTree = "<group>"; };
C4742B5E2984205000D5D950 /* ViewModifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModifiers.swift; sourceTree = "<group>"; };
C4F8B1542988751B005C86A5 /* DialView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialView.swift; sourceTree = "<group>"; };
C4F8B15629891271005C86A5 /* Conductor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Conductor.swift; sourceTree = "<group>"; };
C4F8B15829891528005C86A5 /* forest_stream.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = forest_stream.mp3; sourceTree = "<group>"; };
C4F8B15E298961A7005C86A5 /* ReorderableForEach.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReorderableForEach.swift; sourceTree = "<group>"; };
C4F8B160298A90E8005C86A5 /* LeCountdown.0.3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = LeCountdown.0.3.xcdatamodel; sourceTree = "<group>"; };
C4F8B161298A9A1F005C86A5 /* NewAlarmView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewAlarmView.swift; sourceTree = "<group>"; };
C4F8B163298A9A92005C86A5 /* AlarmFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmFormView.swift; sourceTree = "<group>"; };
C4F8B165298A9ABB005C86A5 /* SoundImageFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundImageFormView.swift; sourceTree = "<group>"; };
C4F8B168298AA236005C86A5 /* StopwatchFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StopwatchFormView.swift; sourceTree = "<group>"; };
C4F8B16A298AA240005C86A5 /* NewStopwatchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewStopwatchView.swift; sourceTree = "<group>"; };
/* 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 = "<group>";
};
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 = "<group>";
};
/* 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 = "<group>";
versionGroupType = wrapper.xcdatamodel;

@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>LeCountdown.0.2.xcdatamodel</string>
<string>LeCountdown.0.3.xcdatamodel</string>
</dict>
</plist>

@ -0,0 +1,33 @@
<?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="">
<entity name="AbstractTimer" representedClassName="AbstractTimer" isAbstract="YES" syncable="YES" codeGenerationType="class">
<attribute name="image" optional="YES" attributeType="String"/>
<attribute name="order" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="activity" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Activity" inverseName="timers" inverseEntity="Activity"/>
</entity>
<entity name="Activity" representedClassName="Activity" syncable="YES" codeGenerationType="class">
<attribute name="name" attributeType="String" defaultValueString=""/>
<relationship name="records" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Record" inverseName="activity" inverseEntity="Record"/>
<relationship name="timers" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="AbstractTimer" inverseName="activity" inverseEntity="AbstractTimer"/>
</entity>
<entity name="Alarm" representedClassName="Alarm" parentEntity="SoundTimer" syncable="YES" codeGenerationType="class">
<attribute name="fireDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
</entity>
<entity name="Countdown" representedClassName="Countdown" parentEntity="SoundTimer" syncable="YES" codeGenerationType="class">
<attribute name="duration" attributeType="Double" defaultValueString="0" usesScalarValueType="YES"/>
</entity>
<entity name="Record" representedClassName="Record" syncable="YES" codeGenerationType="class">
<attribute name="end" attributeType="Date" defaultDateTimeInterval="696425400" usesScalarValueType="NO"/>
<attribute name="start" attributeType="Date" defaultDateTimeInterval="696425400" usesScalarValueType="NO"/>
<relationship name="activity" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Activity" inverseName="records" inverseEntity="Activity"/>
</entity>
<entity name="SoundTimer" representedClassName="SoundTimer" isAbstract="YES" parentEntity="AbstractTimer" syncable="YES" codeGenerationType="class">
<attribute name="repeats" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="sound" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
</entity>
<entity name="Stopwatch" representedClassName="Stopwatch" parentEntity="AbstractTimer" syncable="YES" codeGenerationType="class">
<attribute name="end" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="sound" optional="YES" attributeType="Integer 16" usesScalarValueType="YES"/>
<attribute name="start" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
</entity>
</model>

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

@ -0,0 +1,32 @@
//
// AlarmFormView.swift
// LeCountdown
//
// Created by Laurent Morvillier on 01/02/2023.
//
import SwiftUI
struct AlarmFormView: View {
var dateBinding: Binding<Date>
var imageBinding: Binding<CoolPic>
var soundBinding: Binding<Sound>
var repeatsBinding: Binding<Bool>
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))
}
}

@ -20,8 +20,6 @@ struct CountdownFormView : View {
var textFieldIsFocused: FocusState<Bool>.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)
}
}

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

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

@ -0,0 +1,89 @@
//
// SoundImageFormView.swift
// LeCountdown
//
// Created by Laurent Morvillier on 01/02/2023.
//
import SwiftUI
struct SoundImageFormView : View {
var imageBinding: Binding<CoolPic>
var soundBinding: Binding<Sound>
var repeatsBinding: Binding<Bool>? = nil
var optionalSound: Binding<Bool>? = 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))
}
}
}

@ -0,0 +1,44 @@
//
// StopwatchFormView.swift
// LeCountdown
//
// Created by Laurent Morvillier on 01/02/2023.
//
import SwiftUI
struct StopwatchFormView: View {
var nameBinding: Binding<String>
var imageBinding: Binding<CoolPic>
var soundBinding: Binding<Sound>
var playSoundBinding: Binding<Bool>
var textFieldIsFocused: FocusState<Bool>.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)
}
}

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

@ -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> = Countdown.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(keyPath: (\Countdown.order), ascending: true)]
return try context.fetch(request)
}

Loading…
Cancel
Save