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:
3
Podfile
3
Podfile
@ -5,7 +5,6 @@ target 'suixinkan_ios' do
|
|||||||
|
|
||||||
pod 'SnapKit', '~> 5.7'
|
pod 'SnapKit', '~> 5.7'
|
||||||
pod 'Kingfisher', '~> 8.0'
|
pod 'Kingfisher', '~> 8.0'
|
||||||
pod 'CYLTabBarController', '~> 1.99.38'
|
|
||||||
|
|
||||||
# 高德地图 SDK 仅支持真机;模拟器通过下方 post_install 剥离链接
|
# 高德地图 SDK 仅支持真机;模拟器通过下方 post_install 剥离链接
|
||||||
pod 'AMap3DMap-NO-IDFA', '~> 11.1'
|
pod 'AMap3DMap-NO-IDFA', '~> 11.1'
|
||||||
@ -20,7 +19,7 @@ end
|
|||||||
# 模拟器构建时不链接高德(真机专用 framework),仅保留通用 Pod 依赖
|
# 模拟器构建时不链接高德(真机专用 framework),仅保留通用 Pod 依赖
|
||||||
SIMULATOR_OTHER_LDFLAGS = <<~FLAGS.squish
|
SIMULATOR_OTHER_LDFLAGS = <<~FLAGS.squish
|
||||||
-ObjC -l"c++" -l"swiftCoreGraphics" -l"z"
|
-ObjC -l"c++" -l"swiftCoreGraphics" -l"z"
|
||||||
-framework "Accelerate" -framework "CFNetwork" -framework "CYLTabBarController"
|
-framework "Accelerate" -framework "CFNetwork"
|
||||||
-framework "Kingfisher" -framework "SnapKit"
|
-framework "Kingfisher" -framework "SnapKit"
|
||||||
-framework "CoreGraphics" -framework "CoreLocation" -framework "CoreTelephony"
|
-framework "CoreGraphics" -framework "CoreLocation" -framework "CoreTelephony"
|
||||||
-framework "CoreText" -framework "ExternalAccessory" -framework "GLKit"
|
-framework "CoreText" -framework "ExternalAccessory" -framework "GLKit"
|
||||||
|
|||||||
@ -6,9 +6,6 @@ PODS:
|
|||||||
- AMapFoundation-NO-IDFA (>= 1.9.0)
|
- AMapFoundation-NO-IDFA (>= 1.9.0)
|
||||||
- AMapSearch-NO-IDFA (9.8.0):
|
- AMapSearch-NO-IDFA (9.8.0):
|
||||||
- AMapFoundation-NO-IDFA (>= 1.9.0)
|
- AMapFoundation-NO-IDFA (>= 1.9.0)
|
||||||
- CYLTabBarController (1.99.38):
|
|
||||||
- CYLTabBarController/Core (= 1.99.38)
|
|
||||||
- CYLTabBarController/Core (1.99.38)
|
|
||||||
- Kingfisher (8.10.0)
|
- Kingfisher (8.10.0)
|
||||||
- SnapKit (5.7.1)
|
- SnapKit (5.7.1)
|
||||||
|
|
||||||
@ -16,7 +13,6 @@ DEPENDENCIES:
|
|||||||
- AMap3DMap-NO-IDFA (~> 11.1)
|
- AMap3DMap-NO-IDFA (~> 11.1)
|
||||||
- AMapLocation-NO-IDFA (~> 2.11)
|
- AMapLocation-NO-IDFA (~> 2.11)
|
||||||
- AMapSearch-NO-IDFA (~> 9.7)
|
- AMapSearch-NO-IDFA (~> 9.7)
|
||||||
- CYLTabBarController (~> 1.99.38)
|
|
||||||
- Kingfisher (~> 8.0)
|
- Kingfisher (~> 8.0)
|
||||||
- SnapKit (~> 5.7)
|
- SnapKit (~> 5.7)
|
||||||
|
|
||||||
@ -26,7 +22,6 @@ SPEC REPOS:
|
|||||||
- AMapFoundation-NO-IDFA
|
- AMapFoundation-NO-IDFA
|
||||||
- AMapLocation-NO-IDFA
|
- AMapLocation-NO-IDFA
|
||||||
- AMapSearch-NO-IDFA
|
- AMapSearch-NO-IDFA
|
||||||
- CYLTabBarController
|
|
||||||
- Kingfisher
|
- Kingfisher
|
||||||
- SnapKit
|
- SnapKit
|
||||||
|
|
||||||
@ -35,10 +30,9 @@ SPEC CHECKSUMS:
|
|||||||
AMapFoundation-NO-IDFA: a2e3c895398d7ee757278e1a0a8f9359da4b146e
|
AMapFoundation-NO-IDFA: a2e3c895398d7ee757278e1a0a8f9359da4b146e
|
||||||
AMapLocation-NO-IDFA: 7cd8fc837ea41edfbf4d937cd20572e277b77d18
|
AMapLocation-NO-IDFA: 7cd8fc837ea41edfbf4d937cd20572e277b77d18
|
||||||
AMapSearch-NO-IDFA: c0afd2a69a076d4228becda4401dbe4a279a03ef
|
AMapSearch-NO-IDFA: c0afd2a69a076d4228becda4401dbe4a279a03ef
|
||||||
CYLTabBarController: 8d46d501757c00e36499ea6b95e86e492fab3a61
|
|
||||||
Kingfisher: db468f911dd666c9134dcbeec4db8bed52e4132d
|
Kingfisher: db468f911dd666c9134dcbeec4db8bed52e4132d
|
||||||
SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a
|
SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a
|
||||||
|
|
||||||
PODFILE CHECKSUM: 9659fc63d0173f0286a7da18725e548d86c24ca6
|
PODFILE CHECKSUM: 0755512570f68a2459c7ca54e87c58e5a09f215a
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
@ -66,6 +66,7 @@ enum AppTab: String, CaseIterable, Identifiable, Hashable {
|
|||||||
image: unselectedImage,
|
image: unselectedImage,
|
||||||
selectedImage: selectedImage
|
selectedImage: selectedImage
|
||||||
)
|
)
|
||||||
|
item.accessibilityIdentifier = "main.tab.\(rawValue)"
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -106,7 +106,7 @@ final class RootViewController: UIViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 登录后将 `MainTabBarController` 设为 window 根控制器,满足 CYLTabBarController 挂载要求。
|
/// 登录后将 `MainTabBarController` 设为 window 根控制器。
|
||||||
private func showMainTabBarAsWindowRoot() {
|
private func showMainTabBarAsWindowRoot() {
|
||||||
removeCurrentAuthChild()
|
removeCurrentAuthChild()
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
Main 模块负责登录后的主界面 Tab 容器。
|
Main 模块负责登录后的主界面 Tab 容器。
|
||||||
|
|
||||||
主界面由 `MainTabBarController` 承载,使用 **CYLTabBarController** 展示首页、订单、数据、我的四个 Tab,并在中间提供扫码核销按钮。每个 Tab 内部包裹独立的 `TabNavigationController`,页面路径以 UIKit 导航栈为唯一事实源。
|
主界面由 `MainTabBarController` 承载,使用系统 `UITabBarController` 展示首页、订单、扫码、数据、我的五个底部入口。首页、订单、数据、我的四个业务 Tab 内部包裹独立的 `TabNavigationController`,页面路径以 UIKit 导航栈为唯一事实源;中间扫码入口只触发全局扫码,不切换选中页面。
|
||||||
|
|
||||||
## Tab 结构
|
## Tab 结构
|
||||||
|
|
||||||
@ -15,6 +15,7 @@ Main 模块负责登录后的主界面 Tab 容器。
|
|||||||
- `profile`:我的
|
- `profile`:我的
|
||||||
|
|
||||||
各 Tab 根页面已接入真实业务页面:`HomeViewController`、`OrdersViewController`、`StatisticsViewController`、`ProfileViewController`。
|
各 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:)` 切换至订单核销入口并传入扫码结果。
|
1. 扫码成功 → `AppNavigator.openOrdersEntry(.verificationOrders, scannedCode:)` 切换至订单核销入口并传入扫码结果。
|
||||||
2. `OrdersViewController` 本地保存并消费扫码结果后进入确认核销流程。
|
2. `OrdersViewController` 本地保存并消费扫码结果后进入确认核销流程。
|
||||||
|
|||||||
@ -3,26 +3,60 @@
|
|||||||
// suixinkan
|
// suixinkan
|
||||||
//
|
//
|
||||||
|
|
||||||
import CYLTabBarController
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
/// 主 Tab 容器,使用 CYLTabBarController 展示四个一级入口及中间扫码按钮。
|
/// 主 Tab 容器,使用系统 TabBar 展示四个一级入口及中间扫码按钮。
|
||||||
final class MainTabBarController: CYLTabBarController {
|
final class MainTabBarController: UITabBarController {
|
||||||
|
|
||||||
private let services: AppServices
|
private let services: AppServices
|
||||||
private let badgeViewModel = MainTabBadgeViewModel()
|
private let badgeViewModel = MainTabBadgeViewModel()
|
||||||
private var tabNavigationControllers: [AppTab: TabNavigationController] = [:]
|
private var tabNavigationControllers: [AppTab: TabNavigationController] = [:]
|
||||||
private var isSyncingTabSelection = false
|
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) {
|
init(services: AppServices) {
|
||||||
self.services = services
|
self.services = services
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
MainScanPlusButton.onScanTap = { [weak self] in
|
delegate = self
|
||||||
self?.presentGlobalScanner()
|
|
||||||
}
|
|
||||||
// delegate = self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(*, unavailable)
|
@available(*, unavailable)
|
||||||
@ -44,13 +78,6 @@ final class MainTabBarController: CYLTabBarController {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 控制器释放前清理全局扫码按钮回调。
|
|
||||||
deinit {
|
|
||||||
Task { @MainActor in
|
|
||||||
MainScanPlusButton.onScanTap = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 配置 TabBar 外观,对齐参考工程 CustomMainTabBar 样式。
|
/// 配置 TabBar 外观,对齐参考工程 CustomMainTabBar 样式。
|
||||||
private func configureTabBarAppearance() {
|
private func configureTabBarAppearance() {
|
||||||
tabBar.tintColor = AppDesign.primary
|
tabBar.tintColor = AppDesign.primary
|
||||||
@ -76,24 +103,20 @@ final class MainTabBarController: CYLTabBarController {
|
|||||||
tabBar.scrollEdgeAppearance = appearance
|
tabBar.scrollEdgeAppearance = appearance
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 为每个 Tab 创建独立导航栈,并按 CYLTabBarController 要求先配置 Tab 属性再设置控制器。
|
/// 为每个业务 Tab 创建独立导航栈,并在中间插入系统扫码槽位。
|
||||||
private func configureTabs() {
|
private func configureTabs() {
|
||||||
let tabs = AppTab.allCases
|
tabNavigationControllers.removeAll()
|
||||||
tabBarItemsAttributes = tabs.map { makeTabBarItemAttributes(for: $0) }
|
viewControllers = TabSlot.allCases.map { slot in
|
||||||
viewControllers = tabs.map { tab in
|
if let tab = slot.appTab {
|
||||||
let navigationController = TabNavigationController(tab: tab, services: services)
|
let navigationController = TabNavigationController(tab: tab, services: services)
|
||||||
|
navigationController.tabBarItem = tab.makeTabBarItem()
|
||||||
tabNavigationControllers[tab] = navigationController
|
tabNavigationControllers[tab] = navigationController
|
||||||
return 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 回调。
|
/// 绑定角标 ViewModel 回调。
|
||||||
@ -132,6 +155,8 @@ final class MainTabBarController: CYLTabBarController {
|
|||||||
|
|
||||||
/// 从 TabBar 中间按钮打开全局扫码页,成功后路由至订单核销入口。
|
/// 从 TabBar 中间按钮打开全局扫码页,成功后路由至订单核销入口。
|
||||||
private func presentGlobalScanner() {
|
private func presentGlobalScanner() {
|
||||||
|
guard presentedViewController == nil else { return }
|
||||||
|
|
||||||
let scanner = OrderCodeScannerViewController()
|
let scanner = OrderCodeScannerViewController()
|
||||||
scanner.onScanResult = { [weak self, weak scanner] result in
|
scanner.onScanResult = { [weak self, weak scanner] result in
|
||||||
scanner?.dismiss(animated: true)
|
scanner?.dismiss(animated: true)
|
||||||
@ -147,9 +172,25 @@ final class MainTabBarController: CYLTabBarController {
|
|||||||
navigationController.modalPresentationStyle = .fullScreen
|
navigationController.modalPresentationStyle = .fullScreen
|
||||||
present(navigationController, animated: true)
|
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 时刷新订单角标。
|
/// 用户点击 TabBar 时刷新订单角标。
|
||||||
override func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
|
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
|
||||||
guard !isSyncingTabSelection,
|
guard !isSyncingTabSelection,
|
||||||
let navigationController = viewController as? TabNavigationController else { return }
|
let navigationController = viewController as? TabNavigationController else { return }
|
||||||
|
|
||||||
@ -162,7 +203,7 @@ final class MainTabBarController: CYLTabBarController {
|
|||||||
extension MainTabBarController: AppNavigationHost {
|
extension MainTabBarController: AppNavigationHost {
|
||||||
/// 选中指定主 Tab。
|
/// 选中指定主 Tab。
|
||||||
func selectTab(_ tab: AppTab) {
|
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 }
|
guard selectedIndex != index else { return }
|
||||||
isSyncingTabSelection = true
|
isSyncingTabSelection = true
|
||||||
selectedIndex = index
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,7 +6,6 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import CYLTabBarController
|
|
||||||
|
|
||||||
/// 窗口 Scene 代理,负责创建根窗口并挂载会话协调器。
|
/// 窗口 Scene 代理,负责创建根窗口并挂载会话协调器。
|
||||||
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
@ -24,8 +23,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||||||
) {
|
) {
|
||||||
guard let windowScene = scene as? UIWindowScene else { return }
|
guard let windowScene = scene as? UIWindowScene else { return }
|
||||||
|
|
||||||
MainScanPlusButton.register()
|
|
||||||
|
|
||||||
AppUITestLaunchState.resetIfNeeded()
|
AppUITestLaunchState.resetIfNeeded()
|
||||||
|
|
||||||
let rootViewController = RootViewController()
|
let rootViewController = RootViewController()
|
||||||
|
|||||||
@ -1,73 +0,0 @@
|
|||||||
//
|
|
||||||
// MainScanPlusButtonTests.swift
|
|
||||||
// suixinkanTests
|
|
||||||
//
|
|
||||||
|
|
||||||
import XCTest
|
|
||||||
@testable import suixinkan_ios
|
|
||||||
|
|
||||||
/// 主 TabBar 中间扫码按钮测试,覆盖 CYL PlusButton 配置。
|
|
||||||
@MainActor
|
|
||||||
final class MainScanPlusButtonTests: XCTestCase {
|
|
||||||
|
|
||||||
/// 测试扫码按钮位于订单与数据 Tab 之间的视觉槽位。
|
|
||||||
func testIndexOfPlusButtonInTabBarIsBetweenOrdersAndStatistics() {
|
|
||||||
XCTAssertEqual(MainScanPlusButton.indexOfPlusButtonInTabBar(), 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 测试扫码按钮 accessibility 标识与参考工程一致。
|
|
||||||
func testPlusButtonAccessibilityIdentifier() {
|
|
||||||
guard let button = MainScanPlusButton.plusButton() as? UIButton else {
|
|
||||||
XCTFail("plusButton 应返回 UIButton 实例")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
XCTAssertEqual(button.accessibilityIdentifier, "main.scan")
|
|
||||||
XCTAssertEqual(button.accessibilityLabel, "扫码核销")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 测试主 Tab 容器加载时,CYL 控制器数组与 Tab 属性数组保持一致。
|
|
||||||
func testMainTabBarControllerLoadsWithMatchingCYLAttributes() {
|
|
||||||
let services = AppServices(apiClient: APIClient(session: MainTabRecordingURLSession()))
|
|
||||||
let controller = MainTabBarController(services: services)
|
|
||||||
|
|
||||||
controller.loadViewIfNeeded()
|
|
||||||
|
|
||||||
for tab in AppTab.allCases {
|
|
||||||
XCTAssertNotNil(controller.navigationController(for: tab))
|
|
||||||
XCTAssertEqual(
|
|
||||||
controller.navigationController(for: tab)?.tabBarItem.accessibilityIdentifier,
|
|
||||||
"main.tab.\(tab.rawValue)"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 测试各 Tab 的图标资源可被加载并挂载到 tabBarItem。
|
|
||||||
func testMainTabBarControllerTabBarItemsHaveImages() {
|
|
||||||
let services = AppServices(apiClient: APIClient(session: MainTabRecordingURLSession()))
|
|
||||||
let controller = MainTabBarController(services: services)
|
|
||||||
controller.loadViewIfNeeded()
|
|
||||||
|
|
||||||
for tab in AppTab.allCases {
|
|
||||||
let item = controller.navigationController(for: tab)?.tabBarItem
|
|
||||||
XCTAssertNotNil(item?.image, "tab \(tab.rawValue) image should not be nil")
|
|
||||||
XCTAssertNotNil(item?.selectedImage, "tab \(tab.rawValue) selectedImage should not be nil")
|
|
||||||
XCTAssertGreaterThan(item?.image?.size.width ?? 0, 0)
|
|
||||||
XCTAssertGreaterThan(item?.selectedImage?.size.width ?? 0, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 主 Tab 测试使用的空网络会话,确保容器加载不触发真实请求。
|
|
||||||
private final class MainTabRecordingURLSession: URLSessionProtocol {
|
|
||||||
/// 返回空成功响应。
|
|
||||||
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
|
|
||||||
let response = HTTPURLResponse(
|
|
||||||
url: request.url ?? URL(string: "https://example.com")!,
|
|
||||||
statusCode: 200,
|
|
||||||
httpVersion: nil,
|
|
||||||
headerFields: nil
|
|
||||||
)!
|
|
||||||
return (Data(), response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
115
suixinkan_iosTests/MainScanTabItemTests.swift
Normal file
115
suixinkan_iosTests/MainScanTabItemTests.swift
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
//
|
||||||
|
// MainScanTabItemTests.swift
|
||||||
|
// suixinkanTests
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
@testable import suixinkan_ios
|
||||||
|
|
||||||
|
/// 主 TabBar 中间扫码入口测试,覆盖系统 TabBar 槽位配置。
|
||||||
|
@MainActor
|
||||||
|
final class MainScanTabItemTests: XCTestCase {
|
||||||
|
|
||||||
|
/// 测试主 Tab 容器加载后包含四个业务 Tab 和中间扫码入口。
|
||||||
|
func testMainTabBarControllerLoadsFiveSystemTabItems() {
|
||||||
|
let services = AppServices(apiClient: APIClient(session: MainTabRecordingURLSession()))
|
||||||
|
let controller = MainTabBarController(services: services)
|
||||||
|
|
||||||
|
controller.loadViewIfNeeded()
|
||||||
|
|
||||||
|
XCTAssertEqual(controller.viewControllers?.count, 5)
|
||||||
|
for tab in AppTab.allCases {
|
||||||
|
XCTAssertNotNil(controller.navigationController(for: tab))
|
||||||
|
XCTAssertEqual(
|
||||||
|
controller.navigationController(for: tab)?.tabBarItem.accessibilityIdentifier,
|
||||||
|
"main.tab.\(tab.rawValue)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试扫码入口位于订单与数据之间,并保留指定无障碍标识。
|
||||||
|
func testScanTabItemIsCenteredBetweenOrdersAndStatistics() throws {
|
||||||
|
let services = AppServices(apiClient: APIClient(session: MainTabRecordingURLSession()))
|
||||||
|
let controller = MainTabBarController(services: services)
|
||||||
|
|
||||||
|
controller.loadViewIfNeeded()
|
||||||
|
|
||||||
|
let scanItem = try XCTUnwrap(controller.viewControllers?[2].tabBarItem)
|
||||||
|
XCTAssertEqual(scanItem.title, "扫码")
|
||||||
|
XCTAssertEqual(scanItem.accessibilityIdentifier, "main.scan")
|
||||||
|
XCTAssertEqual(scanItem.accessibilityLabel, "扫码核销")
|
||||||
|
XCTAssertNotNil(scanItem.image)
|
||||||
|
XCTAssertNotNil(scanItem.selectedImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试点击扫码槽位会被拦截且不改变当前选中页面。
|
||||||
|
func testSelectingScanTabPresentsScannerWithoutSelectingPlaceholder() throws {
|
||||||
|
let services = AppServices(apiClient: APIClient(session: MainTabRecordingURLSession()))
|
||||||
|
let controller = MainTabBarController(services: services)
|
||||||
|
var scanTapCount = 0
|
||||||
|
controller.onScanTabSelected = {
|
||||||
|
scanTapCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.loadViewIfNeeded()
|
||||||
|
controller.selectedIndex = 1
|
||||||
|
|
||||||
|
let scanController = try XCTUnwrap(controller.viewControllers?[2])
|
||||||
|
XCTAssertFalse(controller.tabBarController(controller, shouldSelect: scanController))
|
||||||
|
XCTAssertEqual(scanTapCount, 1)
|
||||||
|
XCTAssertEqual(controller.selectedIndex, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试业务 Tab 仍允许系统 TabBar 正常选中。
|
||||||
|
func testSelectingBusinessTabIsAllowed() throws {
|
||||||
|
let services = AppServices(apiClient: APIClient(session: MainTabRecordingURLSession()))
|
||||||
|
let controller = MainTabBarController(services: services)
|
||||||
|
|
||||||
|
controller.loadViewIfNeeded()
|
||||||
|
|
||||||
|
let ordersController = try XCTUnwrap(controller.viewControllers?[1])
|
||||||
|
XCTAssertTrue(controller.tabBarController(controller, shouldSelect: ordersController))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试各 Tab 的图标资源可被加载并挂载到 tabBarItem。
|
||||||
|
func testMainTabBarControllerTabBarItemsHaveImages() {
|
||||||
|
let services = AppServices(apiClient: APIClient(session: MainTabRecordingURLSession()))
|
||||||
|
let controller = MainTabBarController(services: services)
|
||||||
|
controller.loadViewIfNeeded()
|
||||||
|
|
||||||
|
for tab in AppTab.allCases {
|
||||||
|
let item = controller.navigationController(for: tab)?.tabBarItem
|
||||||
|
XCTAssertNotNil(item?.image, "tab \(tab.rawValue) image should not be nil")
|
||||||
|
XCTAssertNotNil(item?.selectedImage, "tab \(tab.rawValue) selectedImage should not be nil")
|
||||||
|
XCTAssertGreaterThan(item?.image?.size.width ?? 0, 0)
|
||||||
|
XCTAssertGreaterThan(item?.selectedImage?.size.width ?? 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试订单角标仍挂载在订单业务导航控制器的系统 TabBarItem 上。
|
||||||
|
func testOrderBadgeUsesOrdersNavigationTabBarItem() throws {
|
||||||
|
let services = AppServices(apiClient: APIClient(session: MainTabRecordingURLSession()))
|
||||||
|
let controller = MainTabBarController(services: services)
|
||||||
|
|
||||||
|
controller.loadViewIfNeeded()
|
||||||
|
|
||||||
|
let ordersNavigationController = try XCTUnwrap(controller.navigationController(for: .orders))
|
||||||
|
ordersNavigationController.tabBarItem.badgeValue = "3"
|
||||||
|
|
||||||
|
XCTAssertEqual(controller.viewControllers?[1].tabBarItem.badgeValue, "3")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 主 Tab 测试使用的空网络会话,确保容器加载不触发真实请求。
|
||||||
|
private final class MainTabRecordingURLSession: URLSessionProtocol {
|
||||||
|
/// 返回空成功响应。
|
||||||
|
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
|
||||||
|
let response = HTTPURLResponse(
|
||||||
|
url: request.url ?? URL(string: "https://example.com")!,
|
||||||
|
statusCode: 200,
|
||||||
|
httpVersion: nil,
|
||||||
|
headerFields: nil
|
||||||
|
)!
|
||||||
|
return (Data(), response)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -29,7 +29,7 @@
|
|||||||
| 状态 | 模块 | UIKit 实现 | 剩余事项 |
|
| 状态 | 模块 | UIKit 实现 | 剩余事项 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| 完成 | App 根结构 | `RootViewController` 监听登录阶段;登录后 `MainTabBarController` 作为 window 根控制器 | 持续随路由扩展 |
|
| 完成 | App 根结构 | `RootViewController` 监听登录阶段;登录后 `MainTabBarController` 作为 window 根控制器 | 持续随路由扩展 |
|
||||||
| 完成 | Tab 导航 | CYLTabBarController 四 Tab + 中间扫码按钮 + 独立 `TabNavigationController`;push 子页隐藏 TabBar。 | 后续新增真实子页面时继续复用 `AppRoute`。 |
|
| 完成 | Tab 导航 | 系统 UITabBarController 五入口(首页、订单、扫码、数据、我的)+ 独立 `TabNavigationController`;扫码入口拦截后 present 扫码页,push 子页隐藏 TabBar。 | 后续新增真实子页面时继续复用 `AppRoute`。 |
|
||||||
| 完成 | 全局状态 | `AppServices.shared` 注入 Session / Context / Router / Toast / Loading | 避免新增 monolith 状态 |
|
| 完成 | 全局状态 | `AppServices.shared` 注入 Session / Context / Router / Toast / Loading | 避免新增 monolith 状态 |
|
||||||
| 完成 | 登录态缓存 | Keychain token + UserDefaults 快照 / 偏好 + 冷启动恢复 | — |
|
| 完成 | 登录态缓存 | Keychain token + UserDefaults 快照 / 偏好 + 冷启动恢复 | — |
|
||||||
| 完成 | 网络框架 | `APIClient` / `APIEnvelope` / 统一错误与 token | — |
|
| 完成 | 网络框架 | `APIClient` / `APIEnvelope` / 统一错误与 token | — |
|
||||||
|
|||||||
Reference in New Issue
Block a user