Skip to content

Commit

Permalink
The line chart for the odometer is ready. (#374)
Browse files Browse the repository at this point in the history
* The line chart for the odometer is ready.
* PR suggestion

---------

Co-authored-by: Mikaela Caron <mikaelacaron@gmail.com>
  • Loading branch information
lukasskolek and mikaelacaron authored Jan 10, 2025
1 parent 7598272 commit 86f2ee2
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 33 deletions.
3 changes: 3 additions & 0 deletions Basic-Car-Maintenance/Shared/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -5241,6 +5241,9 @@
}
}
}
},
"Time Range" : {

},
"Title" : {
"comment" : "Maintenance event title text field header",
Expand Down
157 changes: 124 additions & 33 deletions Basic-Car-Maintenance/Shared/Odometer/Views/OdometerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,48 +7,85 @@
//

import SwiftUI
import Charts

struct OdometerView: View {
@Environment(ActionService.self) var actionService

@State private var viewModel: OdometerViewModel

@State private var selectedTimeRange: TimeRange = .all

init(userUID: String?) {
self.init(viewModel: OdometerViewModel(userUID: userUID))
}

fileprivate init(viewModel: OdometerViewModel) {
self.viewModel = viewModel
}

var body: some View {
NavigationStack {
List {
ForEach(viewModel.readings) { reading in
let vehicleName = viewModel.vehicles.first { $0.id == reading.vehicleID }?.name
OdometerRowView(reading: reading, vehicleName: vehicleName)
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive) {
Task {
await viewModel.deleteReading(reading)
VStack {
Picker("Time Range", selection: $selectedTimeRange) {
ForEach(TimeRange.allCases) { timeRange in
Text(timeRange.rawValue).tag(timeRange)
}
}
.pickerStyle(.segmented)
.padding(.horizontal)

if !viewModel.readings.isEmpty {
GroupBox {
Chart {
ForEach(viewModel.vehicles) { vehicle in
let vehicleReadings = filteredReadings(for: vehicle)

if !vehicleReadings.isEmpty {
ForEach(vehicleReadings) { reading in
LineMark(
x: .value("Date", reading.date, unit: .day),
y: .value("Odometer", reading.distance)
)
}
.foregroundStyle(by: .value("Vehicle", vehicle.name))
.symbol(by: .value("Vehicle", vehicle.name))
.interpolationMethod(.monotone)
}
} label: {
Image(systemName: SFSymbol.trash)
}

Button {
viewModel.selectedReading = reading
viewModel.isShowingEditReadingView = true
} label: {
Label {
Text("Edit")
} icon: {
Image(systemName: SFSymbol.pencil)
}
.frame(height: 200)
}
.padding(.horizontal)
.listRowSeparator(.hidden)
}

List {
ForEach(viewModel.readings) { reading in
let vehicleName = viewModel.vehicles.first { $0.id == reading.vehicleID }?.name
OdometerRowView(reading: reading, vehicleName: vehicleName)
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive) {
Task {
await viewModel.deleteReading(reading)
}
} label: {
Image(systemName: SFSymbol.trash)
}

Button {
viewModel.selectedReading = reading
viewModel.isShowingEditReadingView = true
} label: {
Label {
Text("Edit")
} icon: {
Image(systemName: SFSymbol.pencil)
}
}
}
}
}
.listStyle(.inset)
}
.listStyle(.inset)
}
.overlay {
if viewModel.readings.isEmpty {
Expand Down Expand Up @@ -86,11 +123,38 @@ struct OdometerView: View {
}
}
}

}
.analyticsView("\(Self.self)")
}

/// Filter the readins based on the selected time range, and if there are no readings in the last 30 days, just show the last reading.

Check warning on line 130 in Basic-Car-Maintenance/Shared/Odometer/Views/OdometerView.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Line should be 110 characters or less; currently it has 138 characters (line_length)
/// - Parameter vehicle: The vehicle for these readings.
/// - Returns: The `[OdometerReading]`s for this vehicle in the time range.
private func filteredReadings(for vehicle: Vehicle) -> [OdometerReading] {
let vehicleReadings = viewModel.readings.filter { $0.vehicleID == vehicle.id }

switch selectedTimeRange {
case .all:
return vehicleReadings
case .last30Days:
guard let lastReadingDate = vehicleReadings.map({ $0.date }).max() else {
return []
}
let thirtyDaysAgo = Calendar.current.date(byAdding: .day, value: -30, to: Date()) ?? Date()

// If the last reading is older than 30 days, include only the last reading
if lastReadingDate < thirtyDaysAgo {
if let lastReading = vehicleReadings.max(by: { $0.date < $1.date }) {
return [lastReading]
} else {
return []
}
} else {
return vehicleReadings.filter { $0.date >= thirtyDaysAgo }
}
}
}

private func makeAddOdometerView() -> some View {
AddOdometerReadingView(vehicles: viewModel.vehicles) { reading in
do {
Expand All @@ -112,25 +176,52 @@ struct OdometerView: View {
}
}

/// The time range options in the picker for the graph.
enum TimeRange: String, CaseIterable, Identifiable {
case all = "All readings"
case last30Days = "Latest readings"

var id: String { self.rawValue }
}

#Preview {
let viewModel = OdometerViewModel(userUID: nil)
let firstCar = createVehicle(id: "id1", name: "My 1st car")
let secondCar = createVehicle(id: "id2", name: "2nd Car")

viewModel.vehicles.append(contentsOf: [firstCar, secondCar])

let firstReading = createReading(vehicleID: secondCar.id!,
let firstReading = createReading(vehicleID: firstCar.id!,
date: "2024/10/18",
distance: 20)
distance: 35)
let secondReading = createReading(vehicleID: firstCar.id!,
date: "2024/10/15",
distance: 1000)

date: "2024/10/19",
distance: 564)
let thirdReading = createReading(vehicleID: firstCar.id!,
date: "2024/11/23",
distance: 1000)

let fourthReading = createReading(vehicleID: firstCar.id!,
date: "2024/11/30",
distance: 1024)
let fifthReading = createReading(vehicleID: secondCar.id!,
date: "2024/10/1",
distance: 1000)

let sixthReading = createReading(vehicleID: secondCar.id!,
date: "2024/10/13",
distance: 10)
viewModel.readings.append(contentsOf: [firstReading, secondReading, thirdReading])

distance: 1144)
let seventhReading = createReading(vehicleID: secondCar.id!,
date: "2024/10/15",
distance: 1412)

let eighthReading = createReading(vehicleID: secondCar.id!,
date: "2024/11/13",
distance: 1542)

// swiftlint:disable:next line_length
viewModel.readings.append(contentsOf: [firstReading, secondReading, thirdReading, fourthReading, fifthReading, sixthReading, seventhReading, eighthReading])

return OdometerView(viewModel: viewModel)
.environment(ActionService.shared)

Expand All @@ -145,7 +236,7 @@ struct OdometerView: View {
vin: nil,
licensePlateNumber: nil)
}

func createReading(vehicleID: String, date: String, distance: Int) -> OdometerReading {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy/MM/dd"
Expand Down

0 comments on commit 86f2ee2

Please sign in to comment.