Skip to content

Commit

Permalink
FEATURE - Home Screen Quick Actions (#78)
Browse files Browse the repository at this point in the history
  • Loading branch information
OmarHegazy93 committed Oct 9, 2023
1 parent 4351728 commit 10d1d8d
Show file tree
Hide file tree
Showing 8 changed files with 247 additions and 28 deletions.
12 changes: 12 additions & 0 deletions Basic-Car-Maintenance.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
/* Begin PBXBuildFile section */
023057F22ACFAD79006C5A73 /* EditEventDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 023057F12ACFAD79006C5A73 /* EditEventDetailView.swift */; };
898009792AD1899700604E7C /* ContributorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 898009782AD1899700604E7C /* ContributorTests.swift */; };
8AEE816F2ACF37F800FC0C2A /* Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AEE816E2ACF37F800FC0C2A /* Action.swift */; };
8AEE81722ACF384D00FC0C2A /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AEE81712ACF384D00FC0C2A /* MainTabView.swift */; };
E58499662ACDDA8B00634660 /* ContributorsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E58499652ACDDA8B00634660 /* ContributorsListView.swift */; };
E58499682ACDDA9A00634660 /* ContributorsProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E58499672ACDDA9A00634660 /* ContributorsProfileView.swift */; };
E584996A2ACDDAFF00634660 /* Contributor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E58499692ACDDAFF00634660 /* Contributor.swift */; };
Expand Down Expand Up @@ -86,6 +88,9 @@
/* Begin PBXFileReference section */
023057F12ACFAD79006C5A73 /* EditEventDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditEventDetailView.swift; sourceTree = "<group>"; };
898009782AD1899700604E7C /* ContributorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContributorTests.swift; sourceTree = "<group>"; };
8AEE816E2ACF37F800FC0C2A /* Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Action.swift; sourceTree = "<group>"; };
8AEE81712ACF384D00FC0C2A /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = "<group>"; };
8AEE81732ACF394E00FC0C2A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
E58499652ACDDA8B00634660 /* ContributorsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContributorsListView.swift; sourceTree = "<group>"; };
E58499672ACDDA9A00634660 /* ContributorsProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContributorsProfileView.swift; sourceTree = "<group>"; };
E58499692ACDDAFF00634660 /* Contributor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contributor.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -285,7 +290,9 @@
children = (
FF755B422A90915E00F49A13 /* Localizable.xcstrings */,
FFC8CDA32AA385E800D129A6 /* GoogleService-Info.plist */,
8AEE81732ACF394E00FC0C2A /* Info.plist */,
FF5D13A62A86C2D600BC9BD6 /* BasicCarMaintenanceApp.swift */,
8AEE81712ACF384D00FC0C2A /* MainTabView.swift */,
FFBFE08F2A98EFDD000A9BEB /* Models */,
FF755B3F2A908EC400F49A13 /* Dashboard */,
FF755B402A908EC900F49A13 /* Settings */,
Expand Down Expand Up @@ -319,6 +326,7 @@
FFBFE0902A98EFEC000A9BEB /* MaintenanceEvent.swift */,
E58499692ACDDAFF00634660 /* Contributor.swift */,
FFBFE0922A98F212000A9BEB /* Vehicle.swift */,
8AEE816E2ACF37F800FC0C2A /* Action.swift */,
);
path = Models;
sourceTree = "<group>";
Expand Down Expand Up @@ -571,10 +579,12 @@
023057F22ACFAD79006C5A73 /* EditEventDetailView.swift in Sources */,
FF755B3E2A908E7A00F49A13 /* SettingsView.swift in Sources */,
FF3DDF522AA4D28F009D91C4 /* DashboardViewModel.swift in Sources */,
8AEE816F2ACF37F800FC0C2A /* Action.swift in Sources */,
FFBFE0972A98F7CB000A9BEB /* AddVehicleView.swift in Sources */,
E584996A2ACDDAFF00634660 /* Contributor.swift in Sources */,
FFC67D1D2AAEF7920073B338 /* SettingsViewModel.swift in Sources */,
FF755B3C2A908E3E00F49A13 /* DashboardView.swift in Sources */,
8AEE81722ACF384D00FC0C2A /* MainTabView.swift in Sources */,
E58499662ACDDA8B00634660 /* ContributorsListView.swift in Sources */,
FFBFE0932A98F212000A9BEB /* Vehicle.swift in Sources */,
FF5D13A72A86C2D600BC9BD6 /* BasicCarMaintenanceApp.swift in Sources */,
Expand Down Expand Up @@ -759,6 +769,7 @@
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Basic-Car-Maintenance/Shared/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Basic Car";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
Expand Down Expand Up @@ -797,6 +808,7 @@
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Basic-Car-Maintenance/Shared/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Basic Car";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
Expand Down
75 changes: 59 additions & 16 deletions Basic-Car-Maintenance/Shared/BasicCarMaintenanceApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,69 @@ import SwiftUI

@main
struct BasicCarMaintenanceApp: App {
@State private var authenticationViewModel: AuthenticationViewModel
@State private var actionService = ActionService.shared
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate

init() {
var body: some Scene {
WindowGroup {
MainTabView()
.environment(actionService)
}
}
}

class AppDelegate: NSObject, UIApplicationDelegate {
private let actionService = ActionService.shared

func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
FirebaseApp.configure()
_authenticationViewModel = .init(initialValue: AuthenticationViewModel())
return true
}

var body: some Scene {
WindowGroup {
TabView {
DashboardView(authenticationViewModel: authenticationViewModel)
.tabItem {
Label("Dashboard", systemImage: "list.dash.header.rectangle")
}

SettingsView(authenticationViewModel: authenticationViewModel)
.tabItem {
Label("Settings", systemImage: "gear")
}
}
func application(
_ application: UIApplication,
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions
) -> UISceneConfiguration {
// get the shortcut when the app is launching
if let shortcutItem = options.shortcutItem,
let action = Action(shortcutItem: shortcutItem) {
actionService.updateIncoming(action)
}

let configuration = UISceneConfiguration(
name: connectingSceneSession.configuration.name,
sessionRole: connectingSceneSession.role
)
configuration.delegateClass = SceneDelegate.self
return configuration
}
}

class SceneDelegate: NSObject, UIWindowSceneDelegate {
private let actionService = ActionService.shared

func windowScene(
_ windowScene: UIWindowScene,
performActionFor shortcutItem: UIApplicationShortcutItem,
completionHandler: @escaping (Bool) -> Void
) {
// get the shortcut if the app is already running
let action = Action(shortcutItem: shortcutItem)
actionService.updateIncoming(action)
completionHandler(true)
}
}

@Observable
class ActionService: ObservableObject {
static let shared = ActionService()
private(set) var action: Action?

fileprivate func updateIncoming(_ action: Action?) {
self.action = action
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ struct AddMaintenanceView: View {
@State private var title = ""
@State private var date = Date()
@State private var notes = ""
@Environment(\.dismiss) var dismiss

var body: some View {
NavigationStack {
Expand Down Expand Up @@ -42,6 +43,7 @@ struct AddMaintenanceView: View {
Button {
let event = MaintenanceEvent(title: title, date: date, notes: notes)
addTapped(event)
dismiss()
} label: {
Text("Add")
}
Expand Down
47 changes: 37 additions & 10 deletions Basic-Car-Maintenance/Shared/Dashboard/Views/DashboardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
import SwiftUI

struct DashboardView: View {

@Environment(ActionService.self) var actionService
@Environment(\.scenePhase) var scenePhase
@State private var isShowingAddView = false
@Bindable private var viewModel: DashboardViewModel
@State private var isShowingEditView = false
@State private var selectedMaintenanceEvent: MaintenanceEvent?
Expand Down Expand Up @@ -71,20 +73,12 @@ struct DashboardView: View {
Text(viewModel.errorMessage).padding()
}
.navigationDestination(isPresented: $viewModel.isShowingAddMaintenanceEvent) {
AddMaintenanceView { event in
viewModel.addEvent(event)
}
.alert("An Error Occurred", isPresented: $viewModel.showAddErrorAlert) {
Button("OK", role: .cancel) {}
} message: {
Text(viewModel.errorMessage)
}
makeAddMaintenanceView()
}
.toolbar {
ToolbarItemGroup(placement: .primaryAction) {
Button {
viewModel.isShowingAddMaintenanceEvent = true

} label: {
Image(systemName: "plus")
}
Expand All @@ -106,6 +100,39 @@ struct DashboardView: View {
.task {
await viewModel.getMaintenanceEvents()
}
.sheet(isPresented: $isShowingAddView) {
makeAddMaintenanceView()
}
}
.onChange(of: scenePhase) { _, newScenePhase in
guard case .active = newScenePhase else { return }

guard let action = actionService.action,
action == .newMaintenance
else {
// another action has been triggered, so we will need to dismiss the current presented view
isShowingAddView = false
return
}

// if the view is already presented, do nothing
guard !isShowingAddView else { return }
// delay the presentation of the view a bit
// to make sure the already presented view is dismissed.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
isShowingAddView = true
}
}
}

private func makeAddMaintenanceView() -> some View {
AddMaintenanceView { event in
viewModel.addEvent(event)
}
.alert("An Error Occurred", isPresented: $viewModel.showAddErrorAlert) {
Button("OK", role: .cancel) {}
} message: {
Text(viewModel.errorMessage)
}
}
}
Expand Down
25 changes: 25 additions & 0 deletions Basic-Car-Maintenance/Shared/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UIApplicationShortcutItems</key>
<array>
<dict>
<key>UIApplicationShortcutItemIconSymbolName</key>
<string>wrench.and.screwdriver.fill</string>
<key>UIApplicationShortcutItemTitle</key>
<string>New Maintenence</string>
<key>UIApplicationShortcutItemType</key>
<string>NewMaintenance</string>
</dict>
<dict>
<key>UIApplicationShortcutItemIconSymbolName</key>
<string>plus.circle.fill</string>
<key>UIApplicationShortcutItemTitle</key>
<string>Add Vehicle</string>
<key>UIApplicationShortcutItemType</key>
<string>AddVehicle</string>
</dict>
</array>
</dict>
</plist>
56 changes: 56 additions & 0 deletions Basic-Car-Maintenance/Shared/MainTabView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// MainTabView.swift
// Basic-Car-Maintenance
//
// Created by Omar Hegazy on 05/10/2023.
//

import SwiftUI

enum TabSelection: Int {
case dashboard = 0
case settings = 1
}

@MainActor
struct MainTabView: View {
@Environment(ActionService.self) var actionService
@Environment(\.scenePhase) var scenePhase
@State var authenticationViewModel = AuthenticationViewModel()
@State var selectedTab: TabSelection = .dashboard

var body: some View {
TabView(selection: $selectedTab) {
DashboardView(authenticationViewModel: authenticationViewModel)
.tag(TabSelection.dashboard)
.tabItem {
Label("Dashboard", systemImage: "list.dash.header.rectangle")
}

SettingsView(authenticationViewModel: authenticationViewModel)
.tag(TabSelection.settings)
.tabItem {
Label("Settings", systemImage: "gear")
}
}
.onChange(of: scenePhase) { _, newScenePhase in
guard
case .active = newScenePhase,
let action = actionService.action
else { return }

// select the tab where the desired view is located to make sure
// it is presented from the proper hierarchy.
switch action {
case .newMaintenance:
selectedTab = .dashboard
case .addVehicle:
selectedTab = .settings
}
}
}
}

#Preview {
MainTabView()
}
31 changes: 31 additions & 0 deletions Basic-Car-Maintenance/Shared/Models/Action.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// Action.swift
// Basic-Car-Maintenance
//
// Created by Omar Hegazy on 05/10/2023.
//

import UIKit

enum ActionType: String {
case newMaintenance = "NewMaintenance"
case addVehicle = "AddVehicle"
}

enum Action: Equatable {
case newMaintenance
case addVehicle

init?(shortcutItem: UIApplicationShortcutItem) {
guard let type = ActionType(rawValue: shortcutItem.type) else {
return nil
}

switch type {
case .newMaintenance:
self = .newMaintenance
case .addVehicle:
self = .addVehicle
}
}
}
27 changes: 25 additions & 2 deletions Basic-Car-Maintenance/Shared/Settings/Views/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ struct SettingsView: View {
@State private var viewModel: SettingsViewModel
@State private var isShowingAddVehicle = false
@State private var showDeleteVehicleError = false
@State var errorDetails: Error?
@Environment(ActionService.self) var actionService
@Environment(\.scenePhase) var scenePhase
@State private var errorDetails: Error?

init(authenticationViewModel: AuthenticationViewModel) {
viewModel = SettingsViewModel(authenticationViewModel: authenticationViewModel)
let settingsViewModel = SettingsViewModel(authenticationViewModel: authenticationViewModel)
_viewModel = .init(initialValue: settingsViewModel)
}

var body: some View {
Expand Down Expand Up @@ -144,6 +147,26 @@ struct SettingsView: View {
}
}
}
.onChange(of: scenePhase) { _, newScenePhase in
guard case .active = newScenePhase else { return }

guard let action = actionService.action,
action == .addVehicle
else {
// another action has been triggered
// so we will need to dismiss the current presented view
isShowingAddVehicle = false
return
}

// if the view is already presented, do nothing
guard !isShowingAddVehicle else { return }
// delay the presentation of the view a bit
// to make sure the already presented view is dismissed.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
isShowingAddVehicle = true
}
}
}
}

Expand Down

0 comments on commit 10d1d8d

Please sign in to comment.