Skip to content

Commit

Permalink
Implement AddOdometerReading AppIntent
Browse files Browse the repository at this point in the history
  • Loading branch information
Omar Hegazy authored and Omar Hegazy committed Oct 27, 2024
1 parent ba71d02 commit b79dcc0
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 15 deletions.
138 changes: 138 additions & 0 deletions Basic-Car-Maintenance/Shared/AppIntents/AddOdometerReadingIntent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
//
// VehicleQuery.swift
// Basic-Car-Maintenance
//
// https://github.com/mikaelacaron/Basic-Car-Maintenance
// See LICENSE for license information.
//


Check warning on line 9 in Basic-Car-Maintenance/Shared/AppIntents/AddOdometerReadingIntent.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Limit vertical whitespace to a single empty line; currently 2 (vertical_whitespace)
import AppIntents

/// The query used to retrieve vehicles for adding odometer.
struct VehicleQuery: EntityQuery {

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 authViewModel = await AuthenticationViewModel()
let odometerVM = OdometerViewModel(userUID: authViewModel.user?.uid)
await odometerVM.getVehicles()
guard !odometerVM.vehicles.isEmpty else {
throw OdometerReadingError.emptyVehicles
}
return odometerVM.vehicles
}
}

/// 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, CaseIterable {
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Distance Type")
static var caseDisplayRepresentations: [DistanceUnit: DisplayRepresentation] {
[
.mile: "Miles",
.kilometer: "Kilometers"
]
}

case mile
case kilometer
}

/// 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 {
@Parameter(title: "Distance")
var distance: Int

@Parameter(
title: LocalizedStringResource(
"Distance Unit",
comment: "The distance unit in miles or kilometers"
)
)
var distanceType: DistanceUnit

@Parameter(title: "Vehicle")
var vehicle: Vehicle

@Parameter(title: "Date")
var date: Date

static var title = LocalizedStringResource(
"Add Odometer Reading",
comment: "Title for the app intent when adding an odometer reading"
)

func perform() async throws -> some IntentResult & ProvidesDialog {
if distance < 1 {
throw OdometerReadingError.invalidDistance
}

let reading = OdometerReading(
date: date,
distance: distance,
isMetric: distanceType == .kilometer,
vehicleID: vehicle.id
)
let authViewModel = await AuthenticationViewModel()
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"

Check warning on line 103 in Basic-Car-Maintenance/Shared/AppIntents/AddOdometerReadingIntent.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Line should be 110 characters or less; currently it has 114 characters (line_length)
)
)
)
}
}

/// 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.

Check warning on line 115 in Basic-Car-Maintenance/Shared/AppIntents/AddOdometerReadingIntent.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Line should be 110 characters or less; currently it has 112 characters (line_length)
/// - `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 OdometerReadingError: Error, CustomLocalizedStringResourceConvertible {
case invalidDistance
case emptyVehicles

var localizedStringResource: LocalizedStringResource {
switch self {
case .invalidDistance:
LocalizedStringResource(
"You can not select distance number less than 1 km or mi",
comment: "an error shown when entering a zero or negative value for distance"
)
case .emptyVehicles:
LocalizedStringResource(
"No vehicles available, please add a vehicle using the app and try again",
comment: "an error shown when attempting to add an odometer while there are no vehicles added"
)

}
}
}

Check warning on line 138 in Basic-Car-Maintenance/Shared/AppIntents/AddOdometerReadingIntent.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Files should have a single trailing newline (trailing_newline)
18 changes: 18 additions & 0 deletions Basic-Car-Maintenance/Shared/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,9 @@
}
}
},
"Add Odometer Reading" : {
"comment" : "Title for the app intent when adding an odometer reading"
},
"Add Reading" : {
"comment" : "Title for form when adding an odometer reading",
"localizations" : {
Expand Down Expand Up @@ -695,6 +698,9 @@
}
}
},
"Added reading successfully" : {
"comment" : "The message shown when successfully adding an odometer reading using the app intent"
},
"AddEvent" : {
"comment" : "Label for adding maintenance event on Dashboard view",
"localizations" : {
Expand Down Expand Up @@ -1917,6 +1923,12 @@
}
}
},
"Distance Type" : {

},
"Distance Unit" : {
"comment" : "The distance unit in miles or kilometers"
},
"Edit" : {
"comment" : "Button label to edit this maintenance",
"localizations" : {
Expand Down Expand Up @@ -3556,6 +3568,9 @@
}
}
},
"No vehicles available, please add a vehicle using the app and try again" : {
"comment" : "an error shown when attempting to add an odometer while there are no vehicles added"
},
"Notes" : {
"comment" : "Maintenance event notes text field label",
"localizations" : {
Expand Down Expand Up @@ -6421,6 +6436,9 @@
}
}
},
"You can not select distance number less than 1 km or mi" : {
"comment" : "an error shown when entering a zero or negative value for distance"
},
"your vehicle" : {
"localizations" : {
"fa" : {
Expand Down
47 changes: 36 additions & 11 deletions Basic-Car-Maintenance/Shared/Models/Vehicle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -19,17 +20,23 @@ struct Vehicle: Codable, Identifiable, Hashable {
let color: String?
let vin: String?
let licensePlateNumber: String?
var displayRepresentation: DisplayRepresentation { DisplayRepresentation(title: "\(name)") }

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
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.documentID = id
self.userID = userID
self.name = name
self.make = make
Expand All @@ -39,4 +46,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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,13 @@ final class SettingsViewModel {
func updateVehicle(_ vehicle: Vehicle) async {

if let userUID = authenticationViewModel.user?.uid {
guard let vehicleID = vehicle.id else { return }
var vehicleToUpdate = vehicle
vehicleToUpdate.userID = userUID

do {
try Firestore.firestore()
.collection(FirestoreCollection.vehicles)
.document(vehicleID)
.document(vehicle.id)
.setData(from: vehicleToUpdate)

AnalyticsService.shared.logEvent(.vehicleUpdate)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,15 @@ struct EditVehicleView: View, Observable {
ToolbarItem(placement: .topBarTrailing) {
Button {
if let selectedVehicle {
var vehicle = Vehicle(
let vehicle = Vehicle(
id: selectedVehicle.id,
name: name,
make: make,
model: model,
year: year,
color: color,
vin: VIN,
licensePlateNumber: licensePlateNumber)
vehicle.id = selectedVehicle.id
Task {
await viewModel.updateVehicle(vehicle)
dismiss()
Expand Down

0 comments on commit b79dcc0

Please sign in to comment.