Refactor stats

main
Laurent 3 years ago
parent b8293b3674
commit 19fca143bc
  1. 24
      LeCountdown.xcodeproj/project.pbxproj
  2. 4
      LeCountdown/Conductor.swift
  3. 39
      LeCountdown/Model/CoreDataRequests.swift
  4. 3
      LeCountdown/Model/LiveTimer.swift
  5. 4
      LeCountdown/Model/NSManagedContext+Extensions.swift
  6. 2
      LeCountdown/Stats/Filter.swift
  7. 2
      LeCountdown/Stats/Stat.swift
  8. 1
      LeCountdown/Views/LiveTimerListView.swift
  9. 2
      LeCountdown/Views/Stats/ActivitiesView.swift
  10. 13
      LeCountdown/Views/Stats/ActivityCalendarView.swift
  11. 141
      LeCountdown/Views/Stats/ActivityStatsView.swift
  12. 103
      LeCountdown/Views/Stats/ActivityView.swift
  13. 109
      LeCountdown/Views/Stats/RecordsView.swift
  14. 42
      LeCountdown/Views/Stats/StatisticsSubView.swift
  15. 85
      LeCountdown/Views/Stats/StatisticsView.swift
  16. 2
      LeCountdown/Widget/IntentDataProvider.swift
  17. 1
      LeCountdown/fr.lproj/Localizable.strings

@ -42,6 +42,8 @@
C42E96FD29E5B06D005B1B8C /* ActivityCalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42E96FC29E5B06D005B1B8C /* ActivityCalendarView.swift */; };
C42E96FE29E5B5CD005B1B8C /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B6429A3C37D00CB4FBA /* Filter.swift */; };
C42E970229E6B32B005B1B8C /* CalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42E970129E6B32B005B1B8C /* CalendarView.swift */; };
C42E970529E6E4F7005B1B8C /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42E970429E6E4F7005B1B8C /* ActivityView.swift */; };
C42E970729E6EDF5005B1B8C /* StatisticsSubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42E970629E6EDF5005B1B8C /* StatisticsSubView.swift */; };
C438C7C12980228B00BF3EF9 /* CountdownScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C7C02980228B00BF3EF9 /* CountdownScheduler.swift */; };
C438C7C5298024E900BF3EF9 /* NSManagedContext+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C7C4298024E900BF3EF9 /* NSManagedContext+Extensions.swift */; };
C438C7C929803CA000BF3EF9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C7C829803CA000BF3EF9 /* AppDelegate.swift */; };
@ -197,7 +199,7 @@
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 /* GraphsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B3D299FC86800CB4FBA /* GraphsView.swift */; };
C4BA2B3E299FC86800CB4FBA /* StatisticsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B3D299FC86800CB4FBA /* StatisticsView.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 */; };
@ -210,7 +212,7 @@
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 */; };
C4BA2B6129A3C02400CB4FBA /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B6029A3C02400CB4FBA /* ActivityView.swift */; };
C4BA2B6129A3C02400CB4FBA /* ActivityStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B6029A3C02400CB4FBA /* ActivityStatsView.swift */; };
C4BA2B6329A3C34600CB4FBA /* Stat.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B6229A3C34600CB4FBA /* Stat.swift */; };
C4BA2B6529A3C37D00CB4FBA /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B6429A3C37D00CB4FBA /* Filter.swift */; };
C4BA2B6829A3C4AC00CB4FBA /* Context+Calculations.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B6729A3C4AC00CB4FBA /* Context+Calculations.swift */; };
@ -368,6 +370,8 @@
C42E96FA29E59E72005B1B8C /* BackgroundBlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundBlurView.swift; sourceTree = "<group>"; };
C42E96FC29E5B06D005B1B8C /* ActivityCalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityCalendarView.swift; sourceTree = "<group>"; };
C42E970129E6B32B005B1B8C /* CalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarView.swift; sourceTree = "<group>"; };
C42E970429E6E4F7005B1B8C /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = "<group>"; };
C42E970629E6EDF5005B1B8C /* StatisticsSubView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsSubView.swift; sourceTree = "<group>"; };
C438C7C02980228B00BF3EF9 /* CountdownScheduler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CountdownScheduler.swift; sourceTree = "<group>"; };
C438C7C4298024E900BF3EF9 /* NSManagedContext+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedContext+Extensions.swift"; sourceTree = "<group>"; };
C438C7C829803CA000BF3EF9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@ -454,7 +458,7 @@
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 /* GraphsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphsView.swift; sourceTree = "<group>"; };
C4BA2B3D299FC86800CB4FBA /* StatisticsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsView.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>"; };
@ -462,7 +466,7 @@
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>"; };
C4BA2B6029A3C02400CB4FBA /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = "<group>"; };
C4BA2B6029A3C02400CB4FBA /* ActivityStatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityStatsView.swift; sourceTree = "<group>"; };
C4BA2B6229A3C34600CB4FBA /* Stat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stat.swift; sourceTree = "<group>"; };
C4BA2B6429A3C37D00CB4FBA /* Filter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filter.swift; sourceTree = "<group>"; };
C4BA2B6729A3C4AC00CB4FBA /* Context+Calculations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Context+Calculations.swift"; sourceTree = "<group>"; };
@ -834,9 +838,11 @@
children = (
C438C80E29828B8600BF3EF9 /* ActivitiesView.swift */,
C42E96FC29E5B06D005B1B8C /* ActivityCalendarView.swift */,
C4BA2B6029A3C02400CB4FBA /* ActivityView.swift */,
C4BA2B3D299FC86800CB4FBA /* GraphsView.swift */,
C4BA2B6029A3C02400CB4FBA /* ActivityStatsView.swift */,
C42E970429E6E4F7005B1B8C /* ActivityView.swift */,
C4BA2B42299FCB2B00CB4FBA /* RecordsView.swift */,
C42E970629E6EDF5005B1B8C /* StatisticsSubView.swift */,
C4BA2B3D299FC86800CB4FBA /* StatisticsView.swift */,
);
path = Stats;
sourceTree = "<group>";
@ -1224,6 +1230,7 @@
C4BA2B5F299FFC8400CB4FBA /* StoreView.swift in Sources */,
C4BA2B7929A65C1400CB4FBA /* UIDevice+Extensions.swift in Sources */,
C4F8B17E298AC234005C86A5 /* Alarm+CoreDataClass.swift in Sources */,
C42E970729E6EDF5005B1B8C /* StatisticsSubView.swift in Sources */,
C438C81129829EAF00BF3EF9 /* PropertyWrappers.swift in Sources */,
C4F8B1BF298ACA0B005C86A5 /* StopwatchDialView.swift in Sources */,
C438C807298195E600BF3EF9 /* Model+Extensions.swift in Sources */,
@ -1238,7 +1245,7 @@
C4E5D66629B73AED008E7465 /* StartTimerIntent.swift in Sources */,
C4BA2AD62993F62700CB4FBA /* SoundSelectionView.swift in Sources */,
C498E5A5299152B400E90DE0 /* GreenCheckmarkView.swift in Sources */,
C4BA2B3E299FC86800CB4FBA /* GraphsView.swift in Sources */,
C4BA2B3E299FC86800CB4FBA /* StatisticsView.swift in Sources */,
C4F8B1B8298AC81D005C86A5 /* CountdownDialView.swift in Sources */,
C473C2F929A8DC0A0056B38A /* LaunchWidgetAttributes.swift in Sources */,
C445FA922987CC8A0054D761 /* Sound.swift in Sources */,
@ -1272,6 +1279,7 @@
C4BA2ADB299549BC00CB4FBA /* TimerModel.swift in Sources */,
C4BA2B6529A3C37D00CB4FBA /* Filter.swift in Sources */,
C4F8B16B298AA240005C86A5 /* NewStopwatchView.swift in Sources */,
C42E970529E6E4F7005B1B8C /* ActivityView.swift in Sources */,
C4BA2AF22996A11900CB4FBA /* CustomSound+CoreDataProperties.swift in Sources */,
C4060DF5297AE9A7003FAB80 /* TimeInterval+Extensions.swift in Sources */,
C4F8B166298A9ABB005C86A5 /* SoundFormView.swift in Sources */,
@ -1298,7 +1306,7 @@
C438C7C5298024E900BF3EF9 /* NSManagedContext+Extensions.swift in Sources */,
C4F8B15F298961A7005C86A5 /* ReorderableForEach.swift in Sources */,
C4A16D9529C4B06400143D5E /* StatePlayer.swift in Sources */,
C4BA2B6129A3C02400CB4FBA /* ActivityView.swift in Sources */,
C4BA2B6129A3C02400CB4FBA /* ActivityStatsView.swift in Sources */,
C4BA2B6A29A4BE1800CB4FBA /* Date+Extensions.swift in Sources */,
C4BA2B10299BE61E00CB4FBA /* Interval+CoreDataProperties.swift in Sources */,
C4F8B1AC298AC3A0005C86A5 /* Alarm+CoreDataProperties.swift in Sources */,

@ -119,7 +119,7 @@ class Conductor: ObservableObject {
fileprivate func _recordActivity(countdownId: String) {
let context = PersistenceController.shared.container.viewContext
if let countdown = context.object(stringId: countdownId) as? Countdown,
if let countdown: Countdown = context.object(stringId: countdownId),
let dateInterval = self.currentCountdowns[countdownId] {
do {
try CoreDataRequests.recordActivity(timer: countdown, dateInterval: dateInterval)
@ -239,7 +239,7 @@ class Conductor: ObservableObject {
if !self._delayedSoundPlayers.contains(where: { $0.key == countdownId }) {
let context = PersistenceController.shared.container.viewContext
if let countdown = context.object(stringId: countdownId) as? Countdown {
if let countdown: Countdown = context.object(stringId: countdownId) {
do {
let sound: Sound = countdown.someSound

@ -58,6 +58,45 @@ class CoreDataRequests {
try context.save()
}
static func years(context: NSManagedObjectContext, activity: Activity) throws -> [Int] {
let predicate: NSPredicate = NSPredicate(format: "activity = %@", activity)
let distinct = try context.distinct(entityName: "Record", attributes: ["year"], predicate: predicate)
if let distinctYears = distinct as? [[String : Int]] {
return distinctYears.compactMap {
if let year = $0["year"] {
return year
} else {
Logger.w("issue with dictionary \($0)")
return nil
}
}
} else {
Logger.w("Could not cast \(distinct) as [Int]")
return []
}
}
static func months(context: NSManagedObjectContext, activity: Activity, year: Int) throws -> [Int] {
let predicate: NSPredicate = NSPredicate(format: "activity = %@ AND year = %i", activity, year)
let distinct = try context.distinct(entityName: "Record", attributes: ["month"], predicate: predicate)
if let distinctMonths = distinct as? [[String : Int]] {
return distinctMonths.compactMap {
if let month = $0["month"] {
return month
} else {
Logger.w("issue with dictionary \($0)")
return nil
}
}
} else {
Logger.w("Could not cast \(distinct) as [Int]")
return []
}
}
static func months(context: NSManagedObjectContext, activity: Activity) throws -> [Month] {
let predicate: NSPredicate = NSPredicate(format: "activity = %@", activity)

@ -18,10 +18,11 @@ struct LiveTimer: Identifiable, Comparable {
}
func timer(context: NSManagedObjectContext) -> AbstractTimer? {
return context.object(stringId: self.id) as? AbstractTimer
return context.object(stringId: self.id)
}
var ended: Bool {
return self.date < Date()
}
}

@ -10,10 +10,10 @@ import CoreData
extension NSManagedObjectContext {
func object(stringId: String) -> NSManagedObject? {
func object<T: NSManagedObject>(stringId: String) -> T? {
guard let url = URL(string: stringId) else { return nil }
guard let objectId = PersistenceController.shared.container.persistentStoreCoordinator.managedObjectID(forURIRepresentation: url) else { return nil }
return self.object(with: objectId)
return self.object(with: objectId) as? T
}
func distinct(entityName: String, attributes: [String], predicate: NSPredicate? = nil) throws -> [Any] {

@ -28,7 +28,7 @@ enum Filter: Identifiable, Hashable {
switch self {
case .none: return NSLocalizedString("All", comment: "")
case .year(let year): return "\(year)"
case .month(let month): return month.localizedString
case .month(let month): return month.localizedString.capitalized
}
}

@ -111,7 +111,7 @@ struct StatValue: Identifiable {
let identifier: String
switch timeFrame {
case .all: identifier = point.date.formattedYear
// case .all: identifier = point.date.formattedYear
case .year: identifier = point.date.formattedMonth
case .month: identifier = point.date.formattedDay
}

@ -227,7 +227,6 @@ struct LiveCountdownView: View {
.foregroundColor(.white)
.background(Color.accentColor)
.cornerRadius(8.0)
.font(.title)
}.onTapGesture {
self.showConfirmationPopup = false
}

@ -32,7 +32,7 @@ struct ActivitiesView: View {
ForEach(self.activities) { activity in
NavigationLink {
ActivityCalendarView(activity: activity)
ActivityView(activity: activity)
} label: {
HStack {
Text(activity.name ?? "no activity")

@ -67,7 +67,7 @@ struct ActivityCalendarView: View {
}
}
}
.navigationTitle(activity.name ?? "Activity")
.navigationTitle(self.activity.name ?? "no name")
.onAppear {
self._load()
}
@ -94,17 +94,8 @@ struct ActivityCalendarView: View {
struct ActivityCalendarView_Previews: PreviewProvider {
static var activity: Activity {
let context = PersistenceController.preview.container.viewContext
do {
return try context.fetch(Activity.fetchRequest()).first ?? Activity.fake(context: context)
} catch {
return Activity.fake(context: context)
}
}
static var previews: some View {
ActivityCalendarView(activity: self.activity)
ActivityCalendarView(activity: ActivityView_Previews.activity)
.environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}

@ -0,0 +1,141 @@
//
// ActivityView.swift
// LeCountdown
//
// Created by Laurent Morvillier on 20/02/2023.
//
import SwiftUI
import CoreData
struct ActivityStatsView: View {
@Environment(\.managedObjectContext) private var viewContext
let activity: Activity
var filter: Filter? = nil
@State var subFilters: [Filter] = []
var body: some View {
List {
Section {
StatisticsView(activity: self.activity, filter: self.filter) { statValues in
ForEach(statValues) { statValue in
StatView(statValue: statValue)
}
}.environment(\.managedObjectContext, viewContext)
}
// Picker("Time", selection: $selectedTimeFrame) {
// ForEach(TimeFrame.allCases) { timeFrame in
// Text(timeFrame.localizedString).tag(timeFrame)
// }
// }
// .pickerStyle(.segmented)
// .padding(.horizontal)
StatisticsSubView(activity: self.activity, subFilters: self.subFilters)
.environment(\.managedObjectContext, viewContext)
}
.navigationTitle(self.filter?.localizedString ?? self.activity.name ?? "no name")
.onAppear {
self._load()
}
}
fileprivate func _load() {
do {
switch self.filter {
case nil:
let years = try CoreDataRequests.years(context: self.viewContext, activity: self.activity)
self.subFilters = years.map { Filter.year($0) }
case .some(let filter):
switch filter {
case .year(let year):
let months = try CoreDataRequests.months(context: self.viewContext, activity: self.activity, year: year)
self.subFilters = months.map { Filter.month(Month(month: $0, year: year)) }
default:
break
}
}
} catch {
Logger.error(error)
}
}
}
enum TimeFrame: Int, Identifiable, CaseIterable {
var id: Int { return self.rawValue }
// case all
case year
case month
var localizedString: String {
switch self {
// case .all: return NSLocalizedString("All", comment: "")
case .year: return NSLocalizedString("Year", comment: "")
case .month: return NSLocalizedString("Month", comment: "")
}
}
func filters(context: NSManagedObjectContext, activity: Activity) -> [Filter] {
let predicate = NSPredicate(format: "activity = %@", activity)
do {
switch self {
// case .all:
// return [.none]
case .year:
let distinct = try context.distinct(entityName: "Record", attributes: ["year"], predicate: predicate)
if let yearsMap = distinct as? [[String : Int]] {
let years = yearsMap.compactMap { $0.first?.value }
return years.map { Filter.year($0) }
} else {
Logger.w("Could not cast \(distinct) as [Int]")
}
case .month:
let monthes = try CoreDataRequests.months(context: context, activity: activity)
return monthes.map { Filter.month($0) }
// let distinct = try context.distinct(entityName: "Record", attributes: ["year", "month"], predicate: predicate)
// if let distinctMonths = distinct as? [[String : Int]] {
//
// let months = distinctMonths.compactMap {
// if let month = $0["month"],
// let year = $0["year"] {
// return Month(month: month, year: year)
// } else {
// Logger.w("issue with dictionary \($0)")
// return nil
// }
// }
// return months.map { Filter.month($0) }
// } else {
// Logger.w("Could not cast \(distinct) as [Int]")
// }
}
} catch {
Logger.error(error)
}
return []
}
}
struct ActivityStatsView_Previews: PreviewProvider {
static var previews: some View {
ActivityStatsView(activity: Activity.fake(context: PersistenceController.preview.container.viewContext))
.environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}

@ -2,112 +2,49 @@
// ActivityView.swift
// LeCountdown
//
// Created by Laurent Morvillier on 20/02/2023.
// Created by Laurent Morvillier on 12/04/2023.
//
import SwiftUI
import CoreData
struct ActivityView: View {
@Environment(\.managedObjectContext) private var viewContext
let activity: Activity
@State var selectedTimeFrame: TimeFrame = .all
var body: some View {
VStack {
Picker("Time", selection: $selectedTimeFrame) {
ForEach(TimeFrame.allCases) { timeFrame in
Text(timeFrame.localizedString).tag(timeFrame)
TabView {
ActivityCalendarView(activity: self.activity).tabItem {
Label("Calendar", systemImage: "calendar")
}.environment(\.managedObjectContext, viewContext)
ActivityStatsView(activity: self.activity).tabItem {
Label("Stats", systemImage: "function")
}.environment(\.managedObjectContext, viewContext)
RecordsView(activity: self.activity)
.tabItem {
Label("Stats", systemImage: "list.bullet")
}
}
.pickerStyle(.segmented)
.padding(.horizontal)
List {
GraphsView(activity: self.activity,
timeFrame: self.selectedTimeFrame)
.environment(\.managedObjectContext, viewContext)
let filters = self.selectedTimeFrame.filters(context: viewContext, activity: activity)
RecordsView(activity: self.activity, filters: filters)
.environment(\.managedObjectContext, viewContext)
}
}
}.navigationTitle(self.activity.name ?? "no name")
}
}
enum TimeFrame: Int, Identifiable, CaseIterable {
var id: Int { return self.rawValue }
case all
case year
case month
var localizedString: String {
switch self {
case .all: return NSLocalizedString("All", comment: "")
case .year: return NSLocalizedString("Year", comment: "")
case .month: return NSLocalizedString("Month", comment: "")
}
}
struct ActivityView_Previews: PreviewProvider {
func filters(context: NSManagedObjectContext, activity: Activity) -> [Filter] {
let predicate = NSPredicate(format: "activity = %@", activity)
static var activity: Activity {
let context = PersistenceController.preview.container.viewContext
do {
switch self {
case .all:
return [.none]
case .year:
let distinct = try context.distinct(entityName: "Record", attributes: ["year"], predicate: predicate)
if let yearsMap = distinct as? [[String : Int]] {
let years = yearsMap.compactMap { $0.first?.value }
return years.map { Filter.year($0) }
} else {
Logger.w("Could not cast \(distinct) as [Int]")
}
case .month:
let monthes = try CoreDataRequests.months(context: context, activity: activity)
return monthes.map { Filter.month($0) }
// let distinct = try context.distinct(entityName: "Record", attributes: ["year", "month"], predicate: predicate)
// if let distinctMonths = distinct as? [[String : Int]] {
//
// let months = distinctMonths.compactMap {
// if let month = $0["month"],
// let year = $0["year"] {
// return Month(month: month, year: year)
// } else {
// Logger.w("issue with dictionary \($0)")
// return nil
// }
// }
// return months.map { Filter.month($0) }
// } else {
// Logger.w("Could not cast \(distinct) as [Int]")
// }
}
return try context.fetch(Activity.fetchRequest()).first ?? Activity.fake(context: context)
} catch {
Logger.error(error)
return Activity.fake(context: context)
}
return []
}
}
struct ActivityView_Previews: PreviewProvider {
static var previews: some View {
ActivityView(activity: Activity.fake(context: PersistenceController.preview.container.viewContext), selectedTimeFrame: .all)
ActivityView(activity: self.activity)
.environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}

@ -8,46 +8,49 @@
import SwiftUI
import CoreData
class RecordsModel: ObservableObject {
@Published var filters: [Filter] = []
func loadFilters(activity: Activity, timeFrame: TimeFrame, context: NSManagedObjectContext) {
self.filters = timeFrame.filters(context: context, activity: activity)
}
}
struct RecordsView: View {
@Environment(\.managedObjectContext) private var viewContext
let activity: Activity
let filters: [Filter]
var activity: Activity
@StateObject var model: RecordsSectionModel = RecordsSectionModel()
var body: some View {
ForEach(self.filters) { filter in
RecordsSectionView(activity: self.activity, filter: filter)
.environment(\.managedObjectContext, viewContext)
List {
ForEach(self.model.records) { record in
HStack {
Text(record.formattedDay)
Spacer()
Text(record.duration.minuteSecond)
}
}.onDelete(perform: _deleteItem)
}.onAppear {
self.model.loadRecords(activity: self.activity,
context: viewContext)
}
}
fileprivate func _deleteItem(_ indexSet: IndexSet) {
self.model.deleteItem(indexSet, context: self.viewContext)
}
}
class RecordsSectionModel: ObservableObject {
@Published var records: [Record] = []
func loadRecords(activity: Activity, filter: Filter, context: NSManagedObjectContext) {
let records = self._retrieveRecords(activity: activity, filter: filter, context: context)
func loadRecords(activity: Activity, context: NSManagedObjectContext) {
let records = self._retrieveRecords(activity: activity, context: context)
self.records = records
}
fileprivate func _retrieveRecords(activity: Activity, filter: Filter, context: NSManagedObjectContext) -> [Record] {
fileprivate func _retrieveRecords(activity: Activity, context: NSManagedObjectContext) -> [Record] {
do {
let request = Record.fetchRequest()
let activityPredicate = NSPredicate(format: "activity = %@", activity)
request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [activityPredicate, filter.predicate])
request.predicate = NSPredicate(format: "activity = %@", activity)
request.sortDescriptors = [NSSortDescriptor(key: "start", ascending: false)]
return try context.fetch(request)
} catch {
@ -73,44 +76,42 @@ class RecordsSectionModel: ObservableObject {
}
struct RecordsSectionView: View {
@Environment(\.managedObjectContext) private var viewContext
var activity: Activity
@State var filter: Filter
@StateObject var model: RecordsSectionModel = RecordsSectionModel()
var body: some View {
Section(filter.localizedString) {
ForEach(self.model.records) { record in
HStack {
Text(record.formattedDay)
Spacer()
Text(record.duration.minuteSecond)
}
}.onDelete(perform: _deleteItem)
}.onAppear {
self.model.loadRecords(activity: self.activity,
filter: self.filter,
context: viewContext)
}
}
fileprivate func _deleteItem(_ indexSet: IndexSet) {
self.model.deleteItem(indexSet, context: self.viewContext)
}
}
//struct RecordsSectionView: View {
//
// @Environment(\.managedObjectContext) private var viewContext
//
// var activity: Activity
//// @State var filter: Filter
//
// @StateObject var model: RecordsSectionModel = RecordsSectionModel()
//
// var body: some View {
//
// Section {
// ForEach(self.model.records) { record in
// HStack {
// Text(record.formattedDay)
// Spacer()
// Text(record.duration.minuteSecond)
// }
// }.onDelete(perform: _deleteItem)
// }.onAppear {
// self.model.loadRecords(activity: self.activity,
// context: viewContext)
// }
//
// }
//
// fileprivate func _deleteItem(_ indexSet: IndexSet) {
// self.model.deleteItem(indexSet, context: self.viewContext)
// }
//
//}
struct RecordsView_Previews: PreviewProvider {
static var previews: some View {
RecordsView(activity:
Activity.fake(context: PersistenceController.preview.container.viewContext),
filters: [Filter.none])
Activity.fake(context: PersistenceController.preview.container.viewContext))
.environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}

@ -0,0 +1,42 @@
//
// StatisticsSubView.swift
// LeCountdown
//
// Created by Laurent Morvillier on 12/04/2023.
//
import SwiftUI
struct StatisticsSubView: View {
@Environment(\.managedObjectContext) private var viewContext
let activity: Activity
var subFilters: [Filter] = []
var body: some View {
Section {
ForEach(self.subFilters) { filter in
NavigationLink {
ActivityStatsView(activity: self.activity, filter: filter)
} label: {
StatisticsView(activity: self.activity, filter: filter) { statValues in
HStack {
if let count = statValues.first(where: { $0.stat == .count }) {
LabeledContent(filter.localizedString, value: count.formattedValue)
}
}
}
}
}
}
}
}
struct StatisticsSubView_Previews: PreviewProvider {
static var previews: some View {
StatisticsSubView(activity: ActivityView_Previews.activity)
}
}

@ -41,36 +41,31 @@ class StatModel: ObservableObject {
}
struct GraphsView: View {
struct StatisticsView<ContentView>: View where ContentView: View {
var activity: Activity
var timeFrame: TimeFrame
var filter: Filter? = nil
let content: ([StatValue]) -> ContentView
@StateObject var model: StatModel = StatModel()
var body: some View {
Section {
VStack() {
if self.model.isComputing {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
} else {
VStack(alignment: .leading) {
ForEach(self.model.statValues) { statValue in
GraphView(statValue: statValue, timeFrame: self.timeFrame)
// .padding(.vertical)
}
}
VStack() {
if self.model.isComputing {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
} else {
VStack(alignment: .leading) {
self.content(self.model.statValues)
}
}
}
.onAppear() {
self.model.compute(activity: self.activity, filter: self.filter)
}
.navigationTitle(self.activity.name ?? "")
}
}
@ -99,30 +94,27 @@ struct GraphView: View {
VStack(alignment: .leading) {
StatView(statValue: self.statValue)
// HStack {
// Text(self.statValue.stat.localizedName.uppercased())
// Text(self.statValue.formattedValue)
// }
Chart(self.statValue.chartPoint(timeFrame: self.timeFrame)) { point in
let stat: Stat = self.statValue.stat
switch stat {
case .count, .totalDuration:
BarMark(x: .value("date", point.index),
y: .value("value", point.value.doubleValue))
default:
LineMark(x: .value("date", point.index),
y: .value("value", point.value.doubleValue))
}
}.chartXAxis(.hidden)
.chartYAxis {
AxisMarks() { value in
AxisValueLabel {
Text(self.statValue.stat.format(axisValue: value))
}
}
}.frame(height: 200.0)
// Chart(self.statValue.chartPoint(timeFrame: self.timeFrame)) { point in
//
// let stat: Stat = self.statValue.stat
//
// switch stat {
// case .count, .totalDuration:
// BarMark(x: .value("date", point.index),
// y: .value("value", point.value.doubleValue))
// default:
// LineMark(x: .value("date", point.index),
// y: .value("value", point.value.doubleValue))
// }
// }.chartXAxis(.hidden)
// .chartYAxis {
// AxisMarks() { value in
// AxisValueLabel {
// Text(self.statValue.stat.format(axisValue: value))
// }
// }
// }.frame(height: 200.0)
}
}
@ -150,7 +142,9 @@ struct StatGraphView_Previews: PreviewProvider {
Point(date: Date(timeIntervalSince1970: 100000), value: 1)]
static var previews: some View {
GraphView(statValue: StatValue(stat: .count, value: NSDecimalNumber(integerLiteral: 3), points: points), timeFrame: .all)
GraphView(statValue: StatValue(stat: .count,
value: NSDecimalNumber(integerLiteral: 3),
points: points), timeFrame: .month)
}
}
@ -162,7 +156,12 @@ struct StatView_Previews: PreviewProvider {
struct StatsView_Previews: PreviewProvider {
static var previews: some View {
GraphsView(activity: Activity.fake(context: PersistenceController.preview.container.viewContext),
timeFrame: .all)
StatisticsView(activity: Activity.fake(context: PersistenceController.preview.container.viewContext)) { values in
VStack {
ForEach(values) { value in
StatView(statValue: value)
}
}
}
}
}

@ -24,7 +24,7 @@ class IntentDataProvider {
func timer(id: String) -> AbstractTimer? {
let context = PersistenceController.shared.container.viewContext
return context.object(stringId: id) as? AbstractTimer
return context.object(stringId: id)
}
}

@ -264,3 +264,4 @@
"Hours" = "Heures";
"Default Volume" = "Volume par défaut";
"Cancel %@" = "Annuler %@";
"Calendar" = "Calendrier";

Loading…
Cancel
Save