diff --git a/WaiterRobot/Core/Mvi/KotlinFlowPublisher.swift b/WaiterRobot/Core/Mvi/KotlinFlowPublisher.swift deleted file mode 100644 index 802c71c..0000000 --- a/WaiterRobot/Core/Mvi/KotlinFlowPublisher.swift +++ /dev/null @@ -1,60 +0,0 @@ -/// Base on -/// - https://johnoreilly.dev/posts/kotlinmultiplatform-swift-combine_publisher-flow/ -/// - https://proandroiddev.com/kotlin-multiplatform-mobile-sharing-the-ui-state-management-a67bd9a49882 -/// - https://github.com/orbit-mvi/orbit-swift-gradle-plugin/blob/main/src/main/resources/Publisher.swift.mustache - -import Combine -import Foundation -import shared - -public extension Kotlinx_coroutines_coreFlow { - func asPublisher() -> AnyPublisher { - (KotlinFlowPublisher(flow: self) as KotlinFlowPublisher).eraseToAnyPublisher() - } -} - -private struct KotlinFlowPublisher: Publisher { - public typealias Output = T - public typealias Failure = Never - private let flow: Kotlinx_coroutines_coreFlow - - public init(flow: Kotlinx_coroutines_coreFlow) { - self.flow = flow - } - - public func receive(subscriber: S) where S.Input == T, S.Failure == Failure { - let subscription = FlowSubscription(flow: flow, subscriber: subscriber) - subscriber.receive(subscription: subscription) - } - - final class FlowSubscription: Subscription where S.Input == T, S.Failure == Failure { - private var subscriber: S? - private let job: Kotlinx_coroutines_coreJob? - private let flow: Kotlinx_coroutines_coreFlow - - init(flow: Kotlinx_coroutines_coreFlow, subscriber: S) { - self.flow = flow - self.subscriber = subscriber - job = FlowUtilsKt.subscribe( - flow, - onEach: { position in - if let position = position as? T { - _ = subscriber.receive(position) - } - }, - onComplete: { subscriber.receive(completion: .finished) }, - onThrow: { _ in - // TODO: - // logger.error(message: "got error in Flow subscription", throwable: error) - } - ) - } - - func cancel() { - subscriber = nil - job?.cancel(cause: nil) - } - - func request(_: Subscribers.Demand) {} - } -} diff --git a/WaiterRobot/Core/Mvi/ObservableViewModel.swift b/WaiterRobot/Core/Mvi/ObservableViewModel.swift index 9ca5086..cf1ceb0 100644 --- a/WaiterRobot/Core/Mvi/ObservableViewModel.swift +++ b/WaiterRobot/Core/Mvi/ObservableViewModel.swift @@ -3,29 +3,40 @@ /// - https://proandroiddev.com/kotlin-multiplatform-mobile-sharing-the-ui-state-management-a67bd9a49882 /// - https://github.com/orbit-mvi/orbit-swift-gradle-plugin/blob/main/src/main/resources/stateObject.swift.mustache -import Combine import Foundation import shared @MainActor class ObservableViewModel>: ObservableObject { @Published public private(set) var state: S - public private(set) var sideEffect: AnyPublisher, Never> public let actual: VM + private var task: Task? = nil + init(vm: VM) { actual = vm // 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! S - sideEffect = actual.container.sideEffectFlow.asPublisher() as AnyPublisher, Never> - (actual.container.stateFlow.asPublisher() as AnyPublisher) - .receive(on: RunLoop.main) - .assign(to: &$state) + activate() } deinit { actual.onCleared() + task?.cancel() + } + + @MainActor + private func activate() { + guard task == nil else { + return + } + task = Task { + let logger = koin.logger(tag: VM.description()) + for await state in actual.container.stateFlow { + self.state = state as! S + } + } } } diff --git a/WaiterRobot/Ui/Billing/BillingScreen.swift b/WaiterRobot/Ui/Billing/BillingScreen.swift index 6bce706..f7eb52d 100644 --- a/WaiterRobot/Ui/Billing/BillingScreen.swift +++ b/WaiterRobot/Ui/Billing/BillingScreen.swift @@ -83,10 +83,10 @@ struct BillingScreen: View { } .navigationTitle(localize.billing.title(value0: table.number.description, value1: table.groupName)) .navigationBarTitleDisplayMode(.inline) - .customBackNavigation(title: localize.dialog.cancel(), icon: nil, action: vm.actual.goBack) // TODO: + .customBackNavigation(title: localize.dialog.cancel(), icon: nil, action: { vm.actual.goBack() }) // TODO: .confirmationDialog(localize.billing.notSent.title(), isPresented: Binding.constant(vm.state.showConfirmationDialog), titleVisibility: .visible) { - Button(localize.dialog.closeAnyway(), role: .destructive, action: vm.actual.abortBill) - Button(localize.dialog.cancel(), role: .cancel, action: vm.actual.keepBill) + Button(localize.dialog.closeAnyway(), role: .destructive, action: { vm.actual.abortBill() }) + Button(localize.dialog.cancel(), role: .cancel, action: { vm.actual.keepBill() }) } message: { Text(localize.billing.notSent.desc()) } diff --git a/WaiterRobot/Ui/Core/Navigation.swift b/WaiterRobot/Ui/Core/Navigation.swift index 13308b8..a3a5ab7 100644 --- a/WaiterRobot/Ui/Core/Navigation.swift +++ b/WaiterRobot/Ui/Core/Navigation.swift @@ -54,22 +54,45 @@ extension View { @MainActor func handleSideEffects( - of vm: some ObservableViewModel>, _ navigator: UIPilot, + of vm: some ObservableViewModel>, + _ navigator: UIPilot, handler: ((E) -> Bool)? = nil ) -> some View where S: ViewModelState, E: ViewModelEffect { - onReceive(vm.sideEffect) { effect in - debugPrint("Got Sideeffect \(effect)") - - switch effect { - case let navEffect as NavOrViewModelEffectNavEffect: - navigator.navigate(navEffect.action) + handleSideEffects2(of: vm.actual, navigator, handler: handler) + } - case let sideEffect as NavOrViewModelEffectVMEffect: - if handler?(sideEffect.effect) != true { - koin.logger(tag: "handleSideEffects").w { "Side effect \(sideEffect.effect) was not handled." } + @MainActor + func handleSideEffects2( + of vm: some AbstractViewModel, + _ navigator: UIPilot, + handler: ((E) -> Bool)? = nil + ) -> some View where E: ViewModelEffect { + task { + let logger = koin.logger(tag: "handleSideEffects") + for await sideEffect in vm.container.sideEffectFlow { + logger.d { "Got sideEffect: \(sideEffect)" } + switch onEnum(of: sideEffect as! NavOrViewModelEffect) { + case let .navEffect(navEffect): + navigator.navigate(navEffect.action) + case let .vMEffect(effect): + if handler?(effect.effect) != true { + logger.w { "Side effect \(effect.effect) was not handled." } + } } - default: - koin.logger(tag: "handleSideEffects").w { "Unhandled effect type \(effect)." } + } + } + } + + @MainActor + func observeState( + of vm: some AbstractViewModel, + stateBinding: Binding + ) -> some View where S: ViewModelState { + task { + let logger = koin.logger(tag: "ObservableViewModel") + for await state in vm.container.stateFlow { + logger.d { "New state: \(state)" } + stateBinding.wrappedValue = state as! S } } } diff --git a/WaiterRobot/Ui/Core/PullToRefresh.swift b/WaiterRobot/Ui/Core/PullToRefresh.swift new file mode 100644 index 0000000..1adc249 --- /dev/null +++ b/WaiterRobot/Ui/Core/PullToRefresh.swift @@ -0,0 +1,53 @@ +// +// PullToRefresh.swift +// WaiterRobot +// +// Created by Fabian Schedler on 20.11.23. +// + +import SwiftUI + +struct PullToRefresh: View { + let coordinateSpaceName: String + let isRefreshing: Bool + let onRefresh: () -> Void + + @State var needRefresh: Bool = false + + var body: some View { + if isRefreshing { + EmptyView() + } else { + GeometryReader { geo in + if geo.frame(in: .named(coordinateSpaceName)).midY > 50 { + Spacer() + .onAppear { + needRefresh = true + print("NeedRefresh") + } + } else if geo.frame(in: .named(coordinateSpaceName)).maxY < 10 { + Spacer() + .onAppear { + if needRefresh { + onRefresh() + needRefresh = false + print("RefreshStarted") + } + } + } + + let pullProgress = max(0, min(50, geo.frame(in: .named(coordinateSpaceName)).midY)) / 50 + + HStack { + Spacer() + ProgressView() + .opacity(pullProgress) + .controlSize(.large) + .tint(.gray) + .padding(.bottom, 50) + Spacer() + } + }.padding(.top, -50) + } + } +} diff --git a/WaiterRobot/Ui/Core/RefreshableScrollView.swift b/WaiterRobot/Ui/Core/RefreshableScrollView.swift new file mode 100644 index 0000000..5eeb78b --- /dev/null +++ b/WaiterRobot/Ui/Core/RefreshableScrollView.swift @@ -0,0 +1,83 @@ +// +// RefreshableScrollView.swift +// WaiterRobot +// +// Created by Fabian Schedler on 20.11.23. +// + +import shared +import SwiftUI + +struct RefreshableScrollView: View { + private let isRefreshing: Bool + private let onRefresh: () -> Void + private let content: () -> Content + + init( + isRefreshing: Bool, + onRefresh: @escaping () -> Void, + @ViewBuilder content: @escaping () -> Content + ) { + self.isRefreshing = isRefreshing + self.onRefresh = onRefresh + self.content = content + } + + init( + resource: Resource, + onRefresh: @escaping () -> Void, + @ViewBuilder content: @escaping () -> Content + ) { + self.init(for: onEnum(of: resource), onRefresh: onRefresh, content: content) + } + + init( + for resource: Skie.org_datepollsystems_waiterrobot__shared.Resource.__Sealed, + onRefresh: @escaping () -> Void, + @ViewBuilder content: @escaping () -> Content + ) { + if case .loading = resource { + self.init(isRefreshing: true, onRefresh: onRefresh, content: content) + } else { + self.init(isRefreshing: false, onRefresh: onRefresh, content: content) + } + } + + var body: some View { + ScrollView { + PullToRefresh( + coordinateSpaceName: "PullToRefresh", + isRefreshing: isRefreshing, + onRefresh: onRefresh + ) + if isRefreshing { + ProgressView() + .controlSize(.large) + .tint(.gray) + } + content() + }.coordinateSpace(name: "PullToRefresh") + } +} + +struct Refreshing_Preview: PreviewProvider { + static var previews: some View { + RefreshableScrollView( + isRefreshing: true, + onRefresh: {} + ) { + Text("Refreshing") + } + } +} + +struct PullToRefresh_Preview: PreviewProvider { + static var previews: some View { + RefreshableScrollView( + isRefreshing: false, + onRefresh: {} + ) { + Text("Pull to refresh") + } + } +} diff --git a/WaiterRobot/Ui/Login/RegisterScreen.swift b/WaiterRobot/Ui/Login/RegisterScreen.swift index bf95dbc..1e66ff2 100644 --- a/WaiterRobot/Ui/Login/RegisterScreen.swift +++ b/WaiterRobot/Ui/Login/RegisterScreen.swift @@ -15,10 +15,10 @@ struct RegisterScreen: View { ScreenContainer(vm.state) { VStack { - Text(localize.register_.name.desc()) + Text(localize.register.name.desc()) .font(.body) - TextField(localize.register_.name.title(), text: $name) + TextField(localize.register.name.title(), text: $name) .font(.body) .fixedSize() .padding() @@ -35,12 +35,12 @@ struct RegisterScreen: View { Button { vm.actual.onRegister(name: name, createToken: createToken) } label: { - Text(localize.register_.login()) + Text(localize.register.login()) } } .padding() - Label(localize.register_.alreadyRegisteredInfo(), systemImage: "info.circle.fill") + Label(localize.register.alreadyRegisteredInfo(), systemImage: "info.circle.fill") } .padding() } diff --git a/WaiterRobot/Ui/Order/OrderScreen.swift b/WaiterRobot/Ui/Order/OrderScreen.swift index 1e4ff31..df651dc 100644 --- a/WaiterRobot/Ui/Order/OrderScreen.swift +++ b/WaiterRobot/Ui/Order/OrderScreen.swift @@ -65,10 +65,10 @@ struct OrderScreen: View { .disabled(vm.state.currentOrder.isEmpty || vm.state.viewState != ViewState.Idle.shared) } } - .customBackNavigation(title: localize.dialog.cancel(), icon: "chevron.backward", action: vm.actual.goBack) + .customBackNavigation(title: localize.dialog.cancel(), icon: "chevron.backward", action: { vm.actual.goBack() }) .confirmationDialog(localize.order.notSent.title(), isPresented: Binding.constant(vm.state.showConfirmationDialog), titleVisibility: .visible) { - Button(localize.dialog.closeAnyway(), role: .destructive, action: vm.actual.abortOrder) - Button(localize.order.keepOrder(), role: .cancel, action: vm.actual.keepOrder) + Button(localize.dialog.closeAnyway(), role: .destructive, action: { vm.actual.abortOrder() }) + Button(localize.order.keepOrder(), role: .cancel, action: { vm.actual.keepOrder() }) } message: { Text(localize.order.notSent.desc()) } diff --git a/WaiterRobot/Ui/Order/Search/ProductSearch.swift b/WaiterRobot/Ui/Order/Search/ProductSearch.swift index 90c9c31..89960c3 100644 --- a/WaiterRobot/Ui/Order/Search/ProductSearch.swift +++ b/WaiterRobot/Ui/Order/Search/ProductSearch.swift @@ -56,7 +56,7 @@ struct ProductSearch: View { } .tabViewStyle(.page(indexDisplayMode: .never)) .searchable(text: $search, placement: .navigationBarDrawer(displayMode: .always)) - .onChange(of: search, perform: vm.actual.filterProducts) + .onChange(of: search, perform: { vm.actual.filterProducts(filter: $0) }) .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button(localize.dialog.cancel()) { diff --git a/WaiterRobot/Ui/Settings/SettingsScreen.swift b/WaiterRobot/Ui/Settings/SettingsScreen.swift index f75c347..cb53e7a 100644 --- a/WaiterRobot/Ui/Settings/SettingsScreen.swift +++ b/WaiterRobot/Ui/Settings/SettingsScreen.swift @@ -27,14 +27,18 @@ struct SettingsScreen: View { icon: "arrow.triangle.2.circlepath", title: localize.settings.refresh.title(), subtitle: localize.settings.refresh.desc(), - action: vm.actual.refreshAll + action: { + vm.actual.refreshAll() + } ) SettingsItem( icon: "person.3", title: localize.switchEvent.title(), subtitle: CommonApp.shared.settings.eventName, - action: vm.actual.switchEvent + action: { + vm.actual.switchEvent() + } ) } @@ -66,7 +70,7 @@ struct SettingsScreen: View { } } .confirmationDialog(localize.settings.logout.title(value0: CommonApp.shared.settings.organisationName), isPresented: $showConfirmLogout, titleVisibility: .visible) { - Button(localize.settings.logout.action(), role: .destructive, action: vm.actual.logout) + Button(localize.settings.logout.action(), role: .destructive, action: { vm.actual.logout() }) Button(localize.settings.keepLoggedIn(), role: .cancel, action: { showConfirmLogout = false }) } message: { Text(localize.settings.logout.desc(value0: CommonApp.shared.settings.organisationName)) diff --git a/WaiterRobot/Ui/TableDetail/TableDetailScreen.swift b/WaiterRobot/Ui/TableDetail/TableDetailScreen.swift index 41fbbb1..dfb25a1 100644 --- a/WaiterRobot/Ui/TableDetail/TableDetailScreen.swift +++ b/WaiterRobot/Ui/TableDetail/TableDetailScreen.swift @@ -15,32 +15,35 @@ struct TableDetailScreen: View { var body: some View { unowned let vm = strongVM + let resource = onEnum(of: vm.state.orderedItemsResource) + let orderedItems = vm.state.orderedItemsResource.data as? [OrderedItem] - ScreenContainer(vm.state) { - ZStack { - List { - if vm.state.orderedItems.isEmpty { - Text(localize.tableDetail.noOrder(value0: table.number.description, value1: table.groupName)) - .multilineTextAlignment(.center) - .frame(maxWidth: .infinity) - .padding() - } else { - ForEach(vm.state.orderedItems, id: \.id) { item in + // TODO: add refreshing and loading indicator (also check android) + ZStack { + VStack { + if case let .error(value) = resource { + Text(value.exception.getLocalizedUserMessage()) + } + if orderedItems?.isEmpty == true { + Text(localize.tableDetail.noOrder(value0: table.number.description, value1: table.groupName)) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + .padding() + } else if let orderedItems { + List { + ForEach(orderedItems, id: \.id) { item in OrderedItemView(item: item) { vm.actual.openOrderScreen(initialItemId: item.id.toKotlinLong()) } } } } + } - EmbeddedFloatingActionButton(icon: "plus") { - vm.actual.openOrderScreen(initialItemId: nil) - } + EmbeddedFloatingActionButton(icon: "plus") { + vm.actual.openOrderScreen(initialItemId: nil) } } - .refreshable { - vm.actual.loadOrder() - } .navigationTitle(localize.tableDetail.title(value0: table.number.description, value1: table.groupName)) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { @@ -48,7 +51,7 @@ struct TableDetailScreen: View { vm.actual.openBillingScreen() } label: { Image(systemName: "creditcard") - }.disabled(vm.state.orderedItems.isEmpty) + }.disabled(orderedItems?.isEmpty != false) } } .handleSideEffects(of: vm, navigator) diff --git a/WaiterRobot/Ui/TableList/TableGroupSection.swift b/WaiterRobot/Ui/TableList/TableGroupSection.swift index db76311..7b82484 100644 --- a/WaiterRobot/Ui/TableList/TableGroupSection.swift +++ b/WaiterRobot/Ui/TableList/TableGroupSection.swift @@ -2,12 +2,12 @@ import shared import SwiftUI struct TableGroupSection: View { - let groupWithTables: TableGroupWithTables + let tableGroup: TableGroup let onTableClick: (shared.Table) -> Void var body: some View { Section { - ForEach(groupWithTables.tables, id: \.id) { table in + ForEach(tableGroup.tables, id: \.id) { table in Table( text: table.number.description, onClick: { @@ -19,7 +19,7 @@ struct TableGroupSection: View { } header: { HStack { Color(UIColor.lightGray).frame(height: 1) - Text(groupWithTables.group.name) + Text(tableGroup.name) Color(UIColor.lightGray).frame(height: 1) } } @@ -30,13 +30,18 @@ struct TableGroupSection_Previews: PreviewProvider { static var previews: some View { LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))]) { TableGroupSection( - groupWithTables: TableGroupWithTables( - group: TableGroup(id: 1, name: "Test Group"), + tableGroup: TableGroup( + id: 1, + name: "Test Group", + eventId: 1, + position: 1, + color: nil, + hidden: false, tables: [ - shared.Table(id: 1, number: 1, groupName: "Test Group"), - shared.Table(id: 2, number: 2, groupName: "Test Group"), - shared.Table(id: 3, number: 3, groupName: "Test Group"), - shared.Table(id: 4, number: 4, groupName: "Test Group"), + shared.Table(id: 1, number: 1, groupName: "Test Group", hasOrders: true), + shared.Table(id: 2, number: 2, groupName: "Test Group", hasOrders: false), + shared.Table(id: 3, number: 3, groupName: "Test Group", hasOrders: false), + shared.Table(id: 4, number: 4, groupName: "Test Group", hasOrders: true), ] ), onTableClick: { _ in } diff --git a/WaiterRobot/Ui/TableList/TableListFilterRow.swift b/WaiterRobot/Ui/TableList/TableListFilterRow.swift index f4f4922..2d4c3dd 100644 --- a/WaiterRobot/Ui/TableList/TableListFilterRow.swift +++ b/WaiterRobot/Ui/TableList/TableListFilterRow.swift @@ -2,43 +2,41 @@ import shared import SwiftUI struct TableListFilterRow: View { - let selectedTableGroups: [TableGroup] - let unselectedTableGroups: [TableGroup] + let tableGroups: [TableGroup] let onToggleFilter: (TableGroup) -> Void - let onClearFilter: () -> Void + let onSelectAll: () -> Void + let onUnselectAll: () -> Void var body: some View { HStack { ScrollView(.horizontal) { HStack { - ForEach(selectedTableGroups, id: \.id) { group in + ForEach(tableGroups, id: \.id) { group in Button { onToggleFilter(group) // vm.actual.toggleFilter(tableGroup: group) } label: { Text(group.name) } .buttonStyle(.bordered) - .tint(.blue) - } - - ForEach(unselectedTableGroups, id: \.id) { group in - Button { - onToggleFilter(group) - } label: { - Text(group.name) - } - .buttonStyle(.bordered) - .tint(.primary) + .tint(group.hidden ? .primary : .blue) } }.padding(.horizontal) } Button { - onClearFilter() + onUnselectAll() } label: { Image(systemName: "xmark") } - .padding() - .disabled(selectedTableGroups.isEmpty) + .padding(.trailing) + .disabled(tableGroups.allSatisfy(\.hidden)) + Button { + onSelectAll() + } label: { + Image(systemName: "checkmark") + } + .padding(.trailing) + + .disabled(tableGroups.allSatisfy { !$0.hidden }) } } } @@ -46,15 +44,13 @@ struct TableListFilterRow: View { struct TableListFilterRow_Previews: PreviewProvider { static var previews: some View { TableListFilterRow( - selectedTableGroups: [ - TableGroup(id: 1, name: "Test Group1"), - TableGroup(id: 2, name: "Test Group2"), - ], - unselectedTableGroups: [ - TableGroup(id: 3, name: "Test Group3"), + tableGroups: [ + TableGroup(id: 1, name: "Test Group1", eventId: 1, position: 1, color: nil, hidden: false, tables: []), + TableGroup(id: 2, name: "Test Group2", eventId: 1, position: 1, color: nil, hidden: false, tables: []), ], onToggleFilter: { _ in }, - onClearFilter: {} + onSelectAll: {}, + onUnselectAll: {} ) } } diff --git a/WaiterRobot/Ui/TableList/TableListScreen.swift b/WaiterRobot/Ui/TableList/TableListScreen.swift index ace8c45..0e6c72d 100644 --- a/WaiterRobot/Ui/TableList/TableListScreen.swift +++ b/WaiterRobot/Ui/TableList/TableListScreen.swift @@ -11,42 +11,44 @@ struct TableListScreen: View { GridItem(.adaptive(minimum: 100)), ] + // TODO: table has open/unpaid order indicator var body: some View { unowned let vm = strongVM - ScreenContainer(vm.state) { - VStack { - if (vm.state.unselectedTableGroupList.count + vm.state.selectedTableGroupList.count) > 1 { - TableListFilterRow( - selectedTableGroups: vm.state.selectedTableGroupList, - unselectedTableGroups: vm.state.unselectedTableGroupList, - onToggleFilter: { vm.actual.toggleFilter(tableGroup: $0) }, - onClearFilter: vm.actual.clearFilter - ) + let tableGroups = vm.state.tableGroups.data as? [TableGroup] + let tableGroupResource = onEnum(of: vm.state.tableGroups) + VStack { + if let tableGroups, tableGroups.count > 1 { + TableListFilterRow( + tableGroups: tableGroups, + onToggleFilter: { vm.actual.toggleFilter(tableGroup: $0) }, + onSelectAll: { vm.actual.showAll() }, + onUnselectAll: { vm.actual.hideAll() } + ) + } + + RefreshableScrollView(for: tableGroupResource, onRefresh: { vm.actual.loadTables(forceUpdate: true) }) { + if case let .error(resource) = tableGroupResource { + Text(resource.exception.getLocalizedUserMessage()) } - ScrollView { - if vm.state.filteredTableGroups.isEmpty { - Text(localize.tableList.noTableFound()) - .multilineTextAlignment(.center) - .frame(maxWidth: .infinity) - .padding() - } else { - LazyVGrid(columns: layout) { - ForEach(vm.state.filteredTableGroups, id: \.group.id) { groupWithTables in - if !groupWithTables.tables.isEmpty { - TableGroupSection( - groupWithTables: groupWithTables, - onTableClick: vm.actual.onTableClick - ) - } + if tableGroups?.isEmpty == true { + Text(localize.tableList.noTableFound()) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + .padding() + } else if let tableGroups { + LazyVGrid(columns: layout) { + ForEach(tableGroups.filter { !$0.hidden }, id: \.id) { group in + if !group.tables.isEmpty { + TableGroupSection( + tableGroup: group, + onTableClick: { vm.actual.onTableClick(table: $0) } + ) } } - .padding() } - } - .refreshable { - vm.actual.loadTables(forceUpdate: true) + .padding() } } } diff --git a/WaiterRobot/WaiterRobotApp.swift b/WaiterRobot/WaiterRobotApp.swift index a56caae..a899e4d 100644 --- a/WaiterRobot/WaiterRobotApp.swift +++ b/WaiterRobot/WaiterRobotApp.swift @@ -73,14 +73,12 @@ struct WaiterRobotApp: App { } } .handleSideEffects(of: vm, navigator) { effect in - switch effect { - case let snackBar as RootEffect.ShowSnackBar: + switch onEnum(of: effect) { + case let .showSnackBar(snackBar): snackBarMessage = snackBar.message DispatchQueue.main.asyncAfter(deadline: .now() + 5) { snackBarMessage = nil } - default: - return false } return true } diff --git a/project.yml b/project.yml index 11f34d8..e9035c2 100644 --- a/project.yml +++ b/project.yml @@ -17,7 +17,7 @@ fileGroups: packages: shared: url: https://github.com/DatepollSystems/WaiterRobot-Shared-Android.git - version: "1.1.3" + version: "1.2.1" UIPilot: url: https://github.com/canopas/UIPilot.git from: 1.3.1