- 发送端流式(R-iOS-4):新增 FileSource 抽象(web/src/features/transfer/source.ts),iOS 经 HTTP Range 惰性拉取(原生 WKURLSchemeHandler 认 Range 头 seek 回 206),整文件不进 WebView 内存;web/桌面经 fileSource 包装字节不变。p2p/relay/transfer 改吃 FileSource - 后台续传:iOS 26 BGContinuedProcessingTask(BackgroundTaskManager + AppDelegate 注册 + BGTaskSchedulerPermittedIdentifiers),引擎传输进度驱动系统进度 UI - APNs:后端 internal/apns(ES256 .p8 JWT + HTTP/2,仿 internal/push)+ push_subscriptions.platform 列 + POST /api/push/apns/register + transfer/message 未投递分支分流;原生 PushRegistry 授权 + 设备令牌经引擎桥 registerPush 上报;aps-environment + remote-notification 后台模式 - Share Extension:CDropShare appex 抓文件 → App Group 收件箱 → cdrop://share 深链唤主程序选设备发送(只交接、不跑引擎) - 控制中心剪贴板两控件:CDropWidgets widgetkit appex(ControlWidget + AppIntent + 纯原生 ClipboardClient REST);用专用 broker 设备会话(主程序经引擎 provisionWidgetSession 代铸独立 device_id 会话存 App Group,控件自刷不与引擎抢 refresh 轮换) - 真机分发预备:committed 签名改 ad-hoc 使模拟器 entitlement 生效;真机手动签名 scaffold(Signing.xcconfig 仅 device SDK 套 Manual + gitignore 的 Local.xcconfig 填 Team/profile)+ just ios-device / ios-devices 全 CLI 装机;包名改 net.commilitia.Commilitia-Drop(含 .share/.widgets,BG task 前缀同改);REALDEVICE.md 重写为门户 + CLI 装机手册 - I18n.swift 移 Shared/ 供三 target 共用;i18n 加 ios.bg/share/control.* 键 - ultracode 多 agent 审查修复(0 HIGH,2 MED + 7 LOW):分享 send 后留收件箱致重发→move 私有暂存;WidgetSession 锁屏文件保护→UntilFirstUserAuthentication;outgoing/安全作用域登出释放;控件 device_id 登出清理;并发控件 refresh CAS-before-clear;APNs 读 reason + prune bad token + bust 过期 JWT;APNSEnv 大小写归一;Share-ext completeRequest 挪进 open 回调
27 KiB
cdrop iOS 客户端实施计划
原生 SwiftUI 界面(液态玻璃)· 离屏无头 WebView 复用传输引擎 · 控制中心剪贴板 + Share Extension + APNs 状态:架构已定(2026-06-24 修订),账号无关部分即刻可开工;真机 / 推送 / 扩展实测硬等 Apple Developer 账号。 关联:根
README.md、desktop/PLAN.md、internal/push、ios/PARITY.md(web↔iOS 同步检查单)。记忆cdrop-state/cdrop-deploy-pointers。
0. 定位与关键决策(2026-06-24 修订)
定位变更:iOS 客户端不再是“web UI 装进 WebView”,而是“原生 SwiftUI 界面 + 复用的无头传输引擎”。起因——新增“服从 iOS 设计语言(液态玻璃)”硬要求,与“WebView 里跑 React UI”直接冲突:液态玻璃是 iOS 26 系统级材质,只有原生 SwiftUI 的 .glassEffect 拿得到,网页只能 CSS 仿。故 UI 转原生,但保留引擎复用。
决策 A(修订):架构 = 原生 SwiftUI 壳 + 离屏无头 WebView 引擎
取代 2026-06-18 的“WebView 复用 React UI”决策。
- 所有可见 UI 用原生 SwiftUI(iOS 26 液态玻璃;旧版优雅回落,见 §7)。
- 文件传输的 P2P + relay + 会话 + 信令逻辑 = 现有
web/src的 JS 引擎本体,跑在离屏(无 UI)WKWebView 里,原生经桥驱动。零重写、保住同内网 P2P、行为与 web 天然同构(上轮刚修的“按已交付字节算进度”“抽干再标完成”等自动一致)。 - 兜底方案 B:若真机上无头 WKWebView 跑 WebRTC DataChannel 不稳,退“全原生 + relay-only 重写 + 放弃 P2P”。该验证留到真机可测时优先做(R-iOS-3)。
桥方向与桌面相反(关键,易踩坑):
- 桌面(Wails):React UI 在 WebView 顶层,调 DOWN 到 Go 拿原生能力。
- iOS(arch A):SwiftUI 原生在顶层,调 DOWN 到离屏 WebView 的 JS 引擎跑传输。
- 即 iOS 的 WebView 是“无头 worker”,不是“UI 容器”。需要一个独立的无头引擎入口(不加载 React、只装传输模块 + RPC 桥),而非复用 React app 再隐藏 UI。
决策 B(修订):分发 = 非 App Store,但仍守苹果开发标准
- 必须付费 Apple Developer Program(ADP,$99/年):cdrop 四项能力——APNs、App Groups、Keychain 共享、Associated Domains——免费 Personal Team 一律不支持(账号策略,非 OS 封锁),且免费证书 7 天过期。故“等账号”=等付费 ADP。
- 分发走 Ad Hoc(最贴近桌面 Dropbox 旁加载的等价物):100 台 / 年上限、设备 UDID 须预先登记、profile 年度更新。自用(自己几台设备)足够;比桌面“下载即用”多一道 UDID 登记摩擦——iOS 无更省的合规途径。
- 备选 TestFlight:体验最好(无需 UDID、可达万级),但需经 Apple Beta 审核、build 90 天过期——与“不上架”初衷部分相悖,留作扩面再议。
- Enterprise(ADEP $299/年):无设备上限,但仅限“内部员工”,对外分发违反协议、证书可被吊销——不取。
- 不过审 ≠ 不守标准:仍遵守 HIG、液态玻璃规范、App Intents / WidgetKit 约定、隐私(权限说明 / just-in-time 请求)、entitlements 最小化。原 R-iOS-5(Guideline 4.2 过审风险)在 Ad Hoc 下作废;若走 TestFlight 则 Beta 审核仍在。
决策 C(新):剪贴板 = 控制中心两控件,零推送
- 通道用持久的
/api/clipboard(REST、LWW、版本探针、作用域令牌可访问),不用 ephemeral 的/api/message。 - iOS 18 控制中心控件(
ControlWidget+ App Intent)提供“上 / 下”两件:上 = 读UIPasteboard→PUT /api/clipboard;下 =GET→ 写UIPasteboard。用户主动点、零推送(频繁剪贴板变动推送干扰极大)。 - 平台天花板:程序化读剪贴板弹一次系统横幅(写不弹);后台无法监听复制。故“后台自动同步”降级为“一键发送 / 一键拉取”。
- 这条链纯原生 URLSession,不依赖无头 WebView——与文件传输引擎彻底解耦,互不拖累。
决策 D(新):视图层分叉,先吃强约束、残余进检查单
接受 web(React)与 iOS(SwiftUI)视图层是两套实现(液态玻璃决定不可能像素一致)。一致性按两层管,详见 §5:
- 强约束同步(机制保证、漂移不可能):① 传输行为靠 arch A 共享引擎;② i18n 文案单一源(iOS 读 web 那份 JSON);③ 品牌资产 + 主色单一源;④ 后端 API 即共享契约。
- 检查单(机制管不住、人判定的残余):元素 / 屏 / 功能存在性对等、非引擎驱动的新流程、后端字段变更提醒、允许分叉登记。
- 明确不做:设计令牌 codegen 管线(过度工程,且与液态玻璃系统材质 / 语义色 / SF Symbol 母语相冲)——间距 / 圆角 / 字阶归“允许分叉”,仅品牌主色 + logo 作共享源。
决策 E(新):权限策略 = 持久权限首启显式询问 + 缺失则降级或阻塞提示
- 持久型权限(本地网络、相机)在首次启动的引导中显式询问:先用一屏说明用途、再触发系统弹窗——既满足“首启显式获取”,又不踩 HIG 的“无上下文裸弹窗”反模式。这类权限一次授予长期持久(iOS 18 有重装后状态不重置的已知 bug,需 UI 兜底)。
- 缺失 / 拒绝时按特性二选一:① 有兜底则降级——本地网络→回退中继并提示“仅中继、无法同内网直连”;通知→无后台推送但前台仍可用。② 无兜底则提示“无权限”并阻塞该特性——相机→扫码不可用,提示 + 跳系统设置;阻塞的是该特性、非整个 app。
- 不拿任一权限当整个 app 的门槛(HIG 5.1.2)。详见 §9.2 逐权限映射。
1. 后端现状(决定 iOS 能做什么,2026-06-24 扫描)
| 能力 | 结论 | 对 iOS 的含义 |
|---|---|---|
| 文件传输 | 纯实时(P2P / 实时 relay,relay 是内存环 64MiB、2 分钟空闲释放),零落盘暂存 | Share Extension 不靠服务端暂存;接收方须同时在线 |
剪贴板 /api/clipboard |
持久 LWW + REST,作用域令牌可访问 | 决策 C 的纯原生链路 |
消息 /api/message |
临时、不持久、不可轮询 | 文本同步走剪贴板通道,消息仅尽力而为实时 |
| 作用域令牌 | 仅 clipboard 一个 scope,rejectScoped 挡死 /transfer |
发送须完整会话;Share Extension 用交接而非自带令牌 |
| 推送 | 仅 Web Push(VAPID),无 APNs | APNs 须新建(§3) |
| 原生鉴权 | IdP bearer(RS256)/ 扫码自签会话;设备隐式登记(X-Device-Name) |
原生 app 可正常登录持会话 |
Share Extension 落法(据上):抓文件 → 写 App Group 容器 → 深链唤起主程序选设备发送(接收方须在线、大文件主程序稳跑、扩展不持完整令牌更安全)。
2. 桥协议(arch A 的接口契约,账号无关、可先冻结)
原生 ↔ 无头 JS 引擎的 RPC:WKScriptMessageHandler(JS→Swift)+ evaluateJavaScript(Swift→JS),类比 Wails 的 EventsEmit / bound methods,但方向相反(原生在顶层)。
- 平台判定:
isIOSShell()(web/src/net/ios.ts)查window.webkit.messageHandlers.cdropEngine在场(类比isDesktop()查window.go.main.App)。无头入口(web/src/engine/main.ts)据此装配桥。 - 注入:原生在加载 bundle 前经 WKUserScript 注入
window.__CDROP_BOOT__ = { session, device_name, api_base, device_type:"ios" },store 在 import 时即水合(沿桌面同一注入范式);JS 永不持有长寿命密钥(refresh_token 只在原生 Keychain)。 - 原生 → 引擎命令(原生
evaluateJavaScript调window.__cdropEngineEvent(name, payload)):sendFile { target, url, name, size, type? }、cancelTransfer { sessionId }、switchToRelay { sessionId }、session { access_token, user }(续期令牌)、shutdown。 - 引擎 → 原生:① 单向通知(
postMessage({ notify, payload })):ready/transfers/transferDone/sendStarted/error;② 请求 / 响应 RPC(postMessage({ id, method, payload }),原生回调window.__cdropEngineResolve(id, ok, value)):接收落盘saveDownload/beginDownload/appendDownload/finalizeDownload/abortDownload(data 为 base64,复用桌面同形)。剪贴板不走此桥(决策 C 纯原生)。 - 复用边界:接收侧字节桥沿用桌面已验证的 base64 分批、有界内存(iOS 上限更小,64MB/16MB 避 jetsam);发送侧经
url取回文件为唯一新实现点——当前整文件 fetch 进内存(小 / 中文件可行),大文件按 slice 向原生拉取是真机优化点(R-iOS-4)。 - 已落地(web 侧,账号无关、
tsc/vite build双过、对现网零影响):net/ios.ts(桥 +isIOSShell+ 接收封装 +notifyNative)、engine/main.ts(无头入口)、incomingSink.ts(通用混合槽路由 iOS)、engine.html+vite.config.ts多入口。 - 加载源(关键):无头引擎必须从安全上下文加载——
RTCPeerConnection在非安全 origin(如自定义cdrop://scheme,WebKit 视作 non-secure context)下可能被禁。故引擎页走https://:线上drop.commilitia.net或应用内本地 https 服务的静态 bundle;自定义 scheme 仅用于WKURLSchemeHandler给 JS 流式喂文件字节,不可用它直载引擎页。见 R-iOS-3。
3. 服务端 APNs 通道(账号无关的结构,.p8 到位再落代码)
新增 internal/apns 与 internal/push 并列,只换发送后端:
- 库:APNs 走 HTTP/2 + JWT(
.p8的 ES256 签名),可用github.com/sideshow/apns2,或自手写(与现有 go-jose 一致)。 - 配置(koanf,
CDROP_APNS_*,全可选,缺失即 iOS 推送惰性关闭):APNS_KEY_PATH、APNS_KEY_ID、APNS_TEAM_ID、APNS_TOPIC(bundle id)、APNS_ENV。 - 存储:
push_subscriptions加platform列,iOS 行存 APNs token。SubscriptionID仍用 token 哈希保幂等 upsert。 - 端点:
POST /api/push/apns/register {device_token, locale}(鉴权 +X-Device-Name,与/api/push/subscribe同组)。 - 触发:在
transfer:incoming/transfer:done|failed/message的“未投递”分支按platform分流。文案复用internal/push的localize。注意——剪贴板不触发推送(决策 C 走控制中心拉取)。
代码结构账号无关,但真发 / 真测要 .p8,故与 iOS 客户端同期落(账号到位后),避免悬空实现。
4. 里程碑(修订)
| 里程碑 | 内容 | 账号依赖 |
|---|---|---|
| I0 账号与证书前置 | 付费 ADP($99/年,4 项能力全要);主 app + 各扩展 App ID(扩展 id 须主 app 前缀);开启 Push / App Groups / Keychain / Associated Domains;注册 App Group ID;APNs Auth Key .p8(仅一次下载);Distribution 证书 + Ad Hoc Profile(登记设备 UDID) |
硬等付费账号 |
| I1 原生骨架 + 无头引擎装配 | Xcode 工程(SwiftUI);离屏 WKWebView 载无头引擎入口;isIOSShell 注入桥 |
模拟器可跑 UI;引擎入口 web 侧账号无关 |
| I2 登录态 | ASWebAuthenticationSession PKCE / 扫码;refresh_token 存 Keychain;session 注入 JS |
模拟器可跑 |
| I3 传输(核心) | 无头引擎桥(send 自定义 scheme 流式 / receive 复用 Begin-Append-Finalize)+ 原生传输 UI(液态玻璃、含进度 / 速度 / 状态) |
真机硬等账号 |
| I4 剪贴板 | 控制中心两控件 + App Intents;纯原生 /api/clipboard 读写 + 版本探针 |
控件真机硬等账号;REST 链路可先写 |
| I5 Share Extension | 抓文件 → App Group → 深链主程序选设备发送 | 真机硬等账号 |
| I6 APNs | 服务端通道(§3)+ 原生注册;不含剪贴板 | .p8 + 真机硬等账号 |
| I7 打磨与分发 | 后台窗口;图标 / 启动屏(复用品牌资产);旁加载分发 | 硬等账号 |
实现状态(2026-06-27):I1/I2/I3/I4/I5/I6 的代码全部落地——发送端流式(R-iOS-4,FileSource + 原生 Range,整文件不进 WebView 内存);后台续传(BGContinuedProcessingTask);APNs(后端
internal/apnsES256 + 原生注册经引擎桥);Share Extension(App Group 收件箱 +cdrop://share深链);控制中心两控件(专用 broker 设备会话,纯原生 REST,不与引擎抢 refresh 轮换)。模拟器验 + ultracode 多 agent 审查修讫(0 HIGH,修 2 MED + 7 LOW)。I0 账号 / 证书 + 真机签名 / 真发推送 / 旁加载分发仍账号门控——手册ios/CDrop/REALDEVICE.md(手动签名 +just ios-device全 CLI 装机,门户手动建)。包名net.commilitia.Commilitia-Drop。
5. 一致性:强约束同步 vs 检查单(决策 D 展开)
检查单是兜底,不是默认。 先尽量用强约束(单一真源、两端机械消费、漂移不可能),管不住的残余才进检查单(见 ios/PARITY.md)。
强约束(机制保证)
- 传输行为 → arch A 共享 JS 引擎。 整套状态机 / 进度 / 完成 / 错误 / 协议无法漂移,零额外成本。
- i18n 文案 → 单一源。 iOS 原生侧薄 loader 读 web 那份 i18n JSON(随 bundle 或从服务端取),不写转换器、不做
.xcstrings镜像。缺键即露。 - 品牌资产 + 主色 → 单一源。 资产沿用 Dropbox 那套(web+desktop 已接);品牌主色 + logo 作小共享常量。
- 后端 API → 后端即共享契约。 两端瘦客户端对齐同一 JSON 形状,不加 codegen(过度工程);字段变更的人工残余进检查单。
检查单(人判定的残余)
见 ios/PARITY.md:① 元素 / 屏 / 功能存在性对等;② 非引擎驱动的新流程 / 状态;③ 后端 API 字段变更提醒;④ 允许分叉登记。
明确不做
设计令牌 codegen 管线——过度工程,且与液态玻璃系统材质 / 语义色 / SF Symbol 母语相冲。间距 / 圆角 / 字阶归“允许分叉”。
6. 现在能做 vs 等账号(修订)
账号无关、即刻可做:
- 本计划 +
ios/PARITY.md同步检查单(✔ 本轮)。 - web 侧无头引擎入口(✔ 已实现,账号无关):
net/ios.ts(桥 +isIOSShell()+ 接收封装 +notifyNative)、engine/main.ts(不加载 React 的独立入口:水合 store →startHub→ 暴露 RPC + 订阅推事件)、incomingSink.ts(通用混合槽路由 iOS,内存上限 64MB/16MB 避 jetsam)、engine.html+vite.config.ts多入口。tsc/vite build双过、对现网 web / 桌面零影响。 - 桥协议契约冻结(§2,✔ 已与实现对齐)。
- i18n 单一源:现有 catalog 已是扁平
{ key: string }结构(src/i18n/locales/*.ts),数据形态即可导出 JSON;原生侧薄 loader + 构建期 emit 待原生工程就位时同期落(避免向无消费方空导出)。 - 剪贴板 REST 链路(
/api/clipboard读写 + 版本探针)的数据流设计(控件实现等账号,协议可先定)。 - iOS 原生 scaffold(✔ 已实现,账号无关):
ios/CDrop/(xcodegenproject.yml)——液态玻璃壳(TabView +.tabBarMinimizeBehavior+.buttonStyle(.glassProminent))+ 离屏引擎WKWebView宿主 + 桥EngineController(对侧契约同net/ios.ts)+DownloadManager(落沙盒)。已编过iphonesimulator26.5(iOS 26 SDK,含液态玻璃)——证 arch A Swift 侧成立。启动 / 截图待 iOS 26 模拟器运行时(约 7GB,账号无关)下载(xcodebuild -downloadPlatform iOS)。 - 明确不做(避免悬空 / 过度工程):设计令牌 codegen;APNs 发送代码(
.p8到位再落)。(注:原“Xcode 工程等账号”已作废——模拟器构建不需签名 / Provisioning,工程已建且 compile-verified;只有真机签名 / APNs 真发 / 分发才等付费 ADP。)
硬等付费 ADP 账号:真机签名 / 跑、APNs 真发、Share Extension、控制中心控件真机、旁加载分发。
7. 风险与未决(修订)
- R-iOS-1 后台挂起 / JS 全停:app 退后台即挂起、WKWebView JS(含 WebRTC 事件循环)停摆,DataChannel 无媒体轨后台不保活。文件传输本需前台(双方在线、用户看进度),可接受;用户主动触发的传输用 iOS 26
BGContinuedProcessingTask续跑(带系统进度 UI)。剪贴板走控制中心 App Intent,不依赖常驻 WebView。 - R-iOS-2 剪贴板后台限制:读弹横幅、后台不可监听 → 控制中心手动两控件已据此设计;app 内读用
UIPasteControl、探测用hasStrings(均不弹横幅)。 - R-iOS-3 无头 WebRTC 真机稳定性 + 安全上下文:arch A 最大未验证点——①
RTCPeerConnection须在安全 origin;②BGContinuedProcessingTask能否让 WebView JS 不被挂起待验。2026-06-24 模拟器已部分证实:iOS 26 WKWebView 从http://127.0.0.1(isSecureContext=true,loopback = 潜在可信源)跑RTCPeerConnection+ DataChannel +createOffer= RTC:ok、STUN srflx 候选正常、原生↔JS 桥往返正常 → arch A 核心成立、不转 B。注:安全源不止 https,loopback 亦可(可选应用内本地 https/loopback 服务内嵌 bundle,离线可用)。余真机验:host / 同内网直连候选 + 本地网络权限(R-iOS-6)、后台挂起续传(BGContinuedProcessingTask)。 - R-iOS-4 字节桥:接收侧复用桌面
Begin/Append/Finalize(base64 分批、有界)已验证;发送侧自定义 scheme(WKURLSchemeHandler)流式喂 JS 为新实现点。 - R-iOS-5(原 App Store 过审)在 Ad Hoc 旁加载下作废;若改走 TestFlight 则 Beta 审核仍在。
- R-iOS-6 本地网络权限(同内网 P2P 硬墙):WebRTC host 候选 / mDNS 触发
NSLocalNetworkUsageDescription(须配NSBonjourServices),拒绝即只剩中继候选 → 回退 relay(cdrop 已有兜底);mDNS 失败不可区分“无服务”,UI 须降级。不可拿它当核心功能门槛。 - R-iOS-7 付费账号 + 分发摩擦:4 项能力 + 分发全需付费 ADP;Ad Hoc 100 台 + UDID 预登记是旁加载硬约束——比桌面 Dropbox“下载即用”多一道摩擦。
8. iOS 设计规范要点(HIG · 液态玻璃)
抓取自 Apple HIG / WWDC25(2026-06-24),只取对 cdrop 界面有约束力的重点。
8.1 液态玻璃(材质用法)
- 两层模型:玻璃只用于功能 / 导航层(Tab bar / Nav bar / Toolbar / 浮动控件 / sheet 边框);内容层禁用——列表、设备卡、传输卡属内容层,不加玻璃。
- 用 iOS 26 SDK 编译,Tab bar / Nav bar / Toolbar / sheet 自动玻璃化;同时移除所有自定义 bar 背景 / 装饰色(否则破坏系统玻璃)。
- 自定义悬浮控件(如传输卡上的取消 / 暂停 pill)才手动
.glassEffect(.regular.interactive()),多块玻璃须放进GlassEffectContainer共享采样;主操作用.buttonStyle(.glassProminent)。 - 禁忌:玻璃叠玻璃、滚动内容区铺玻璃、大面积内容玻璃背景、非主操作滥用 tint。
- Tab bar 滚动收起
.tabBarMinimizeBehavior(.onScrollDown);全局传输状态可置.tabViewBottomAccessory(类 Apple Music 迷你条)。圆角走同心.rect(cornerRadius: .containerConcentric),sheet / popover 系统自动同心。
8.2 基础(色彩 / 字体 / 图标 / 布局)
- 色彩:用语义色(
label/secondaryLabel、systemBackground层级、systemFill、separator),不硬编码 hex——暗色 / 对比度 / vibrancy 免费。品牌主色以 Asset Catalog 的 Color Set(含 Dark 变体)接入(Color("BrandPrimary"))——这正是强约束③(§5)在 iOS 侧的落地形态。 - 字体 / Dynamic Type:系统 text styles(Body 17 / Headline / Title…)+ 必须支持 Dynamic Type(正文 / 标题 / 主按钮可缩放)。与“字体归允许分叉”不矛盾——分叉的是字体选择,Dynamic Type 支持是硬要求。
- SF Symbols:优先系统符号(自动与文字对齐 / 字重匹配),四种渲染模式(mono / hierarchical / palette / multicolor);品牌专属才自定义 symbol(须注解四模式以获 Dynamic Type / 无障碍)。
- 布局:内容 / 控件留安全区,最小命中区 44×44pt,对比度 4.5:1(正文)/ 3:1(大字 / 非文字),不硬编码宽度。
8.3 控件 / App Intents(剪贴板两控件落地)
- 剪贴板“上 / 下”用
ControlWidgetButton(一次性动作);SF Symbol 必备(锁屏 / 操作按钮只显示图标,须独立达意)。跨设备状态靠推送 reload(ControlCenter.reloadControls)、不轮询;控件内不发网络请求,经 App Group 容器读主 app 写入。 - App Intent:动词+宾语命名、
title编译期常量本地化串、默认后台执行(返回 dialog / snippet、不开 app);iOS 26 Interactive Snippets 可在不开 app 下做“选设备”二级操作。
8.4 分享(Share Extension)
- 扩展只做轻:内容预览 + 选设备 + 验证 → 写 App Group 容器 + 交接主 app;不在扩展内跑传输引擎(内存 ~120MB,见 §9.1)。宽度系统固定不可改;完成即“已提交”退出,不等传输完成。
8.5 通知(APNs)
- 文案具体(“文件 X 已接收(3.2MB)”);中断级别用
.active(传输完成 / 失败),不滥用.timeSensitive;threadIdentifier按“完成 / 失败 / 消息”分组;首个有价值操作后再 just-in-time 请求权限,按类别细分开关。剪贴板不推送(决策 C)。
8.6 无障碍(玻璃语境)
- Reduce Transparency / Increase Contrast / Reduce Motion:系统对 Tab / Nav 自动回落;自定义玻璃控件须自行提供不透明回落 + 关动效。传输进度动画检测
accessibilityReduceMotion降级为渐隐 / 变色。VoiceOver:图标按钮accessibilityLabel用动词(“发送文件”而非“箭头向上”)。
8.7 App 图标(iOS 26)
- 用 Icon Composer 做分层图标(logo 前景 + 品牌色渐变背景),系统材质自动出高光 / 折射 / 阴影——源稿不要烘焙圆角 / 阴影 / 斜面。须验明 / 暗 / clear / tinted 各变体(暗背景下 logo 填充色可辨)。接品牌资产源(强约束③)。
9. 开发禁区 / 平台约束(硬性 vs 审核 / 账号)
分级:【OS 硬】=系统强制、绕不过;【账号】=付费 / 账号策略;【审核】=仅 App Store / TestFlight 审核约束(Ad Hoc 旁加载不触发)。
9.1 运行 / 生命周期
- 【OS 硬】后台一律挂起、JS 全停:app 退后台即被挂起,WKWebView 的 JS(含 WebRTC 事件循环)停摆,DataChannel(无媒体轨)后台不保活 → 传输必须前台发起;用户主动触发的传输用 iOS 26
BGContinuedProcessingTask续跑(带系统进度 UI,能否让 WebView JS 不被挂起须真机验,R-iOS-3)。 - 【OS 硬】无后台常驻监听:剪贴板 / 消息不能后台轮询监听 → 决策 C 的控制中心手动触发正据此。
- 【OS 硬】自定义 scheme = 非安全上下文 + OPFS 单文件 ~10MB:故引擎从 https 安全源加载(§2)、大文件不走 OPFS、经原生桥
Begin/Append/Finalize落沙盒(接收侧已是此设计)。 - 【OS 硬】Share Extension ~120MB jetsam:扩展内装不下 WKWebView + WebRTC 引擎 → 只做交接(§8.4)。
9.2 权限 / 隐私 / 网络(落实决策 E 的逐权限映射)
- 【OS 硬】本地网络权限(持久):同内网 P2P 直连(WebRTC host 候选 / mDNS)触发
NSLocalNetworkUsageDescription,须配NSBonjourServices;一次授予长期持久 → 首启引导显式询问(决策 E)。缺失静默失败、拒绝则只剩中继候选 → 降级回退 relay 并提示“仅中继”(cdrop 已有兜底;mDNS 失败不可区分“无服务”,UI 须降级)。 - 【OS 硬】相机权限(持久,扫码登录):
NSCameraUsageDescription缺失即崩;首启 / 首次扫码时显式询问。无兜底 → 拒绝则提示“无权限”并阻塞“扫码”特性(跳系统设置);登录可走 OIDC 等其他途径,阻塞的是扫码、非整个 app。 - 【OS 硬】Info.plist 用途说明缺失即崩:相机、相册键(若选图)必声明;剪贴板�� iOS plist key(横幅系统自动、关不掉)。
- 【OS 硬】ATS 强制 HTTPS / TLS1.2+:cdrop 已全 https,无需例外。
- 【OS 硬】剪贴板:后台不可读、读内容弹横幅(关不掉)、写不弹、
hasStrings探测不弹、UIPasteControl读不弹 → “上”控件接受一次横幅、app 内读用UIPasteControl、“下”控件写无感。 - 【审核】隐私清单
PrivacyInfo.xcprivacy+ Required Reason API(UserDefaultsCA92.1等):上架 / TestFlight 才强制;Ad Hoc 非硬性,但第三方 WebRTC SDK 自带清单仍建议补。
9.3 能力 / 扩展 / 签名 / 分发
- 【账号】4 项能力全需付费 ADP:APNs / App Groups / Keychain 共享 / Associated Domains——免费 Personal Team 一律不支持(决策 B / I0)。
- 【OS 硬】扩展归属:Share Extension / ControlWidget 的 bundle id 须主 app 前缀、同 Team 签名;与主 app 共享数据唯一合法途径 = App Group 容器。
- 【OS 硬 + 账号】Ad Hoc 100 台 / 年 + UDID 预登记:旁加载的硬上限与摩擦(决策 B)。
- 【OS 硬】APNs 环境隔离:dev profile=sandbox、ad-hoc / 分发=production,token 不互通。
附:与桌面端策略对照(修订)
| 维度 | 桌面(Wails) | iOS(arch A) |
|---|---|---|
| UI | 复用 web/src |
原生 SwiftUI(分叉,PARITY 管) |
| 引擎 | Go 原生 | 复用 web/src JS(无头) |
| 桥方向 | JS-UI 顶层 → Go | SwiftUI 顶层 → 无头 JS |
| refresh_token | Go keyring | iOS Keychain |
| 登录 | loopback PKCE | ASWebAuthenticationSession / 扫码 |
| 后台通知 | Go 常驻进程 + 原生 API | 服务端 APNs |
| 剪贴板 | Go Monitor 实时双向 | 控制中心手动两控件(纯原生 REST) |
| 平台判定 | isDesktop()(window.runtime) |
isIOSShell()(window.webkit.messageHandlers) |
| 分发 | Dropbox 旁加载 | 旁加载(非 App Store) |