Stats + Logger

release
Laurent 3 years ago
parent ee186e7330
commit 5681991ece
  1. 2
      LaunchIntents/IntentHandler.swift
  2. 88
      LeCountdown.xcodeproj/project.pbxproj
  3. 10
      LeCountdown/Conductor.swift
  4. 1
      LeCountdown/Model/CoreDataRequests.swift
  5. 10
      LeCountdown/Model/Fakes.swift
  6. 21
      LeCountdown/Model/Generation/IntervalGroup+CoreDataProperties.swift
  7. 3
      LeCountdown/Model/Generation/Record+CoreDataProperties.swift
  8. 2
      LeCountdown/Model/Generation/Stopwatch+CoreDataProperties.swift
  9. 2
      LeCountdown/Model/LeCountdown.xcdatamodeld/.xccurrentversion
  10. 49
      LeCountdown/Model/LeCountdown.xcdatamodeld/LeCountdown.0.6.1.xcdatamodel/contents
  11. 21
      LeCountdown/Model/Model+Extensions.swift
  12. 67
      LeCountdown/Model/NSManagedContext+Extensions.swift
  13. 1
      LeCountdown/Model/Persistence.swift
  14. 148
      LeCountdown/Subscription/AppGuard.swift
  15. 20
      LeCountdown/Subscription/StoreView.swift
  16. 62
      LeCountdown/Utils/Logger.swift
  17. 1
      LeCountdown/Utils/TextToSpeechRecorder.swift
  18. 1
      LeCountdown/Views/Alarm/NewAlarmView.swift
  19. 1
      LeCountdown/Views/ContentView.swift
  20. 1
      LeCountdown/Views/Countdown/NewCountdownView.swift
  21. 10
      LeCountdown/Views/HomeView.swift
  22. 49
      LeCountdown/Views/RecordsView.swift
  23. 62
      LeCountdown/Views/Stats/ActivitiesView.swift
  24. 43
      LeCountdown/Views/Stats/RecordsView.swift
  25. 162
      LeCountdown/Views/Stats/StatsView.swift
  26. 1
      LeCountdown/Views/Stopwatch/NewStopwatchView.swift
  27. 4
      LeCountdown/Views/TestView.swift

@ -51,7 +51,7 @@ class IntentHandler: INExtension, SelectTimerIntentHandling {
return collection
} catch {
print("error = \(error)")
Logger.error(error)
throw error
// completion(nil, error)
}

@ -44,7 +44,7 @@
C438C80529813FB400BF3EF9 /* TimeInterval+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4060DF4297AE9A7003FAB80 /* TimeInterval+Extensions.swift */; };
C438C807298195E600BF3EF9 /* Model+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C806298195E600BF3EF9 /* Model+Extensions.swift */; };
C438C80D2982847300BF3EF9 /* CoreDataRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C80C2982847300BF3EF9 /* CoreDataRequests.swift */; };
C438C80F29828B8600BF3EF9 /* RecordsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C80E29828B8600BF3EF9 /* RecordsView.swift */; };
C438C80F29828B8600BF3EF9 /* ActivitiesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C80E29828B8600BF3EF9 /* ActivitiesView.swift */; };
C438C81129829EAF00BF3EF9 /* PropertyWrappers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C81029829EAF00BF3EF9 /* PropertyWrappers.swift */; };
C438C8152982BD9000BF3EF9 /* IntentDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C7FE2981300500BF3EF9 /* IntentDataProvider.swift */; };
C438C8162982BE1E00BF3EF9 /* LeCountdown.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = C4060DCA297AE73D003FAB80 /* LeCountdown.xcdatamodeld */; };
@ -75,7 +75,6 @@
C4BA2AE62995AC3F00CB4FBA /* Loop_ToneSD_Boavista.wav in Resources */ = {isa = PBXBuildFile; fileRef = C4BA2AE52995AC3E00CB4FBA /* Loop_ToneSD_Boavista.wav */; };
C4BA2AE82995ACC200CB4FBA /* Clave_Loop_LLL.wav in Resources */ = {isa = PBXBuildFile; fileRef = C4BA2AE72995ACC200CB4FBA /* Clave_Loop_LLL.wav */; };
C4BA2AEA2995AD1C00CB4FBA /* SEM_Synths_Loop4_Nothing_Like_You.wav in Resources */ = {isa = PBXBuildFile; fileRef = C4BA2AE92995AD1C00CB4FBA /* SEM_Synths_Loop4_Nothing_Like_You.wav */; };
C4BA2AF02996A11900CB4FBA /* Stopwatch+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2AEC2996A11900CB4FBA /* Stopwatch+CoreDataProperties.swift */; };
C4BA2AF12996A11900CB4FBA /* CustomSound+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2AED2996A11900CB4FBA /* CustomSound+CoreDataClass.swift */; };
C4BA2AF22996A11900CB4FBA /* CustomSound+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2AEE2996A11900CB4FBA /* CustomSound+CoreDataProperties.swift */; };
C4BA2AF32996A11900CB4FBA /* AbstractSoundTimer+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2AEF2996A11900CB4FBA /* AbstractSoundTimer+CoreDataProperties.swift */; };
@ -89,18 +88,13 @@
C4BA2B0F299BE61E00CB4FBA /* Interval+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B0A299BE61E00CB4FBA /* Interval+CoreDataClass.swift */; };
C4BA2B10299BE61E00CB4FBA /* Interval+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B0B299BE61E00CB4FBA /* Interval+CoreDataProperties.swift */; };
C4BA2B11299BE61E00CB4FBA /* IntervalGroup+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B0C299BE61E00CB4FBA /* IntervalGroup+CoreDataClass.swift */; };
C4BA2B12299BE61E00CB4FBA /* IntervalGroup+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B0D299BE61E00CB4FBA /* IntervalGroup+CoreDataProperties.swift */; };
C4BA2B13299BE61E00CB4FBA /* Countdown+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B0E299BE61E00CB4FBA /* Countdown+CoreDataProperties.swift */; };
C4BA2B14299BE6A000CB4FBA /* IntervalGroup+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B0D299BE61E00CB4FBA /* IntervalGroup+CoreDataProperties.swift */; };
C4BA2B15299BE6A000CB4FBA /* Interval+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B0B299BE61E00CB4FBA /* Interval+CoreDataProperties.swift */; };
C4BA2B16299BE6A000CB4FBA /* IntervalGroup+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B0C299BE61E00CB4FBA /* IntervalGroup+CoreDataClass.swift */; };
C4BA2B17299BE6A000CB4FBA /* Stopwatch+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2AEC2996A11900CB4FBA /* Stopwatch+CoreDataProperties.swift */; };
C4BA2B18299BE6A000CB4FBA /* Interval+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B0A299BE61E00CB4FBA /* Interval+CoreDataClass.swift */; };
C4BA2B19299BE6A000CB4FBA /* Countdown+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B0E299BE61E00CB4FBA /* Countdown+CoreDataProperties.swift */; };
C4BA2B1A299BE6A100CB4FBA /* IntervalGroup+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B0D299BE61E00CB4FBA /* IntervalGroup+CoreDataProperties.swift */; };
C4BA2B1B299BE6A100CB4FBA /* Interval+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B0B299BE61E00CB4FBA /* Interval+CoreDataProperties.swift */; };
C4BA2B1C299BE6A100CB4FBA /* IntervalGroup+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B0C299BE61E00CB4FBA /* IntervalGroup+CoreDataClass.swift */; };
C4BA2B1D299BE6A100CB4FBA /* Stopwatch+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2AEC2996A11900CB4FBA /* Stopwatch+CoreDataProperties.swift */; };
C4BA2B1E299BE6A100CB4FBA /* Interval+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B0A299BE61E00CB4FBA /* Interval+CoreDataClass.swift */; };
C4BA2B1F299BE6A100CB4FBA /* Countdown+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B0E299BE61E00CB4FBA /* Countdown+CoreDataProperties.swift */; };
C4BA2B22299BE82E00CB4FBA /* AbstractSoundTimer+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2AEF2996A11900CB4FBA /* AbstractSoundTimer+CoreDataProperties.swift */; };
@ -115,6 +109,21 @@
C4BA2B3A299F838000CB4FBA /* Model+SharedExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B39299F838000CB4FBA /* Model+SharedExtensions.swift */; };
C4BA2B3B299F838000CB4FBA /* Model+SharedExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B39299F838000CB4FBA /* Model+SharedExtensions.swift */; };
C4BA2B3C299F838000CB4FBA /* Model+SharedExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B39299F838000CB4FBA /* Model+SharedExtensions.swift */; };
C4BA2B3E299FC86800CB4FBA /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B3D299FC86800CB4FBA /* StatsView.swift */; };
C4BA2B3F299FC86800CB4FBA /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B3D299FC86800CB4FBA /* StatsView.swift */; };
C4BA2B40299FC86800CB4FBA /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B3D299FC86800CB4FBA /* StatsView.swift */; };
C4BA2B43299FCB2B00CB4FBA /* RecordsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B42299FCB2B00CB4FBA /* RecordsView.swift */; };
C4BA2B49299FCE0C00CB4FBA /* IntervalGroup+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B47299FCE0C00CB4FBA /* IntervalGroup+CoreDataProperties.swift */; };
C4BA2B4A299FCE0C00CB4FBA /* IntervalGroup+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B47299FCE0C00CB4FBA /* IntervalGroup+CoreDataProperties.swift */; };
C4BA2B4B299FCE0C00CB4FBA /* IntervalGroup+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B47299FCE0C00CB4FBA /* IntervalGroup+CoreDataProperties.swift */; };
C4BA2B4C299FCE0C00CB4FBA /* Stopwatch+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B48299FCE0C00CB4FBA /* Stopwatch+CoreDataProperties.swift */; };
C4BA2B4D299FCE0C00CB4FBA /* Stopwatch+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B48299FCE0C00CB4FBA /* Stopwatch+CoreDataProperties.swift */; };
C4BA2B4E299FCE0C00CB4FBA /* Stopwatch+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B48299FCE0C00CB4FBA /* Stopwatch+CoreDataProperties.swift */; };
C4BA2B57299FFA4F00CB4FBA /* AppGuard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B56299FFA4F00CB4FBA /* AppGuard.swift */; };
C4BA2B5B299FFAB000CB4FBA /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B5A299FFAB000CB4FBA /* Logger.swift */; };
C4BA2B5C299FFAB000CB4FBA /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B5A299FFAB000CB4FBA /* Logger.swift */; };
C4BA2B5D299FFAB000CB4FBA /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B5A299FFAB000CB4FBA /* Logger.swift */; };
C4BA2B5F299FFC8400CB4FBA /* StoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B5E299FFC8300CB4FBA /* StoreView.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 */; };
@ -254,7 +263,7 @@
C438C80429813B3100BF3EF9 /* LeCountdown.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = LeCountdown.entitlements; sourceTree = "<group>"; };
C438C806298195E600BF3EF9 /* Model+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Model+Extensions.swift"; sourceTree = "<group>"; };
C438C80C2982847300BF3EF9 /* CoreDataRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataRequests.swift; sourceTree = "<group>"; };
C438C80E29828B8600BF3EF9 /* RecordsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordsView.swift; sourceTree = "<group>"; };
C438C80E29828B8600BF3EF9 /* ActivitiesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivitiesView.swift; sourceTree = "<group>"; };
C438C81029829EAF00BF3EF9 /* PropertyWrappers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyWrappers.swift; sourceTree = "<group>"; };
C445FA8E2987B83B0054D761 /* SoundPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundPlayer.swift; sourceTree = "<group>"; };
C445FA902987C0CF0054D761 /* LeCountdown.0.2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = LeCountdown.0.2.xcdatamodel; sourceTree = "<group>"; };
@ -279,7 +288,6 @@
C4BA2AE72995ACC200CB4FBA /* Clave_Loop_LLL.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = Clave_Loop_LLL.wav; sourceTree = "<group>"; };
C4BA2AE92995AD1C00CB4FBA /* SEM_Synths_Loop4_Nothing_Like_You.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = SEM_Synths_Loop4_Nothing_Like_You.wav; sourceTree = "<group>"; };
C4BA2AEB2996A09600CB4FBA /* LeCountdown.0.5.1.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = LeCountdown.0.5.1.xcdatamodel; sourceTree = "<group>"; };
C4BA2AEC2996A11900CB4FBA /* Stopwatch+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Stopwatch+CoreDataProperties.swift"; sourceTree = "<group>"; };
C4BA2AED2996A11900CB4FBA /* CustomSound+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CustomSound+CoreDataClass.swift"; sourceTree = "<group>"; };
C4BA2AEE2996A11900CB4FBA /* CustomSound+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CustomSound+CoreDataProperties.swift"; sourceTree = "<group>"; };
C4BA2AEF2996A11900CB4FBA /* AbstractSoundTimer+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AbstractSoundTimer+CoreDataProperties.swift"; sourceTree = "<group>"; };
@ -291,7 +299,6 @@
C4BA2B0A299BE61E00CB4FBA /* Interval+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Interval+CoreDataClass.swift"; sourceTree = "<group>"; };
C4BA2B0B299BE61E00CB4FBA /* Interval+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Interval+CoreDataProperties.swift"; sourceTree = "<group>"; };
C4BA2B0C299BE61E00CB4FBA /* IntervalGroup+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IntervalGroup+CoreDataClass.swift"; sourceTree = "<group>"; };
C4BA2B0D299BE61E00CB4FBA /* IntervalGroup+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IntervalGroup+CoreDataProperties.swift"; sourceTree = "<group>"; };
C4BA2B0E299BE61E00CB4FBA /* Countdown+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Countdown+CoreDataProperties.swift"; sourceTree = "<group>"; };
C4BA2B24299D35C100CB4FBA /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
C4BA2B2C299E2DEE00CB4FBA /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
@ -299,6 +306,14 @@
C4BA2B30299F759700CB4FBA /* DefaultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultView.swift; sourceTree = "<group>"; };
C4BA2B35299F82FB00CB4FBA /* Fakes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fakes.swift; sourceTree = "<group>"; };
C4BA2B39299F838000CB4FBA /* Model+SharedExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Model+SharedExtensions.swift"; sourceTree = "<group>"; };
C4BA2B3D299FC86800CB4FBA /* StatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsView.swift; sourceTree = "<group>"; };
C4BA2B42299FCB2B00CB4FBA /* RecordsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordsView.swift; sourceTree = "<group>"; };
C4BA2B46299FCD8B00CB4FBA /* LeCountdown.0.6.1.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = LeCountdown.0.6.1.xcdatamodel; sourceTree = "<group>"; };
C4BA2B47299FCE0C00CB4FBA /* IntervalGroup+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IntervalGroup+CoreDataProperties.swift"; sourceTree = "<group>"; };
C4BA2B48299FCE0C00CB4FBA /* Stopwatch+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Stopwatch+CoreDataProperties.swift"; sourceTree = "<group>"; };
C4BA2B56299FFA4F00CB4FBA /* AppGuard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppGuard.swift; sourceTree = "<group>"; };
C4BA2B5A299FFAB000CB4FBA /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
C4BA2B5E299FFC8300CB4FBA /* StoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreView.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>"; };
@ -408,6 +423,7 @@
C438C80A2981DE1A00BF3EF9 /* Utils */,
C438C8082981DDD200BF3EF9 /* Widget */,
C445FA962987D0CF0054D761 /* Sound_Assets */,
C4BA2B55299FFA3700CB4FBA /* Subscription */,
C4060DC3297AE73D003FAB80 /* Assets.xcassets */,
C438C80429813B3100BF3EF9 /* LeCountdown.entitlements */,
C4060DCD297AE73D003FAB80 /* Info.plist */,
@ -506,6 +522,7 @@
children = (
C4BA2AFC299A3A3700CB4FBA /* AppleMusicPlayer.swift */,
C4742B5629840F6400D5D950 /* CoolPic.swift */,
C4BA2B5A299FFAB000CB4FBA /* Logger.swift */,
C4BA2B2C299E2DEE00CB4FBA /* Preferences.swift */,
C438C81029829EAF00BF3EF9 /* PropertyWrappers.swift */,
C40FDB612992985C0042A390 /* TextToSpeechRecorder.swift */,
@ -522,13 +539,13 @@
C4F8B1B9298AC830005C86A5 /* Countdown */,
C4F8B1BB298AC848005C86A5 /* Stopwatch */,
C4F8B1D3298BF686005C86A5 /* Components */,
C4BA2B41299FCB0100CB4FBA /* Stats */,
C4060DC1297AE73B003FAB80 /* ContentView.swift */,
C4F8B1CF298BF2E2005C86A5 /* DialView.swift */,
C4BA2B24299D35C100CB4FBA /* HomeView.swift */,
C498E59E298D4DEA00E90DE0 /* LiveTimerListView.swift */,
C4BA2B03299A42EF00CB4FBA /* NewDataView.swift */,
C4BA2B05299A8F8D00CB4FBA /* PresetsView.swift */,
C438C80E29828B8600BF3EF9 /* RecordsView.swift */,
C498E5A2298D720600E90DE0 /* TestView.swift */,
C4F8B15E298961A7005C86A5 /* ReorderableForEach.swift */,
);
@ -568,6 +585,25 @@
path = Stephan_Bodzin;
sourceTree = "<group>";
};
C4BA2B41299FCB0100CB4FBA /* Stats */ = {
isa = PBXGroup;
children = (
C438C80E29828B8600BF3EF9 /* ActivitiesView.swift */,
C4BA2B42299FCB2B00CB4FBA /* RecordsView.swift */,
C4BA2B3D299FC86800CB4FBA /* StatsView.swift */,
);
path = Stats;
sourceTree = "<group>";
};
C4BA2B55299FFA3700CB4FBA /* Subscription */ = {
isa = PBXGroup;
children = (
C4BA2B56299FFA4F00CB4FBA /* AppGuard.swift */,
C4BA2B5E299FFC8300CB4FBA /* StoreView.swift */,
);
path = Subscription;
sourceTree = "<group>";
};
C4F8B188298AC248005C86A5 /* Generation */ = {
isa = PBXGroup;
children = (
@ -586,11 +622,11 @@
C4BA2B0A299BE61E00CB4FBA /* Interval+CoreDataClass.swift */,
C4BA2B0B299BE61E00CB4FBA /* Interval+CoreDataProperties.swift */,
C4BA2B0C299BE61E00CB4FBA /* IntervalGroup+CoreDataClass.swift */,
C4BA2B0D299BE61E00CB4FBA /* IntervalGroup+CoreDataProperties.swift */,
C4BA2B47299FCE0C00CB4FBA /* IntervalGroup+CoreDataProperties.swift */,
C4F8B16E298AC234005C86A5 /* Record+CoreDataClass.swift */,
C4F8B16F298AC234005C86A5 /* Record+CoreDataProperties.swift */,
C4F8B174298AC234005C86A5 /* Stopwatch+CoreDataClass.swift */,
C4BA2AEC2996A11900CB4FBA /* Stopwatch+CoreDataProperties.swift */,
C4BA2B48299FCE0C00CB4FBA /* Stopwatch+CoreDataProperties.swift */,
);
path = Generation;
sourceTree = "<group>";
@ -843,28 +879,31 @@
C4060DC9297AE73D003FAB80 /* Persistence.swift in Sources */,
C4BA2B36299F82FB00CB4FBA /* Fakes.swift in Sources */,
C498E5A1298D543900E90DE0 /* LiveTimer.swift in Sources */,
C438C80F29828B8600BF3EF9 /* RecordsView.swift in Sources */,
C438C80F29828B8600BF3EF9 /* ActivitiesView.swift in Sources */,
C4F8B187298AC234005C86A5 /* Activity+CoreDataProperties.swift in Sources */,
C438C80D2982847300BF3EF9 /* CoreDataRequests.swift in Sources */,
C4742B5F2984205000D5D950 /* ViewModifiers.swift in Sources */,
C4F8B1D8298C0727005C86A5 /* TimerRouter.swift in Sources */,
C4BA2B13299BE61E00CB4FBA /* Countdown+CoreDataProperties.swift in Sources */,
C4F8B186298AC234005C86A5 /* Activity+CoreDataClass.swift in Sources */,
C4BA2B57299FFA4F00CB4FBA /* AppGuard.swift in Sources */,
C4F8B15729891271005C86A5 /* Conductor.swift in Sources */,
C4BA2B5F299FFC8400CB4FBA /* StoreView.swift in Sources */,
C4F8B17E298AC234005C86A5 /* Alarm+CoreDataClass.swift in Sources */,
C438C81129829EAF00BF3EF9 /* PropertyWrappers.swift in Sources */,
C4F8B1BF298ACA0B005C86A5 /* StopwatchDialView.swift in Sources */,
C438C807298195E600BF3EF9 /* Model+Extensions.swift in Sources */,
C4BA2B04299A42EF00CB4FBA /* NewDataView.swift in Sources */,
C4BA2B4C299FCE0C00CB4FBA /* Stopwatch+CoreDataProperties.swift in Sources */,
C438C7FF2981300500BF3EF9 /* IntentDataProvider.swift in Sources */,
C4BA2AD62993F62700CB4FBA /* SoundSelectionView.swift in Sources */,
C498E5A5299152B400E90DE0 /* GreenCheckmarkView.swift in Sources */,
C4BA2B3E299FC86800CB4FBA /* StatsView.swift in Sources */,
C4F8B1B8298AC81D005C86A5 /* CountdownDialView.swift in Sources */,
C445FA922987CC8A0054D761 /* Sound.swift in Sources */,
C4F8B1D0298BF2E2005C86A5 /* DialView.swift in Sources */,
C4BA2B11299BE61E00CB4FBA /* IntervalGroup+CoreDataClass.swift in Sources */,
C4BA2AFD299A3A3700CB4FBA /* AppleMusicPlayer.swift in Sources */,
C4BA2B12299BE61E00CB4FBA /* IntervalGroup+CoreDataProperties.swift in Sources */,
C4BA2B3A299F838000CB4FBA /* Model+SharedExtensions.swift in Sources */,
C4BA2AF32996A11900CB4FBA /* AbstractSoundTimer+CoreDataProperties.swift in Sources */,
C4742B59298411E800D5D950 /* CountdownFormView.swift in Sources */,
@ -884,7 +923,6 @@
C4F8B1532987FE6F005C86A5 /* LaunchWidgetLiveActivity.swift in Sources */,
C4F8B182298AC234005C86A5 /* Stopwatch+CoreDataClass.swift in Sources */,
C4BA2ADB299549BC00CB4FBA /* TimerModel.swift in Sources */,
C4BA2AF02996A11900CB4FBA /* Stopwatch+CoreDataProperties.swift in Sources */,
C4F8B16B298AA240005C86A5 /* NewStopwatchView.swift in Sources */,
C4BA2AF22996A11900CB4FBA /* CustomSound+CoreDataProperties.swift in Sources */,
C4060DF5297AE9A7003FAB80 /* TimeInterval+Extensions.swift in Sources */,
@ -895,10 +933,13 @@
C4BA2B0F299BE61E00CB4FBA /* Interval+CoreDataClass.swift in Sources */,
C4F8B164298A9A92005C86A5 /* AlarmFormView.swift in Sources */,
C40FDB622992985C0042A390 /* TextToSpeechRecorder.swift in Sources */,
C4BA2B43299FCB2B00CB4FBA /* RecordsView.swift in Sources */,
C4BA2B2F299E69A000CB4FBA /* View+Extension.swift in Sources */,
C4060DF7297AFEF2003FAB80 /* NewCountdownView.swift in Sources */,
C4060DCC297AE73D003FAB80 /* LeCountdown.xcdatamodeld in Sources */,
C4BA2B49299FCE0C00CB4FBA /* IntervalGroup+CoreDataProperties.swift in Sources */,
C4F8B17C298AC234005C86A5 /* Record+CoreDataClass.swift in Sources */,
C4BA2B5B299FFAB000CB4FBA /* Logger.swift in Sources */,
C4F8B162298A9A1F005C86A5 /* NewAlarmView.swift in Sources */,
C4742B5B298414B000D5D950 /* ImageSelectionView.swift in Sources */,
C438C7C5298024E900BF3EF9 /* NSManagedContext+Extensions.swift in Sources */,
@ -943,15 +984,18 @@
C4F8B195298AC288005C86A5 /* Activity+CoreDataClass.swift in Sources */,
C4BA2B22299BE82E00CB4FBA /* AbstractSoundTimer+CoreDataProperties.swift in Sources */,
C4F8B193298AC288005C86A5 /* Countdown+CoreDataClass.swift in Sources */,
C4BA2B17299BE6A000CB4FBA /* Stopwatch+CoreDataProperties.swift in Sources */,
C4BA2AF72996A4EF00CB4FBA /* CustomSound+CoreDataClass.swift in Sources */,
C4F8B1AD298AC451005C86A5 /* AbstractSoundTimer+CoreDataClass.swift in Sources */,
C445FA87298448730054D761 /* CoolPic.swift in Sources */,
C4BA2B3F299FC86800CB4FBA /* StatsView.swift in Sources */,
C438C8162982BE1E00BF3EF9 /* LeCountdown.xcdatamodeld in Sources */,
C4BA2B4A299FCE0C00CB4FBA /* IntervalGroup+CoreDataProperties.swift in Sources */,
C4BA2B4D299FCE0C00CB4FBA /* Stopwatch+CoreDataProperties.swift in Sources */,
C4F8B194298AC288005C86A5 /* Record+CoreDataProperties.swift in Sources */,
C438C8152982BD9000BF3EF9 /* IntentDataProvider.swift in Sources */,
C438C7DF2981216300BF3EF9 /* LaunchWidget.intentdefinition in Sources */,
C4F8B15B29892D40005C86A5 /* SoundPlayer.swift in Sources */,
C4BA2B5C299FFAB000CB4FBA /* Logger.swift in Sources */,
C438C7E82981255D00BF3EF9 /* TimeInterval+Extensions.swift in Sources */,
C4BA2B37299F82FF00CB4FBA /* Fakes.swift in Sources */,
C4F8B18F298AC288005C86A5 /* AbstractTimer+CoreDataClass.swift in Sources */,
@ -967,7 +1011,6 @@
C4F8B1AE298AC451005C86A5 /* Alarm+CoreDataProperties.swift in Sources */,
C4BA2B32299F75DE00CB4FBA /* DefaultView.swift in Sources */,
C4F8B18D298AC288005C86A5 /* Stopwatch+CoreDataClass.swift in Sources */,
C4BA2B14299BE6A000CB4FBA /* IntervalGroup+CoreDataProperties.swift in Sources */,
C438C8182982BFC100BF3EF9 /* Persistence.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -996,15 +1039,17 @@
C438C81A2982BFF100BF3EF9 /* NSManagedContext+Extensions.swift in Sources */,
C4F8B19D298AC288005C86A5 /* AbstractTimer+CoreDataClass.swift in Sources */,
C4BA2B3C299F838000CB4FBA /* Model+SharedExtensions.swift in Sources */,
C4BA2B40299FC86800CB4FBA /* StatsView.swift in Sources */,
C4BA2AF82996A4F000CB4FBA /* CustomSound+CoreDataProperties.swift in Sources */,
C4F8B199298AC288005C86A5 /* Record+CoreDataClass.swift in Sources */,
C438C8012981327600BF3EF9 /* Persistence.swift in Sources */,
C438C7FD29812BF700BF3EF9 /* LaunchWidget.intentdefinition in Sources */,
C4BA2B5D299FFAB000CB4FBA /* Logger.swift in Sources */,
C4BA2B4B299FCE0C00CB4FBA /* IntervalGroup+CoreDataProperties.swift in Sources */,
C4F8B1B1298AC451005C86A5 /* AbstractSoundTimer+CoreDataClass.swift in Sources */,
C4BA2B1E299BE6A100CB4FBA /* Interval+CoreDataClass.swift in Sources */,
C4BA2B1A299BE6A100CB4FBA /* IntervalGroup+CoreDataProperties.swift in Sources */,
C4BA2B1D299BE6A100CB4FBA /* Stopwatch+CoreDataProperties.swift in Sources */,
C445FA86298448720054D761 /* CoolPic.swift in Sources */,
C4BA2B4E299FCE0C00CB4FBA /* Stopwatch+CoreDataProperties.swift in Sources */,
C4BA2B23299BE82E00CB4FBA /* AbstractSoundTimer+CoreDataProperties.swift in Sources */,
C438C800298130E900BF3EF9 /* IntentDataProvider.swift in Sources */,
);
@ -1461,6 +1506,7 @@
C4060DCA297AE73D003FAB80 /* LeCountdown.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
C4BA2B46299FCD8B00CB4FBA /* LeCountdown.0.6.1.xcdatamodel */,
C4BA2B07299BDAE000CB4FBA /* LeCountdown.0.6.xcdatamodel */,
C4BA2AEB2996A09600CB4FBA /* LeCountdown.0.5.1.xcdatamodel */,
C4BA2AD72993F7D200CB4FBA /* LeCountdown.0.5.xcdatamodel */,
@ -1470,7 +1516,7 @@
C418A14F298428CB00C22230 /* LeCountdown.0.1.xcdatamodel */,
C4060DCB297AE73D003FAB80 /* LeCountdown.xcdatamodel */,
);
currentVersion = C4BA2B07299BDAE000CB4FBA /* LeCountdown.0.6.xcdatamodel */;
currentVersion = C4BA2B46299FCD8B00CB4FBA /* LeCountdown.0.6.1.xcdatamodel */;
path = LeCountdown.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;

@ -106,7 +106,7 @@ class Conductor: ObservableObject {
do {
try CoreDataRequests.recordActivity(timer: countdown, dateInterval: dateInterval)
} catch {
print("Could not record activity = \(error)")
Logger.error(error)
// TODO: show error to user
}
}
@ -170,7 +170,7 @@ class Conductor: ObservableObject {
do {
try CoreDataRequests.recordActivity(timer: stopwatch, dateInterval: DateInterval(start: start, end: end ?? Date()))
} catch {
print("could not record")
Logger.error(error)
}
self._endLiveActivity(timerId: stopwatch.stringId)
}
@ -207,7 +207,7 @@ class Conductor: ObservableObject {
self.soundPlayer = soundPlayer
try soundPlayer.playSound(soundFile: soundFile, repeats: false)
} catch {
print("error = \(error)")
Logger.error(error)
// TODO: manage error
}
}
@ -231,7 +231,7 @@ class Conductor: ObservableObject {
let liveActivity = try ActivityKit.Activity.request(attributes: attributes, content: activityContent)
print("Requested a Live Activity: \(String(describing: liveActivity.id)).")
} catch (let error) {
print("Error requesting Live Activity \(error.localizedDescription).")
Logger.error(error)
}
// self._scheduleAppRefresh(countdown: countdown)
@ -246,7 +246,7 @@ class Conductor: ObservableObject {
try BGTaskScheduler.shared.submit(request)
print("request submitted with date: \(String(describing: request.earliestBeginDate))")
} catch {
print("Could not schedule app refresh: \(error)")
Logger.error(error)
}
}

@ -34,6 +34,7 @@ class CoreDataRequests {
return activity
}
} catch {
Logger.error(error)
print("error = \(error)")
}
let context = PersistenceController.shared.container.viewContext

@ -33,3 +33,13 @@ extension Alarm {
}
}
extension Activity {
static func fake(context: NSManagedObjectContext) -> Activity {
let activity = Activity(context: context)
activity.name = "Tea"
return activity
}
}

@ -2,7 +2,7 @@
// IntervalGroup+CoreDataProperties.swift
// LeCountdown
//
// Created by Laurent Morvillier on 14/02/2023.
// Created by Laurent Morvillier on 17/02/2023.
//
//
@ -17,8 +17,25 @@ extension IntervalGroup {
}
@NSManaged public var repeatCount: Int16
@NSManaged public var intervals: Interval?
@NSManaged public var countdown: Countdown?
@NSManaged public var intervals: NSSet?
}
// MARK: Generated accessors for intervals
extension IntervalGroup {
@objc(addIntervalsObject:)
@NSManaged public func addToIntervals(_ value: Interval)
@objc(removeIntervalsObject:)
@NSManaged public func removeFromIntervals(_ value: Interval)
@objc(addIntervals:)
@NSManaged public func addToIntervals(_ values: NSSet)
@objc(removeIntervals:)
@NSManaged public func removeFromIntervals(_ values: NSSet)
}

@ -2,7 +2,7 @@
// Record+CoreDataProperties.swift
// LeCountdown
//
// Created by Laurent Morvillier on 01/02/2023.
// Created by Laurent Morvillier on 17/02/2023.
//
//
@ -18,6 +18,7 @@ extension Record {
@NSManaged public var end: Date?
@NSManaged public var start: Date?
@NSManaged public var duration: Double
@NSManaged public var activity: Activity?
}

@ -2,7 +2,7 @@
// Stopwatch+CoreDataProperties.swift
// LeCountdown
//
// Created by Laurent Morvillier on 10/02/2023.
// Created by Laurent Morvillier on 17/02/2023.
//
//

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

@ -0,0 +1,49 @@
<?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="AbstractSoundTimer" representedClassName="AbstractSoundTimer" isAbstract="YES" parentEntity="AbstractTimer" syncable="YES">
<attribute name="repeatCount" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="soundList" optional="YES" attributeType="String"/>
</entity>
<entity name="AbstractTimer" representedClassName="AbstractTimer" isAbstract="YES" syncable="YES">
<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">
<attribute name="name" attributeType="String" defaultValueString=""/>
<relationship name="records" optional="YES" toMany="YES" deletionRule="Cascade" 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="AbstractSoundTimer" syncable="YES">
<attribute name="fireDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
</entity>
<entity name="Countdown" representedClassName="Countdown" parentEntity="AbstractSoundTimer" syncable="YES">
<attribute name="duration" attributeType="Double" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="group" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="IntervalGroup" inverseName="countdown" inverseEntity="IntervalGroup"/>
</entity>
<entity name="CustomSound" representedClassName="CustomSound" syncable="YES">
<attribute name="file" optional="YES" attributeType="String"/>
<attribute name="text" optional="YES" attributeType="String"/>
</entity>
<entity name="Interval" representedClassName="Interval" syncable="YES">
<attribute name="duration" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="soundList" optional="YES" attributeType="String"/>
<relationship name="group" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="IntervalGroup" inverseName="intervals" inverseEntity="IntervalGroup"/>
</entity>
<entity name="IntervalGroup" representedClassName="IntervalGroup" syncable="YES">
<attribute name="repeatCount" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="countdown" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Countdown" inverseName="group" inverseEntity="Countdown"/>
<relationship name="intervals" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="Interval" inverseName="group" inverseEntity="Interval"/>
</entity>
<entity name="Record" representedClassName="Record" syncable="YES">
<attribute name="duration" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<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="Stopwatch" representedClassName="Stopwatch" parentEntity="AbstractSoundTimer" syncable="YES">
<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>

@ -67,14 +67,31 @@ extension Stopwatch {
extension Record {
public override func didChangeValue(forKey key: String) {
super.didChangeValue(forKey: key)
switch key {
case "start", "end":
self.computeDuration()
default:
break
}
}
func computeDuration() {
if let start, let end {
self.duration = end.timeIntervalSince(start)
}
}
var details: String {
if let start, let end {
return "\(start) - \(end)"
return "\(start.formatted()) - \(end.formatted())"
} else {
return "no details"
}
}
}
extension Activity {

@ -15,7 +15,72 @@ extension NSManagedObjectContext {
guard let objectId = PersistenceController.shared.container.persistentStoreCoordinator.managedObjectID(forURIRepresentation: url) else { return nil }
return self.object(with: objectId)
}
func compute(activity: Activity, stats: [Stat]) throws -> [StatValue] {
// Step 1:
// - Create the summing expression on the amount attribute.
// - Name the expression result as 'amountTotal'.
// - Assign the expression result data type as a Double.
var expressions: [NSExpressionDescription] = []
for stat in stats {
let expression = NSExpressionDescription()
expression.expression = NSExpression(forFunction: stat.function, arguments:[NSExpression(forKeyPath: stat.field)])
expression.name = stat.stringIdentifier;
expression.expressionResultType = NSAttributeType.doubleAttributeType
expressions.append(expression)
}
// Step 2:
// - Create the fetch request for the Movement entity.
// - Indicate that the fetched properties are those that were
// described in `expression`.
// - Indicate that the result type is a dictionary.
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Record")
fetchRequest.predicate = NSPredicate(format: "activity = %@", activity)
fetchRequest.propertiesToFetch = expressions
fetchRequest.resultType = NSFetchRequestResultType.dictionaryResultType
// Step 3:
// - Execute the fetch request which returns an array.
// - There will only be one result. Get the first array
// element and assign to 'resultMap'.
// - The summed amount value is in the dictionary as
// 'amountTotal'. This will be summed value.
var statValues: [StatValue] = []
let results = try self.fetch(fetchRequest)
if let resultMap = results.first as? [String : Any] {
for stat in stats {
if let result = resultMap[stat.stringIdentifier] {
var value: NSDecimalNumber? = nil
switch result {
case let double as Double:
value = NSDecimalNumber(value: double)
case let integer as Int:
value = NSDecimalNumber(integerLiteral: integer)
default:
print("unmanaged value type = \(String(describing: result))")
break
}
if let value {
let sv = StatValue(stat: stat, value: value)
statValues.append(sv)
}
}
}
}
return statValues
}
}
extension NSManagedObject {

@ -34,6 +34,7 @@ struct PersistenceController {
do {
try viewContext.save()
} catch {
Logger.error(error)
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError

@ -0,0 +1,148 @@
//
// Guard.swift
// Poker Analytics 6
//
// Created by Laurent Morvillier on 20/04/2022.
//
import Foundation
import StoreKit
public enum StoreError: Error {
case failedVerification
}
enum StorePlan : String, CaseIterable {
case none
case unlimited = "com.staxriver.lecountdown.unlimited"
}
extension Notification.Name {
static let StoreEventHappened = Notification.Name("storeEventHappened")
}
@objc class AppGuard : NSObject {
static var main: AppGuard = AppGuard()
@Published private(set) var purchasedTransactions = Set<StoreKit.Transaction>()
var currentBestPlan: StoreKit.Transaction? = nil
var updateListenerTask: Task<Void, Error>? = nil
override init() {
super.init()
self.updateListenerTask = self.listenForTransactions()
Task {
await self.refreshPurchasedProducts()
}
}
func refreshPurchasedProducts() async {
// Iterate through the user's purchased products.
for await verificationResult in Transaction.currentEntitlements {
do {
let transaction = try await self.processTransactionResult(verificationResult)
DispatchQueue.main.async {
NotificationCenter.default.post(name: Notification.Name.StoreEventHappened, object: nil)
}
await transaction.finish()
} catch {
Logger.error(error)
}
}
}
func listenForTransactions() -> Task<Void, Error> {
return Task.detached {
//Iterate through any transactions which didn't come from a direct call to `purchase()`.
for await result in Transaction.updates {
do {
let transaction = try self.checkVerified(result)
//Deliver content to the user.
await self.updatePurchasedIdentifiers(transaction)
//Always finish a transaction.
await transaction.finish()
} catch {
Logger.error(error)
//StoreKit has a receipt it can read but it failed verification. Don't deliver content to the user.
print("Transaction failed verification")
}
}
}
}
func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
//Check if the transaction passes StoreKit verification.
switch result {
case .unverified:
//StoreKit has parsed the JWS but failed verification. Don't deliver content to the user.
throw StoreError.failedVerification
case .verified(let safe):
//If the transaction is verified, unwrap and return it.
return safe
}
}
@MainActor
func updatePurchasedIdentifiers(_ transaction: StoreKit.Transaction) async {
if transaction.revocationDate == nil {
// If the App Store has not revoked the transaction, add it to the list of `purchasedIdentifiers`.
purchasedTransactions.insert(transaction)
} else {
// If the App Store has revoked this transaction, remove it from the list of `purchasedIdentifiers`.
purchasedTransactions.remove(transaction)
}
self._updateBestPlan()
}
func processTransactionResult(_ result: VerificationResult<StoreKit.Transaction>) async throws -> StoreKit.Transaction {
let transaction = try checkVerified(result)
// Deliver content to the user.
await updatePurchasedIdentifiers(transaction)
return transaction
}
var isAuthorized: Bool {
return self.currentPlan == .unlimited
}
var currentPlan: StorePlan {
#if DEBUG
return .unlimited
#else
if let currentBestPlan = self.currentBestPlan, let plan = StorePlan(rawValue: currentBestPlan.productID) {
return plan
}
if let vf = Preferences.verifiedTransaction(),
vf.expiryDate > Date(), vf.graceDate > Date(),
let plan = StorePlan(rawValue: vf.productId) {
return plan
}
return .none
#endif
}
fileprivate func _updateBestPlan() {
if let unlimited = self.purchasedTransactions.first(where: { $0.productID == StorePlan.unlimited.rawValue }) {
self.currentBestPlan = unlimited
} else {
self.currentBestPlan = nil
}
}
}

@ -0,0 +1,20 @@
//
// StoreView.swift
// LeCountdown
//
// Created by Laurent Morvillier on 17/02/2023.
//
import SwiftUI
struct StoreView: View {
var body: some View {
Text("Hello Store!")
}
}
struct StoreView_Previews: PreviewProvider {
static var previews: some View {
StoreView()
}
}

@ -0,0 +1,62 @@
//
// Logger.swift
// Poker Analytics 4
//
// Created by Laurent Morvillier on 29/03/2018.
//
import Foundation
@objc public class Logger : NSObject {
@objc static public func log(_ message: Any, file: String = #file, function: String = #function, line: Int = #line) {
let filestr: NSString = NSString(string: file)
print("\(filestr.lastPathComponent).\(line).\(function): \(message)")
}
@objc static public func error(_ error: Error) {
Logger.error(error, file: #file, function: #function, line: #line)
}
static public func error(_ error: Error, file: String = #file, function: String = #function, line: Int = #line) {
let filestr: NSString = NSString(string: file)
var fireBaseError: Error {
if let customError = error as? CustomNSError & LocalizedError {
return customError.fireBaseError
} else {
return error
}
}
print("ERROR: \(filestr.lastPathComponent).\(line).\(function): \(fireBaseError)")
}
@objc static public func w(_ message: Any, file: String = #file, function: String = #function, line: Int = #line) {
let filestr: NSString = NSString(string: file)
print("Warning: \(filestr.lastPathComponent).\(line).\(function): \(message)")
}
@objc static public func crashLogging(_ message: String, file: String = #file, function: String = #function, line: Int = #line) {
let fileName: String = file.components(separatedBy: "/").last ?? file
#if DEBUG
NSLogv("%@.%i.%@: %@", getVaList([fileName, line, function, message]))
#else
NSLogv("%@.%i.%@: %@", getVaList([fileName, line, function, message]))
#endif
}
}
extension LocalizedError where Self: CustomNSError {
var simpleErrorDescription: String {
let mirror = Mirror(reflecting: self)
if let label = mirror.children.first?.label {
return label
} else {
return String(describing:self)
}
}
var fireBaseError: NSError {
NSError(domain: Self.errorDomain + "." + self.simpleErrorDescription, code: self.errorCode, userInfo: self.errorUserInfo)
}
}

@ -46,6 +46,7 @@ class TextToSpeechRecorder {
}
try output?.write(from: pcmBuffer)
} catch {
Logger.error(error)
handler(.failure(error))
}
}

@ -239,6 +239,7 @@ struct AlarmEditView: View {
do {
try viewContext.save()
} catch {
Logger.error(error)
self.errorShown = true
self.error = error
}

@ -264,6 +264,7 @@ struct ContentView<T : AbstractTimer>: View {
do {
try viewContext.save()
} catch {
Logger.error(error)
self.boringContext.error = error
}
}

@ -310,6 +310,7 @@ struct CountdownEditView : View {
do {
try viewContext.save()
} catch {
Logger.error(error)
self.errorShown = true
self.error = error
}

@ -26,7 +26,7 @@ struct CompactHomeView: View {
.environmentObject(Conductor.maestro)
.tabItem { Label("Home", systemImage: "clock.fill") }
.tag(1)
RecordsView()
ActivitiesView()
.environment(\.managedObjectContext, viewContext)
.tabItem { Label("Stats", systemImage: "chart.bar.fill") }
.tag(2)
@ -59,7 +59,7 @@ struct RegularHomeView: View {
.environmentObject(Conductor.maestro)
.tabItem { Label("Home", systemImage: "clock.fill") }
.tag(1)
RecordsView()
ActivitiesView()
.environment(\.managedObjectContext, viewContext)
.tabItem { Label("Stats", systemImage: "chart.bar.fill") }
.tag(2)
@ -76,11 +76,17 @@ struct RegularHomeView: View {
struct CompactHomeView_Previews: PreviewProvider {
static var previews: some View {
CompactHomeView()
.environmentObject(Conductor.maestro)
.environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
struct RegularHomeView_Previews: PreviewProvider {
static var previews: some View {
RegularHomeView()
.environmentObject(Conductor.maestro)
.environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}

@ -1,49 +0,0 @@
//
// RecordsView.swift
// LeCountdown
//
// Created by Laurent Morvillier on 26/01/2023.
//
import SwiftUI
struct RecordsView: View {
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Record.start, ascending: false)],
animation: .default)
private var records: FetchedResults<Record>
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Activity.name, ascending: true)],
predicate: NSPredicate(format: "records.@count > 0"), animation: .default)
private var activities: FetchedResults<Activity>
var body: some View {
VStack {
if records.isEmpty {
Text("You don't have any recorded activity yet")
} else {
List {
ForEach(activities) { activity in
HStack {
Text(activity.name ?? "no activity")
Spacer()
Text(activity.recordCount)
}
}
}
}
}.navigationTitle("Activities")
}
}
struct RecordsView_Previews: PreviewProvider {
static var previews: some View {
NavigationStack {
RecordsView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
}

@ -0,0 +1,62 @@
//
// RecordsView.swift
// LeCountdown
//
// Created by Laurent Morvillier on 26/01/2023.
//
import SwiftUI
struct ActivitiesView: View {
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Record.start, ascending: false)],
animation: .default)
private var records: FetchedResults<Record>
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Activity.name, ascending: true)],
predicate: NSPredicate(format: "records.@count > 0"), animation: .default)
private var activities: FetchedResults<Activity>
var body: some View {
VStack {
if self.records.isEmpty {
Text("You don't have any recorded activity yet")
} else {
List {
ForEach(self.activities) { activity in
NavigationLink {
HStack {
StatsView(activity: activity).frame(width: 300)
.background(.red)
RecordsView(activity: activity)
.frame(maxWidth: .infinity)
.background(.green)
}
} label: {
HStack {
Text(activity.name ?? "no activity")
Spacer()
Text(activity.recordCount)
}
}
}
}
}
}
.navigationTitle("Activities")
}
}
struct ActivitiesView_Previews: PreviewProvider {
static var previews: some View {
NavigationStack {
ActivitiesView()
.environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
}

@ -0,0 +1,43 @@
//
// RecordsView.swift
// LeCountdown
//
// Created by Laurent Morvillier on 17/02/2023.
//
import SwiftUI
struct RecordsView: View {
var activity: Activity
var body: some View {
List {
if let records = self.activity.records as? Set<Record> {
let array: [Record] = Array(records)
ForEach(array) { record in
HStack {
Text(record.details)
Spacer()
if let duration = record.duration {
Text(duration.minuteSecond)
}
}
}
}
}
}
}
struct RecordsView_Previews: PreviewProvider {
static var previews: some View {
RecordsView(activity:
Activity.fake(context: PersistenceController.preview.container.viewContext))
}
}

@ -0,0 +1,162 @@
//
// StatsView.swift
// LeCountdown
//
// Created by Laurent Morvillier on 17/02/2023.
//
import SwiftUI
import CoreData
enum Stat: Int, CaseIterable {
case count
case totalDuration
case averageDuration
var localizedName: String {
switch self {
case .count: return NSLocalizedString("Count", comment: "")
case .totalDuration: return NSLocalizedString("Duration", comment: "")
case .averageDuration: return NSLocalizedString("Average duration", comment: "")
}
}
var field: String {
switch self {
case .count:
return "duration"
case .totalDuration:
return "duration"
case .averageDuration:
return "duration"
}
}
var function: String {
switch self {
case .count:
return "count:"
case .totalDuration:
return "sum:"
case .averageDuration:
return "average:"
}
}
var stringIdentifier: String {
return self.function + self.field
}
var type: NSAttributeType {
switch self {
case .count:
return .integer32AttributeType
default:
return .doubleAttributeType
}
}
}
struct StatValue: Identifiable {
var id: Int { return self.stat.rawValue }
var stat: Stat
var value: NSDecimalNumber
var formattedValue: String {
switch self.stat {
case .averageDuration, .totalDuration:
return self.value.doubleValue.minuteSecond
default:
let formatter: NumberFormatter = NumberFormatter()
return formatter.string(from: self.value) ?? "--"
}
}
}
class StatModel: ObservableObject {
@Published var statValues: [StatValue] = []
@Published var isComputing = false
@Published var error: Error? = nil
func compute(activity: Activity) {
self.isComputing = true
PersistenceController.shared.container.performBackgroundTask { context in
var values: [StatValue] = []
do {
values = try context.compute(activity: activity, stats: Stat.allCases)
} catch {
Logger.error(error)
self.error = error
}
DispatchQueue.main.async {
self.statValues = values
self.isComputing = false
}
}
}
}
struct StatsView: View {
var activity: Activity
@StateObject var model: StatModel = StatModel()
var body: some View {
VStack() {
if self.model.isComputing {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
} else {
HStack {
ForEach(self.model.statValues) { statValue in
StatView(name: statValue.stat.localizedName, value: statValue.formattedValue)
}
}
}
Spacer()
}
.onAppear() {
self.model.compute(activity: self.activity)
}
.navigationTitle(self.activity.name ?? "")
}
}
struct StatView: View {
var name: String
var value: String
var body: some View {
VStack(alignment: .leading) {
Text(self.name.uppercased()).font(.caption)
Text(self.value).font(.title)
}
}
}
struct StatsView_Previews: PreviewProvider {
static var previews: some View {
StatsView(activity: Activity.fake(context: PersistenceController.preview.container.viewContext))
}
}

@ -290,6 +290,7 @@ struct StopwatchEditView: View {
do {
try viewContext.save()
} catch {
Logger.error(error)
self.errorShown = true
self.error = error
}

@ -15,8 +15,8 @@ struct TestSubView: View {
Spacer()
Image(systemName: "xmark").frame(minWidth: 0.0, maxWidth: 60.0, minHeight: 0.0, maxHeight: .infinity).background(.red)
}
.background(.gray)
.cornerRadius(16.0)
.background(.gray)
.cornerRadius(16.0)
}
}

Loading…
Cancel
Save