Advance UIKit rewrite with AMap integration and core UI modules.
Integrate高德 SDK with simulator-safe build flags, add map views for operating area and punch points, refactor main tabs and key feature screens to UIKit with Diffable lists, and document Swift concurrency defaults in AGENTS.md. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -13,8 +13,9 @@ import AMapLocationKit
|
||||
import AMapSearchKit
|
||||
import MAMapKit
|
||||
|
||||
/// 高德地图 SDK 初始化封装,真机构建时配置 Key 与隐私合规。
|
||||
enum AMapBootstrap {
|
||||
/// 在 App 启动时调用一次。将 `apiKey` 替换为高德控制台申请的 Key。
|
||||
/// 在 App 启动时调用一次,配置 API Key 并声明隐私政策已展示与同意。
|
||||
static func configure(apiKey: String) {
|
||||
guard !apiKey.isEmpty else { return }
|
||||
|
||||
@ -30,7 +31,9 @@ enum AMapBootstrap {
|
||||
}
|
||||
}
|
||||
#else
|
||||
/// 模拟器构建占位,不链接高德 SDK。
|
||||
enum AMapBootstrap {
|
||||
/// 空实现,保持调用方编译通过。
|
||||
static func configure(apiKey: String) {}
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -67,6 +67,7 @@ final class AppServices {
|
||||
accountContext.profile?.userId
|
||||
}
|
||||
|
||||
/// 初始化实例。
|
||||
private init() {
|
||||
let client = APIClient()
|
||||
let tokenStore = SessionTokenStore()
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
#if DEBUG
|
||||
import UIKit
|
||||
|
||||
/// UI Test 专用路由直达器,通过启动参数打开目标页,绕开自定义 TabBar 下 XCUITest 难以触发的 push。
|
||||
/// UI Test 专用路由直达器,通过启动参数打开目标页,绕开 Tab 切换与 push 链路。
|
||||
@MainActor
|
||||
enum AppUITestRouteDriver {
|
||||
private static var didApply = false
|
||||
|
||||
@ -164,6 +164,7 @@ enum AppRouteViewControllerFactory {
|
||||
}
|
||||
}
|
||||
|
||||
/// 将首页路由编码为 URI 字符串。
|
||||
private static func homeRouteURI(_ route: HomeRoute) -> String {
|
||||
if case let .modulePlaceholder(uri, _) = route {
|
||||
return uri
|
||||
@ -175,6 +176,7 @@ enum AppRouteViewControllerFactory {
|
||||
@MainActor
|
||||
/// 首页路由 ViewController 工厂,供 `UIKitAppNavigation` 复用。
|
||||
enum HomeRouteViewControllerFactory {
|
||||
/// 创建实例。
|
||||
static func make(for route: HomeRoute) -> UIViewController {
|
||||
AppRouteViewControllerFactory.makeViewController(for: route, services: AppServices.shared)
|
||||
}
|
||||
|
||||
@ -29,19 +29,47 @@ enum AppTab: String, CaseIterable, Identifiable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
var systemImage: String {
|
||||
/// 选中态 TabBar 图标资源名。
|
||||
var selectedImageName: String {
|
||||
switch self {
|
||||
case .home:
|
||||
"house"
|
||||
"TabHomeSelected"
|
||||
case .orders:
|
||||
"doc.text"
|
||||
"TabOrderSelected"
|
||||
case .statistics:
|
||||
"chart.bar"
|
||||
"TabDataSelected"
|
||||
case .profile:
|
||||
"person"
|
||||
"TabProfileSelected"
|
||||
}
|
||||
}
|
||||
|
||||
/// 未选中态 TabBar 图标资源名。
|
||||
var unselectedImageName: String {
|
||||
switch self {
|
||||
case .home:
|
||||
"TabHomeUnselected"
|
||||
case .orders:
|
||||
"TabOrderUnselected"
|
||||
case .statistics:
|
||||
"TabDataUnselected"
|
||||
case .profile:
|
||||
"TabProfileUnselected"
|
||||
}
|
||||
}
|
||||
|
||||
/// 构建系统 TabBarItem,供 `UITabBarController` 使用。
|
||||
func makeTabBarItem() -> UITabBarItem {
|
||||
let unselectedImage = UIImage(named: unselectedImageName)?.withRenderingMode(.alwaysOriginal)
|
||||
let selectedImage = UIImage(named: selectedImageName)?.withRenderingMode(.alwaysOriginal)
|
||||
let item = UITabBarItem(
|
||||
title: title,
|
||||
image: unselectedImage,
|
||||
selectedImage: selectedImage
|
||||
)
|
||||
item.accessibilityIdentifier = "main.tab.\(rawValue)"
|
||||
return item
|
||||
}
|
||||
|
||||
/// 构建 Tab 根页面 ViewController。
|
||||
func makeRootViewController(services: AppServices) -> UIViewController {
|
||||
switch self {
|
||||
|
||||
@ -53,6 +53,7 @@ final class AppRouter {
|
||||
|
||||
private let routers: [AppTab: RouterPath]
|
||||
|
||||
/// 初始化实例。
|
||||
init() {
|
||||
var builtRouters: [AppTab: RouterPath] = [:]
|
||||
for tab in AppTab.allCases {
|
||||
|
||||
@ -13,8 +13,7 @@ final class TabNavigationController: UINavigationController, UINavigationControl
|
||||
private let services: AppServices
|
||||
private var isSyncingStack = false
|
||||
|
||||
weak var tabBarHost: MainTabBarController?
|
||||
|
||||
/// 初始化实例。
|
||||
init(tab: AppTab, services: AppServices) {
|
||||
self.appTab = tab
|
||||
self.services = services
|
||||
@ -43,15 +42,7 @@ final class TabNavigationController: UINavigationController, UINavigationControl
|
||||
pushViewController(viewController, animated: animated)
|
||||
}
|
||||
|
||||
func navigationController(
|
||||
_ navigationController: UINavigationController,
|
||||
willShow viewController: UIViewController,
|
||||
animated: Bool
|
||||
) {
|
||||
let isRoot = viewController === viewControllers.first
|
||||
tabBarHost?.setCustomTabBarHidden(!isRoot, animated: animated)
|
||||
}
|
||||
|
||||
/// navigationController相关逻辑。
|
||||
func navigationController(
|
||||
_ navigationController: UINavigationController,
|
||||
didShow viewController: UIViewController,
|
||||
@ -60,6 +51,7 @@ final class TabNavigationController: UINavigationController, UINavigationControl
|
||||
syncRouterPathFromStack()
|
||||
}
|
||||
|
||||
/// 注册NavigationBridge。
|
||||
private func registerNavigationBridge() {
|
||||
switch appTab {
|
||||
case .home:
|
||||
@ -71,6 +63,7 @@ final class TabNavigationController: UINavigationController, UINavigationControl
|
||||
}
|
||||
}
|
||||
|
||||
/// 同步FromRouterPath状态。
|
||||
private func syncFromRouterPath() {
|
||||
guard !isSyncingStack else { return }
|
||||
|
||||
@ -79,6 +72,7 @@ final class TabNavigationController: UINavigationController, UINavigationControl
|
||||
let currentDepth = max(viewControllers.count - 1, 0)
|
||||
|
||||
if targetDepth > currentDepth {
|
||||
// Router 路径变深:按路径依次 push 缺失页面
|
||||
for index in currentDepth..<targetDepth {
|
||||
let route = routerPath.path[index]
|
||||
let viewController = AppRouteViewControllerFactory.makeViewController(for: route, services: services)
|
||||
@ -89,6 +83,7 @@ final class TabNavigationController: UINavigationController, UINavigationControl
|
||||
}
|
||||
|
||||
if targetDepth < currentDepth {
|
||||
// Router 路径变浅:pop 至目标深度
|
||||
if targetDepth == 0 {
|
||||
popToRootViewController(animated: true)
|
||||
} else if targetDepth < viewControllers.count {
|
||||
@ -97,6 +92,7 @@ final class TabNavigationController: UINavigationController, UINavigationControl
|
||||
}
|
||||
}
|
||||
|
||||
/// 同步RouterPathFromStack状态。
|
||||
private func syncRouterPathFromStack() {
|
||||
guard !isSyncingStack else { return }
|
||||
|
||||
|
||||
@ -106,6 +106,7 @@ final class ToastOverlayView: UIView {
|
||||
nil
|
||||
}
|
||||
|
||||
/// 配置Views展示内容。
|
||||
private func configureViews() {
|
||||
bannerView.backgroundColor = AppDesign.primary
|
||||
bannerView.isHidden = true
|
||||
@ -134,6 +135,7 @@ final class ToastOverlayView: UIView {
|
||||
])
|
||||
}
|
||||
|
||||
/// 刷新Presentation展示。
|
||||
private func refreshPresentation(animated: Bool) {
|
||||
guard let message = toastCenter?.message, !message.isEmpty else {
|
||||
hideBanner(animated: animated)
|
||||
@ -144,6 +146,7 @@ final class ToastOverlayView: UIView {
|
||||
showBanner(animated: animated)
|
||||
}
|
||||
|
||||
/// 展示Banner。
|
||||
private func showBanner(animated: Bool) {
|
||||
guard bannerView.isHidden || bannerView.alpha < 1 else { return }
|
||||
|
||||
@ -162,6 +165,7 @@ final class ToastOverlayView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
/// 隐藏Banner。
|
||||
private func hideBanner(animated: Bool) {
|
||||
guard !bannerView.isHidden else { return }
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ final class RootViewController: UIViewController {
|
||||
private var currentChild: UIViewController?
|
||||
private let restoringView = UIView()
|
||||
|
||||
/// 初始化实例。
|
||||
init(services: AppServices = .shared) {
|
||||
self.services = services
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
@ -24,6 +25,7 @@ final class RootViewController: UIViewController {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
/// 初始化视图、监听登录阶段变化并启动会话恢复。
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .systemBackground
|
||||
@ -38,11 +40,13 @@ final class RootViewController: UIViewController {
|
||||
configurePushNotifications()
|
||||
}
|
||||
|
||||
/// 窗口展示后将 Toast 与全局 Loading 挂载到 window 层级。
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
attachGlobalOverlays()
|
||||
}
|
||||
|
||||
/// 冷启动时恢复本地会话,并在已登录时申请推送权限。
|
||||
private func bootstrapSession() {
|
||||
Task {
|
||||
await services.globalLoading.withLoading {
|
||||
@ -62,16 +66,19 @@ final class RootViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 配置推送管理器与路由跳转回调。
|
||||
private func configurePushNotifications() {
|
||||
services.configurePushNotifications()
|
||||
}
|
||||
|
||||
/// 将全局 Toast 与 Loading 视图附加到当前 window。
|
||||
private func attachGlobalOverlays() {
|
||||
guard let window = view.window else { return }
|
||||
services.toastCenter.attachToWindow(window)
|
||||
services.globalLoading.attachToWindow(window)
|
||||
}
|
||||
|
||||
/// 登录阶段变化时切换子页面,并在登出时清理各上下文状态。
|
||||
private func handleSessionPhaseChange() {
|
||||
mountChild(for: services.appSession.phase)
|
||||
|
||||
@ -82,6 +89,7 @@ final class RootViewController: UIViewController {
|
||||
}
|
||||
Task { await reloadScenicSpotContextIfNeeded() }
|
||||
case .loggedOut:
|
||||
// 登出后重置账号、权限、路由与排队运行时,避免残留旧状态
|
||||
services.accountContext.reset()
|
||||
services.permissionContext.reset()
|
||||
services.scenicSpotContext.reset()
|
||||
@ -93,6 +101,7 @@ final class RootViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 登录成功后按当前景区 ID 刷新景区上下文。
|
||||
private func reloadScenicSpotContextIfNeeded() async {
|
||||
guard services.appSession.isLoggedIn else {
|
||||
services.scenicSpotContext.reset()
|
||||
@ -104,6 +113,7 @@ final class RootViewController: UIViewController {
|
||||
)
|
||||
}
|
||||
|
||||
/// 按登录阶段挂载对应子控制器:登录页、恢复占位或主 Tab。
|
||||
private func mountChild(for phase: AuthPhase) {
|
||||
let nextChild: UIViewController
|
||||
switch phase {
|
||||
@ -130,6 +140,7 @@ final class RootViewController: UIViewController {
|
||||
currentChild = nextChild
|
||||
}
|
||||
|
||||
/// 会话恢复阶段的空白占位页,避免闪烁登录页。
|
||||
private func restoringViewController() -> UIViewController {
|
||||
let controller = UIViewController()
|
||||
controller.view.backgroundColor = .systemBackground
|
||||
|
||||
Reference in New Issue
Block a user