-
-
Notifications
You must be signed in to change notification settings - Fork 148
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement AddOdometerReading AppIntent #302
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
// | ||
// VehicleQuery.swift | ||
// Basic-Car-Maintenance | ||
// | ||
// https://github.com/mikaelacaron/Basic-Car-Maintenance | ||
// See LICENSE for license information. | ||
// | ||
|
||
import AppIntents | ||
|
||
/// An `AppIntent` that allows the user to add an odometer reading for a specified vehicle. | ||
/// | ||
/// This intent accepts the distance traveled, the unit of distance (miles or kilometers), | ||
/// the vehicle for which the odometer reading is being recorded, and the date of the reading. | ||
/// | ||
/// The intent validates the input, ensuring that the distance is a positive integer. | ||
/// If the input is valid, the intent creates an `OdometerReading` and saves it using the `OdometerViewModel`. | ||
/// Upon successful completion, a confirmation dialog is presented to the user. | ||
struct AddOdometerReadingIntent: AppIntent { | ||
@Dependency private var authViewModel: AuthenticationViewModel | ||
|
||
@Parameter(title: LocalizedStringResource( | ||
"Vehicle", | ||
comment: "The selected vehicle to add the odometer reading to.") | ||
) | ||
var vehicle: Vehicle? | ||
|
||
@Parameter(title: LocalizedStringResource( | ||
"Date", | ||
comment: "The date when the reading should be logged.") | ||
) | ||
var date: Date | ||
|
||
@Parameter( | ||
title: LocalizedStringResource( | ||
"Distance Unit", | ||
comment: "The distance unit in miles or kilometers" | ||
), | ||
requestValueDialog: IntentDialog("In which distance unit would you like to save the entered value?") | ||
) | ||
var distanceType: DistanceUnit | ||
|
||
@Parameter(title: LocalizedStringResource( | ||
"Distance", | ||
comment: "The distance value") | ||
) | ||
var distance: Int | ||
|
||
static var title = LocalizedStringResource( | ||
"Add Odometer Reading", | ||
comment: "Title for the app intent when adding an odometer reading" | ||
) | ||
|
||
private func fetchVehicles() async throws -> [Vehicle] { | ||
let odometerVM = OdometerViewModel(userUID: authViewModel.user?.uid) | ||
await odometerVM.getVehicles() | ||
guard !odometerVM.vehicles.isEmpty else { | ||
throw OdometerReadingIntentError.emptyVehicles | ||
} | ||
return odometerVM.vehicles | ||
} | ||
|
||
func perform() async throws -> some IntentResult & ProvidesDialog { | ||
if distance < 1 { | ||
throw OdometerReadingIntentError.invalidDistance | ||
} | ||
|
||
let selectVehicle: Vehicle | ||
if let vehicle { | ||
selectVehicle = vehicle | ||
} else { | ||
let fetchedVehicles = try await fetchVehicles() | ||
selectVehicle = try await $vehicle.requestDisambiguation( | ||
among: fetchedVehicles, | ||
dialog: IntentDialog("Which vehicle would you like to add this to?") | ||
) | ||
} | ||
|
||
let reading = OdometerReading( | ||
date: date, | ||
distance: distance, | ||
isMetric: distanceType == .kilometer, | ||
vehicleID: selectVehicle.id | ||
) | ||
|
||
let odometerVM = OdometerViewModel(userUID: authViewModel.user?.uid) | ||
try odometerVM.addReading(reading) | ||
return .result( | ||
dialog: IntentDialog( | ||
LocalizedStringResource( | ||
"Added reading successfully", | ||
comment: "The message shown when successfully adding an odometer reading using the app intent" | ||
) | ||
) | ||
) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// | ||
// DistanceUnit.swift | ||
// Basic-Car-Maintenance | ||
// | ||
// https://github.com/mikaelacaron/Basic-Car-Maintenance | ||
// See LICENSE for license information. | ||
// | ||
|
||
import AppIntents | ||
|
||
/// An enumeration representing the units of distance used for odometer readings. | ||
/// | ||
/// This enum conforms to `AppEnum` and `CaseIterable` to provide display representations | ||
/// for the available distance units: miles and kilometers. | ||
/// | ||
/// - `mile`: Represents distance in miles. | ||
/// - `kilometer`: Represents distance in kilometers. | ||
enum DistanceUnit: String, AppEnum { | ||
case mile | ||
case kilometer | ||
|
||
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Distance Type") | ||
static var caseDisplayRepresentations: [DistanceUnit: DisplayRepresentation] { | ||
[ | ||
.mile: "Miles", | ||
.kilometer: "Kilometers" | ||
] | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
// | ||
// OdometerReadingIntentError.swift | ||
// Basic-Car-Maintenance | ||
// | ||
// https://github.com/mikaelacaron/Basic-Car-Maintenance | ||
// See LICENSE for license information. | ||
// | ||
|
||
import AppIntents | ||
|
||
/// An enumeration representing errors that can occur when adding an odometer reading. | ||
/// | ||
/// This enum conforms to `Error` and `CustomLocalizedStringResourceConvertible` to provide | ||
/// localized error messages for specific conditions: | ||
/// | ||
/// - `invalidDistance`: Triggered when a distance value less than 1 (either in kilometers or miles) is entered. | ||
/// - `emptyVehicles`: Triggered when there are no vehicles available to select for the odometer reading. | ||
/// | ||
/// Each case provides a user-friendly localized string resource that describes the error. | ||
enum OdometerReadingIntentError: Error, CustomLocalizedStringResourceConvertible { | ||
case invalidDistance | ||
case emptyVehicles | ||
|
||
var localizedStringResource: LocalizedStringResource { | ||
switch self { | ||
case .invalidDistance: | ||
LocalizedStringResource( | ||
"Please add a distance of at least 1 kilometer or mile.", | ||
comment: "an error shown when entering a zero or negative value for distance" | ||
) | ||
case .emptyVehicles: | ||
LocalizedStringResource( | ||
"Sorry, there're no vehicles saved to add a reading, please make sure you've saved at least one vehicle. You can do this in the app and then try adding the reading again.", | ||
comment: "an error shown when attempting to add an odometer while there are no vehicles added" | ||
) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
// | ||
// VehicleQuery.swift | ||
// Basic-Car-Maintenance | ||
// | ||
// https://github.com/mikaelacaron/Basic-Car-Maintenance | ||
// See LICENSE for license information. | ||
// | ||
|
||
import AppIntents | ||
|
||
/// The query used to retrieve vehicles for adding odometer. | ||
struct VehicleQuery: EntityQuery { | ||
@Dependency private var authViewModel: AuthenticationViewModel | ||
|
||
func entities(for identifiers: [Vehicle.ID]) async throws -> [Vehicle] { | ||
try await fetchVehicles() | ||
} | ||
|
||
func suggestedEntities() async throws -> [Vehicle] { | ||
try await fetchVehicles() | ||
} | ||
|
||
private func fetchVehicles() async throws -> [Vehicle] { | ||
let odometerVM = OdometerViewModel(userUID: authViewModel.user?.uid) | ||
await odometerVM.getVehicles() | ||
guard !odometerVM.vehicles.isEmpty else { | ||
throw OdometerReadingIntentError.emptyVehicles | ||
} | ||
return odometerVM.vehicles | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,9 +8,10 @@ | |
|
||
import FirebaseFirestoreSwift | ||
import Foundation | ||
import AppIntents | ||
|
||
struct Vehicle: Codable, Identifiable, Hashable { | ||
@DocumentID var id: String? | ||
@DocumentID private var documentID: String? | ||
var userID: String? | ||
let name: String | ||
let make: String | ||
|
@@ -19,17 +20,25 @@ struct Vehicle: Codable, Identifiable, Hashable { | |
let color: String? | ||
let vin: String? | ||
let licensePlateNumber: String? | ||
var displayRepresentation: DisplayRepresentation { | ||
DisplayRepresentation(title: "\(name)") | ||
} | ||
|
||
static var defaultQuery = VehicleQuery() | ||
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Vehicle") | ||
|
||
init(id: String? = nil, | ||
userID: String? = nil, | ||
name: String, | ||
make: String, | ||
model: String, | ||
year: String? = nil, | ||
color: String? = nil, | ||
vin: String? = nil, | ||
licensePlateNumber: String? = nil) { | ||
self.id = id | ||
init( | ||
id: String? = nil, | ||
userID: String? = nil, | ||
name: String, | ||
make: String, | ||
model: String, | ||
year: String? = nil, | ||
color: String? = nil, | ||
vin: String? = nil, | ||
licensePlateNumber: String? = nil | ||
) { | ||
self.documentID = id | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The documentID shouldn't be set from the id, because it would always be empty this gets set properly when fetching from Firebase There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So there are a couple of things going on here:
Hopefully that made it clear |
||
self.userID = userID | ||
self.name = name | ||
self.make = make | ||
|
@@ -39,4 +48,22 @@ struct Vehicle: Codable, Identifiable, Hashable { | |
self.vin = vin | ||
self.licensePlateNumber = licensePlateNumber | ||
} | ||
|
||
enum CodingKeys: String, CodingKey { | ||
case documentID = "_id" | ||
case userID | ||
case name | ||
case make | ||
case model | ||
case year | ||
case color | ||
case vin | ||
case licensePlateNumber | ||
} | ||
} | ||
|
||
extension Vehicle: AppEntity { | ||
var id: String { | ||
documentID ?? UUID().uuidString | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's add
OdometerViewModel
as a dependency too