
- 在DetailFeature和FeedListFeature中增强点赞功能的状态管理,确保用户交互流畅。 - 新增API加载效果视图,提升用户在操作过程中的反馈体验。 - 更新视图组件以支持点赞加载状态,优化用户界面交互。 - 改进错误处理逻辑,确保在API请求失败时提供友好的错误提示。
152 lines
4.9 KiB
Swift
152 lines
4.9 KiB
Swift
import Foundation
|
||
import SwiftUI
|
||
import Combine
|
||
|
||
// MARK: - API Loading Manager
|
||
|
||
/// 全局 API 加载状态管理器
|
||
///
|
||
/// 该管理器负责:
|
||
/// - 跟踪多个并发的 API 调用状态
|
||
/// - 管理 loading 和错误信息的显示
|
||
/// - 自动清理过期的错误信息
|
||
/// - 提供线程安全的状态更新
|
||
@MainActor
|
||
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
|
||
)
|
||
loadingItems.append(loadingItem)
|
||
return loadingId
|
||
}
|
||
/// 更新 loading 状态为成功
|
||
/// - Parameter id: 加载 ID
|
||
func finishLoading(_ id: UUID) {
|
||
removeLoading(id)
|
||
}
|
||
/// 更新 loading 状态为错误
|
||
/// - Parameters:
|
||
/// - id: 加载 ID
|
||
/// - errorMessage: 错误信息
|
||
func setError(_ id: UUID, errorMessage: String) {
|
||
guard let index = loadingItems.firstIndex(where: { $0.id == id }) else { return }
|
||
let currentItem = loadingItems[index]
|
||
if currentItem.shouldShowError {
|
||
let errorItem = APILoadingItem(
|
||
id: id,
|
||
state: .error(message: errorMessage),
|
||
shouldShowError: true,
|
||
shouldShowLoading: currentItem.shouldShowLoading
|
||
)
|
||
loadingItems[index] = errorItem
|
||
setupErrorCleanup(for: id)
|
||
} else {
|
||
loadingItems.removeAll { $0.id == id }
|
||
}
|
||
}
|
||
/// 手动移除特定的加载项
|
||
/// - Parameter id: 加载 ID
|
||
private func removeLoading(_ id: UUID) {
|
||
cancelErrorCleanup(for: id)
|
||
loadingItems.removeAll { $0.id == id }
|
||
}
|
||
/// 清空所有加载项(用于应急情况)
|
||
func clearAll() {
|
||
errorCleanupTasks.values.forEach { $0.cancel() }
|
||
errorCleanupTasks.removeAll()
|
||
loadingItems.removeAll()
|
||
}
|
||
// MARK: - Computed Properties
|
||
/// 是否有正在显示的 loading
|
||
var hasActiveLoading: Bool {
|
||
loadingItems.contains { $0.state == .loading && $0.shouldDisplay }
|
||
}
|
||
/// 是否有正在显示的错误
|
||
var hasActiveError: Bool {
|
||
loadingItems.contains { $0.isError && $0.shouldDisplay }
|
||
}
|
||
// 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: 操作结果
|
||
@MainActor
|
||
func withLoading<T: Sendable>(
|
||
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)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - Global Convenience Methods
|
||
|
||
/// 全局便捷方法:同步设置错误信息(fire-and-forget)
|
||
/// - Parameters:
|
||
/// - id: 加载 ID
|
||
/// - errorMessage: 错误信息
|
||
func setAPILoadingErrorSync(_ id: UUID, errorMessage: String) {
|
||
Task {
|
||
await MainActor.run {
|
||
APILoadingManager.shared.setError(id, errorMessage: errorMessage)
|
||
}
|
||
}
|
||
}
|