
- 在AppDelegate中集成数据迁移管理器,支持从UserDefaults迁移到Keychain。 - 重构UserInfoManager,使用Keychain存储用户信息,增加内存缓存以提升性能。 - 添加API加载效果视图,增强用户体验。 - 更新SplashFeature以支持自动登录和认证状态检查。 - 语言设置迁移至Keychain,确保用户设置的安全性。
198 lines
6.0 KiB
Swift
198 lines
6.0 KiB
Swift
import Foundation
|
||
import SwiftUI
|
||
import Combine
|
||
|
||
// MARK: - API Loading Manager
|
||
|
||
/// 全局 API 加载状态管理器
|
||
///
|
||
/// 该管理器负责:
|
||
/// - 跟踪多个并发的 API 调用状态
|
||
/// - 管理 loading 和错误信息的显示
|
||
/// - 自动清理过期的错误信息
|
||
/// - 提供线程安全的状态更新
|
||
class APILoadingManager: ObservableObject {
|
||
|
||
// MARK: - Properties
|
||
|
||
/// 单例实例
|
||
static let shared = APILoadingManager()
|
||
|
||
/// 当前活动的加载项
|
||
@Published private(set) var loadingItems: [APILoadingItem] = []
|
||
|
||
/// 错误清理任务
|
||
private var errorCleanupTasks: [UUID: DispatchWorkItem] = [:]
|
||
|
||
/// 私有初始化器,确保单例
|
||
private init() {}
|
||
|
||
// MARK: - Public Methods
|
||
|
||
/// 开始显示 loading
|
||
/// - Parameters:
|
||
/// - shouldShowLoading: 是否显示 loading 动画
|
||
/// - shouldShowError: 是否显示错误信息
|
||
/// - Returns: 唯一的加载 ID,用于后续更新状态
|
||
func startLoading(shouldShowLoading: Bool = true, shouldShowError: Bool = true) -> UUID {
|
||
let loadingId = UUID()
|
||
|
||
let loadingItem = APILoadingItem(
|
||
id: loadingId,
|
||
state: .loading,
|
||
shouldShowError: shouldShowError,
|
||
shouldShowLoading: shouldShowLoading
|
||
)
|
||
|
||
// 🚨 重要:必须在主线程更新 @Published 属性,否则会崩溃!
|
||
DispatchQueue.main.async { [weak self] in
|
||
self?.loadingItems.append(loadingItem)
|
||
}
|
||
|
||
return loadingId
|
||
}
|
||
|
||
/// 更新 loading 状态为成功
|
||
/// - Parameter id: 加载 ID
|
||
func finishLoading(_ id: UUID) {
|
||
DispatchQueue.main.async { [weak self] in
|
||
self?.removeLoading(id)
|
||
}
|
||
}
|
||
|
||
/// 更新 loading 状态为错误
|
||
/// - Parameters:
|
||
/// - id: 加载 ID
|
||
/// - errorMessage: 错误信息
|
||
func setError(_ id: UUID, errorMessage: String) {
|
||
DispatchQueue.main.async { [weak self] in
|
||
guard let self = self else { return }
|
||
|
||
// 查找并更新项目
|
||
if let index = self.loadingItems.firstIndex(where: { $0.id == id }) {
|
||
let currentItem = self.loadingItems[index]
|
||
|
||
// 只有需要显示错误时才更新状态
|
||
if currentItem.shouldShowError {
|
||
let errorItem = APILoadingItem(
|
||
id: id,
|
||
state: .error(message: errorMessage),
|
||
shouldShowError: true,
|
||
shouldShowLoading: currentItem.shouldShowLoading
|
||
)
|
||
self.loadingItems[index] = errorItem
|
||
|
||
// 设置自动清理
|
||
self.setupErrorCleanup(for: id)
|
||
} else {
|
||
// 不显示错误,直接移除
|
||
self.loadingItems.removeAll { $0.id == id }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 手动移除特定的加载项
|
||
/// - Parameter id: 加载 ID
|
||
private func removeLoading(_ id: UUID) {
|
||
cancelErrorCleanup(for: id)
|
||
// 🚨 重要:必须在主线程更新 @Published 属性
|
||
if Thread.isMainThread {
|
||
loadingItems.removeAll { $0.id == id }
|
||
} else {
|
||
DispatchQueue.main.async { [weak self] in
|
||
self?.loadingItems.removeAll { $0.id == id }
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 清空所有加载项(用于应急情况)
|
||
func clearAll() {
|
||
DispatchQueue.main.async { [weak self] in
|
||
guard let self = self else { return }
|
||
|
||
// 取消所有清理任务
|
||
self.errorCleanupTasks.values.forEach { $0.cancel() }
|
||
self.errorCleanupTasks.removeAll()
|
||
|
||
// 清空所有项目
|
||
self.loadingItems.removeAll()
|
||
}
|
||
}
|
||
|
||
// MARK: - Computed Properties
|
||
|
||
/// 是否有正在显示的 loading
|
||
var hasActiveLoading: Bool {
|
||
if Thread.isMainThread {
|
||
return loadingItems.contains { $0.state == .loading && $0.shouldDisplay }
|
||
} else {
|
||
return false
|
||
}
|
||
}
|
||
|
||
/// 是否有正在显示的错误
|
||
var hasActiveError: Bool {
|
||
if Thread.isMainThread {
|
||
return loadingItems.contains { $0.isError && $0.shouldDisplay }
|
||
} else {
|
||
return false
|
||
}
|
||
}
|
||
|
||
// MARK: - Private Methods
|
||
|
||
/// 设置错误信息自动清理
|
||
/// - Parameter id: 加载 ID
|
||
private func setupErrorCleanup(for id: UUID) {
|
||
let workItem = DispatchWorkItem { [weak self] in
|
||
self?.removeLoading(id)
|
||
}
|
||
|
||
errorCleanupTasks[id] = workItem
|
||
|
||
DispatchQueue.main.asyncAfter(
|
||
deadline: .now() + APILoadingConfiguration.errorDisplayDuration,
|
||
execute: workItem
|
||
)
|
||
}
|
||
|
||
/// 取消错误清理任务
|
||
/// - Parameter id: 加载 ID
|
||
private func cancelErrorCleanup(for id: UUID) {
|
||
errorCleanupTasks[id]?.cancel()
|
||
errorCleanupTasks.removeValue(forKey: id)
|
||
}
|
||
}
|
||
|
||
// MARK: - Convenience Extensions
|
||
|
||
extension APILoadingManager {
|
||
|
||
/// 便捷方法:执行带 loading 的异步操作
|
||
/// - Parameters:
|
||
/// - shouldShowLoading: 是否显示 loading
|
||
/// - shouldShowError: 是否显示错误
|
||
/// - operation: 异步操作
|
||
/// - Returns: 操作结果
|
||
func withLoading<T>(
|
||
shouldShowLoading: Bool = true,
|
||
shouldShowError: Bool = true,
|
||
operation: @escaping () async throws -> T
|
||
) async -> Result<T, Error> {
|
||
let loadingId = startLoading(
|
||
shouldShowLoading: shouldShowLoading,
|
||
shouldShowError: shouldShowError
|
||
)
|
||
|
||
do {
|
||
let result = try await operation()
|
||
finishLoading(loadingId)
|
||
return .success(result)
|
||
} catch {
|
||
setError(loadingId, errorMessage: error.localizedDescription)
|
||
return .failure(error)
|
||
}
|
||
}
|
||
}
|