Files
photo-live/apps/mobile/ios/Runner/UploadBridge.swift
T
浅念 42e1c2e34b feat(mobile): ios background upload bridge (compile-gated at G3)
Swift 侧(协议与 Android UploadPlugin.kt 逐字一致,编译/真机留 G3):
- UploadBridge.swift:通道/方法/事件/失败码常量 + registerTask 参数解析
  (镜像 UploadProtocol.kt)
- UploadEventBus.swift:单调 seq + 终态 ack 缓存重放(镜像
  UploadEventBus.kt),iOS 增强——未 ack 终态落盘(系统后台拉起投递的
  回调在进程重启后同样重放,技术方案 §6.2 持久化原生回调)
- UploadTaskStore.swift:taskId→URLSession taskIdentifier 映射 plist 持久化
  (Application Support;对应 Android WorkManager 自持映射)
- UploadPlugin.swift:URLSession background
  (com.photolive.photo_live.uploads)+ delegate 失败分类(2xx→completed+etag
  / 403→needsRenew / 4xx→HTTP_4XX / 网络→重试上限后 NETWORK,镜像
  UploadPutExecutor/UploadWorker)、原生重试(5 次上限、10s 指数退避)、
  进度 500ms 节流、queryTask/cancelTask/ackEvents、静默取消
- AppDelegate.swift:implicit engine 通道注册 + 启动重建会话 +
  handleEventsForBackgroundURLSession completionHandler 暂存
- Info.plist:UIBackgroundModes 有意不声明(background URLSession 无需;
  计划措辞按技术方案 §6.2 修正,见 README-ios.md)
- project.pbxproj 登记新 Swift 文件;README-ios.md(Xcode 步骤 + G3 验证
  清单:实际文件上传/持久化回调窗口/服务端补偿/重启重联校准/强制退出/
  低电量延迟/100 张批量)

Dart:NativeTransport 选择矩阵并入 iOS(Platform.isAndroid || isIOS,
FORCE_DART_TRANSPORT 覆盖不变)——协议两侧一致,无新通道代码。

flutter analyze 0 issue;flutter test 219 全绿。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-19 03:42:49 +08:00

108 lines
3.9 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
/// 上传桥接协议常量与 registerTask 参数解析(P3 计划 Task 8)。
///
/// 与 Android 侧 `UploadProtocol.kt` 逐字对齐:通道名、方法名、事件名、
/// 失败码完全一致;Dart 侧 `lib/features/uploads/native_transport.dart`
/// 的 [UploadBridgeChannels] 为同一协议的单源镜像。
enum UploadBridge {
/// 命令通道(registerTask / cancelTask / queryTask / ackEvents)。
static let methodChannel = "photolive/uploads"
/// 事件流(progress / completed / needsRenew / failed,带单调 seq)。
static let eventChannel = "photolive/uploads/events"
static let methodRegister = "registerTask"
static let methodCancel = "cancelTask"
static let methodQuery = "queryTask"
static let methodAck = "ackEvents"
static let eventProgress = "progress"
static let eventCompleted = "completed"
static let eventNeedsRenew = "needsRenew"
static let eventFailed = "failed"
/// 网络不可达/超时(重试上限耗尽后上报)。
static let failNetwork = "NETWORK"
/// 4xx 客户端错误(status 随事件携带;403 单独走 needsRenew)。
static let failHttp4xx = "HTTP_4XX"
/// 5xx 服务端错误(重试上限耗尽后上报)。
static let failServerError = "SERVER_ERROR"
/// 源文件缺失(受管副本被清理,不可恢复)。
static let failSourceMissing = "SOURCE_MISSING"
/// 后台 URLSession 标识(App 重启后系统按此重联会话)。
static let sessionIdentifier = "com.photolive.photo_live.uploads"
}
/// registerTask 参数(Dart → 原生),字段与 Kotlin `UploadRegisterSpec` 一致。
struct UploadRegisterSpec {
let taskId: String
let url: String
let method: String
let headers: [String: String]
let filePath: String
let uploadId: String?
let attempt: Int
}
/// 参数形状不符(插件层转 bad_args 错误,镜像 Kotlin IllegalArgumentException)。
struct UploadBridgeBadArgsError: Error {
let message: String
}
/// registerTask 参数解析(纯函数;与 Kotlin `parseRegisterSpec` 同语义)。
///
/// 通道解码所得字典为 [String: Any](Flutter 标准编解码 null → NSNull),
/// 显式 null 与类型不符同样归为 bad_args。
func parseRegisterSpec(_ args: [String: Any]) throws -> UploadRegisterSpec {
func str(_ key: String) throws -> String {
guard let v = args[key] as? String else {
throw UploadBridgeBadArgsError(
message: args[key] == nil ? "missing '\(key)'" : "'\(key)' must be a String"
)
}
return v
}
let taskId = try str("taskId")
let url = try str("url")
let methodRaw = try str("method")
let method = methodRaw.isEmpty ? "PUT" : methodRaw
let filePath = try str("filePath")
var headers: [String: String] = [:]
if let raw = args["headers"] as? [String: Any] {
for (k, v) in raw {
guard let value = v as? String else {
throw UploadBridgeBadArgsError(message: "'headers' must be Map<String, String>")
}
headers[k] = value
}
} else if args["headers"] != nil {
throw UploadBridgeBadArgsError(message: "'headers' must be a Map")
}
var uploadId: String?
var attempt = 0
if let metadata = args["metadata"] as? [String: Any] {
uploadId = metadata["uploadId"] as? String
if let v = metadata["attempt"] as? NSNumber {
attempt = v.intValue
} else if let v = metadata["attempt"] as? Int {
attempt = v
} else if metadata["attempt"] != nil {
throw UploadBridgeBadArgsError(message: "'metadata.attempt' must be an int")
}
}
return UploadRegisterSpec(
taskId: taskId,
url: url,
method: method,
headers: headers,
filePath: filePath,
uploadId: uploadId,
attempt: attempt
)
}