diff --git a/WaiterRobot/AppDelegate.swift b/WaiterRobot/AppDelegate.swift deleted file mode 100644 index ae13469..0000000 --- a/WaiterRobot/AppDelegate.swift +++ /dev/null @@ -1,38 +0,0 @@ -import shared -import SwiftUI - -class AppDelegate: NSObject, UIApplicationDelegate { - func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { - // Init CommonApp right at the start as e.g. koin might depend on some properties of it - var appVersion = readFromInfoPlist(withKey: "CFBundleShortVersionString") - let versionSuffix = readFromInfoPlist(withKey: "VERSION_SUFFIX") - if !versionSuffix.isEmpty { - appVersion += "-\(versionSuffix)" - } - - CommonApp.shared.doInit( - appVersion: appVersion, - appBuild: Int32(readFromInfoPlist(withKey: "CFBundleVersion"))!, - phoneModel: UIDevice.current.deviceType, - os: OS.Ios(version: UIDevice.current.systemVersion), - apiBaseUrl: readFromInfoPlist(withKey: "API_BASE") - ) - - KoinKt.doInitKoinIos() - let logger = koin.logger(tag: "AppDelegate") - logger.d { "initialized Koin" } - - KMMResourcesLocalizationKt.localizationBundle = Bundle(for: shared.L.self) - logger.d { "initialized localization bundle" } - - return true - } - - private func readFromInfoPlist(withKey key: String) -> String { - guard let value = Bundle.main.infoDictionary?[key] as? String else { - fatalError("Could not find key '\(key)' in info.plist file.") - } - - return value - } -} diff --git a/WaiterRobot/LaunchScreen.storyboard b/WaiterRobot/LaunchScreen.storyboard deleted file mode 100644 index 9dfceba..0000000 --- a/WaiterRobot/LaunchScreen.storyboard +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/WaiterRobot/LaunchScreen.swift b/WaiterRobot/LaunchScreen.swift new file mode 100644 index 0000000..68d676e --- /dev/null +++ b/WaiterRobot/LaunchScreen.swift @@ -0,0 +1,99 @@ +import Foundation +import shared +import SwiftUI + +struct LaunchScreen: View { + private let minimumOnScreenTimeSeconds = 3.0 + private let device = UIDevice.current.userInterfaceIdiom + + @State private var startupFinished = false + + var body: some View { + VStack { + if case .phone = device { + VStack { + Spacer() + + Image(.launch) + .resizable() + .scaledToFit() + } + .padding(.horizontal, -2) + .ignoresSafeArea() + } else { + ZStack { + Image(.logoRounded) + .resizable() + .scaledToFit() + .frame(width: 150) + .padding() + + VStack { + Spacer() + + ProgressView() + .padding() + .padding(.bottom) + } + } + } + } + .onAppear { + Task { + async let setup: () = setupApp() + async let delay: () = delay() + + _ = await [setup, delay] + + startupFinished = true + } + } + .fullScreenCover(isPresented: $startupFinished) { + MainView() + } + } + + /// Setup of frameworks and all the other related stuff which is needed everywhere in the app + private func setupApp() { + print("started app setup") + var appVersion = readFromInfoPlist(withKey: "CFBundleShortVersionString") + let versionSuffix = readFromInfoPlist(withKey: "VERSION_SUFFIX") + if !versionSuffix.isEmpty { + appVersion += "-\(versionSuffix)" + } + + CommonApp.shared.doInit( + appVersion: appVersion, + appBuild: Int32(readFromInfoPlist(withKey: "CFBundleVersion"))!, + phoneModel: UIDevice.current.deviceType, + os: OS.Ios(version: UIDevice.current.systemVersion), + apiBaseUrl: readFromInfoPlist(withKey: "API_BASE") + ) + + KoinKt.doInitKoinIos() + let logger = koin.logger(tag: "AppDelegate") + logger.d { "initialized Koin" } + + KMMResourcesLocalizationKt.localizationBundle = Bundle(for: shared.L.self) + logger.d { "initialized localization bundle" } + print("finished app setup") + } + + private func delay() async { + print("started delay") + try? await Task.sleep(seconds: minimumOnScreenTimeSeconds) + print("finished delay") + } + + private func readFromInfoPlist(withKey key: String) -> String { + guard let value = Bundle.main.infoDictionary?[key] as? String else { + fatalError("Could not find key '\(key)' in info.plist file.") + } + + return value + } +} + +#Preview { + LaunchScreen() +} diff --git a/WaiterRobot/MainView.swift b/WaiterRobot/MainView.swift new file mode 100644 index 0000000..b252bef --- /dev/null +++ b/WaiterRobot/MainView.swift @@ -0,0 +1,126 @@ +// +// MainView.swift +// WaiterRobot +// +// Created by Alexander Kauer on 29.12.23. +// + +import shared +import SwiftUI +import UIPilot + +struct MainView: View { + @State private var snackBarMessage: String? + @State private var showUpdateAvailableAlert: Bool = false + @StateObject private var navigator: UIPilot = UIPilot(initial: Screen.RootScreen.shared, debug: true) + @StateObject private var strongVM = ObservableViewModel(vm: koin.rootVM()) + + private var selectedScheme: ColorScheme? { + switch strongVM.state.selectedTheme { + case .dark: + .dark + case .light: + .light + default: + nil + } + } + + var body: some View { + unowned let vm = strongVM + + ZStack { + UIPilotHost(navigator) { route in + switch route { + case is Screen.RootScreen: RootScreen(strongVM: vm) + case is Screen.LoginScannerScreen: LoginScannerScreen() + case is Screen.SwitchEventScreen: SwitchEventScreen() + case is Screen.SettingsScreen: SettingsScreen() + case is Screen.UpdateApp: UpdateAppScreen() + case let screen as Screen.RegisterScreen: RegisterScreen(createToken: screen.createToken) + case let screen as Screen.TableDetailScreen: TableDetailScreen(table: screen.table) + case let screen as Screen.OrderScreen: OrderScreen(table: screen.table, initialItemId: screen.initialItemId) + case let screen as Screen.BillingScreen: BillingScreen(table: screen.table) + default: + Text("No view defined for \(route.description)") // TODO: + Button { + navigator.pop() + } label: { + Text("Back") + }.onAppear { + koin.logger(tag: "WaiterRobotApp").e { "No view defined for \(route.description)" } + } + } + } + } + .preferredColorScheme(selectedScheme) + .overlay(alignment: .bottom) { + if let message = snackBarMessage { + ZStack { + HStack { + Text(message) + .foregroundColor(.white) + Spacer() + Button { + snackBarMessage = nil + } label: { + Image(systemName: "xmark.circle") + } + } + .frame(maxWidth: .infinity) + .padding() + .background(.gray) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } + .padding() + } + } + .handleSideEffects(of: vm, navigator) { effect in + switch effect { + case let snackBar as RootEffect.ShowSnackBar: + snackBarMessage = snackBar.message + DispatchQueue.main.asyncAfter(deadline: .now() + 5) { + snackBarMessage = nil + } + default: + return false + } + return true + } + .onOpenURL { url in + vm.actual.onDeepLink(url: url.absoluteString) + } + .alert( + localize.app.updateAvailable.title(), + isPresented: $showUpdateAvailableAlert + ) { + Button(localize.dialog.cancel(), role: .cancel) { + showUpdateAvailableAlert = false + } + + Button(localize.app.forceUpdate.openStore(value0: "App Store")) { + guard let storeUrl = VersionChecker.shared.storeUrl, + let url = URL(string: storeUrl) + else { + return + } + + DispatchQueue.main.async { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } + } + } message: { + Text(localize.app.updateAvailable.message()) + } + .onAppear { + VersionChecker.shared.checkVersion { + showUpdateAvailableAlert = true + } + } + .tint(.main) + } +} + +#Preview { + MainView() +} diff --git a/WaiterRobot/Util/Extensions/Task+Extensions.swift b/WaiterRobot/Util/Extensions/Task+Extensions.swift new file mode 100644 index 0000000..e1b3cf6 --- /dev/null +++ b/WaiterRobot/Util/Extensions/Task+Extensions.swift @@ -0,0 +1,8 @@ +import Foundation + +extension Task where Success == Never, Failure == Never { + static func sleep(seconds: Double) async throws { + let duration = UInt64(seconds * 1_000_000_000) + try await Task.sleep(nanoseconds: duration) + } +} diff --git a/WaiterRobot/WaiterRobotApp.swift b/WaiterRobot/WaiterRobotApp.swift index a56caae..4ae1e7e 100644 --- a/WaiterRobot/WaiterRobotApp.swift +++ b/WaiterRobot/WaiterRobotApp.swift @@ -4,117 +4,9 @@ import UIPilot @main struct WaiterRobotApp: App { - @UIApplicationDelegateAdaptor var appDelegate: AppDelegate - - @State private var snackBarMessage: String? - @State private var showUpdateAvailableAlert: Bool = false - @StateObject private var navigator: UIPilot = UIPilot(initial: Screen.RootScreen.shared, debug: true) - @StateObject private var strongVM = ObservableViewModel(vm: koin.rootVM()) - - private var selectedScheme: ColorScheme? { - switch strongVM.state.selectedTheme { - case .dark: - .dark - case .light: - .light - default: - nil - } - } - var body: some Scene { - unowned let vm = strongVM - WindowGroup { - ZStack { - UIPilotHost(navigator) { route in - switch route { - case is Screen.RootScreen: RootScreen(strongVM: vm) - case is Screen.LoginScannerScreen: LoginScannerScreen() - case is Screen.SwitchEventScreen: SwitchEventScreen() - case is Screen.SettingsScreen: SettingsScreen() - case is Screen.UpdateApp: UpdateAppScreen() - case let screen as Screen.RegisterScreen: RegisterScreen(createToken: screen.createToken) - case let screen as Screen.TableDetailScreen: TableDetailScreen(table: screen.table) - case let screen as Screen.OrderScreen: OrderScreen(table: screen.table, initialItemId: screen.initialItemId) - case let screen as Screen.BillingScreen: BillingScreen(table: screen.table) - default: - Text("No view defined for \(route.description)") // TODO: - Button { - navigator.pop() - } label: { - Text("Back") - }.onAppear { - koin.logger(tag: "WaiterRobotApp").e { "No view defined for \(route.description)" } - } - } - } - } - .preferredColorScheme(selectedScheme) - .overlay(alignment: .bottom) { - if let message = snackBarMessage { - ZStack { - HStack { - Text(message) - .foregroundColor(.white) - Spacer() - Button { - snackBarMessage = nil - } label: { - Image(systemName: "xmark.circle") - } - } - .frame(maxWidth: .infinity) - .padding() - .background(.gray) - .clipShape(RoundedRectangle(cornerRadius: 10)) - } - .padding() - } - } - .handleSideEffects(of: vm, navigator) { effect in - switch effect { - case let snackBar as RootEffect.ShowSnackBar: - snackBarMessage = snackBar.message - DispatchQueue.main.asyncAfter(deadline: .now() + 5) { - snackBarMessage = nil - } - default: - return false - } - return true - } - .onOpenURL { url in - vm.actual.onDeepLink(url: url.absoluteString) - } - .alert( - localize.app.updateAvailable.title(), - isPresented: $showUpdateAvailableAlert - ) { - Button(localize.dialog.cancel(), role: .cancel) { - showUpdateAvailableAlert = false - } - - Button(localize.app.forceUpdate.openStore(value0: "App Store")) { - guard let storeUrl = VersionChecker.shared.storeUrl, - let url = URL(string: storeUrl) - else { - return - } - - DispatchQueue.main.async { - UIApplication.shared.open(url, options: [:], completionHandler: nil) - } - } - } message: { - Text(localize.app.updateAvailable.message()) - } - .onAppear { - VersionChecker.shared.checkVersion { - showUpdateAvailableAlert = true - } - } - .tint(.main) + LaunchScreen() } } } diff --git a/project.yml b/project.yml index 11f34d8..9e0e0fd 100644 --- a/project.yml +++ b/project.yml @@ -67,13 +67,12 @@ targetTemplates: CFBundleName: "${target_name}" CFBundlePackageType: "$(PRODUCT_BUNDLE_PACKAGE_TYPE)" ITSAppUsesNonExemptEncryption: false - LSRequiresIPhoneOS: true + UILaunchStoryboardName: "" NSAppTransportSecurity: NSAllowsLocalNetworking: true NSCameraUsageDescription: "Camera is needed to scan QR-Codes" UIApplicationSceneManifest: UIApplicationSupportsMultipleScenes: false - UILaunchStoryboardName: "LaunchScreen" UIRequiredDeviceCapabilities: - armv7 UISupportedInterfaceOrientations: