Replace CYLTabBarController with native UITabBarController for main tabs.
Use a system scan tab slot with MainScanTabItem interception so scanner presentation no longer depends on CYL PlusButton, and remove the CYLTabBarController pod dependency. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -4,7 +4,7 @@
|
||||
|
||||
Main 模块负责登录后的主界面 Tab 容器。
|
||||
|
||||
主界面由 `MainTabBarController` 承载,使用 **CYLTabBarController** 展示首页、订单、数据、我的四个 Tab,并在中间提供扫码核销按钮。每个 Tab 内部包裹独立的 `TabNavigationController`,页面路径以 UIKit 导航栈为唯一事实源。
|
||||
主界面由 `MainTabBarController` 承载,使用系统 `UITabBarController` 展示首页、订单、扫码、数据、我的五个底部入口。首页、订单、数据、我的四个业务 Tab 内部包裹独立的 `TabNavigationController`,页面路径以 UIKit 导航栈为唯一事实源;中间扫码入口只触发全局扫码,不切换选中页面。
|
||||
|
||||
## Tab 结构
|
||||
|
||||
@ -15,6 +15,7 @@ Main 模块负责登录后的主界面 Tab 容器。
|
||||
- `profile`:我的
|
||||
|
||||
各 Tab 根页面已接入真实业务页面:`HomeViewController`、`OrdersViewController`、`StatisticsViewController`、`ProfileViewController`。
|
||||
系统 TabBar 中间额外插入扫码槽位,显示 `MainScanTabItem` 构建的扫码 item。
|
||||
|
||||
## 导航流程
|
||||
|
||||
@ -32,7 +33,7 @@ Main 模块负责登录后的主界面 Tab 容器。
|
||||
|
||||
## 扫码核销
|
||||
|
||||
TabBar 中间通过 `MainScanPlusButton`(CYLTabBarController PlusButton)展示全局扫码入口,点击后全屏打开 `OrderCodeScannerViewController`:
|
||||
TabBar 中间通过 `MainScanTabItem` 展示全局扫码入口,`MainTabBarController` 在 `shouldSelect` 中拦截点击并全屏打开 `OrderCodeScannerViewController`,因此关闭扫码页后仍显示点击前的页面:
|
||||
|
||||
1. 扫码成功 → `AppNavigator.openOrdersEntry(.verificationOrders, scannedCode:)` 切换至订单核销入口并传入扫码结果。
|
||||
2. `OrdersViewController` 本地保存并消费扫码结果后进入确认核销流程。
|
||||
|
||||
@ -3,26 +3,60 @@
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import CYLTabBarController
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
/// 主 Tab 容器,使用 CYLTabBarController 展示四个一级入口及中间扫码按钮。
|
||||
final class MainTabBarController: CYLTabBarController {
|
||||
/// 主 Tab 容器,使用系统 TabBar 展示四个一级入口及中间扫码按钮。
|
||||
final class MainTabBarController: UITabBarController {
|
||||
|
||||
private let services: AppServices
|
||||
private let badgeViewModel = MainTabBadgeViewModel()
|
||||
private var tabNavigationControllers: [AppTab: TabNavigationController] = [:]
|
||||
private var isSyncingTabSelection = false
|
||||
|
||||
/// 测试时可注入扫码点击处理,生产环境为空时执行真实扫码展示。
|
||||
var onScanTabSelected: (() -> Void)?
|
||||
|
||||
/// 主 TabBar 固定槽位,扫码槽位只负责触发全局扫码,不参与业务路由。
|
||||
private enum TabSlot: CaseIterable {
|
||||
case home
|
||||
case orders
|
||||
case scan
|
||||
case statistics
|
||||
case profile
|
||||
|
||||
/// 返回槽位对应的业务 Tab,扫码槽位无业务根页面。
|
||||
var appTab: AppTab? {
|
||||
switch self {
|
||||
case .home:
|
||||
return .home
|
||||
case .orders:
|
||||
return .orders
|
||||
case .scan:
|
||||
return nil
|
||||
case .statistics:
|
||||
return .statistics
|
||||
case .profile:
|
||||
return .profile
|
||||
}
|
||||
}
|
||||
|
||||
/// 返回指定业务 Tab 在系统 TabBar 中的槽位索引。
|
||||
static func index(for tab: AppTab) -> Int? {
|
||||
allCases.firstIndex { $0.appTab == tab }
|
||||
}
|
||||
|
||||
/// 返回扫码槽位在系统 TabBar 中的索引。
|
||||
static var scanIndex: Int? {
|
||||
allCases.firstIndex(of: .scan)
|
||||
}
|
||||
}
|
||||
|
||||
/// 初始化实例。
|
||||
init(services: AppServices) {
|
||||
self.services = services
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
MainScanPlusButton.onScanTap = { [weak self] in
|
||||
self?.presentGlobalScanner()
|
||||
}
|
||||
// delegate = self
|
||||
delegate = self
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
@ -44,13 +78,6 @@ final class MainTabBarController: CYLTabBarController {
|
||||
#endif
|
||||
}
|
||||
|
||||
/// 控制器释放前清理全局扫码按钮回调。
|
||||
deinit {
|
||||
Task { @MainActor in
|
||||
MainScanPlusButton.onScanTap = nil
|
||||
}
|
||||
}
|
||||
|
||||
/// 配置 TabBar 外观,对齐参考工程 CustomMainTabBar 样式。
|
||||
private func configureTabBarAppearance() {
|
||||
tabBar.tintColor = AppDesign.primary
|
||||
@ -76,26 +103,22 @@ final class MainTabBarController: CYLTabBarController {
|
||||
tabBar.scrollEdgeAppearance = appearance
|
||||
}
|
||||
|
||||
/// 为每个 Tab 创建独立导航栈,并按 CYLTabBarController 要求先配置 Tab 属性再设置控制器。
|
||||
/// 为每个业务 Tab 创建独立导航栈,并在中间插入系统扫码槽位。
|
||||
private func configureTabs() {
|
||||
let tabs = AppTab.allCases
|
||||
tabBarItemsAttributes = tabs.map { makeTabBarItemAttributes(for: $0) }
|
||||
viewControllers = tabs.map { tab in
|
||||
let navigationController = TabNavigationController(tab: tab, services: services)
|
||||
tabNavigationControllers[tab] = navigationController
|
||||
return navigationController
|
||||
tabNavigationControllers.removeAll()
|
||||
viewControllers = TabSlot.allCases.map { slot in
|
||||
if let tab = slot.appTab {
|
||||
let navigationController = TabNavigationController(tab: tab, services: services)
|
||||
navigationController.tabBarItem = tab.makeTabBarItem()
|
||||
tabNavigationControllers[tab] = navigationController
|
||||
return navigationController
|
||||
}
|
||||
let scannerPlaceholder = UIViewController()
|
||||
scannerPlaceholder.tabBarItem = MainScanTabItem.makeTabBarItem()
|
||||
return scannerPlaceholder
|
||||
}
|
||||
}
|
||||
|
||||
/// 构建 CYLTabBarController 识别的 TabBarItem 属性字典。
|
||||
private func makeTabBarItemAttributes(for tab: AppTab) -> [String: String] {
|
||||
[
|
||||
CYLTabBarItemTitle: tab.title,
|
||||
CYLTabBarItemImage: tab.unselectedImageName,
|
||||
CYLTabBarItemSelectedImage: tab.selectedImageName
|
||||
]
|
||||
}
|
||||
|
||||
/// 绑定角标 ViewModel 回调。
|
||||
private func bindBadgeViewModel() {
|
||||
badgeViewModel.onChange = { [weak self] in
|
||||
@ -132,6 +155,8 @@ final class MainTabBarController: CYLTabBarController {
|
||||
|
||||
/// 从 TabBar 中间按钮打开全局扫码页,成功后路由至订单核销入口。
|
||||
private func presentGlobalScanner() {
|
||||
guard presentedViewController == nil else { return }
|
||||
|
||||
let scanner = OrderCodeScannerViewController()
|
||||
scanner.onScanResult = { [weak self, weak scanner] result in
|
||||
scanner?.dismiss(animated: true)
|
||||
@ -147,9 +172,25 @@ final class MainTabBarController: CYLTabBarController {
|
||||
navigationController.modalPresentationStyle = .fullScreen
|
||||
present(navigationController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension MainTabBarController: UITabBarControllerDelegate {
|
||||
/// 用户选择 TabBar 槽位前拦截扫码入口,使关闭扫码页后保留原页面。
|
||||
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
|
||||
guard viewControllers?.firstIndex(of: viewController) == TabSlot.scanIndex else {
|
||||
return true
|
||||
}
|
||||
|
||||
if let onScanTabSelected {
|
||||
onScanTabSelected()
|
||||
} else {
|
||||
presentGlobalScanner()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/// 用户点击 TabBar 时刷新订单角标。
|
||||
override func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
|
||||
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
|
||||
guard !isSyncingTabSelection,
|
||||
let navigationController = viewController as? TabNavigationController else { return }
|
||||
|
||||
@ -162,7 +203,7 @@ final class MainTabBarController: CYLTabBarController {
|
||||
extension MainTabBarController: AppNavigationHost {
|
||||
/// 选中指定主 Tab。
|
||||
func selectTab(_ tab: AppTab) {
|
||||
guard let index = AppTab.allCases.firstIndex(of: tab) else { return }
|
||||
guard let index = TabSlot.index(for: tab) else { return }
|
||||
guard selectedIndex != index else { return }
|
||||
isSyncingTabSelection = true
|
||||
selectedIndex = index
|
||||
|
||||
@ -1,87 +0,0 @@
|
||||
//
|
||||
// MainScanPlusButton.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import CYLTabBarController
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
@objcMembers
|
||||
/// 主 TabBar 中间扫码核销按钮,基于 CYLTabBarController 的 PlusButton 机制实现。
|
||||
final class MainScanPlusButton: CYLPlusButton, CYLPlusButtonSubclassing {
|
||||
|
||||
/// 点击扫码按钮时的回调,由 `MainTabBarController` 注入。
|
||||
static var onScanTap: (() -> Void)?
|
||||
|
||||
/// 构建中间扫码按钮实例,供 CYLTabBarController 注册使用。
|
||||
static func plusButton() -> Any {
|
||||
let button = MainScanPlusButton()
|
||||
button.setImage(UIImage(named: "icon_scan"), for: .normal)
|
||||
button.titleLabel?.textAlignment = .center
|
||||
|
||||
button.titleLabel?.font = UIFont.systemFont(ofSize: 10)
|
||||
|
||||
button.setTitle("发布", for: .normal)
|
||||
button.setTitleColor(UIColor.gray, for: .normal)
|
||||
|
||||
button.setTitle("选中", for: .selected)
|
||||
button.setTitleColor(UIColor(red:255.0/255.0,green:102.0/255.0,blue:0,alpha:1.0), for: .selected)
|
||||
|
||||
button.adjustsImageWhenHighlighted = false
|
||||
button.sizeToFit()
|
||||
return button
|
||||
}
|
||||
|
||||
/// 指定扫码按钮在 TabBar 中的视觉槽位(位于订单与数据之间)。
|
||||
static func indexOfPlusButtonInTabBar() -> UInt {
|
||||
2
|
||||
}
|
||||
|
||||
/// 调整扫码按钮垂直位置,使其略微凸出 TabBar 顶部。
|
||||
static func constantOfPlusButtonCenterYOffset(forTabBarHeight tabBarHeight: CGFloat) -> CGFloat {
|
||||
-6
|
||||
}
|
||||
|
||||
/// 配置圆形背景与扫码图标,对齐参考工程 CustomMainTabBarCenterAction。
|
||||
private func configureAppearance() {
|
||||
backgroundColor = .clear
|
||||
|
||||
let circleSize: CGFloat = 58
|
||||
let circleView = UIView()
|
||||
circleView.isUserInteractionEnabled = false
|
||||
circleView.backgroundColor = AppDesign.primary
|
||||
circleView.layer.cornerRadius = circleSize / 2
|
||||
addSubview(circleView)
|
||||
|
||||
let symbolConfig = UIImage.SymbolConfiguration(pointSize: 30, weight: .bold)
|
||||
let iconView = UIImageView(
|
||||
image: UIImage(named: "icon_scan")
|
||||
)
|
||||
iconView.tintColor = .white
|
||||
iconView.contentMode = .scaleAspectFit
|
||||
iconView.isUserInteractionEnabled = false
|
||||
addSubview(iconView)
|
||||
|
||||
circleView.translatesAutoresizingMaskIntoConstraints = false
|
||||
iconView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
circleView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
circleView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
circleView.widthAnchor.constraint(equalToConstant: circleSize),
|
||||
circleView.heightAnchor.constraint(equalToConstant: circleSize),
|
||||
iconView.centerXAnchor.constraint(equalTo: circleView.centerXAnchor),
|
||||
iconView.centerYAnchor.constraint(equalTo: circleView.centerYAnchor),
|
||||
iconView.widthAnchor.constraint(equalToConstant: 30),
|
||||
iconView.heightAnchor.constraint(equalToConstant: 30)
|
||||
])
|
||||
|
||||
accessibilityLabel = "扫码核销"
|
||||
accessibilityIdentifier = "main.scan"
|
||||
}
|
||||
|
||||
/// 响应用户点击,触发全局扫码流程。
|
||||
@objc private func handleScanTap() {
|
||||
Self.onScanTap?()
|
||||
}
|
||||
}
|
||||
21
suixinkan_ios/Features/Main/Views/MainScanTabItem.swift
Normal file
21
suixinkan_ios/Features/Main/Views/MainScanTabItem.swift
Normal file
@ -0,0 +1,21 @@
|
||||
//
|
||||
// MainScanTabItem.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
/// 主 TabBar 中间扫码核销入口,负责构建系统 TabBarItem。
|
||||
enum MainScanTabItem {
|
||||
|
||||
/// 构建系统扫码 TabBarItem,点击行为由 `MainTabBarController` 拦截处理。
|
||||
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)
|
||||
item.accessibilityLabel = "扫码核销"
|
||||
item.accessibilityIdentifier = "main.scan"
|
||||
return item
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user