Tauri 2.12.3, swift-rs 1.0.9 and the official plugin releases mark the Swift `@_cdecl` entry points that Rust links against `public`, so iOS release builds made with Xcode 27 link again. With the runtime exports kept in optimized builds, drop the dev-profile overrides that built Tauri and the Swift plugins unoptimized. fs 2.7.0 returns only the bytes it read; the file stream already validates the trailing count, so only its comment changes. The dialog wrapper stays: 2.8.2 still turns confirm() into async IPC. OpenHarmony stays on its 2.11 forks until they follow. The Nix pnpmDeps hash is updated separately. Validation: pnpm run check. Pending: iOS dev build on device and release build with Xcode 27.
10 KiB
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。
排查灰带或双重缩小时,对照 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 构建
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 中止,模拟器不会暴露。
2. 文件导入与导出
iOS 是文件传输的一个平台实现:
- 选取: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.rssrc-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. iOS 18+ App Icon 外观变体
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
重新生成:
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
回归验证:
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 变体。
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 工程,功能边界见同步总览。
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。