diff --git a/WaiterRobot/Core/KermitLoggerExtension.swift b/WaiterRobot/Core/KermitLoggerExtension.swift new file mode 100644 index 0000000..e56ba20 --- /dev/null +++ b/WaiterRobot/Core/KermitLoggerExtension.swift @@ -0,0 +1,15 @@ +import shared + +extension KermitLogger { + func d(message: @escaping () -> String) { + d(throwable: nil, tag: tag, message: message) + } + + func w(message: @escaping () -> String) { + w(throwable: nil, tag: tag, message: message) + } + + func e(message: @escaping () -> String) { + e(throwable: nil, tag: tag, message: message) + } +} diff --git a/WaiterRobot/Core/Mvi/ObservableViewModel.swift b/WaiterRobot/Core/Mvi/ObservableViewModel.swift index 4992484..7804087 100644 --- a/WaiterRobot/Core/Mvi/ObservableViewModel.swift +++ b/WaiterRobot/Core/Mvi/ObservableViewModel.swift @@ -12,54 +12,33 @@ class ObservableViewModel? = nil - - init(viewModel: ViewModel, subscribe: Bool = true) { + init(viewModel: ViewModel) { actual = viewModel // This is save, as the constraint is required by the generics (S must be the state of the provided VM) state = actual.container.stateFlow.value as! State - - if subscribe { - Task { - await activate() - } - } } @MainActor - func activate() { - guard task == nil else { return } - task = Task { [weak self] in - guard let stateFlow = self?.actual.container.stateFlow else { return } - - for await state in stateFlow { - self?.state = state as! State - } + func activate() async { + for await state in actual.container.refCountStateFlow { + self.state = state as! State } } - func deactivate() { - task?.cancel() - task = nil - } - deinit { actual.onCleared() - - task?.cancel() - task = nil } } class ObservableTableListViewModel: ObservableViewModel { init() { - super.init(viewModel: koin.tableListVM(), subscribe: false) + super.init(viewModel: koin.tableListVM()) } } class ObservableTableDetailViewModel: ObservableViewModel { init(table: Table) { - super.init(viewModel: koin.tableDetailVM(table: table), subscribe: false) + super.init(viewModel: koin.tableDetailVM(table: table)) } } diff --git a/WaiterRobot/Entitlements/WaiterRobot.entitlements b/WaiterRobot/Entitlements/WaiterRobot.entitlements new file mode 100644 index 0000000..06c2c9a --- /dev/null +++ b/WaiterRobot/Entitlements/WaiterRobot.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.associated-domains + + applinks:my.kellner.team + + + diff --git a/WaiterRobot/Entitlements/WaiterRobotLava.entitlements b/WaiterRobot/Entitlements/WaiterRobotLava.entitlements new file mode 100644 index 0000000..b12d50c --- /dev/null +++ b/WaiterRobot/Entitlements/WaiterRobotLava.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.associated-domains + + applinks:lava.kellner.team + + + diff --git a/WaiterRobot/MainView.swift b/WaiterRobot/MainView.swift index 46db551..5302b21 100644 --- a/WaiterRobot/MainView.swift +++ b/WaiterRobot/MainView.swift @@ -94,7 +94,7 @@ struct MainView: View { .padding() } } - .handleSideEffects(of: viewModel, navigator) { effect in + .withViewModel(viewModel, navigator) { effect in switch onEnum(of: effect) { case let .showSnackBar(snackBar): snackBarMessage = snackBar.message diff --git a/WaiterRobot/Ui/Billing/BillingScreen.swift b/WaiterRobot/Ui/Billing/BillingScreen.swift index ee58a47..ed9f6aa 100644 --- a/WaiterRobot/Ui/Billing/BillingScreen.swift +++ b/WaiterRobot/Ui/Billing/BillingScreen.swift @@ -18,7 +18,9 @@ struct BillingScreen: View { } var body: some View { - content() + let billItems = Array(viewModel.state.billItemsArray) + + content(billItems: billItems) .navigationTitle(localize.billing.title(value0: table.number.description, value1: table.groupName)) .navigationBarTitleDisplayMode(.inline) .customBackNavigation( @@ -42,36 +44,35 @@ struct BillingScreen: View { } message: { Text(localize.billing.notSent.desc()) } -// TODO: Needs shared modification to be accessible from here -// .refreshable { -// viewModel.actual.loadBill() -// } .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { - Button { - viewModel.actual.selectAll() - } label: { - Image(systemName: "checkmark") + if !billItems.isEmpty { + Button { + viewModel.actual.selectAll() + } label: { + Image(systemName: "checkmark") + } } - Button { - viewModel.actual.unselectAll() - } label: { - Image(systemName: "xmark") + if !billItems.isEmpty { + Button { + viewModel.actual.unselectAll() + } label: { + Image(systemName: "xmark") + } } } } + // TODO: make only half screen when ios 15 is dropped .sheet(isPresented: $showPayDialog) { PayDialog(viewModel: viewModel) } - .handleSideEffects(of: viewModel, navigator) + .withViewModel(viewModel, navigator) } @ViewBuilder - private func content() -> some View { - let billItems = Array(viewModel.state.billItemsArray) - + private func content(billItems: [BillItem]) -> some View { VStack { List { if billItems.isEmpty { diff --git a/WaiterRobot/Ui/Core/DynamicGrid.swift b/WaiterRobot/Ui/Core/DynamicGrid.swift index ea3ee36..43d8a06 100644 --- a/WaiterRobot/Ui/Core/DynamicGrid.swift +++ b/WaiterRobot/Ui/Core/DynamicGrid.swift @@ -39,7 +39,7 @@ public struct DynamicGrid: Layout, Sendable { public func placeSubviews( in bounds: CGRect, - proposal: ProposedViewSize, + proposal _: ProposedViewSize, subviews: Subviews, cache _: inout () ) { diff --git a/WaiterRobot/Ui/Core/Navigation.swift b/WaiterRobot/Ui/Core/Navigation.swift index 573262e..08c904c 100644 --- a/WaiterRobot/Ui/Core/Navigation.swift +++ b/WaiterRobot/Ui/Core/Navigation.swift @@ -4,6 +4,7 @@ import SwiftUI import UIPilot extension UIPilot { + @MainActor func navigate(_ navAction: NavAction) { koin.logger(tag: "Navigation").d { "Handle navigation: \(navAction.description)" } @@ -61,28 +62,18 @@ extension View { } } - @MainActor - func handleSideEffects( - of viewModel: some ObservableViewModel>, - _ navigator: UIPilot, - handler: ((E) -> Bool)? = nil - ) -> some View where S: ViewModelState, E: ViewModelEffect { - handleSideEffects2(of: viewModel.actual, navigator, handler: handler) - } - - @MainActor - func handleSideEffects2( - of viewModel: some AbstractViewModel, + func handleSideEffects( + of viewModel: some ObservableViewModel>, _ navigator: UIPilot, - handler: ((E) -> Bool)? = nil - ) -> some View where E: ViewModelEffect { + handler: ((SideEffect) -> Bool)? = nil + ) -> some View where State: ViewModelState, SideEffect: ViewModelEffect { task { let logger = koin.logger(tag: "handleSideEffects") - for await sideEffect in viewModel.container.sideEffectFlow { + for await sideEffect in viewModel.actual.container.refCountSideEffectFlow { logger.d { "Got sideEffect: \(sideEffect)" } - switch onEnum(of: sideEffect as! NavOrViewModelEffect) { + switch onEnum(of: sideEffect as! NavOrViewModelEffect) { case let .navEffect(navEffect): - navigator.navigate(navEffect.action) + await navigator.navigate(navEffect.action) case let .vMEffect(effect): if handler?(effect.effect) != true { logger.w { "Side effect \(effect.effect) was not handled." } @@ -92,17 +83,20 @@ extension View { } } - @MainActor - func observeState( - of viewModel: some AbstractViewModel, - stateBinding: Binding - ) -> some View where S: ViewModelState { + func observeState( + of viewModel: some ObservableViewModel> + ) -> some View where State: ViewModelState, SideEffect: ViewModelEffect { task { - let logger = koin.logger(tag: "ObservableViewModel") - for await state in viewModel.container.stateFlow { - logger.d { "New state: \(state)" } - stateBinding.wrappedValue = state as! S - } + await viewModel.activate() } } + + func withViewModel( + _ viewModel: some ObservableViewModel>, + _ navigator: UIPilot, + handler _: ((SideEffect) -> Bool)? = nil + ) -> some View where State: ViewModelState, SideEffect: ViewModelEffect { + handleSideEffects(of: viewModel, navigator) + .observeState(of: viewModel) + } } diff --git a/WaiterRobot/Ui/Login/LoginScannerScreen.swift b/WaiterRobot/Ui/Login/LoginScannerScreen.swift index 45941a3..7434f4c 100644 --- a/WaiterRobot/Ui/Login/LoginScannerScreen.swift +++ b/WaiterRobot/Ui/Login/LoginScannerScreen.swift @@ -54,7 +54,7 @@ struct LoginScannerScreen: View { Text(localize.dialog.cancel()) } } - .handleSideEffects(of: viewModel, navigator) + .withViewModel(viewModel, navigator) } } diff --git a/WaiterRobot/Ui/Login/LoginScreen.swift b/WaiterRobot/Ui/Login/LoginScreen.swift index fd29f14..7f1829d 100644 --- a/WaiterRobot/Ui/Login/LoginScreen.swift +++ b/WaiterRobot/Ui/Login/LoginScreen.swift @@ -56,6 +56,6 @@ struct LoginScreen: View { Spacer() } - .handleSideEffects(of: viewModel, navigator) + .withViewModel(viewModel, navigator) } } diff --git a/WaiterRobot/Ui/Login/RegisterScreen.swift b/WaiterRobot/Ui/Login/RegisterScreen.swift index c4e475c..7cf48a0 100644 --- a/WaiterRobot/Ui/Login/RegisterScreen.swift +++ b/WaiterRobot/Ui/Login/RegisterScreen.swift @@ -65,7 +65,7 @@ struct RegisterScreen: View { } .padding() .navigationBarHidden(true) - .handleSideEffects(of: viewModel, navigator) + .withViewModel(viewModel, navigator) } } diff --git a/WaiterRobot/Ui/Order/OrderScreen.swift b/WaiterRobot/Ui/Order/OrderScreen.swift index 6c4e510..207b321 100644 --- a/WaiterRobot/Ui/Order/OrderScreen.swift +++ b/WaiterRobot/Ui/Order/OrderScreen.swift @@ -29,11 +29,13 @@ struct OrderScreen: View { case let .error(error): Text(error.userMessage) + .foregroundStyle(.red) + .padding(.horizontal) + + currentOder(error.data) case let .success(resource): - if let data = resource.data { - currentOder(data) - } + currentOder(resource.data) } } .navigationTitle(localize.order.title(value0: table.number.description, value1: table.groupName)) @@ -53,14 +55,15 @@ struct OrderScreen: View { .sheet(isPresented: $showProductSearch) { ProductSearch(viewModel: viewModel) } - .handleSideEffects(of: viewModel, navigator) + .withViewModel(viewModel, navigator) + .animation(.default, value: viewModel.state.currentOrder) } @ViewBuilder private func currentOder( - _ currentOrderArray: KotlinArray + _ currentOrderArray: KotlinArray? ) -> some View { - let currentOrder = Array(currentOrderArray) + let currentOrder = currentOrderArray.map { Array($0) } ?? Array() VStack(spacing: 0) { if currentOrder.isEmpty { diff --git a/WaiterRobot/Ui/Order/Search/ProductSearch.swift b/WaiterRobot/Ui/Order/Search/ProductSearch.swift index 20d9ec3..0a6b1b0 100644 --- a/WaiterRobot/Ui/Order/Search/ProductSearch.swift +++ b/WaiterRobot/Ui/Order/Search/ProductSearch.swift @@ -25,7 +25,7 @@ struct ProductSearch: View { productsGroupsList(productGroups: productGroups) } } - } + }.observeState(of: viewModel) } @ViewBuilder diff --git a/WaiterRobot/Ui/Settings/SettingsScreen.swift b/WaiterRobot/Ui/Settings/SettingsScreen.swift index cb1da51..d25dd5a 100644 --- a/WaiterRobot/Ui/Settings/SettingsScreen.swift +++ b/WaiterRobot/Ui/Settings/SettingsScreen.swift @@ -95,6 +95,6 @@ struct SettingsScreen: View { } message: { Text(localize.settings.logout.desc(value0: CommonApp.shared.settings.organisationName)) } - .handleSideEffects(of: viewModel, navigator) + .withViewModel(viewModel, navigator) } } diff --git a/WaiterRobot/Ui/SwitchEvent/SwitchEventScreen.swift b/WaiterRobot/Ui/SwitchEvent/SwitchEventScreen.swift index 3186bb1..feebfbd 100644 --- a/WaiterRobot/Ui/SwitchEvent/SwitchEventScreen.swift +++ b/WaiterRobot/Ui/SwitchEvent/SwitchEventScreen.swift @@ -7,26 +7,26 @@ struct SwitchEventScreen: View { @StateObject private var viewModel = ObservableSwitchEventViewModel() - @SwiftUI.State private var selectedEvent: Event? + @State private var selectedEvent: Event? var body: some View { - switch viewModel.state.viewState { - case is ViewState.Loading: - ProgressView() - case is ViewState.Idle: - content() - case let error as ViewState.Error: - content() - .alert(isPresented: Binding.constant(true)) { - Alert( - title: Text(error.title), - message: Text(error.message), - dismissButton: .cancel(Text("OK"), action: error.onDismiss) - ) - } - default: - fatalError("Unexpected ViewState: \(viewModel.state.viewState.description)") - } + VStack { + switch onEnum(of: viewModel.state.viewState) { + case .loading: + ProgressView() + case .idle: + content() + case let .error(error): + content() + .alert(isPresented: Binding.constant(true)) { + Alert( + title: Text(error.title), + message: Text(error.message), + dismissButton: .cancel(Text("OK"), action: error.onDismiss) + ) + } + } + }.withViewModel(viewModel, navigator) } private func content() -> some View { @@ -68,7 +68,6 @@ struct SwitchEventScreen: View { viewModel.actual.loadEvents() } } - .handleSideEffects(of: viewModel, navigator) } } diff --git a/WaiterRobot/Ui/TableDetail/TableDetailScreen.swift b/WaiterRobot/Ui/TableDetail/TableDetailScreen.swift index 11cb64f..67ef363 100644 --- a/WaiterRobot/Ui/TableDetail/TableDetailScreen.swift +++ b/WaiterRobot/Ui/TableDetail/TableDetailScreen.swift @@ -18,7 +18,7 @@ struct TableDetailScreen: View { var body: some View { content() .navigationTitle(localize.tableDetail.title(value0: table.number.description, value1: table.groupName)) - .handleSideEffects(of: viewModel, navigator) + .withViewModel(viewModel, navigator) } // TODO: add refreshing and loading indicator (also check android) @@ -37,12 +37,6 @@ struct TableDetailScreen: View { } } } - .onAppear { - viewModel.activate() - } - .onDisappear { - viewModel.deactivate() - } } private func tableDetails(orderedItems: [OrderedItem]) -> some View { diff --git a/WaiterRobot/Ui/TableList/TableGroupSection.swift b/WaiterRobot/Ui/TableList/TableGroupSection.swift index af44973..3cfe524 100644 --- a/WaiterRobot/Ui/TableList/TableGroupSection.swift +++ b/WaiterRobot/Ui/TableList/TableGroupSection.swift @@ -19,9 +19,16 @@ struct TableGroupSection: View { } } header: { HStack { - Color(UIColor.lightGray).frame(height: 1) Text(tableGroup.name) - Color(UIColor.lightGray).frame(height: 1) + .font(.title2) + .foregroundStyle(.white) + .padding(6) + .background { + RoundedRectangle(cornerRadius: 8.0) + .foregroundStyle(Color(.main)) + } + + Spacer() } } } diff --git a/WaiterRobot/Ui/TableList/TableListScreen.swift b/WaiterRobot/Ui/TableList/TableListScreen.swift index e81a652..e38a070 100644 --- a/WaiterRobot/Ui/TableList/TableListScreen.swift +++ b/WaiterRobot/Ui/TableList/TableListScreen.swift @@ -43,14 +43,8 @@ struct TableListScreen: View { } .navigationTitle(CommonApp.shared.settings.eventName) .navigationBarTitleDisplayMode(.inline) - .handleSideEffects(of: viewModel, navigator) .animation(.spring, value: viewModel.state.tableGroupsArray) - .onAppear { - viewModel.activate() - } - .onDisappear { - viewModel.deactivate() - } + .withViewModel(viewModel, navigator) } private func content() -> some View { diff --git a/project.yml b/project.yml index 80ed6fd..9fdf820 100644 --- a/project.yml +++ b/project.yml @@ -48,7 +48,7 @@ targetTemplates: info: path: ".generated/${target_name}.plist" properties: - CFBundleShortVersionString: "2.2.0" + CFBundleShortVersionString: "2.2.1" # Generate VersionCode from VersionName (major * 10_000 + minor * 100 + patch, e.g. 1.2.3 -> 10203, 1.23.45 -> 12345) # Only used for prod releases. Lava uses epochMinute (same as on Android) CFBundleVersion: "20007" @@ -86,19 +86,13 @@ targetTemplates: - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - entitlements: - path: ".generated/${target_name}.entitlements" - properties: - com.apple.developer.associated-domains: - - "applinks:${deeplinkDomain}" settings: base: PRODUCT_BUNDLE_IDENTIFIER: "${identifier}" INFOPLIST_FILE: ".generated/${target_name}.plist" CODE_SIGN_STYLE: "Manual" - CODE_SIGN_ENTITLEMENTS: ".generated/${target_name}.entitlements" + CODE_SIGN_ENTITLEMENTS: "WaiterRobot/Entitlements/${target_name}.entitlements" DEVELOPMENT_TEAM: "28TM58T3GZ" PRODUCT_NAME: "${displayName}" ENABLE_PREVIEWS: "YES" @@ -121,7 +115,6 @@ targets: templateAttributes: identifier: "org.datepollsystems.waiterrobot" displayName: "kellner.team" - deeplinkDomain: "my.kellner.team" versionSuffix: "" allowedHosts: "my.kellner.team" @@ -135,7 +128,6 @@ targets: templateAttributes: identifier: "org.datepollsystems.waiterrobot.beta" displayName: "lava.kellner.team" - deeplinkDomain: "lava.kellner.team" versionSuffix: "lava" allowedHosts: "*" preBuildScripts: