diff --git a/suixinkan_ios/App/App.md b/suixinkan_ios/App/App.md index f63ba37..d45e78e 100644 --- a/suixinkan_ios/App/App.md +++ b/suixinkan_ios/App/App.md @@ -112,4 +112,6 @@ Toast 展示为顶部全宽横幅,背景使用不透明主色并延伸到屏 Tab 根页面显示底部 TabBar。通过 `AppRoute` push 到子页面时,会根据 `AppRoute.hidesTabBarWhenPushed` 统一隐藏 TabBar。 +首页根页面使用自定义顶部栏,因此 `TabNavigationController` 会在展示 `HomeViewController` 时隐藏系统导航栏;push 到其他业务子页面或从子页面返回时,由导航栈统一按栈顶页面恢复系统导航栏显隐,避免首页隐藏状态影响子页面标题栏和返回按钮。 + 跨 Tab、推送通知、UI Test 直达和全局扫码都通过 `AppNavigator` 处理。`AppNavigationPolicy.currentStack` 沿当前栈 push,`.tabRoot` 切到目标 Tab 并先回到 root,`.preserveTab` 只切换 Tab 并保留原栈。新增页面时优先扩展 `AppRoute` / `HomeRoute` / `ProfileRoute` / `OrdersRoute`,并在 `AppRouteViewControllerFactory` 注册对应 ViewController。 diff --git a/suixinkan_ios/App/Navigation/TabNavigationController.swift b/suixinkan_ios/App/Navigation/TabNavigationController.swift index 96772e6..e2f289a 100644 --- a/suixinkan_ios/App/Navigation/TabNavigationController.swift +++ b/suixinkan_ios/App/Navigation/TabNavigationController.swift @@ -17,6 +17,7 @@ final class TabNavigationController: UINavigationController { self.appTab = tab self.services = services super.init(nibName: nil, bundle: nil) + delegate = self navigationBar.prefersLargeTitles = false setViewControllers([tab.makeRootViewController(services: services)], animated: false) } @@ -32,4 +33,66 @@ final class TabNavigationController: UINavigationController { viewController.hidesBottomBarWhenPushed = route.hidesTabBarWhenPushed pushViewController(viewController, animated: animated) } + + /// Push 页面时按目标页同步系统导航栏状态,覆盖各页面直接调用系统 push 的场景。 + override func pushViewController(_ viewController: UIViewController, animated: Bool) { + updateNavigationBarVisibility(for: viewController, animated: animated) + super.pushViewController(viewController, animated: animated) + } + + /// 设置导航栈时按最终栈顶同步系统导航栏状态。 + override func setViewControllers(_ viewControllers: [UIViewController], animated: Bool) { + if let topViewController = viewControllers.last { + updateNavigationBarVisibility(for: topViewController, animated: animated) + } + super.setViewControllers(viewControllers, animated: animated) + } + + /// 弹出页面后按新的栈顶同步系统导航栏状态。 + override func popViewController(animated: Bool) -> UIViewController? { + let poppedViewController = super.popViewController(animated: animated) + if !animated, let topViewController { + updateNavigationBarVisibility(for: topViewController, animated: false) + } + return poppedViewController + } + + /// 回到根页面后按根页面同步系统导航栏状态。 + override func popToRootViewController(animated: Bool) -> [UIViewController]? { + let poppedViewControllers = super.popToRootViewController(animated: animated) + if !animated, let topViewController { + updateNavigationBarVisibility(for: topViewController, animated: false) + } + return poppedViewControllers + } + + /// 弹到指定页面后按该页面同步系统导航栏状态。 + override func popToViewController(_ viewController: UIViewController, animated: Bool) -> [UIViewController]? { + let poppedViewControllers = super.popToViewController(viewController, animated: animated) + if !animated, let topViewController { + updateNavigationBarVisibility(for: topViewController, animated: false) + } + return poppedViewControllers + } + + /// 根据页面类型判断系统导航栏是否应隐藏。 + private func shouldHideNavigationBar(for viewController: UIViewController) -> Bool { + viewController is HomeViewController + } + + /// 将系统导航栏显示状态同步到目标页面,避免首页隐藏状态泄漏到子页面。 + private func updateNavigationBarVisibility(for viewController: UIViewController, animated: Bool) { + setNavigationBarHidden(shouldHideNavigationBar(for: viewController), animated: animated) + } +} + +extension TabNavigationController: UINavigationControllerDelegate { + /// 导航转场即将展示页面时更新系统导航栏显隐状态。 + func navigationController( + _ navigationController: UINavigationController, + willShow viewController: UIViewController, + animated: Bool + ) { + updateNavigationBarVisibility(for: viewController, animated: animated) + } } diff --git a/suixinkan_ios/Features/Main/Views/MainScanTabItem.swift b/suixinkan_ios/Features/Main/Views/MainScanTabItem.swift index 84b2905..5f460c9 100644 --- a/suixinkan_ios/Features/Main/Views/MainScanTabItem.swift +++ b/suixinkan_ios/Features/Main/Views/MainScanTabItem.swift @@ -13,7 +13,7 @@ enum MainScanTabItem { static func makeTabBarItem() -> UITabBarItem { let image = UIImage(named: "icon_scan")?.withRenderingMode(.alwaysOriginal) let selectedImage = UIImage(named: "icon_scan")?.withRenderingMode(.alwaysOriginal) - let item = UITabBarItem(title: "扫码", image: image, selectedImage: selectedImage) + let item = UITabBarItem(title: nil, image: image, selectedImage: selectedImage) item.accessibilityLabel = "扫码核销" item.accessibilityIdentifier = "main.scan" return item diff --git a/suixinkan_iosTests/MainScanTabItemTests.swift b/suixinkan_iosTests/MainScanTabItemTests.swift index c58d07d..0bac426 100644 --- a/suixinkan_iosTests/MainScanTabItemTests.swift +++ b/suixinkan_iosTests/MainScanTabItemTests.swift @@ -35,7 +35,7 @@ final class MainScanTabItemTests: XCTestCase { controller.loadViewIfNeeded() let scanItem = try XCTUnwrap(controller.viewControllers?[2].tabBarItem) - XCTAssertEqual(scanItem.title, "扫码") + XCTAssertNil(scanItem.title) XCTAssertEqual(scanItem.accessibilityIdentifier, "main.scan") XCTAssertEqual(scanItem.accessibilityLabel, "扫码核销") XCTAssertNotNil(scanItem.image) diff --git a/suixinkan_iosTests/NavigationRouterTests.swift b/suixinkan_iosTests/NavigationRouterTests.swift index a237ad9..9782cee 100644 --- a/suixinkan_iosTests/NavigationRouterTests.swift +++ b/suixinkan_iosTests/NavigationRouterTests.swift @@ -27,6 +27,20 @@ final class AppNavigatorTests: XCTestCase { XCTAssertTrue(homeStack.viewControllers.last is PaymentCollectionViewController) } + /// 测试首页隐藏系统导航栏,push 子页面后显示,返回首页后重新隐藏。 + func testHomeNavigationBarVisibilityFollowsTopViewController() { + let services = AppServices(apiClient: APIClient(session: AppNavigatorRecordingURLSession())) + let navigationController = TabNavigationController(tab: .home, services: services) + + XCTAssertTrue(navigationController.isNavigationBarHidden) + + navigationController.push(route: .home(.paymentCollection), animated: false) + XCTAssertFalse(navigationController.isNavigationBarHidden) + + navigationController.popToRootViewController(animated: false) + XCTAssertTrue(navigationController.isNavigationBarHidden) + } + /// 测试 tabRoot 会切换到指定 Tab、清空旧栈并打开目标页。 func testTabRootSelectsTabResetsStackAndPushesRoute() { let services = AppServices(apiClient: APIClient(session: AppNavigatorRecordingURLSession()))