Lower deployment target to iOS 16, replace @Observable with ObservableObject, add navigation and UI compatibility shims, and include login smoke UI tests. Co-authored-by: Cursor <cursoragent@cursor.com>
201 lines
6.5 KiB
Swift
201 lines
6.5 KiB
Swift
//
|
||
// MainTabsView.swift
|
||
// suixinkan
|
||
//
|
||
// Created by Codex on 2026/6/18.
|
||
//
|
||
|
||
import SwiftUI
|
||
|
||
/// 主 TabBar 样式,决定登录后首页使用系统 TabView 还是自定义底部栏。
|
||
enum MainTabBarStyle {
|
||
case system
|
||
case custom
|
||
}
|
||
|
||
/// 主 TabBar 配置,后续需要切回系统 TabView 时只修改 activeStyle。
|
||
enum MainTabBarConfiguration {
|
||
static let activeStyle: MainTabBarStyle = .custom
|
||
}
|
||
|
||
/// 主 Tab 容器视图,根据配置选择系统 TabView 或自定义 TabBar 外壳。
|
||
struct MainTabsView: View {
|
||
var body: some View {
|
||
switch MainTabBarConfiguration.activeStyle {
|
||
case .system:
|
||
SystemMainTabsView()
|
||
case .custom:
|
||
CustomMainTabsView()
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 系统 TabView 外壳,保留原生 TabBar 行为以便后续切换回系统实现。
|
||
private struct SystemMainTabsView: View {
|
||
@EnvironmentObject private var appRouter: AppRouter
|
||
|
||
var body: some View {
|
||
TabView(selection: $appRouter.selectedTab) {
|
||
ForEach(AppTab.allCases) { tab in
|
||
TabNavigationStackHost(tab: tab)
|
||
.tabItem { tab.label }
|
||
.tag(tab)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 自定义 TabBar 外壳,复用每个 Tab 独立 NavigationStack 并保留已访问页面状态。
|
||
private struct CustomMainTabsView: View {
|
||
@EnvironmentObject private var accountContext: AccountContext
|
||
@EnvironmentObject private var appRouter: AppRouter
|
||
@Environment(\.ordersAPI) private var ordersAPI
|
||
@EnvironmentObject private var toastCenter: ToastCenter
|
||
|
||
@StateObject private var badgeViewModel = MainTabBadgeViewModel()
|
||
@State private var showScanner = false
|
||
@State private var loadedTabs: Set<AppTab> = [.home]
|
||
|
||
private var orderBadgeText: String? {
|
||
guard let count = badgeViewModel.pendingWriteOffCount, count > 0 else {
|
||
return nil
|
||
}
|
||
return "\(count)"
|
||
}
|
||
|
||
var body: some View {
|
||
ZStack {
|
||
ForEach(AppTab.allCases) { tab in
|
||
PreservedTabPage(
|
||
tab: tab,
|
||
selectedTab: appRouter.selectedTab,
|
||
isLoaded: loadedTabs.contains(tab)
|
||
) {
|
||
CustomTabNavigationStackHost(
|
||
tab: tab,
|
||
selectedTab: $appRouter.selectedTab,
|
||
orderBadgeText: orderBadgeText,
|
||
onScanTap: { showScanner = true }
|
||
)
|
||
}
|
||
}
|
||
}
|
||
.ignoresSafeArea(.keyboard, edges: .bottom)
|
||
.fullScreenCover(isPresented: $showScanner) {
|
||
OrderScannerPage(
|
||
onClose: { showScanner = false },
|
||
onSuccess: { rawCode in
|
||
showScanner = false
|
||
appRouter.routeToOrderVerification(scannedCode: rawCode)
|
||
},
|
||
onFailure: { error in
|
||
showScanner = false
|
||
toastCenter.show(error.localizedDescription)
|
||
}
|
||
)
|
||
}
|
||
.task {
|
||
loadedTabs.insert(appRouter.selectedTab)
|
||
await refreshOrderBadge()
|
||
}
|
||
.onChange(of: appRouter.selectedTab) { selectedTab in
|
||
loadedTabs.insert(selectedTab)
|
||
guard selectedTab == .orders else { return }
|
||
Task { await refreshOrderBadge() }
|
||
}
|
||
.onChange(of: accountContext.currentScenic?.id) { _ in
|
||
Task { await refreshOrderBadge() }
|
||
}
|
||
.onChange(of: accountContext.currentStore?.id) { _ in
|
||
Task { await refreshOrderBadge() }
|
||
}
|
||
}
|
||
|
||
/// 刷新订单 Tab 待核销角标数量。
|
||
private func refreshOrderBadge() async {
|
||
await badgeViewModel.refreshPendingWriteOffCount(
|
||
api: ordersAPI,
|
||
scenicId: accountContext.currentScenic?.id,
|
||
storeId: accountContext.currentStore?.id
|
||
)
|
||
}
|
||
}
|
||
|
||
/// 自定义 TabBar 模式下的单个 Tab 导航栈,TabBar 只属于根页面内容。
|
||
private struct CustomTabNavigationStackHost: View {
|
||
@EnvironmentObject private var appRouter: AppRouter
|
||
|
||
let tab: AppTab
|
||
@Binding var selectedTab: AppTab
|
||
let orderBadgeText: String?
|
||
let onScanTap: () -> Void
|
||
|
||
var body: some View {
|
||
NavigationStack(path: appRouter.binding(for: tab)) {
|
||
VStack(spacing: 0) {
|
||
tab.rootView
|
||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||
|
||
CustomMainTabBar(
|
||
selectedTab: $selectedTab,
|
||
orderBadgeText: orderBadgeText,
|
||
onScanTap: onScanTap
|
||
)
|
||
}
|
||
.ignoresSafeArea(.keyboard, edges: .bottom)
|
||
.navigationTitle(tab.title)
|
||
.navigationDestination(for: AppRoute.self) { route in
|
||
route.destinationView
|
||
.toolbar(route.hidesTabBarWhenPushed ? .hidden : .visible, for: .tabBar)
|
||
}
|
||
}
|
||
.environmentObject(appRouter.router(for: tab))
|
||
}
|
||
}
|
||
|
||
/// 单个 Tab 对应的导航栈,供系统和自定义 TabBar 外壳共同复用。
|
||
private struct TabNavigationStackHost: View {
|
||
@EnvironmentObject private var appRouter: AppRouter
|
||
|
||
let tab: AppTab
|
||
|
||
var body: some View {
|
||
NavigationStack(path: appRouter.binding(for: tab)) {
|
||
tab.rootView
|
||
.navigationTitle(tab.title)
|
||
.navigationDestination(for: AppRoute.self) { route in
|
||
route.destinationView
|
||
.toolbar(route.hidesTabBarWhenPushed ? .hidden : .visible, for: .tabBar)
|
||
}
|
||
}
|
||
.environmentObject(appRouter.router(for: tab))
|
||
}
|
||
}
|
||
|
||
/// 自定义 TabBar 中保持已加载 Tab 页面存活的容器。
|
||
private struct PreservedTabPage<Content: View>: View {
|
||
let tab: AppTab
|
||
let selectedTab: AppTab
|
||
let isLoaded: Bool
|
||
@ViewBuilder let content: () -> Content
|
||
|
||
var body: some View {
|
||
if isLoaded {
|
||
content()
|
||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||
.opacity(selectedTab == tab ? 1 : 0)
|
||
.zIndex(selectedTab == tab ? 1 : 0)
|
||
.allowsHitTesting(selectedTab == tab)
|
||
.accessibilityHidden(selectedTab != tab)
|
||
}
|
||
}
|
||
}
|
||
|
||
#Preview {
|
||
MainTabsView()
|
||
.environmentObject(AppRouter())
|
||
.environmentObject(AccountContext())
|
||
.environment(\.ordersAPI, OrdersAPI(client: APIClient()))
|
||
.environmentObject(ToastCenter())
|
||
}
|