一个勾选项,背后是两个 App
发布页增加“同时发布到另一个应用”,听起来只是一个 checkbox。背后却是不同 bundle ID、代码库、排期和发版节奏,以及几百 MB 的导出视频。iOS 用沙盒隔开进程,组织边界也隔开团队。
真正的问题是:双方约定让什么跨过边界?这个技术问题,后来也改变了我理解跨团队协作的方式。
方案一:传文件
编辑器将视频导出到自己的沙盒:
let exportURL = FileManager.default
.temporaryDirectory
.appendingPathComponent("export.mp4")
再通过系统分享面板交出去:
let controller = UIActivityViewController(
activityItems: [exportURL],
applicationActivities: nil
)
present(controller, animated: true)
接收方的 Share Extension 从 NSItemProvider 加载表示,并复制到可以保留的位置:
itemProvider.loadFileRepresentation(
forTypeIdentifier: UTType.movie.identifier
) { url, error in
// the URL is temporary; copy the file
// into our own container before it disappears
}
扩展通常再使用 App Group 容器交给主应用。它是通用方案,不要求双方来自同一组织,也不需要相册权限,适合向任意第三方分享。
代价是可能保留多份应用拥有的文件,几百 MB 视频的复制会产生明显存储与等待成本;产品已经选择目的地,用户却还要绕过一次系统分享面板。
方案二:传标识符
先把导出结果保存到系统相册:
var placeholderIdentifier: String?
PHPhotoLibrary.shared().performChanges({
let request = PHAssetChangeRequest
.creationRequestForAssetFromVideo(atFileURL: exportURL)
placeholderIdentifier = request
.placeholderForCreatedAsset?.localIdentifier
}) { saved, error in
guard saved, let identifier = placeholderIdentifier else {
// Surface the save failure; do not hand off an unresolved identifier.
return
}
routeToSocialApp(assetIdentifier: identifier)
}
交给另一方的只是 localIdentifier:
69D53A4B-8F24-4D7A-.../L0/001
接收方通过 PhotoKit 解析:
let assets = PHAsset.fetchAssets(
withLocalIdentifiers: [identifier],
options: nil
)
guard let asset = assets.firstObject else { return }
PHImageManager.default().requestAVAsset(
forVideo: asset,
options: nil
) { avAsset, _, _ in
// feed the publish pipeline
}
相册成为双方可以访问的系统存储,跨边界传递的是引用。它不是字面意义上的共享内存;Photos 仍可能按系统策略下载、转码或生成资源表示。
文件路径为接收者保留应用副本;标识符路径先导入系统相册,再传递引用。
这不需要私有文件通道,只需要双方可访问的系统服务和一份小型版本化协议。
SDK 真正承载的是协议
发送方校验并路由小请求;接收方再次校验、解析相册资源,以一次性 state 关联异步结果。
发送前检查资源存在、类型与时长数量符合约束、目标应用可用且协议版本兼容。尽早失败,可以避免把本地可发现的问题留给另一方解释。
请求载荷只保留媒体引用和业务元数据:
{
"protocol_version": 3,
"media_type": "video",
"local_identifiers": ["69D53A4B-.../L0/001"],
"state": "7F92E1C4-9B7A-4B15-A3F1-82C2F769D8F4",
"title": "…",
"sync_publish": true,
"callback_id": "editor-v1"
}
send() 构造 URL 并请求系统打开目标应用,负责地址与版本,不传输视频字节。到达接收方后,必须重新验证版本、资源、权限与业务约束。发送端校验改善体验,接收端校验维护自身不变量。
发布是异步且可取消的。请求带密码学随机、一次性的 state,完成、取消或失败都回传它,用于关联待处理请求。
接收者不应打开载荷里的任意 callback URL,而应把白名单 callback ID 映射到事先约定的 scheme 或 universal link。发送者校验回调来源、设置超时、只消费 token 一次,拒绝无请求和重放结果。自定义 URL scheme 本身不能证明调用者身份。
request.send { response in
switch response.result {
case .success: markSyncPublished()
case .cancelled: revertCheckbox()
case .failure(let error): surface(error)
}
}
两条路径各自付出什么
| 维度 | 传文件 | 传标识符 |
|---|---|---|
| 跨边界内容 | 文件字节 | 小型资源 ID |
| 持久存储 | 接收应用或 App Group 的副本 | 清理导出临时文件后的 Photos 资源 |
| 相册权限 | 不需要 | 两侧都需要符合访问条件 |
| 体验 | 经过分享面板 | 直接进入约定的发布流程 |
| 适用范围 | 任意兼容应用 | 接入该 SDK 的应用 |
| 失败情况 | 复制失败、扩展资源限制 | 资源被删除、权限不足 |
决定性的要求是:同步发布不能额外保留一份由接收应用长期拥有的视频副本。导入时仍有临时导出文件,系统也可能生成内部表示;保存成功后,编辑器可以清理临时文件,后续目的地只接收标识符。
iOS 14 的有限照片库权限让情况更复杂:接收应用可能无权访问刚保存的资源,用户也可能在交接后删除视频。这些都应成为显式失败状态,而不是假设系统存储永远可用。
为什么这也像团队协作
沙盒像团队边界,不能假设对方能直接读取自己的代码、排期和上下文。复制大文件,也像不断复制文档、数据快照和工具模块:副本从生成开始就可能过期。
更好的接口是一个小引用,加一个双方认可的事实来源。工单 ID、schema 名称和文档链接,允许对方在真正需要时读取当前版本。
协议版本对应不同发版节奏:不能要求双方永远同步上线。带 state 的回调对应异步协作:明确请求、关联标识,允许答案晚到,而不是让会议或整个工作流同步阻塞。
双方校验也不是不信任。请求方尽量准备充分,接收方仍然对自己的验收条件负责。
最终,勾选项上线了,视频本身不再通过应用间的 URL 通道传递。更长久的经验是:跨越边界时,先找双方都能访问的权威来源,再传递足够小、可版本化、可关联的引用。优化协议,往往比继续加粗传输管道更有效。