Integrate CYLTabBar center scan button and unify UIKit navigation.

Add CYLTabBarController with a global scan entry, promote MainTabBarController to the window root after login, replace RouterPath-based navigation with AppNavigator, and fix Profile diffable cell reconfiguration crashes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-26 16:36:18 +08:00
parent 24a7339b68
commit a1c031c9b7
31 changed files with 838 additions and 517 deletions

View File

@ -29,7 +29,7 @@ Home 模块负责登录后的首页工作台,包括当前景区展示、工作
- `unsupported`:已知 iOS 不支持的入口,进入占位说明。
- `placeholder`:未知或未迁移入口,记录诊断并进入占位说明。
`HomeView` 不创建自己的 `NavigationStack`,而是使用 Main Tab 注入的 `RouterPath` 进行页面跳转
`HomeMenuRouting` 只作为首页菜单到 `AppNavigator` 的适配层:本地业务页面沿当前 UIKit 导航栈 push订单和数据等一级入口交给导航器切换 Tab未迁移入口进入占位页
## 后续迁移

View File

@ -19,42 +19,16 @@ enum HomeMenuRouting {
let services = AppServices.shared
switch route {
case .tab(let tab):
services.appRouter.select(tab)
services.appNavigator.selectTab(tab)
case .orders(let entry):
services.appRouter.selectOrders(entry: entry)
services.appNavigator.openOrdersEntry(entry)
case .destination(let homeRoute):
push(homeRoute, from: viewController)
services.appNavigator.push(.home(homeRoute), from: viewController)
case .unsupported(let uri, let title, _):
pushPlaceholder(title: title, uri: uri, from: viewController)
services.appNavigator.push(.home(.modulePlaceholder(uri: uri, title: title)), from: viewController)
case .placeholder(let uri, let title):
HomeRouteDiagnostics.recordUnknown(uri: uri, title: title)
pushPlaceholder(title: title, uri: uri, from: viewController)
services.appNavigator.push(.home(.modulePlaceholder(uri: uri, title: title)), from: viewController)
}
}
/// Push
static func push(_ route: HomeRoute, from viewController: UIViewController) {
let target = AppRouteViewControllerFactory.makeViewController(for: route, services: AppServices.shared)
viewController.navigationController?.pushViewController(target, animated: true)
}
/// Push
static func pushOrders(_ route: OrdersRoute, from viewController: UIViewController) {
let target = AppRouteViewControllerFactory.makeViewController(for: .orders(route), services: AppServices.shared)
viewController.navigationController?.pushViewController(target, animated: true)
}
/// Push
static func pushProfile(_ route: ProfileRoute, from viewController: UIViewController) {
let target = AppRouteViewControllerFactory.makeViewController(for: .profile(route), services: AppServices.shared)
viewController.navigationController?.pushViewController(target, animated: true)
}
/// Push Placeholder
private static func pushPlaceholder(title: String, uri: String, from viewController: UIViewController) {
viewController.navigationController?.pushViewController(
FeaturePlaceholderViewController(title: title, uri: uri),
animated: true
)
}
}

View File

@ -368,7 +368,7 @@ final class HomeViewController: UIViewController {
///
@objc private func scenicTapped() {
HomeMenuRouting.push(.scenicSelection, from: self)
appServices.appNavigator.push(.home(.scenicSelection), from: self)
}
/// 线 / 线

View File

@ -4,7 +4,7 @@
Main 模块负责登录后的主界面 Tab 容器。
主界面由 `MainTabBarController` 承载,使用系统 `UITabBarController` 展示首页、订单、数据、我的四个 Tab。每个 Tab 内部包裹独立的 `TabNavigationController`导航路径由 `AppRouter` 单独保存
主界面由 `MainTabBarController` 承载,使用 **CYLTabBarController** 展示首页、订单、数据、我的四个 Tab,并在中间提供扫码核销按钮。每个 Tab 内部包裹独立的 `TabNavigationController`页面路径以 UIKit 导航栈为唯一事实源
## Tab 结构
@ -18,11 +18,11 @@ Main 模块负责登录后的主界面 Tab 容器。
## 导航流程
1. `MainTabBarController` 读取 `AppServices.appRouter` 作为选中 Tab 状态源
2. 用户点击 TabBar 或通过 `AppRouter.select` 切换 Tab 时,两边状态保持同步
1. `MainTabBarController` 在登录后注册为 `AppNavigator` 的导航宿主
2. 用户点击 TabBar 只改变 UIKit 选中态,并保留各 Tab 原有导航栈
3. 每个 Tab 由 `TabNavigationController` 创建独立 `UINavigationController` 栈。
4. Tab 内 push 子页面时,通过 `AppRoute.hidesTabBarWhenPushed` 控制是否隐藏系统 TabBar默认隐藏。
5. 跨 Tab 跳转通过 `AppRouter.select``AppRouter.selectOrders(entry:)` 处理
5. 跨 Tab、推送通知、UI Test 直达和全局扫码统一通过 `AppNavigator` 执行
## 订单角标
@ -32,15 +32,21 @@ Main 模块负责登录后的主界面 Tab 容器。
## 扫码核销
系统 TabBar 不展示中间扫码按钮;订单页内部的扫码核销入口仍然可用。如需全局扫码入口,可通过首页或订单页进入。
TabBar 中间通过 `MainScanPlusButton`CYLTabBarController PlusButton展示全局扫码入口点击后全屏打开 `OrderCodeScannerViewController`
1. 扫码成功 → `AppNavigator.openOrdersEntry(.verificationOrders, scannedCode:)` 切换至订单核销入口并传入扫码结果。
2. `OrdersViewController` 本地保存并消费扫码结果后进入确认核销流程。
3. 扫码失败 → 通过 `ToastCenter` 提示错误。
订单页内部的扫码核销入口仍然可用,与 TabBar 中间按钮为双入口。
## 后续迁移规则
迁移新页面时:
- 优先替换对应 Tab 的根 ViewController。
- 保持每个 Tab 自己的 `TabNavigationController` 导航栈。
- 跨 Tab 跳转通过 `AppRouter.select` 切换 Tab
- 需要从首页或全局入口进入订单核销时,优先使用 `AppRouter.selectOrders(entry:)``AppRouter.routeToOrderVerification(scannedCode:)`
- Tab 内跳转通过当前 Tab 的 `RouterPath.navigate` 或扩展后的 `AppRoute` 处理。
- 跨 Tab 跳转通过 `AppNavigator.selectTab` 或对应业务入口方法执行
- 需要从首页或全局入口进入订单核销时,优先使用 `AppNavigator.openOrdersEntry(_:scannedCode:)`
- Tab 内跳转通过 `AppNavigator.push(_:from:)` 或扩展后的 `AppRoute` 处理。
- 普通业务子页面默认隐藏 TabBar只有确有产品需求时才在 `AppRoute` 策略中单独放开。
- 真实业务页面接入后,应同步补充该模块文档和单元测试。

View File

@ -3,11 +3,12 @@
// suixinkan
//
import CYLTabBarController
import UIKit
@MainActor
/// Tab 使 `UITabBarController`
final class MainTabBarController: UITabBarController, UITabBarControllerDelegate {
/// Tab 使 CYLTabBarController
final class MainTabBarController: CYLTabBarController {
private let services: AppServices
private let badgeViewModel = MainTabBadgeViewModel()
@ -17,7 +18,11 @@ final class MainTabBarController: UITabBarController, UITabBarControllerDelegate
///
init(services: AppServices) {
self.services = services
MainScanPlusButton.register()
super.init(nibName: nil, bundle: nil)
MainScanPlusButton.onScanTap = { [weak self] in
self?.presentGlobalScanner()
}
delegate = self
}
@ -31,36 +36,72 @@ final class MainTabBarController: UITabBarController, UITabBarControllerDelegate
super.viewDidLoad()
configureTabBarAppearance()
configureTabs()
bindRouter()
services.appNavigator.attach(host: self)
bindBadgeViewModel()
bindAccountContext()
syncSelectedTabFromRouter(animated: false)
Task { await refreshOrderBadge() }
#if DEBUG
Task { await AppUITestRouteDriver.applyIfNeeded(services: services) }
#endif
}
/// TabBar
private func configureTabBarAppearance() {
tabBar.tintColor = AppDesign.primary
tabBar.backgroundColor = .white
}
/// Tab TabBarItem
private func configureTabs() {
viewControllers = AppTab.allCases.map { tab in
let navigationController = TabNavigationController(tab: tab, services: services)
navigationController.tabBarItem = tab.makeTabBarItem()
tabNavigationControllers[tab] = navigationController
return navigationController
///
deinit {
Task { @MainActor in
MainScanPlusButton.onScanTap = nil
}
}
/// Router Tab
private func bindRouter() {
services.appRouter.onChange = { [weak self] in
self?.handleRouterChange()
/// TabBar CustomMainTabBar
private func configureTabBarAppearance() {
tabBar.tintColor = AppDesign.primary
tabBar.backgroundColor = .white
tabBar.shadowImage = UIImage()
let normalAttributes: [NSAttributedString.Key: Any] = [
.foregroundColor: UIColor(hex: 0x7D8DA3),
.font: UIFont.systemFont(ofSize: 12, weight: .regular)
]
let selectedAttributes: [NSAttributedString.Key: Any] = [
.foregroundColor: AppDesign.primary,
.font: UIFont.systemFont(ofSize: 12, weight: .medium)
]
let appearance = UITabBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = .white
appearance.shadowColor = .clear
appearance.stackedLayoutAppearance.normal.titleTextAttributes = normalAttributes
appearance.stackedLayoutAppearance.selected.titleTextAttributes = selectedAttributes
tabBar.standardAppearance = appearance
tabBar.scrollEdgeAppearance = appearance
}
/// Tab CYLTabBarController 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
}
applyTabAccessibilityIdentifiers()
}
/// CYLTabBarController TabBarItem
private func makeTabBarItemAttributes(for tab: AppTab) -> [String: Any] {
[
CYLTabBarItemTitle: tab.title,
CYLTabBarItemImage: tab.unselectedImageName,
CYLTabBarItemSelectedImage: tab.selectedImageName
]
}
/// CYL TabBarItem
private func applyTabAccessibilityIdentifiers() {
for tab in AppTab.allCases {
tabNavigationControllers[tab]?.tabBarItem.accessibilityIdentifier = "main.tab.\(tab.rawValue)"
}
}
@ -78,24 +119,6 @@ final class MainTabBarController: UITabBarController, UITabBarControllerDelegate
}
}
/// Router Tab `AppRouter`
private func handleRouterChange() {
syncSelectedTabFromRouter(animated: false)
if services.appRouter.selectedTab == .orders {
Task { await refreshOrderBadge() }
}
}
/// `AppRouter.selectedTab` TabBar
private func syncSelectedTabFromRouter(animated: Bool) {
guard let index = AppTab.allCases.firstIndex(of: services.appRouter.selectedTab) else { return }
guard selectedIndex != index else { return }
isSyncingTabSelection = true
selectedIndex = index
isSyncingTabSelection = false
}
/// Tab
private func updateOrderBadge() {
let badgeValue: String?
@ -116,14 +139,71 @@ final class MainTabBarController: UITabBarController, UITabBarControllerDelegate
)
}
/// TabBar `AppRouter`
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
/// TabBar
private func presentGlobalScanner() {
let scanner = OrderCodeScannerViewController()
scanner.onScanResult = { [weak self, weak scanner] result in
scanner?.dismiss(animated: true)
guard let self else { return }
switch result {
case .success(let code):
self.services.appNavigator.openOrdersEntry(.verificationOrders, scannedCode: code)
case .failure(let error):
self.services.toastCenter.show(error.localizedDescription)
}
}
let navigationController = UINavigationController(rootViewController: scanner)
navigationController.modalPresentationStyle = .fullScreen
present(navigationController, animated: true)
}
/// TabBar
override func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
guard !isSyncingTabSelection,
let navigationController = viewController as? TabNavigationController else { return }
services.appRouter.select(navigationController.appTab)
if navigationController.appTab == .orders {
Task { await refreshOrderBadge() }
}
}
}
extension MainTabBarController: AppNavigationHost {
/// Tab
func selectTab(_ tab: AppTab) {
guard let index = AppTab.allCases.firstIndex(of: tab) else { return }
guard selectedIndex != index else { return }
isSyncingTabSelection = true
selectedIndex = index
isSyncingTabSelection = false
if tab == .orders {
Task { await refreshOrderBadge() }
}
}
/// Tab
func navigationController(for tab: AppTab) -> TabNavigationController? {
tabNavigationControllers[tab]
}
/// Tab
func selectedNavigationController() -> TabNavigationController? {
selectedViewController as? TabNavigationController
}
/// Tab
func resetAllStacks() {
for navigationController in tabNavigationControllers.values {
navigationController.popToRootViewController(animated: false)
}
selectTab(.home)
}
///
func applyOrdersEntry(_ entry: OrdersEntry, scannedCode: String?) {
guard let ordersController = tabNavigationControllers[.orders]?.viewControllers.first as? OrdersViewController else {
return
}
ordersController.applyOrdersEntry(entry, scannedCode: scannedCode)
}
}

View File

@ -0,0 +1,77 @@
//
// 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(type: .custom)
button.frame = CGRect(x: 0, y: 0, width: 70, height: 64)
button.configureAppearance()
button.addTarget(button, action: #selector(handleScanTap), for: .touchUpInside)
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(systemName: "qrcode.viewfinder", withConfiguration: symbolConfig)
)
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?()
}
}

View File

@ -8,7 +8,7 @@ Orders 模块负责登录后的订单 Tab。当前已覆盖订单管理、核销
## 核心对象
- `OrdersView`:订单 Tab 根视图,读取 `AccountContext``PermissionContext``AppRouter``OrdersAPI`
- `OrdersView`:订单 Tab 根视图,读取 `AccountContext``PermissionContext``OrdersAPI`,并接收 `AppNavigator` 传入的订单入口与扫码结果
- `OrdersViewModel`:管理订单子入口、筛选条件、分页状态、加载状态和手动核销状态。
- `OrderDetailViewModel`:管理订单详情接口加载、错误提示和列表摘要兜底展示。
- `DepositOrderListViewModel`:管理押金订单分页、核销、退款和操作后刷新。
@ -44,6 +44,6 @@ Orders 模块负责登录后的订单 Tab。当前已覆盖订单管理、核销
## 路由边界
首页 `photographer_orders``/scenic-order-manage` 会进入订单管理,`verification_order` 会进入核销订单,`deposit_order_detail``deposit_order``deposit_order_shooting_info` 会进入押金订单页。订单详情、核销订单详情、押金详情、押金拍摄信息、历史拍摄、任务上传和订单尾片已接入 `AppRoute.orders` 真实路由push 后继续隐藏 TabBar。
首页 `photographer_orders``/scenic-order-manage` 会进入订单管理,`verification_order` 会进入核销订单,`deposit_order_detail``deposit_order``deposit_order_shooting_info` 会进入押金订单页。全局扫码和推送通知通过 `AppNavigator.openOrdersEntry` 切到订单 Tab root再由 `OrdersViewController` 应用入口状态;订单卡片、核销卡片、押金详情、押金拍摄信息、历史拍摄、任务上传和订单尾片已接入 `AppRoute.orders` 真实路由push 后继续隐藏 TabBar。
未明确接口的订单后续能力继续进入占位页,避免入口崩溃。

View File

@ -96,7 +96,7 @@ final class DepositOrderListViewController: UIViewController, UITableViewDelegat
let cell = tableView.dequeueReusableCell(withIdentifier: DepositOrderCell.reuseID, for: indexPath) as! DepositOrderCell
cell.configure(item: orderItem, isOperating: self.viewModel.operatingOrderNumber == orderItem.orderNumber)
cell.onDetail = { [weak self] in
self.flatMap { HomeMenuRouting.pushOrders(.depositDetail(orderNumber: orderItem.orderNumber), from: $0) }
self.flatMap { $0.appServices.appNavigator.push(.orders(.depositDetail(orderNumber: orderItem.orderNumber)), from: $0) }
}
cell.onWriteOff = { [weak self] in self?.writeOff(orderItem) }
cell.onRefund = { [weak self] in self?.refund(orderItem) }
@ -154,7 +154,7 @@ final class DepositOrderListViewController: UIViewController, UITableViewDelegat
@objc private func openDetail() {
let text = orderNumberField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !text.isEmpty else { return }
HomeMenuRouting.pushOrders(.depositDetail(orderNumber: text), from: self)
appServices.appNavigator.push(.orders(.depositDetail(orderNumber: text)), from: self)
}
///

View File

@ -289,13 +289,13 @@ final class StoreOrderDetailViewController: UIViewController, UITableViewDelegat
let orderNumber = viewModel.display.orderNumber
switch index {
case 0:
HomeMenuRouting.pushOrders(.historicalShooting(orderNumber: orderNumber), from: self)
appServices.appNavigator.push(.orders(.historicalShooting(orderNumber: orderNumber)), from: self)
case 1 where item.orderType == 19:
HomeMenuRouting.pushOrders(.multiTravelTaskUpload(orderNumber: orderNumber), from: self)
appServices.appNavigator.push(.orders(.multiTravelTaskUpload(orderNumber: orderNumber)), from: self)
case 2:
HomeMenuRouting.pushOrders(.orderTrailer(orderNumber: orderNumber, title: "视频预告"), from: self)
appServices.appNavigator.push(.orders(.orderTrailer(orderNumber: orderNumber, title: "视频预告")), from: self)
case 3:
HomeMenuRouting.pushOrders(.orderTrailer(orderNumber: orderNumber, title: "尾片上传"), from: self)
appServices.appNavigator.push(.orders(.orderTrailer(orderNumber: orderNumber, title: "尾片上传")), from: self)
case 4:
presentRefund()
default:

View File

@ -37,6 +37,7 @@ final class OrdersViewController: UIViewController {
private let viewModel = OrdersViewModel()
private var manualOrderNumber = ""
private var scanHintMessage: String?
private var pendingScanCode: String?
private lazy var collectionView: UICollectionView = {
let layout = makeCollectionLayout()
@ -77,25 +78,34 @@ final class OrdersViewController: UIViewController {
self?.applySnapshot()
}
appServices.appRouter.onChange = { [weak self] in
guard let self else { return }
if self.viewModel.selectedEntry != self.appServices.appRouter.selectedOrdersEntry {
self.viewModel.selectedEntry = self.appServices.appRouter.selectedOrdersEntry
Task { await self.reload(showLoading: true) }
}
Task { await self.consumePendingScanCodeIfNeeded() }
}
appServices.accountContext.onChange = { [weak self] in
Task { await self?.reload(showLoading: true) }
}
viewModel.selectedEntry = appServices.appRouter.selectedOrdersEntry
Task {
await reload(showLoading: true)
await consumePendingScanCodeIfNeeded()
}
}
///
func applyOrdersEntry(_ entry: OrdersEntry, scannedCode: String?) {
let didChangeEntry = viewModel.selectedEntry != entry
viewModel.selectedEntry = entry
if let scannedCode {
pendingScanCode = scannedCode
}
guard isViewLoaded else { return }
Task {
if didChangeEntry {
await reload(showLoading: true)
}
await consumePendingScanCodeIfNeeded()
}
}
/// Compositional Layout section
private func makeCollectionLayout() -> UICollectionViewCompositionalLayout {
UICollectionViewCompositionalLayout { [weak self] sectionIndex, _ in
@ -345,7 +355,6 @@ final class OrdersViewController: UIViewController {
private func switchEntry(_ entry: OrdersEntry) {
guard viewModel.selectedEntry != entry else { return }
viewModel.selectedEntry = entry
appServices.appRouter.selectedOrdersEntry = entry
Task { await reload(showLoading: true) }
}
@ -370,7 +379,8 @@ final class OrdersViewController: UIViewController {
///
private func consumePendingScanCodeIfNeeded() async {
guard viewModel.selectedEntry == .verificationOrders,
let code = appServices.appRouter.consumePendingOrderScanCode() else { return }
let code = pendingScanCode else { return }
pendingScanCode = nil
handleScanResult(code)
}
@ -459,11 +469,11 @@ extension OrdersViewController: UICollectionViewDelegate {
if item.hasPrefix("orders:store:") {
let orderNumber = String(item.dropFirst("orders:store:".count))
guard let order = viewModel.storeOrders.first(where: { $0.orderNumber == orderNumber }) else { return }
HomeMenuRouting.pushOrders(.storeDetail(order), from: self)
appServices.appNavigator.push(.orders(.storeDetail(order)), from: self)
} else if item.hasPrefix("orders:writeoff:") {
let orderNumber = String(item.dropFirst("orders:writeoff:".count))
guard let order = viewModel.writeOffOrders.first(where: { $0.orderNumber == orderNumber }) else { return }
HomeMenuRouting.pushOrders(.writeOffDetail(order), from: self)
appServices.appNavigator.push(.orders(.writeOffDetail(order)), from: self)
}
}

View File

@ -76,7 +76,7 @@ DEBUG 构建下,“我的”列表会额外展示“首页调试”入口,
3. 页面展示景区账号和门店账号,优先选中后端标记的当前账号,否则选中第一项。
4. 用户确认后调用 `AuthAPI.setUser`,使用 `AccountSwitchAccount.toSetUserRequest()` 生成请求体。
5. 切换成功后调用 `AuthSessionCoordinator.completeLogin`,重新写入正式 token、账号快照、权限、景区和门店上下文。
6. `AppRouter.reset()` 清空旧路由栈并回到首页。
6. `AppNavigator.resetAllStacks()` 清空旧 UIKit 导航栈并回到首页。
## 实名认证流程
@ -105,7 +105,7 @@ DEBUG 构建下,“我的”列表会额外展示“首页调试”入口,
1. 用户点击退出登录。
2. `ProfileView` 展示确认弹窗。
3. 用户确认后调用 `AuthSessionCoordinator.logout`
4. 协调器清空正式 token、账号快照、账号上下文、路由栈和 Toast。
4. 协调器清空正式 token、账号快照、账号上下文、全部 UIKit 导航栈和 Toast。
5. `AppSession` 切换为 `loggedOut`,根视图回到登录页。
6. 上次手机号和协议状态等非敏感偏好保留。

View File

@ -187,7 +187,7 @@ final class AccountSwitchViewController: UIViewController, UITableViewDelegate {
profileAPI: appServices.profileAPI,
accountContextAPI: appServices.accountContextAPI
)
appServices.appRouter.reset()
appServices.appNavigator.resetAllStacks()
showToast("账号已切换")
navigationController?.popToRootViewController(animated: true)
} catch {

View File

@ -28,6 +28,24 @@ private enum ProfileRowID {
static let logout = "profile:logout"
}
/// value1 Cell Diffable reconfigure
private final class ProfileValueTableViewCell: UITableViewCell {
/// value1 Cell
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: .value1, reuseIdentifier: reuseIdentifier)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private enum ProfileCellReuseID {
static let value = "profile.value"
static let logout = "profile.logout"
}
///
final class ProfileViewController: UIViewController, UITableViewDelegate {
@ -76,43 +94,57 @@ final class ProfileViewController: UIViewController, UITableViewDelegate {
/// Diffable
private func configureTableDataSource() {
tableView.register(ProfileValueTableViewCell.self, forCellReuseIdentifier: ProfileCellReuseID.value)
tableView.register(UITableViewCell.self, forCellReuseIdentifier: ProfileCellReuseID.logout)
tableDataSource = UITableViewDiffableDataSource<ProfileTableSection, ProfileTableRow>(
tableView: tableView
) { [weak self] (_: UITableView, indexPath: IndexPath, row: ProfileTableRow) -> UITableViewCell? in
) { [weak self] (tableView: UITableView, indexPath: IndexPath, row: ProfileTableRow) -> UITableViewCell? in
guard let self else { return UITableViewCell() }
let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
switch row {
case ProfileRowID.logout:
let cell = tableView.dequeueReusableCell(withIdentifier: ProfileCellReuseID.logout, for: indexPath)
cell.textLabel?.text = "退出登录"
cell.textLabel?.textColor = UIColor(hex: 0xEF4444)
cell.textLabel?.textAlignment = .center
cell.selectionStyle = .default
case ProfileRowID.name:
cell.textLabel?.text = "姓名"
cell.detailTextLabel?.text = self.viewModel.displayRealName
cell.selectionStyle = .none
case ProfileRowID.account:
cell.textLabel?.text = "当前账号"
cell.detailTextLabel?.text = self.appServices.accountContext.profile?.displayName ?? "--"
cell.accessoryType = .disclosureIndicator
cell.selectionStyle = .default
case ProfileRowID.phone:
cell.textLabel?.text = "手机号"
cell.detailTextLabel?.text = self.viewModel.displayPhone
cell.selectionStyle = .none
case ProfileRowID.realName:
cell.textLabel?.text = "实名认证"
cell.detailTextLabel?.text = self.viewModel.realNameStatusText
cell.accessoryType = .disclosureIndicator
cell.selectionStyle = .default
case ProfileRowID.settings:
cell.textLabel?.text = "设置"
cell.accessoryType = .disclosureIndicator
cell.selectionStyle = .default
cell.accessoryType = .none
return cell
default:
break
let cell = tableView.dequeueReusableCell(
withIdentifier: ProfileCellReuseID.value,
for: indexPath
)
cell.textLabel?.textAlignment = .natural
cell.textLabel?.textColor = .label
cell.accessoryType = .none
cell.selectionStyle = .none
switch row {
case ProfileRowID.name:
cell.textLabel?.text = "姓名"
cell.detailTextLabel?.text = self.viewModel.displayRealName
case ProfileRowID.account:
cell.textLabel?.text = "当前账号"
cell.detailTextLabel?.text = self.appServices.accountContext.profile?.displayName ?? "--"
cell.accessoryType = .disclosureIndicator
cell.selectionStyle = .default
case ProfileRowID.phone:
cell.textLabel?.text = "手机号"
cell.detailTextLabel?.text = self.viewModel.displayPhone
case ProfileRowID.realName:
cell.textLabel?.text = "实名认证"
cell.detailTextLabel?.text = self.viewModel.realNameStatusText
cell.accessoryType = .disclosureIndicator
cell.selectionStyle = .default
case ProfileRowID.settings:
cell.textLabel?.text = "设置"
cell.accessoryType = .disclosureIndicator
cell.selectionStyle = .default
default:
break
}
return cell
}
return cell
}
applyTableSnapshot(animated: false)
}
@ -296,7 +328,7 @@ final class ProfileViewController: UIViewController, UITableViewDelegate {
accountContext: self.appServices.accountContext,
permissionContext: self.appServices.permissionContext,
scenicSpotContext: self.appServices.scenicSpotContext,
appRouter: self.appServices.appRouter,
appNavigator: self.appServices.appNavigator,
toastCenter: self.appServices.toastCenter
)
})
@ -311,11 +343,11 @@ final class ProfileViewController: UIViewController, UITableViewDelegate {
case ProfileRowID.logout:
logoutTapped()
case ProfileRowID.account:
HomeMenuRouting.pushProfile(.accountSwitch, from: self)
appServices.appNavigator.push(.profile(.accountSwitch), from: self)
case ProfileRowID.realName:
HomeMenuRouting.pushProfile(.realNameAuth, from: self)
appServices.appNavigator.push(.profile(.realNameAuth), from: self)
case ProfileRowID.settings:
HomeMenuRouting.pushProfile(.settings, from: self)
appServices.appNavigator.push(.profile(.settings), from: self)
default:
break
}

View File

@ -98,7 +98,7 @@ final class SettingsViewController: SimpleTableDiffableViewController {
guard let action = rowActions[row] else { return }
switch action {
case .agreement(let page):
HomeMenuRouting.pushProfile(.agreement(page), from: self)
appServices.appNavigator.push(.profile(.agreement(page)), from: self)
case .download:
copyDownloadLink()
case .version: