stat + ipad navigation

release
Laurent 3 years ago
parent d85761a82f
commit e3839be022
  1. 9
      LeCountdown/Stats/Context+Calculations.swift
  2. 8
      LeCountdown/Stats/Filter.swift
  3. 38
      LeCountdown/Stats/Stat.swift
  4. 17
      LeCountdown/Utils/Date+Extensions.swift
  5. 2
      LeCountdown/Views/ContentView.swift
  6. 2
      LeCountdown/Views/HomeView.swift
  7. 18
      LeCountdown/Views/PresetsView.swift
  8. 15
      LeCountdown/Views/Stats/ActivityView.swift
  9. 2
      LeCountdown/Views/Stats/RecordsView.swift
  10. 24
      LeCountdown/Views/Stats/StatsView.swift

@ -45,7 +45,6 @@ extension NSManagedObjectContext {
predicates.append(filter.predicate) predicates.append(filter.predicate)
} }
fetchRequest.predicate = NSCompoundPredicate.init(andPredicateWithSubpredicates: predicates) fetchRequest.predicate = NSCompoundPredicate.init(andPredicateWithSubpredicates: predicates)
fetchRequest.propertiesToFetch = expressions fetchRequest.propertiesToFetch = expressions
fetchRequest.resultType = NSFetchRequestResultType.dictionaryResultType fetchRequest.resultType = NSFetchRequestResultType.dictionaryResultType
@ -83,9 +82,10 @@ extension NSManagedObjectContext {
request.sortDescriptors = [NSSortDescriptor(key: "start", ascending: true)] request.sortDescriptors = [NSSortDescriptor(key: "start", ascending: true)]
} }
let records: [Record] = try self.fetch(request) let records: [Record] = try self.fetch(request)
let points: [Point] = records.compactMap { $0.point(stat:stat) } let points: [Point] = records.compactMap { $0.point(stat:stat) }
let sv = StatValue(stat: stat, value: value, records: points) let sv = StatValue(stat: stat, value: value, points: points)
statValues.append(sv) statValues.append(sv)
} }
} }
@ -99,12 +99,11 @@ fileprivate extension Record {
func point(stat: Stat) -> Point? { func point(stat: Stat) -> Point? {
if let start { if let start {
let day = start.startOfDay
switch stat { switch stat {
case .count: case .count:
return Point(date: day, value: NSNumber(value: 1)) return Point(date: start, value: NSNumber(value: 1))
case .totalDuration, .averageDuration: case .totalDuration, .averageDuration:
return Point(date: day, value: NSNumber(value: self.duration / 3600.0)) return Point(date: start, value: NSNumber(value: self.duration / 3600.0))
} }
} }
return nil return nil

@ -76,16 +76,10 @@ struct Month {
return NSDate() return NSDate()
} }
fileprivate static let _dateFormatter = {
let df = DateFormatter()
df.dateFormat = "MMMM yyyy"
return df
}()
var localizedString: String { var localizedString: String {
let components = DateComponents(year: self.year, month: self.month) let components = DateComponents(year: self.year, month: self.month)
if let date = Calendar.current.date(from: components) { if let date = Calendar.current.date(from: components) {
return Month._dateFormatter.string(from: date) return Date.monthYearFormatter.string(from: date)
} }
return "invalid date" return "invalid date"
} }

@ -65,13 +65,6 @@ enum Stat: Int, CaseIterable {
} }
} }
// var keyPath: KeyPath<Record, NSNumber> {
// switch self {
// case .count: return \Record.count
// case .averageDuration, .totalDuration: return \Record.duration
// }
// }
} }
struct Point: Identifiable { struct Point: Identifiable {
@ -80,10 +73,14 @@ struct Point: Identifiable {
var id: Date { date } var id: Date { date }
var dateValue: Date {
return Date(timeIntervalSince1970: value.doubleValue)
} }
struct ChartPoint: Identifiable {
var index: Int
var label: String
var value: NSNumber
var id: Int { index }
} }
struct StatValue: Identifiable { struct StatValue: Identifiable {
@ -92,7 +89,7 @@ struct StatValue: Identifiable {
var stat: Stat var stat: Stat
var value: NSDecimalNumber var value: NSDecimalNumber
var records: [Point] = [] var points: [Point] = []
var formattedValue: String { var formattedValue: String {
@ -105,4 +102,25 @@ struct StatValue: Identifiable {
} }
} }
func chartPoint(timeFrame: TimeFrame) -> [ChartPoint] {
var chartPoints: [ChartPoint] = []
for (i, point) in self.points.enumerated() {
let identifier: String
switch timeFrame {
case .all: identifier = point.date.formattedYear
case .year: identifier = point.date.formattedMonth
case .month: identifier = point.date.formattedDay
}
let cp = ChartPoint(index: i, label: identifier, value: point.value)
chartPoints.append(cp)
}
return chartPoints
}
} }

@ -9,6 +9,19 @@ import Foundation
extension Date { extension Date {
static let monthYearFormatter = {
let df = DateFormatter()
df.dateFormat = "MMMM yyyy"
return df
}()
static let dayFormatter = {
let df = DateFormatter()
df.dateStyle = .short
df.timeStyle = .none
return df
}()
var startOfDay: Date { var startOfDay: Date {
return Calendar.current.startOfDay(for: self) return Calendar.current.startOfDay(for: self)
} }
@ -20,4 +33,8 @@ extension Date {
var year: Int { self.get(.year) } var year: Int { self.get(.year) }
var month: Int { self.get(.month) } var month: Int { self.get(.month) }
var formattedYear: String { return "\(self.year)" }
var formattedMonth: String { return Date.monthYearFormatter.string(from: self) }
var formattedDay: String { return Date.dayFormatter.string(from: self) }
} }

@ -125,7 +125,7 @@ struct ContentView<T : AbstractTimer>: View {
// } // }
// } // }
// } // }
ToolbarItem(placement: .navigationBarLeading) { ToolbarItem(placement: .navigationBarTrailing) {
Button { Button {
withAnimation { withAnimation {
self.isEditing.toggle() self.isEditing.toggle()

@ -61,12 +61,12 @@ struct RegularHomeView: View {
var body: some View { var body: some View {
NavigationView { NavigationView {
TabView(selection: $tabSelection) {
PresetsView(tabSelection: $tabSelection) PresetsView(tabSelection: $tabSelection)
.environment(\.managedObjectContext, viewContext) .environment(\.managedObjectContext, viewContext)
.tabItem { Label("Presets", systemImage: "globe") } .tabItem { Label("Presets", systemImage: "globe") }
.tag(0) .tag(0)
TabView(selection: $tabSelection) {
ContentView<AbstractTimer>() ContentView<AbstractTimer>()
.environment(\.managedObjectContext, viewContext) .environment(\.managedObjectContext, viewContext)
.environmentObject(Conductor.maestro) .environmentObject(Conductor.maestro)

@ -155,15 +155,17 @@ struct PresetsView: View {
var tabSelection: Binding<Int> var tabSelection: Binding<Int>
fileprivate func _columnCount() -> Int { fileprivate func _columnCount() -> Int {
#if os(iOS)
if UIDevice.isPhoneIdiom {
return 2 return 2
} else {
return 3 // #if os(iOS)
} // if UIDevice.isPhoneIdiom {
#else // return 2
return 3 // } else {
#endif // return 3
// }
// #else
// return 3
// #endif
} }
fileprivate func _columns() -> [GridItem] { fileprivate func _columns() -> [GridItem] {

@ -27,22 +27,17 @@ struct ActivityView: View {
.pickerStyle(.segmented) .pickerStyle(.segmented)
.padding(.horizontal) .padding(.horizontal)
// List { List {
// Section { StatsView(activity: self.activity,
// StatsView(activity: self.activity, timeFrame: self.selectedTimeFrame)
// timeFrame: self.selectedTimeFrame) .environment(\.managedObjectContext, viewContext)
// .environment(\.managedObjectContext, viewContext)
// }
// Section {
let filters = self.selectedTimeFrame.filters(context: viewContext) let filters = self.selectedTimeFrame.filters(context: viewContext)
RecordsView(activity: self.activity, filters: filters) RecordsView(activity: self.activity, filters: filters)
.environment(\.managedObjectContext, viewContext) .environment(\.managedObjectContext, viewContext)
// } // }
// } }
} }
} }

@ -43,7 +43,7 @@ struct RecordsView: View {
var body: some View { var body: some View {
List { Section {
ForEach(self.filters) { filter in ForEach(self.filters) { filter in
RecordsSectionView(activity: self.activity, filter: filter) RecordsSectionView(activity: self.activity, filter: filter)
.environment(\.managedObjectContext, viewContext) .environment(\.managedObjectContext, viewContext)

@ -53,6 +53,7 @@ struct StatsView: View {
var body: some View { var body: some View {
Section {
VStack() { VStack() {
if self.model.isComputing { if self.model.isComputing {
@ -61,12 +62,13 @@ struct StatsView: View {
} else { } else {
VStack(alignment: .leading) { VStack(alignment: .leading) {
ForEach(self.model.statValues) { statValue in ForEach(self.model.statValues) { statValue in
StatGraphView(statValue: statValue) StatGraphView(statValue: statValue, timeFrame: self.timeFrame)
// .padding(.vertical) // .padding(.vertical)
} }
} }
} }
} }
}
.onAppear() { .onAppear() {
self.model.compute(activity: self.activity, filter: self.filter) self.model.compute(activity: self.activity, filter: self.filter)
} }
@ -93,6 +95,7 @@ struct StatView: View {
struct StatGraphView: View { struct StatGraphView: View {
var statValue: StatValue var statValue: StatValue
var timeFrame: TimeFrame
var body: some View { var body: some View {
VStack { VStack {
@ -102,24 +105,35 @@ struct StatGraphView: View {
Text(self.statValue.formattedValue) Text(self.statValue.formattedValue)
// .font(.system(.title, weight: .bold)) // .font(.system(.title, weight: .bold))
} }
Chart(self.statValue.records) { point in Chart(self.statValue.chartPoint(timeFrame: self.timeFrame)) { point in
let stat: Stat = self.statValue.stat let stat: Stat = self.statValue.stat
switch stat { switch stat {
case .count, .totalDuration: case .count, .totalDuration:
BarMark(x: .value("date", point.date, unit: .day), BarMark(x: .value("date", point.index),
y: .value("value", point.value.doubleValue)) y: .value("value", point.value.doubleValue))
default: default:
LineMark(x: .value("date", point.date, unit: .day), LineMark(x: .value("date", point.index),
y: .value("value", point.value.doubleValue)) y: .value("value", point.value.doubleValue))
} }
}.frame(height: 300.0) }.frame(height: 200.0)
} }
} }
} }
struct StatGraphView_Previews: PreviewProvider {
static let points: [Point] = [Point(date: Date(), value: 1),
Point(date: Date(), value: 1),
Point(date: Date(timeIntervalSince1970: 100000), value: 1)]
static var previews: some View {
StatGraphView(statValue: StatValue(stat: .count, value: NSDecimalNumber(integerLiteral: 3), points: points), timeFrame: .all)
}
}
struct StatView_Previews: PreviewProvider { struct StatView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
StatView(statValue: StatValue(stat: .count, value: NSDecimalNumber(integerLiteral: 3))) StatView(statValue: StatValue(stat: .count, value: NSDecimalNumber(integerLiteral: 3)))

Loading…
Cancel
Save