From 3838276be59a7462484370c4d3ca37e61b3b22f5 Mon Sep 17 00:00:00 2001 From: Omar Hegazy Date: Sun, 27 Oct 2024 14:30:36 +0300 Subject: [PATCH 1/2] Implement AddOdometerReading AppIntent --- .../AppIntents/AddOdometerReadingIntent.swift | 138 ++++++++++++++++++ .../Shared/Localizable.xcstrings | 18 +++ .../Shared/Models/Vehicle.swift | 47 ++++-- .../ViewModels/SettingsViewModel.swift | 3 +- .../Settings/Views/EditVehicleView.swift | 4 +- 5 files changed, 195 insertions(+), 15 deletions(-) create mode 100644 Basic-Car-Maintenance/Shared/AppIntents/AddOdometerReadingIntent.swift diff --git a/Basic-Car-Maintenance/Shared/AppIntents/AddOdometerReadingIntent.swift b/Basic-Car-Maintenance/Shared/AppIntents/AddOdometerReadingIntent.swift new file mode 100644 index 00000000..8da28791 --- /dev/null +++ b/Basic-Car-Maintenance/Shared/AppIntents/AddOdometerReadingIntent.swift @@ -0,0 +1,138 @@ +// +// 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 { + + 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" + ) + ) + ) + } +} + +/// 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 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" + ) + + } + } +} \ No newline at end of file diff --git a/Basic-Car-Maintenance/Shared/Localizable.xcstrings b/Basic-Car-Maintenance/Shared/Localizable.xcstrings index b638c89c..576a73d6 100644 --- a/Basic-Car-Maintenance/Shared/Localizable.xcstrings +++ b/Basic-Car-Maintenance/Shared/Localizable.xcstrings @@ -346,6 +346,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" : { @@ -640,6 +643,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" : { @@ -1862,6 +1868,12 @@ } } }, + "Distance Type" : { + + }, + "Distance Unit" : { + "comment" : "The distance unit in miles or kilometers" + }, "Edit" : { "comment" : "Button label to edit this maintenance", "localizations" : { @@ -3510,6 +3522,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" : { @@ -6387,6 +6402,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" : { diff --git a/Basic-Car-Maintenance/Shared/Models/Vehicle.swift b/Basic-Car-Maintenance/Shared/Models/Vehicle.swift index 0b80d06a..2d665029 100644 --- a/Basic-Car-Maintenance/Shared/Models/Vehicle.swift +++ b/Basic-Car-Maintenance/Shared/Models/Vehicle.swift @@ -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,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 @@ -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 + } } diff --git a/Basic-Car-Maintenance/Shared/Settings/ViewModels/SettingsViewModel.swift b/Basic-Car-Maintenance/Shared/Settings/ViewModels/SettingsViewModel.swift index 2e0af72e..b7609da3 100644 --- a/Basic-Car-Maintenance/Shared/Settings/ViewModels/SettingsViewModel.swift +++ b/Basic-Car-Maintenance/Shared/Settings/ViewModels/SettingsViewModel.swift @@ -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) diff --git a/Basic-Car-Maintenance/Shared/Settings/Views/EditVehicleView.swift b/Basic-Car-Maintenance/Shared/Settings/Views/EditVehicleView.swift index 4fcb9e45..3fe58f3c 100644 --- a/Basic-Car-Maintenance/Shared/Settings/Views/EditVehicleView.swift +++ b/Basic-Car-Maintenance/Shared/Settings/Views/EditVehicleView.swift @@ -90,7 +90,8 @@ 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, @@ -98,7 +99,6 @@ struct EditVehicleView: View, Observable { color: color, vin: VIN, licensePlateNumber: licensePlateNumber) - vehicle.id = selectedVehicle.id Task { await viewModel.updateVehicle(vehicle) if let onVehicleUpdated { From 72bdaa2ba608fa123f83838ba825c768cc8b3770 Mon Sep 17 00:00:00 2001 From: Omar Hegazy Date: Wed, 30 Oct 2024 16:17:52 +0300 Subject: [PATCH 2/2] Make authViewModel a dependency and separate AppIntent logic --- .../AppIntents/AddOdometerReadingIntent.swift | 138 ------------------ .../AddOdometerReadingIntent.swift | 97 ++++++++++++ .../DistanceUnit.swift | 29 ++++ .../OdometerReadingIntentError.swift | 38 +++++ .../VehicleQuery.swift | 31 ++++ .../Shared/BasicCarMaintenanceApp.swift | 8 +- .../Shared/Localizable.xcstrings | 23 ++- .../Shared/Models/Vehicle.swift | 4 +- .../Shared/Odometer/Views/OdometerView.swift | 61 ++++---- .../ViewModels/AuthenticationViewModel.swift | 2 +- 10 files changed, 245 insertions(+), 186 deletions(-) delete mode 100644 Basic-Car-Maintenance/Shared/AppIntents/AddOdometerReadingIntent.swift create mode 100644 Basic-Car-Maintenance/Shared/AppIntents/AddOdometerReadingIntent/AddOdometerReadingIntent.swift create mode 100644 Basic-Car-Maintenance/Shared/AppIntents/AddOdometerReadingIntent/DistanceUnit.swift create mode 100644 Basic-Car-Maintenance/Shared/AppIntents/AddOdometerReadingIntent/OdometerReadingIntentError.swift create mode 100644 Basic-Car-Maintenance/Shared/AppIntents/AddOdometerReadingIntent/VehicleQuery.swift diff --git a/Basic-Car-Maintenance/Shared/AppIntents/AddOdometerReadingIntent.swift b/Basic-Car-Maintenance/Shared/AppIntents/AddOdometerReadingIntent.swift deleted file mode 100644 index 8da28791..00000000 --- a/Basic-Car-Maintenance/Shared/AppIntents/AddOdometerReadingIntent.swift +++ /dev/null @@ -1,138 +0,0 @@ -// -// 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 { - - 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" - ) - ) - ) - } -} - -/// 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 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" - ) - - } - } -} \ No newline at end of file diff --git a/Basic-Car-Maintenance/Shared/AppIntents/AddOdometerReadingIntent/AddOdometerReadingIntent.swift b/Basic-Car-Maintenance/Shared/AppIntents/AddOdometerReadingIntent/AddOdometerReadingIntent.swift new file mode 100644 index 00000000..bec52aa9 --- /dev/null +++ b/Basic-Car-Maintenance/Shared/AppIntents/AddOdometerReadingIntent/AddOdometerReadingIntent.swift @@ -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" + ) + ) + ) + } +} diff --git a/Basic-Car-Maintenance/Shared/AppIntents/AddOdometerReadingIntent/DistanceUnit.swift b/Basic-Car-Maintenance/Shared/AppIntents/AddOdometerReadingIntent/DistanceUnit.swift new file mode 100644 index 00000000..98aebaac --- /dev/null +++ b/Basic-Car-Maintenance/Shared/AppIntents/AddOdometerReadingIntent/DistanceUnit.swift @@ -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" + ] + } +} diff --git a/Basic-Car-Maintenance/Shared/AppIntents/AddOdometerReadingIntent/OdometerReadingIntentError.swift b/Basic-Car-Maintenance/Shared/AppIntents/AddOdometerReadingIntent/OdometerReadingIntentError.swift new file mode 100644 index 00000000..4c096443 --- /dev/null +++ b/Basic-Car-Maintenance/Shared/AppIntents/AddOdometerReadingIntent/OdometerReadingIntentError.swift @@ -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" + ) + } + } +} diff --git a/Basic-Car-Maintenance/Shared/AppIntents/AddOdometerReadingIntent/VehicleQuery.swift b/Basic-Car-Maintenance/Shared/AppIntents/AddOdometerReadingIntent/VehicleQuery.swift new file mode 100644 index 00000000..27f1aba4 --- /dev/null +++ b/Basic-Car-Maintenance/Shared/AppIntents/AddOdometerReadingIntent/VehicleQuery.swift @@ -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 + } +} diff --git a/Basic-Car-Maintenance/Shared/BasicCarMaintenanceApp.swift b/Basic-Car-Maintenance/Shared/BasicCarMaintenanceApp.swift index 77274d0e..d5cfd6fe 100644 --- a/Basic-Car-Maintenance/Shared/BasicCarMaintenanceApp.swift +++ b/Basic-Car-Maintenance/Shared/BasicCarMaintenanceApp.swift @@ -11,12 +11,17 @@ import FirebaseCore import FirebaseFirestore import SwiftUI import TipKit +import AppIntents @main struct BasicCarMaintenanceApp: App { @State private var actionService = ActionService.shared @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate + init() { + FirebaseApp.configure() + AppDependencyManager.shared.add(dependency: AuthenticationViewModel()) + } // Logic to load Onboarding screen when app was first launched // @AppStorage("isFirstTime") private var isFirstTime: Bool = true @@ -43,9 +48,6 @@ class AppDelegate: NSObject, UIApplicationDelegate { _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { - - FirebaseApp.configure() - let useEmulator = UserDefaults.standard.bool(forKey: "useEmulator") if useEmulator { let settings = Firestore.firestore().settings diff --git a/Basic-Car-Maintenance/Shared/Localizable.xcstrings b/Basic-Car-Maintenance/Shared/Localizable.xcstrings index 576a73d6..bd0ffe5c 100644 --- a/Basic-Car-Maintenance/Shared/Localizable.xcstrings +++ b/Basic-Car-Maintenance/Shared/Localizable.xcstrings @@ -1569,7 +1569,7 @@ } }, "Date" : { - "comment" : "Date picker label", + "comment" : "Date picker label\nThe date when the reading should be logged.", "localizations" : { "be" : { "stringUnit" : { @@ -1817,6 +1817,7 @@ } }, "Distance" : { + "comment" : "The distance value", "localizations" : { "de" : { "stringUnit" : { @@ -2736,6 +2737,9 @@ }, "Imperial" : { "comment" : "Imperial unit system" + }, + "In which distance unit would you like to save the entered value?" : { + }, "It's open source and anyone can contribute to it." : { "comment" : "Tells the user they can contribute to the codebase.", @@ -3522,9 +3526,6 @@ } } }, - "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" : { @@ -3877,6 +3878,9 @@ }, "Plate: %@" : { + }, + "Please add a distance of at least 1 kilometer or mile." : { + "comment" : "an error shown when entering a zero or negative value for distance" }, "Preferred System" : { @@ -4610,6 +4614,9 @@ } } }, + "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" + }, "Tap the + to begin" : { "localizations" : { "fa" : { @@ -5381,7 +5388,7 @@ } }, "Vehicle" : { - "comment" : "Maintenance event vehicle picker header", + "comment" : "Maintenance event vehicle picker header\nThe selected vehicle to add the odometer reading to.", "localizations" : { "be" : { "stringUnit" : { @@ -6321,6 +6328,9 @@ } } } + }, + "Which vehicle would you like to add this to?" : { + }, "Year" : { "localizations" : { @@ -6402,9 +6412,6 @@ } } }, - "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" : { diff --git a/Basic-Car-Maintenance/Shared/Models/Vehicle.swift b/Basic-Car-Maintenance/Shared/Models/Vehicle.swift index 2d665029..112b6168 100644 --- a/Basic-Car-Maintenance/Shared/Models/Vehicle.swift +++ b/Basic-Car-Maintenance/Shared/Models/Vehicle.swift @@ -20,7 +20,9 @@ struct Vehicle: Codable, Identifiable, Hashable { let color: String? let vin: String? let licensePlateNumber: String? - var displayRepresentation: DisplayRepresentation { DisplayRepresentation(title: "\(name)") } + var displayRepresentation: DisplayRepresentation { + DisplayRepresentation(title: "\(name)") + } static var defaultQuery = VehicleQuery() static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Vehicle") diff --git a/Basic-Car-Maintenance/Shared/Odometer/Views/OdometerView.swift b/Basic-Car-Maintenance/Shared/Odometer/Views/OdometerView.swift index 68096fde..e4e00a89 100644 --- a/Basic-Car-Maintenance/Shared/Odometer/Views/OdometerView.swift +++ b/Basic-Car-Maintenance/Shared/Odometer/Views/OdometerView.swift @@ -116,45 +116,36 @@ struct OdometerView: View { 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!, - date: "2024/10/18", - distance: 20) - let secondReading = createReading(vehicleID: firstCar.id!, - date: "2024/10/15", - distance: 1000) + let readings = [firstCar, secondCar] + .map { + OdometerReading( + id: UUID().uuidString, + userID: "", + date: .now, + distance: Int.random(in: 10...1000), + isMetric: false, + vehicleID: $0.id + ) + } - let thirdReading = createReading(vehicleID: firstCar.id!, - date: "2024/10/13", - distance: 10) - viewModel.readings.append(contentsOf: [firstReading, secondReading, thirdReading]) + viewModel.readings.append(contentsOf: readings) - return OdometerView(viewModel: viewModel) - .environment(ActionService.shared) - func createVehicle(id: String, name: String) -> Vehicle { - Vehicle(id: id, - userID: nil, - name: name, - make: "", - model: "", - year: nil, - color: nil, - vin: nil, - licensePlateNumber: nil) - } - - func createReading(vehicleID: String, date: String, distance: Int) -> OdometerReading { - let formatter = DateFormatter() - formatter.dateFormat = "yyyy/MM/dd" - let firstDate = formatter.date(from: date)! - return OdometerReading(id: UUID().uuidString, - userID: "", - date: firstDate, - distance: distance, - isMetric: false, - vehicleID: vehicleID) + Vehicle( + id: id, + userID: nil, + name: name, + make: "", + model: "", + year: nil, + color: nil, + vin: nil, + licensePlateNumber: nil + ) } + + return OdometerView(viewModel: viewModel) + .environment(ActionService.shared) } diff --git a/Basic-Car-Maintenance/Shared/Settings/ViewModels/AuthenticationViewModel.swift b/Basic-Car-Maintenance/Shared/Settings/ViewModels/AuthenticationViewModel.swift index 3bae8b5b..69536a53 100644 --- a/Basic-Car-Maintenance/Shared/Settings/ViewModels/AuthenticationViewModel.swift +++ b/Basic-Car-Maintenance/Shared/Settings/ViewModels/AuthenticationViewModel.swift @@ -24,7 +24,7 @@ enum AuthenticationFlow { } @Observable -final class AuthenticationViewModel { +final class AuthenticationViewModel: @unchecked Sendable { var email = "" var password = ""