diff --git a/Basic-Car-Maintenance/Shared/BasicCarMaintenanceApp.swift b/Basic-Car-Maintenance/Shared/BasicCarMaintenanceApp.swift index 77274d0e..087fc684 100644 --- a/Basic-Car-Maintenance/Shared/BasicCarMaintenanceApp.swift +++ b/Basic-Car-Maintenance/Shared/BasicCarMaintenanceApp.swift @@ -17,9 +17,6 @@ struct BasicCarMaintenanceApp: App { @State private var actionService = ActionService.shared @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate - // Logic to load Onboarding screen when app was first launched -// @AppStorage("isFirstTime") private var isFirstTime: Bool = true - var body: some Scene { WindowGroup { MainTabView() @@ -28,10 +25,6 @@ struct BasicCarMaintenanceApp: App { .task { try? Tips.configure() } -// .sheet(isPresented: $isFirstTime) { -// WelcomeView() -// .interactiveDismissDisabled() -// } } } } diff --git a/Basic-Car-Maintenance/Shared/Localizable.xcstrings b/Basic-Car-Maintenance/Shared/Localizable.xcstrings index 071667ed..7c60bfde 100644 --- a/Basic-Car-Maintenance/Shared/Localizable.xcstrings +++ b/Basic-Car-Maintenance/Shared/Localizable.xcstrings @@ -1,6 +1,9 @@ { "sourceLanguage" : "en", "strings" : { + "" : { + + }, "%@" : { "comment" : "Maintenance list item description 'Date' formatted", "localizations" : { @@ -71,6 +74,9 @@ } } } + }, + "%@ added successfully! 🎉" : { + }, "%@ on %@" : { "comment" : "Maintenance list item for a vehicle on a date", @@ -199,9 +205,6 @@ } } } - }, - "about" : { - }, "Add" : { "comment" : "Label for submit button on form to add an entry", @@ -416,7 +419,7 @@ } } }, - "Add the details below" : { + "Add the details below about " : { }, "Add Vehicle" : { @@ -814,9 +817,6 @@ } } } - }, - "Back" : { - }, "Basic" : { @@ -1800,6 +1800,9 @@ } } } + }, + "Error" : { + }, "Failed To Add Vehicle" : { "localizations" : { @@ -4940,6 +4943,9 @@ }, "User-friendly interface for controlling car maintenance tasks." : { + }, + "Validation Error" : { + }, "Vehicle" : { "comment" : "Maintenance event vehicle picker header", @@ -5005,6 +5011,9 @@ } } } + }, + "Vehicle %@" : { + }, "Vehicle Color" : { "localizations" : { @@ -5215,6 +5224,9 @@ } } } + }, + "Vehicle Make cannot be empty! 🚗" : { + }, "Vehicle Model" : { "localizations" : { @@ -5297,6 +5309,9 @@ } } } + }, + "Vehicle Model cannot be empty! 🚗" : { + }, "Vehicle Name" : { "localizations" : { @@ -5379,6 +5394,9 @@ } } } + }, + "Vehicle Name cannot be empty! 🚗" : { + }, "Vehicle VIN" : { "localizations" : { @@ -5784,9 +5802,6 @@ } } } - }, - "Welcome 🥳" : { - }, "Welcome to" : { diff --git a/Basic-Car-Maintenance/Shared/MainView/Views/MainTabView.swift b/Basic-Car-Maintenance/Shared/MainView/Views/MainTabView.swift index 2967873b..eeafd836 100644 --- a/Basic-Car-Maintenance/Shared/MainView/Views/MainTabView.swift +++ b/Basic-Car-Maintenance/Shared/MainView/Views/MainTabView.swift @@ -57,6 +57,9 @@ struct MainTabView: View { @State private var authenticationViewModel = AuthenticationViewModel() @State private var viewModel = MainTabViewModel() + // Logic to load `WelcomeView` when app is first launched + @AppStorage("isFirstTime") private var isFirstTime: Bool = true + init() { _selectedTabId = State(initialValue: selectedTab) } @@ -77,6 +80,10 @@ struct MainTabView: View { AlertView(alert: alert) .presentationDetents([.medium]) } + .sheet(isPresented: $isFirstTime) { + WelcomeView(authenticationViewModel: authenticationViewModel) + .interactiveDismissDisabled() + } .onChange(of: scenePhase) { _, newScenePhase in guard case .active = newScenePhase, diff --git a/Basic-Car-Maintenance/Shared/Onboarding/Views/WelcomeView.swift b/Basic-Car-Maintenance/Shared/Onboarding/Views/WelcomeView.swift index 85ceec7f..26935f95 100644 --- a/Basic-Car-Maintenance/Shared/Onboarding/Views/WelcomeView.swift +++ b/Basic-Car-Maintenance/Shared/Onboarding/Views/WelcomeView.swift @@ -9,6 +9,7 @@ import SwiftUI struct WelcomeView: View { + var authenticationViewModel: AuthenticationViewModel var body: some View { NavigationView { @@ -17,10 +18,10 @@ struct WelcomeView: View { HStack(spacing: 5) { Text("Welcome to") Text("Basic") - .foregroundStyle(Color("basicGreen")) + .foregroundStyle(.basicGreen) } Text("Car Maintenance") - .foregroundStyle(Color("basicGreen")) + .foregroundStyle(.basicGreen) } .font(.largeTitle.bold()) .multilineTextAlignment(.center) @@ -32,19 +33,19 @@ struct WelcomeView: View { pointView( symbol: "car", title: "Dashboard", - subTitle: "User-friendly interface for controlling car maintenance tasks." + subtitle: "User-friendly interface for controlling car maintenance tasks." ) pointView( symbol: "gauge.with.dots.needle.bottom.50percent.badge.plus", title: "Odometer", - subTitle: "Tracks & displays total mileage, aiding timely maintenance planning." + subtitle: "Tracks & displays total mileage, aiding timely maintenance planning." ) pointView( symbol: "lock.open", title: "Open Source", - subTitle: "Built collaboratively with contributors, enhancing the app functionality." + subtitle: "Built collaboratively with contributors, enhancing the app functionality." ) } .frame(maxWidth: .infinity, alignment: .leading) @@ -53,7 +54,8 @@ struct WelcomeView: View { Spacer(minLength: 10) - NavigationLink(destination: WelcomeViewAddVehicle()) { + NavigationLink( + destination: WelcomeViewAddVehicle()) { Text("Continue") .fontWeight(.bold) .foregroundStyle(.white) @@ -74,11 +76,12 @@ struct WelcomeView: View { } @ViewBuilder - func pointView(symbol: String, title: LocalizedStringKey, subTitle: LocalizedStringKey) -> some View { + // swiftlint:disable:next line_length + func pointView(symbol: String, title: LocalizedStringResource, subtitle: LocalizedStringResource) -> some View { HStack(spacing: 20) { Image(systemName: symbol) .font(.largeTitle) - .foregroundStyle(Color("basicGreen")) + .foregroundStyle(.basicGreen) .frame(width: 45) VStack(alignment: .leading, spacing: 6) { @@ -86,7 +89,7 @@ struct WelcomeView: View { .font(.title3) .fontWeight(.semibold) - Text(subTitle) + Text(subtitle) .foregroundStyle(.gray) } } @@ -94,5 +97,5 @@ struct WelcomeView: View { } #Preview { - WelcomeView() + WelcomeView(authenticationViewModel: AuthenticationViewModel()) } diff --git a/Basic-Car-Maintenance/Shared/Onboarding/Views/WelcomeViewAddVehicle.swift b/Basic-Car-Maintenance/Shared/Onboarding/Views/WelcomeViewAddVehicle.swift index 8261ae96..6eb8ff67 100644 --- a/Basic-Car-Maintenance/Shared/Onboarding/Views/WelcomeViewAddVehicle.swift +++ b/Basic-Car-Maintenance/Shared/Onboarding/Views/WelcomeViewAddVehicle.swift @@ -9,97 +9,134 @@ import SwiftUI struct WelcomeViewAddVehicle: View { - - // Logic to remember Onboarding screen to not load again when app is launched -// @AppStorage("isFirstTime") private var isFirstTime: Bool = true @Environment(\.dismiss) var dismiss + @AppStorage("isFirstTime") private var isFirstTime: Bool = true @State private var vehicleName: String = "" @State private var vehicleMake: String = "" @State private var vehicleModel: String = "" + @State private var showAlert: Bool = false + @State private var alertType: AlertType? + var body: some View { - VStack(spacing: 15) { - VStack { - Text("Add the details below") - HStack(spacing: 5) { - Text("about") - Text("your vehicle") - .foregroundStyle(Color("basicGreen")) - } - } - .font(.largeTitle.bold()) - .multilineTextAlignment(.center) - .padding(.top, 65) - .padding(.bottom, 15) - - VStack { - Image(systemName: "car.side.lock.open") - .font(.system(size: 45)) - .foregroundStyle(Color("basicGreen")) + ScrollView { + VStack(spacing: 15) { + headerView - List { - HStack { - Text("Name") - Spacer() - .frame(width: 40) - TextField("Vehicle Name", text: $vehicleName) - } - - HStack { - Text("Make") - Spacer() - .frame(width: 45) - TextField("Vehicle Make", text: $vehicleMake) - } - HStack { - Text("Model") - Spacer() - .frame(width: 40) - TextField("Vehicle Model", text: $vehicleModel) + vehicleDetailsView + + Text("You can edit more data about the vehicle in the 'Settings' tab.") + .foregroundStyle(.gray) + .padding(.horizontal, 25) + + Spacer(minLength: 10) + + addVehicleButton + } + .padding(.horizontal) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color(UIColor.secondarySystemBackground).ignoresSafeArea()) + .alert(isPresented: $showAlert) { + Alert( + title: Text(alertType?.title ?? ""), + message: Text(alertType?.message ?? ""), + dismissButton: .default(Text("OK")) { + if case .vehicleAdded = alertType { + isFirstTime = false + dismiss() } } - .frame(maxHeight: 200) - } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, 15) - - Text("You can edit more data about the vehicle in the 'Settings' tab.") - .foregroundStyle(.gray) - .padding(.horizontal, 25) - - Spacer(minLength: 10) + ) + } + } + + private var headerView: some View { + VStack { + Text("Add the details below about ") + + Text("your vehicle") + .foregroundStyle(Color("basicGreen")) + } + .font(.largeTitle) + .bold() + .multilineTextAlignment(.center) + .padding(.top, 65) + .padding(.bottom, 15) + } + + private var vehicleDetailsView: some View { + VStack(spacing: 20) { + Image(systemName: "car.side.lock.open") + .font(.system(size: 45)) + .foregroundStyle(.basicGreen) - Button { -// isFirstTime = false - } label: { - Text("Welcome 🥳") - .fontWeight(.bold) - .foregroundStyle(.white) - .frame(maxWidth: .infinity) - .padding(.vertical, 14) - .background(Color("basicGreen").gradient, in: .rect(cornerRadius: 12)) - .contentShape(.rect) + VStack(spacing: 0) { + vehicleDetailRow(title: "Name", text: $vehicleName) + .padding(.bottom, 10) + Divider() + vehicleDetailRow(title: "Make", text: $vehicleMake) + .padding(.vertical, 10) + Divider() + vehicleDetailRow(title: "Model", text: $vehicleModel) + .padding(.top, 10) } - .padding(15) - .padding(.horizontal, 15) + .padding() + .background(Color(UIColor.systemBackground)) + .cornerRadius(12) } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background { - Color(UIColor.secondarySystemBackground) - .ignoresSafeArea() + .padding(.horizontal, 15) + } + + private func vehicleDetailRow(title: String, text: Binding) -> some View { + LabeledContent { + TextField("Vehicle \(title)", text: text) + .frame(width: 200) + } label: { + Text(title) } - .navigationBarBackButtonHidden(true) - .toolbar { - ToolbarItem(placement: .topBarLeading) { - Button { - dismiss() - } label: { - HStack { - Image(systemName: "arrow.left.circle") - Text("Back") - } - .tint(Color("basicGreen")) + .showClearButton(text) + } + + private var addVehicleButton: some View { + Button { + addVehicle() + } label: { + Text("Add Vehicle") + .fontWeight(.bold) + .foregroundStyle(.white) + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + .background(.basicGreen.gradient, in: .rect(cornerRadius: 12)) + .contentShape(.rect) + } + .padding(15) + .padding(.horizontal, 15) + } + + private func addVehicle() { + if vehicleName.isEmpty { + alertType = .emptyName + showAlert = true + } else if vehicleMake.isEmpty { + alertType = .emptyMake + showAlert = true + } else if vehicleModel.isEmpty { + alertType = .emptyModel + showAlert = true + } else { + + let newVehicle = Vehicle(name: vehicleName, make: vehicleMake, model: vehicleModel) + + Task { + do { + // add new vehicle here + + alertType = .vehicleAdded(name: vehicleName) + showAlert = true + } catch { + alertType = .error(message: "Failed to add vehicle. Please try again.") + showAlert = true } } } @@ -108,4 +145,42 @@ struct WelcomeViewAddVehicle: View { #Preview { WelcomeViewAddVehicle() + .environment(ActionService.shared) +} + +extension WelcomeViewAddVehicle { + /// Types of alerts to be shown on the `WelcomeViewAddVehicle` + enum AlertType { + case emptyName + case emptyMake + case emptyModel + case vehicleAdded(name: String) + case error(message: String) + + var title: LocalizedStringResource { + switch self { + case .emptyName, .emptyMake, .emptyModel: + return "Validation Error" + case .vehicleAdded(let name): + return "\(name) added successfully! 🎉" + case .error: + return "Error" + } + } + + var message: LocalizedStringResource? { + switch self { + case .emptyName: + return "Vehicle Name cannot be empty! 🚗" + case .emptyMake: + return "Vehicle Make cannot be empty! 🚗" + case .emptyModel: + return "Vehicle Model cannot be empty! 🚗" + case .vehicleAdded: + return nil + case .error(let message): + return LocalizedStringResource(stringLiteral: message) + } + } + } } diff --git a/Basic-Car-Maintenance/Shared/Utilities/TextFieldClearButton.swift b/Basic-Car-Maintenance/Shared/Utilities/TextFieldClearButton.swift new file mode 100644 index 00000000..c02f342d --- /dev/null +++ b/Basic-Car-Maintenance/Shared/Utilities/TextFieldClearButton.swift @@ -0,0 +1,41 @@ +// +// TextFieldClearButton.swift +// Basic-Car-Maintenance +// +// https://github.com/mikaelacaron/Basic-Car-Maintenance +// See LICENSE for license information. +// + +import SwiftUI + +/// An overlay of a button that is used to clear a TextField. +struct TextFieldClearButton: ViewModifier { + @Binding var text: String + + func body(content: Content) -> some View { + content + .overlay { + if !text.isEmpty { + HStack { + Spacer() + Button { + text = "" + } label: { + Image(systemName: "multiply.circle.fill") + .imageScale(.medium) + } + .foregroundColor(.secondary) + .padding(.trailing, 4) + } + } + } + } +} + +extension View { + + /// Add a `TextFieldClearButton` to a view. + func showClearButton(_ text: Binding) -> some View { + self.modifier(TextFieldClearButton(text: text)) + } +}