Files
suixinkan_ios_new/suixinkan/Features/Main/Views/MainTabsView.swift
汉秋 703078352c 从 iOS 17 Observation 迁移至 iOS 16 兼容的 Combine 架构
将最低部署版本降至 iOS 16,以 ObservableObject 替换 @Observable,新增导航与 UI 兼容层,并补充登录冒烟 UI 测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 10:16:35 +08:00

201 lines
6.5 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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())
}