Skip to content

Commit b79dcc0

Browse files
Omar HegazyOmar Hegazy
authored andcommitted
Implement AddOdometerReading AppIntent
1 parent ba71d02 commit b79dcc0

File tree

5 files changed

+195
-15
lines changed

5 files changed

+195
-15
lines changed
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
//
2+
// VehicleQuery.swift
3+
// Basic-Car-Maintenance
4+
//
5+
// https://github.com/mikaelacaron/Basic-Car-Maintenance
6+
// See LICENSE for license information.
7+
//
8+
9+
10+
import AppIntents
11+
12+
/// The query used to retrieve vehicles for adding odometer.
13+
struct VehicleQuery: EntityQuery {
14+
15+
func entities(for identifiers: [Vehicle.ID]) async throws -> [Vehicle] {
16+
try await fetchVehicles()
17+
}
18+
19+
func suggestedEntities() async throws -> [Vehicle] {
20+
try await fetchVehicles()
21+
}
22+
23+
private func fetchVehicles() async throws -> [Vehicle] {
24+
let authViewModel = await AuthenticationViewModel()
25+
let odometerVM = OdometerViewModel(userUID: authViewModel.user?.uid)
26+
await odometerVM.getVehicles()
27+
guard !odometerVM.vehicles.isEmpty else {
28+
throw OdometerReadingError.emptyVehicles
29+
}
30+
return odometerVM.vehicles
31+
}
32+
}
33+
34+
/// An enumeration representing the units of distance used for odometer readings.
35+
///
36+
/// This enum conforms to `AppEnum` and `CaseIterable` to provide display representations
37+
/// for the available distance units: miles and kilometers.
38+
///
39+
/// - `mile`: Represents distance in miles.
40+
/// - `kilometer`: Represents distance in kilometers.
41+
enum DistanceUnit: String, AppEnum, CaseIterable {
42+
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Distance Type")
43+
static var caseDisplayRepresentations: [DistanceUnit: DisplayRepresentation] {
44+
[
45+
.mile: "Miles",
46+
.kilometer: "Kilometers"
47+
]
48+
}
49+
50+
case mile
51+
case kilometer
52+
}
53+
54+
/// An `AppIntent` that allows the user to add an odometer reading for a specified vehicle.
55+
///
56+
/// This intent accepts the distance traveled, the unit of distance (miles or kilometers),
57+
/// the vehicle for which the odometer reading is being recorded, and the date of the reading.
58+
///
59+
/// The intent validates the input, ensuring that the distance is a positive integer.
60+
/// If the input is valid, the intent creates an `OdometerReading` and saves it using the `OdometerViewModel`.
61+
/// Upon successful completion, a confirmation dialog is presented to the user.
62+
struct AddOdometerReadingIntent: AppIntent {
63+
@Parameter(title: "Distance")
64+
var distance: Int
65+
66+
@Parameter(
67+
title: LocalizedStringResource(
68+
"Distance Unit",
69+
comment: "The distance unit in miles or kilometers"
70+
)
71+
)
72+
var distanceType: DistanceUnit
73+
74+
@Parameter(title: "Vehicle")
75+
var vehicle: Vehicle
76+
77+
@Parameter(title: "Date")
78+
var date: Date
79+
80+
static var title = LocalizedStringResource(
81+
"Add Odometer Reading",
82+
comment: "Title for the app intent when adding an odometer reading"
83+
)
84+
85+
func perform() async throws -> some IntentResult & ProvidesDialog {
86+
if distance < 1 {
87+
throw OdometerReadingError.invalidDistance
88+
}
89+
90+
let reading = OdometerReading(
91+
date: date,
92+
distance: distance,
93+
isMetric: distanceType == .kilometer,
94+
vehicleID: vehicle.id
95+
)
96+
let authViewModel = await AuthenticationViewModel()
97+
let odometerVM = OdometerViewModel(userUID: authViewModel.user?.uid)
98+
try odometerVM.addReading(reading)
99+
return .result(
100+
dialog: IntentDialog(
101+
LocalizedStringResource(
102+
"Added reading successfully",
103+
comment: "The message shown when successfully adding an odometer reading using the app intent"
104+
)
105+
)
106+
)
107+
}
108+
}
109+
110+
/// An enumeration representing errors that can occur when adding an odometer reading.
111+
///
112+
/// This enum conforms to `Error` and `CustomLocalizedStringResourceConvertible` to provide
113+
/// localized error messages for specific conditions:
114+
///
115+
/// - `invalidDistance`: Triggered when a distance value less than 1 (either in kilometers or miles) is entered.
116+
/// - `emptyVehicles`: Triggered when there are no vehicles available to select for the odometer reading.
117+
///
118+
/// Each case provides a user-friendly localized string resource that describes the error.
119+
enum OdometerReadingError: Error, CustomLocalizedStringResourceConvertible {
120+
case invalidDistance
121+
case emptyVehicles
122+
123+
var localizedStringResource: LocalizedStringResource {
124+
switch self {
125+
case .invalidDistance:
126+
LocalizedStringResource(
127+
"You can not select distance number less than 1 km or mi",
128+
comment: "an error shown when entering a zero or negative value for distance"
129+
)
130+
case .emptyVehicles:
131+
LocalizedStringResource(
132+
"No vehicles available, please add a vehicle using the app and try again",
133+
comment: "an error shown when attempting to add an odometer while there are no vehicles added"
134+
)
135+
136+
}
137+
}
138+
}

Basic-Car-Maintenance/Shared/Localizable.xcstrings

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,9 @@
401401
}
402402
}
403403
},
404+
"Add Odometer Reading" : {
405+
"comment" : "Title for the app intent when adding an odometer reading"
406+
},
404407
"Add Reading" : {
405408
"comment" : "Title for form when adding an odometer reading",
406409
"localizations" : {
@@ -695,6 +698,9 @@
695698
}
696699
}
697700
},
701+
"Added reading successfully" : {
702+
"comment" : "The message shown when successfully adding an odometer reading using the app intent"
703+
},
698704
"AddEvent" : {
699705
"comment" : "Label for adding maintenance event on Dashboard view",
700706
"localizations" : {
@@ -1917,6 +1923,12 @@
19171923
}
19181924
}
19191925
},
1926+
"Distance Type" : {
1927+
1928+
},
1929+
"Distance Unit" : {
1930+
"comment" : "The distance unit in miles or kilometers"
1931+
},
19201932
"Edit" : {
19211933
"comment" : "Button label to edit this maintenance",
19221934
"localizations" : {
@@ -3556,6 +3568,9 @@
35563568
}
35573569
}
35583570
},
3571+
"No vehicles available, please add a vehicle using the app and try again" : {
3572+
"comment" : "an error shown when attempting to add an odometer while there are no vehicles added"
3573+
},
35593574
"Notes" : {
35603575
"comment" : "Maintenance event notes text field label",
35613576
"localizations" : {
@@ -6421,6 +6436,9 @@
64216436
}
64226437
}
64236438
},
6439+
"You can not select distance number less than 1 km or mi" : {
6440+
"comment" : "an error shown when entering a zero or negative value for distance"
6441+
},
64246442
"your vehicle" : {
64256443
"localizations" : {
64266444
"fa" : {

Basic-Car-Maintenance/Shared/Models/Vehicle.swift

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88

99
import FirebaseFirestoreSwift
1010
import Foundation
11+
import AppIntents
1112

1213
struct Vehicle: Codable, Identifiable, Hashable {
13-
@DocumentID var id: String?
14+
@DocumentID private var documentID: String?
1415
var userID: String?
1516
let name: String
1617
let make: String
@@ -19,17 +20,23 @@ struct Vehicle: Codable, Identifiable, Hashable {
1920
let color: String?
2021
let vin: String?
2122
let licensePlateNumber: String?
23+
var displayRepresentation: DisplayRepresentation { DisplayRepresentation(title: "\(name)") }
2224

23-
init(id: String? = nil,
24-
userID: String? = nil,
25-
name: String,
26-
make: String,
27-
model: String,
28-
year: String? = nil,
29-
color: String? = nil,
30-
vin: String? = nil,
31-
licensePlateNumber: String? = nil) {
32-
self.id = id
25+
static var defaultQuery = VehicleQuery()
26+
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Vehicle")
27+
28+
init(
29+
id: String? = nil,
30+
userID: String? = nil,
31+
name: String,
32+
make: String,
33+
model: String,
34+
year: String? = nil,
35+
color: String? = nil,
36+
vin: String? = nil,
37+
licensePlateNumber: String? = nil
38+
) {
39+
self.documentID = id
3340
self.userID = userID
3441
self.name = name
3542
self.make = make
@@ -39,4 +46,22 @@ struct Vehicle: Codable, Identifiable, Hashable {
3946
self.vin = vin
4047
self.licensePlateNumber = licensePlateNumber
4148
}
49+
50+
enum CodingKeys: String, CodingKey {
51+
case documentID = "_id"
52+
case userID
53+
case name
54+
case make
55+
case model
56+
case year
57+
case color
58+
case vin
59+
case licensePlateNumber
60+
}
61+
}
62+
63+
extension Vehicle: AppEntity {
64+
var id: String {
65+
documentID ?? UUID().uuidString
66+
}
4267
}

Basic-Car-Maintenance/Shared/Settings/ViewModels/SettingsViewModel.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,13 @@ final class SettingsViewModel {
8383
func updateVehicle(_ vehicle: Vehicle) async {
8484

8585
if let userUID = authenticationViewModel.user?.uid {
86-
guard let vehicleID = vehicle.id else { return }
8786
var vehicleToUpdate = vehicle
8887
vehicleToUpdate.userID = userUID
8988

9089
do {
9190
try Firestore.firestore()
9291
.collection(FirestoreCollection.vehicles)
93-
.document(vehicleID)
92+
.document(vehicle.id)
9493
.setData(from: vehicleToUpdate)
9594

9695
AnalyticsService.shared.logEvent(.vehicleUpdate)

Basic-Car-Maintenance/Shared/Settings/Views/EditVehicleView.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,15 +87,15 @@ struct EditVehicleView: View, Observable {
8787
ToolbarItem(placement: .topBarTrailing) {
8888
Button {
8989
if let selectedVehicle {
90-
var vehicle = Vehicle(
90+
let vehicle = Vehicle(
91+
id: selectedVehicle.id,
9192
name: name,
9293
make: make,
9394
model: model,
9495
year: year,
9596
color: color,
9697
vin: VIN,
9798
licensePlateNumber: licensePlateNumber)
98-
vehicle.id = selectedVehicle.id
9999
Task {
100100
await viewModel.updateVehicle(vehicle)
101101
dismiss()

0 commit comments

Comments
 (0)