# 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/apns` ES256 + 原生注册经引擎桥）；Share Extension（App Group 收件箱 + `cdrop://share` 深链）；控制中心两控件（**专用 broker 设备会话**，纯原生 REST，不与引擎抢 refresh 轮换）。模拟器验 + ultracode 多 agent 审查修讫（0 HIGH，修 2 MED + 7 LOW）。**I0 账号 / 证书 + 真机签名 / 真发推送 / 旁加载分发**仍账号门控——手册 `ios/CDrop/REALDEVICE.md`（ASC API Key 自动 provisioning + `just ios-device` 全 CLI 装机，`-allowProvisioningUpdates` 自动登记设备 / 建 profile）。包名 `net.commilitia.Commilitia-Drop`，App Group `group.net.commilitia.Commilitia-Drop`。

---

## 5. 一致性：强约束同步 vs 检查单（决策 D 展开）

**检查单是兜底，不是默认。** 先尽量用强约束（单一真源、两端机械消费、漂移不可能），管不住的残余才进检查单（见 `ios/PARITY.md`）。

### 强约束（机制保证）

1. **传输行为 → arch A 共享 JS 引擎。** 整套状态机 / 进度 / 完成 / 错误 / 协议无法漂移，零额外成本。
2. **i18n 文案 → 单一源。** iOS 原生侧薄 loader 读 web 那份 i18n JSON（随 bundle 或从服务端取），不写转换器、不做 `.xcstrings` 镜像。缺键即露。
3. **品牌资产 + 主色 → 单一源。** 资产沿用 Dropbox 那套（web+desktop 已接）；品牌主色 + logo 作小共享常量。
4. **后端 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/`（xcodegen `project.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**（UserDefaults `CA92.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） |
