mirror of
https://github.com/Darkatse/TauriTavern.git
synced 2026-11-03 20:30:26 +00:00
The canary and stable iOS builds ran on macos-latest, whose Xcode 26.6 links the iOS 26.5 SDK. iPhone Duo runs an app linked that way in a compatibility window at a legacy iPhone size, without the vertical bar, so the drawer bar never appears. These builds now run on the xcode-27 runner and select Xcode 27.1, whose SDK has the bar APIs; the image defaults to 27.0. The deployment target stays at iOS 15.0. Validation: both workflows parse; the first canary run confirms the runner and the TestFlight upload.
176 lines
12 KiB
Markdown
176 lines
12 KiB
Markdown
# TauriTavern iOS 端开发说明
|
||
|
||
本文档记录 iOS 端已经踩过的关键问题、根因与已落地方案。
|
||
|
||
补充:iOS/iPadOS 的 **分发 Policy(profile + capabilities snapshot)** 属于“合规裁剪/能力分级”问题域,其当前实现快照与维护约束已收敛到 `docs/CurrentState/iOSPolicy.md`,本文件仍聚焦 WKWebView 行为差异与 iOS-only 桥接。
|
||
|
||
当前系统支持边界:
|
||
|
||
| iOS/iPadOS | `structuredClone` | Element Fullscreen | 支持级别 |
|
||
| --- | --- | --- | --- |
|
||
| 15.0–15.3 | 上游 vendored polyfill | 开启 | 有限支持 |
|
||
| 15.4–15.x | WebKit 原生实现 | 开启 | 有限支持 |
|
||
| 16.0–16.3 | WebKit 原生实现 | 开启 | 有限支持 |
|
||
| 16.4+ | WebKit 原生实现 | 开启 | 完整支持 |
|
||
|
||
`src/index.html` 已在 `init.js` 前加载 SillyTavern 1.18.0 的 `lib/structured-clone/monkey-patch.js`。它只在全局能力缺失时安装,15.4+ 不替换 WebKit 原生函数。
|
||
|
||
## 1. WKWebView 内容视口
|
||
|
||
`infrastructure/ios_webview.rs::configure_main_wkwebview()` 在主窗口 build 后安装 `platform/ios_window_layout.rs` 的窗口宿主。WKWebView 的上、左、右约束到 safeAreaLayoutGuide,底边约束到 keyboardLayoutGuide.topAnchor;内部 UIScrollView 保持 `.never` 与零 inset,避免同一份遮挡再扣一次。iOS 没有沉浸模式。职责与背景链路见 [CurrentState/MobileStyleAdaptation.md](CurrentState/MobileStyleAdaptation.md)。
|
||
|
||
排查灰带或双重缩小时,对照 WKWebView frame、`innerHeight`/`visualViewport` 与键盘边界:窗口与内容视口的差值应恰好等于安全区与停靠键盘。
|
||
|
||
附属栏移除、下滑收键盘手势与拖动期间的高度保持见同一文档的 iOS 一节。排查收键盘卡顿时数 `innerHeight` 的变化次数:一次下滑收起只应有 1 次 resize;逐帧变化说明 WebView 高度在跟随 guide,WebKit 会逐帧整文档重建样式(`WebPage::viewportConfigurationChanged → updateSizeForCSSDefaultViewportUnits → Style::Scope::didChangeStyleSheetEnvironment`)。
|
||
|
||
### 1.1 UIScene 生命周期
|
||
|
||
iOS 27 SDK 要求 UIScene 生命周期。Tao 只要看到 `UIApplicationSceneManifest` 就启用它,与多窗口无关。`gen/apple/project.yml` 与已提交的 `Info.plist` 保留 manifest 并声明 `UIApplicationSupportsMultipleScenes = false`,应用只有一个主窗口;Tao 动态返回带 `TaoSceneDelegate` 的配置,宿主不监听 `SceneRequested`,也不声明 `UISceneConfigurations`。
|
||
|
||
scene 生命周期相关的改动以 release 构建真机冷启动验收:#337 的崩溃只在 release 构建出现。
|
||
|
||
### 1.2 Xcode 27 构建
|
||
|
||
发布用的 iOS 构建(canary 与 stable 的 `mobile` job)运行在 `xcode-27` runner 上,并选用 Xcode 27.1。应用的行为跟随链接的 SDK:用 iOS 26 SDK 链接的应用在 iPhone Duo 上以兼容模式运行,画布为旧 iPhone 尺寸,没有竖栏。
|
||
|
||
Xcode 27 的优化构建会把非 `public` 的 `@_cdecl` 函数降为本地符号,Rust 因此链接不到 Swift 入口。swift-rs 1.0.9、Tauri 2.12.3 与当前锁定的官方插件已把这些入口声明为 `public`;引入新的带 Swift 包的插件时,确认它的 `init_plugin_*` 也是 `public`。
|
||
|
||
`swift-rs` 按所在 crate 的调试设置选择 Swift 优化级别。Tauri 与带 Swift 包的插件必须以同一模式构建:dev profile 对所有依赖关闭调试信息,不为其中个别 crate 单独开启。模式不一致时,真机在 `registerPlugin` 以 `swift_deletedMethodError` 中止,模拟器不会暴露。
|
||
|
||
### 1.3 iPhone Duo
|
||
|
||
抽屉栏的规则见 [CurrentState/MobileStyleAdaptation.md](CurrentState/MobileStyleAdaptation.md) 的抽屉栏一节。模拟器验证使用 Xcode 27.1 与 iOS 27.1 runtime:
|
||
|
||
- 该 runtime 目前只提供 iPhone Duo,非 Duo 设备上的 27.1 行为无法在模拟器上验证。
|
||
- 开合只能在 Device Hub 中切换。不要用 `simctl io screenConfig ... power` 模拟:它不改变形态,还会让 SpringBoard 崩溃。
|
||
- 外屏可以用 `requestGeometryUpdateWithPreferences:` 旋转,内屏拒绝程序旋转。
|
||
- 截图:外屏 `xcrun simctl io <udid> screenshot --display=primary`,内屏 `--display=primary-1`;加 `--mask=black` 才能看到显示圆角的裁切,外屏四角的圆角并不相同。
|
||
|
||
## 2. 文件导入与导出
|
||
|
||
iOS 是[文件传输](CurrentState/FileTransfer.md)的一个平台实现:
|
||
|
||
- 选取:Document Picker 以 `asCopy` 生成副本,宿主把副本移动进暂存;
|
||
- 交付:使用 Share Sheet,建议文件名由 `NSItemProvider` 提供。
|
||
|
||
UIKit 界面在主线程上展示;在 iPad 上,popover 必须指定 sourceView/sourceRect。实现见 `platform/ios_document_picker.rs` 与 `platform/ios_share_sheet.rs`。
|
||
|
||
## 3. macOS 元数据导致的“布局歧义”问题
|
||
|
||
部分 zip(尤其是从 macOS Finder 打包/转发)会携带 `__MACOSX/**` 资源分叉条目;它会在布局探测阶段制造“存在多个候选根”的假象,触发错误:
|
||
|
||
- `Invalid data: Archive layout is ambiguous`
|
||
|
||
当前实现会在 **布局扫描** 与 **解压归一化** 时忽略 `__MACOSX` 条目,保证这类 zip 可正常导入:
|
||
|
||
- `src-tauri/crates/tt-adapter-archive/src/data_archive/import/layout.rs`
|
||
- `src-tauri/crates/tt-adapter-archive/src/data_archive/import/extract.rs`
|
||
|
||
## 4. WKWebView Element Fullscreen
|
||
|
||
- Element fullscreen 由 Wry 默认开启(iOS 15.4+ 用公开的 `WKPreferences.elementFullscreenEnabled`,更早的系统用 KVC);宿主不配置,也不按版本关闭。支持级别见文首表格。
|
||
- 角色卡、JS-Slash-Runner、同源 iframe 使用标准浏览器 Fullscreen API,不增加 JS-native bridge。退出全屏后视图重挂载,由 `KeyboardScrollView` 恢复约束。
|
||
|
||
### 4.1 构建版本契约
|
||
|
||
- 最低部署版本是 `15.0`,规范值写在 `tauri.conf.json` 的 `bundle.iOS.minimumSystemVersion`。
|
||
- `gen/apple/project.yml`、`Podfile` 与已提交 `.xcodeproj` 必须保持同值;Xcode pre-build script 会将真实 `IPHONEOS_DEPLOYMENT_TARGET` 与 Tauri 配置比较,不一致时直接失败。
|
||
- 当前 `project.yml` 未覆盖已提交 Apple host 的全部自定义 Info.plist、scheme 与签名状态。不要直接运行 XcodeGen 或 `tauri ios init` 覆盖工程;修改后只审查必要 diff,否则可能删除高刷、后台模式与文件类型声明。
|
||
- iOS 签名启用 `Increased Memory Limit`,发布 profile 需包含对应能力。
|
||
|
||
## 5. iOS 分发 Policy(当前状态)
|
||
|
||
iOS 外测/内测分发裁剪与能力分级已落地为 iOS-only 的 `ios_policy` 运行时系统(可被导入 `tauritavern-settings.json` 覆盖 profile/能力边界,且 iOS 上 fail-fast、桌面端忽略)。
|
||
|
||
- 当前实现快照:`docs/CurrentState/iOSPolicy.md`
|
||
|
||
## 6. App Icon 外观变体(iOS 18+ / macOS 26+)
|
||
|
||
### 6.1 现象
|
||
|
||
iOS 深色图标模式下,App 放入文件夹后可能出现“文件夹外仍是普通图标,打开文件夹后变成深色图标”的不一致表现。
|
||
|
||
### 6.2 根因
|
||
|
||
iOS 18+ 支持 Home Screen 图标的 `Any` / `Dark` / `Tinted` 外观。若 AppIcon 只提供传统多尺寸 `Any` 图标,系统会自动生成深色/着色效果;文件夹缩略图和展开文件夹可能走不同缓存或渲染路径,从而出现外观不一致。
|
||
|
||
### 6.3 当前方案
|
||
|
||
`AppIcon.appiconset` 改为 Xcode single-size 1024px 源图,并显式提供:
|
||
|
||
- `AppIcon-Light.png`:基础 `Any` 图标,不透明背景。
|
||
- `AppIcon-Dark.png`:深色图标,透明背景,交给系统深色底承载。
|
||
- `AppIcon-Tinted.png`:着色图标,透明背景,灰度前景。
|
||
|
||
维护入口:
|
||
|
||
- 生成脚本:`scripts/generate-ios-app-icon-variants.swift`
|
||
- 构建期校验/展平:`scripts/ios-opaque-app-icons.swift`
|
||
- 资产目录:`src-tauri/crates/tauritavern/gen/apple/Assets.xcassets/AppIcon.appiconset`
|
||
|
||
重新生成:
|
||
|
||
```sh
|
||
xcrun --sdk macosx swift scripts/generate-ios-app-icon-variants.swift \
|
||
src-tauri/crates/tauritavern/icons/icon.png \
|
||
src-tauri/crates/tauritavern/gen/apple/Assets.xcassets/AppIcon.appiconset
|
||
```
|
||
|
||
回归验证:
|
||
|
||
```sh
|
||
xcrun actool --compile /tmp/tt-appicon \
|
||
--platform iphonesimulator \
|
||
--minimum-deployment-target 15.0 \
|
||
--app-icon AppIcon \
|
||
--output-partial-info-plist /tmp/tt-appicon/partial.plist \
|
||
src-tauri/crates/tauritavern/gen/apple/Assets.xcassets
|
||
|
||
xcrun assetutil --info /tmp/tt-appicon/Assets.car
|
||
```
|
||
|
||
输出应包含 `UIAppearanceDark` 与 `ISAppearanceTintable`。若未来重新运行 `tauri icon`,必须重新生成并保留这三个 appearance 变体。
|
||
|
||
### 6.4 macOS 26+
|
||
|
||
macOS 26 会把不规则形状的 `.icns` 缩小放进灰色底板。`src-tauri/crates/tauritavern/icons/AppIcon.icon` 是 Icon Composer 文档:底色与 iOS `Any` 相同,logo 为单一图层,深色、着色与透明外观由系统生成。Tauri 打包时用 `actool`(Xcode 26+)把它编译为 `Assets.car` 并写入 `CFBundleIconName`;`icon.icns` 仍作为 `CFBundleIconFile` 供 macOS 15 及更早版本使用。
|
||
|
||
Tauri npm CLI 当前会让 `actool` 继承已关闭的 stdin,导致图标编译失败([上游问题 #15315](https://github.com/tauri-apps/tauri/issues/15315))。统一入口 `scripts/tauri-app.mjs` 在 macOS 上把 `scripts/macos` 加到子进程 PATH 前端,`actool` 入口用 `xcrun actool "$@" </dev/null` 显式提供空 stdin。升级到包含 [#15991](https://github.com/tauri-apps/tauri/pull/15991) 修复的 CLI 后,删除这个入口和 PATH 设置。
|
||
|
||
更换 logo 时,用 Icon Composer 打开该文档替换 `Assets/logo.png`(1024px 透明 PNG)。预览各外观:
|
||
|
||
```sh
|
||
"$(xcode-select -p)/../Applications/Icon Composer.app/Contents/Executables/ictool" \
|
||
src-tauri/crates/tauritavern/icons/AppIcon.icon \
|
||
--export-image --output-file /tmp/tt-macos-icon.png \
|
||
--platform macOS --rendition Dark --width 512 --height 512 --scale 1
|
||
```
|
||
|
||
## 7. AI 生成后台执行
|
||
|
||
iOS Chat Completion 的后台租约由 Rust `ChatCompletionService` 持有,不依赖 WKWebView 的 Promise、Channel 或定时器继续运行:
|
||
|
||
- iOS 26+ 的用户可见生成使用 `BGContinuedProcessingTask`,任务由用户操作立即提交,并在系统 Live Activity 中展示;
|
||
- 流式生成以累计收到的 provider 响应字节作为单调递增的真实进度。因为 LLM 输出总量事先未知,`NSProgress` 保持 indeterminate,成功结束时再收敛到 100%;
|
||
- iOS 16–25,以及不会展示系统 UI 的 quiet 内部生成,继续使用 `beginBackgroundTask`。`BGContinuedProcessingTask` 不适合无明确用户意图却会显示 Live Activity 的后台工作。
|
||
|
||
生命周期契约:
|
||
|
||
- 普通与流式 Chat Completion 在网络请求前取得后台租约;
|
||
- 成功、失败或用户取消都会由 Rust 立即完成对应的系统任务;
|
||
- 若系统先触发 expiration handler,原生适配器只结束后台保护并记录警告,不把“后台保护结束”伪装成“生成失败”;
|
||
- 系统随后可以挂起应用;若进程仍存活,生成在恢复执行后继续等待真实 provider 结果、网络错误或用户取消;
|
||
- 已产生的流事件仍由 Rust 进程内会话保留,WebView 恢复后通过 `after_seq` 继续读取;
|
||
- 无法取得后台租约只记录明确警告,不阻塞仍在前台可正常完成的生成。
|
||
|
||
当前不使用 `BGAppRefreshTask` / `BGProcessingTask`,因为它们由系统择机调度,不能表达用户点击后立即开始的聊天生成。`BGContinuedProcessingTask` 只拥有系统后台执行权和展示状态;真正的网络请求、取消和流会话生命周期仍由既有 Rust 服务负责,不引入第二套生成调度状态机。
|
||
|
||
## 8. LAN Sync Bonjour 发现
|
||
|
||
LAN 发现通过系统 Bonjour 浏览和注册 `_tauritavern._tcp`。Info.plist 声明 `NSLocalNetworkUsageDescription` 与 `NSBonjourServices`,由用户授予本地网络访问权限;此路径不需要 Multicast Networking entitlement。macOS 使用相同的系统后端与用途声明。
|
||
|
||
权限行为以真机签名包验证,后台发现受 iOS 生命周期限制。配置入口见 [Apple host 工程](../src-tauri/crates/tauritavern/gen/apple/project.yml),功能边界见[同步总览](CurrentState/Sync.md)。
|
||
|
||
## 9. Tauri Pilot
|
||
|
||
`pnpm run ios:dev:pilot` 在模拟器中启动带 Pilot 的 debug 包。模拟器共享 Mac 的文件系统,插件的 socket 位于 `/tmp/tauri-pilot-com.tauritavern.client.sock`,CLI 直接发现,无需转发。桌面开发版与模拟器使用同一路径,先启动的占用,其余实例不启动 Pilot 服务。真机沙箱对 Mac 隐藏 socket,暂不支持;iOS 上没有 `press`,文字输入用 `fill` 或 `type`。
|