zhixi-document-runtime/docs/ios-integration.md
2026-06-09 19:58:07 +08:00

317 lines
9.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# iOS Integration Guide
## 概述
本文档描述如何将 `zhixi-document-runtime` 集成到 iOS App 中。所有命令已验证可执行。
## 环境要求
- Rust 1.96+ (stable)
- Xcode 15+
- iOS 16+ deployment target
## 1. 构建 Rust 库
```bash
# 添加 iOS target首次
rustup target add aarch64-apple-ios aarch64-apple-ios-sim
# 一键构建(含 XCFramework + Swift 绑定)
./scripts/build-ios.sh
# 或手动构建
cargo build --release --target aarch64-apple-ios -p zx_document_ffi
cargo build --release --target aarch64-apple-ios-sim -p zx_document_ffi
```
产物:
```
target/aarch64-apple-ios/release/libzx_document_ffi.a
target/aarch64-apple-ios-sim/release/libzx_document_ffi.a
```
## 2. 生成 Swift Binding
使用 **UDL bindgen + proc-macro 混合模式**
```bash
# 通过 build-ios.sh 自动生成,或手动:
# 1. 编译 Rustproc-macro 生成 C ABI 符号)
cargo build --release -p zx_document_ffi
# 2. 从 UDL 生成 Swift 绑定(类型 + FFI 调用封装)
uniffi-bindgen generate \
--language swift \
--out-dir bindings/ios/generated \
crates/zx_document_ffi/src/zx_document.udl
```
输出:`bindings/ios/generated/zx_document.swift` (~50KB)
> UDL 定义类型和函数签名,`#[uniffi::export]` proc-macro 生成 C ABI 分发符号bindgen 生成 Swift 调用封装。两者协作,非 library 模式。
## 3. 创建 XCFramework
```bash
xcodebuild -create-xcframework \
-library bindings/ios/device/libzx_document_ffi.a \
-library bindings/ios/simulator/libzx_document_ffi.a \
-output bindings/ios/ZxDocumentRuntime.xcframework
```
产物:`bindings/ios/ZxDocumentRuntime.xcframework`
## 4. 引入 Xcode 项目
1.`bindings/ios/ZxDocumentRuntime.xcframework` 拖入 Xcode 项目
2.`bindings/ios/generated/zx_document.swift` 添加到项目
3. Target → General → Frameworks, Libraries, and Embedded Content确保 `ZxDocumentRuntime.xcframework``Do Not Embed`(静态库)
4. Build Settings → Library Search Paths添加 framework 路径
## 5. Swift 调用 API
### 类型
```swift
import ZxDocumentRuntime
// 枚举类型
let type = MaterialType.markdown
let mode = PreviewMode.nativeReader
// 位置
let pos = ReadingPosition.markdown(blockId: "h1", scrollProgress: 0.5)
// 事件
let event = ReadingEvent.materialOpened(materialId: "abc", timestampMs: 1000)
// 锚点
let anchor = NoteAnchor.markdownBlock(materialId: "abc", blockId: "h1")
// 字典类型
let meta = ImageMeta(width: 800, height: 600, format: "png", fileSize: 12345)
let info = DocumentInfo(materialId: "abc", title: "doc.md",
materialType: .markdown, previewMode: .nativeReader,
fileSize: 1024, pageCount: nil, wordCount: 100, createdAt: nil)
```
### 错误处理
所有函数抛出 `DocumentError`
```swift
do {
let path = "/path/to/file.md"
let type = try detectMaterialType(filePath: path)
print("Detected: \(type)")
} catch let error as DocumentError {
switch error {
case .fileNotFound: print("File not found")
case .unsupportedFormat: print("Unsupported format")
case .parseError: print("Parse error")
case .invalidEncoding: print("Invalid encoding")
case .ioError: print("IO error")
}
}
```
### V2 阅读事件集成(推荐)
V2 API 提供 session 管理、事件缓冲、ack 确认、crash recovery 等完整能力。
#### 创建阅读目标和会话
```swift
// 1. 创建 ReadingMaterialRefRust 不存储 readingTargetTypeiOS 上传时补充)
let material = ReadingMaterialRef(materialId: "mat_001")
// 2. 启动阅读会话(返回 clientSessionId
let timestamp = Int64(Date().timeIntervalSince1970 * 1000)
let sessionId = try startReadingSessionV2(material: material, timestampMs: timestamp)
```
#### 推送 5 种事件
```swift
// MaterialOpened — 打开资料
let opened = try pushMaterialOpenedV2(
sessionId: sessionId,
materialId: "mat_001",
timestampMs: now()
)
// PositionChanged — 位置变化
let pos = ReadingPosition.markdown(blockId: "intro", scrollProgress: 0.25)
let changed = try pushPositionChangedV2(
sessionId: sessionId,
materialId: "mat_001",
position: pos,
timestampMs: now()
)
// Heartbeat — 心跳(含 activeSecondsDelta
let hb = try pushHeartbeatV2(
sessionId: sessionId,
materialId: "mat_001",
activeSecondsDelta: 15,
position: nil,
timestampMs: now()
)
// MarkedAsRead — 标记已读
let mar = try pushMarkedAsReadV2(
sessionId: sessionId,
materialId: "mat_001",
timestampMs: now()
)
// MaterialClosed — 关闭资料
let closed = try pushMaterialClosedV2(
sessionId: sessionId,
materialId: "mat_001",
activeSecondsDelta: 0,
timestampMs: now()
)
```
#### Export + Ack 流程
```swift
// 1. 导出待上传事件(标记为 Exported
let events = exportPendingEventsV2(limit: 100, timestampMs: now())
// 2. 构造上传请求iOS 补充 readingTargetType/platform/appVersion/timezone
let uploadItems = events.map { event in
ReadingEventUploadItem(
eventId: event.eventId,
clientSessionId: event.clientSessionId,
materialId: event.materialId,
eventType: event.eventType,
position: event.position,
activeSecondsDelta: event.activeSecondsDelta,
clientTimestampMs: event.timestampMs,
sequence: event.sequence,
// iOS 补充字段
readingTargetType: getTargetType(for: event.materialId),
platform: "ios",
appVersion: Bundle.main.appVersion,
clientTimezoneOffsetMinutes: Int(TimeZone.current.secondsFromGMT() / 60)
)
}
// 3. 上传到 API
api.uploadEvents(uploadItems) { result in
switch result {
case .success:
// 成功后 ACK从 buffer 移除)
let ids = events.map(\.eventId)
ackEventsV2(eventIds: ids)
case .failure:
// 失败标记为 Failed下次 export 重试)
let ids = events.map(\.eventId)
markEventsFailedV2(eventIds: ids)
}
}
```
#### App 启动恢复
```swift
// App 启动时调用,恢复上次未 ack 的事件
func applicationDidFinishLaunching() {
reloadStaleEventsV2() // 内部 Pending/Exported→Pending可重新 export
}
```
#### 搜索和笔记锚点
```swift
// 1. 解析 Markdown
let blocks = try parseMarkdown(content: mdContent)
// 2. 搜索
let results = searchMarkdownBlocks(blocks: blocks, query: "keyword")
// 3. 从搜索结果创建笔记锚点
if let firstResult = results.first {
let anchor = createNoteAnchorFromSearch(
materialId: "mat_001",
result: firstResult
)
// anchor 包含 materialId/blockId/pageNumber/chapterId/snippet
}
// 4. 从阅读位置创建锚点
let pos = ReadingPosition.pdf(pageNumber: 3, pageProgress: 0.5, overallProgress: 0.1)
let anchor = createNoteAnchor(materialId: "mat_001", position: pos)
// → NoteAnchor.pdfPage(materialId: "mat_001", pageNumber: 3, positionSnapshot: pos)
// 5. 从锚点恢复阅读位置
if let restored = restorePositionFromAnchor(anchor: anchor) {
// navigate to restored position
}
// PDF 搜索
let pages = searchPdfPages(
pageNumbers: [1, 2, 3],
pageTexts: ["page 1 text", "page 2 text", "page 3 text"],
query: "keyword"
) // → [SearchResult] with page_number
// EPUB 搜索
let chapters = searchEpubChaptersFfi(
chapterIds: ["intro", "ch1"],
chapterTexts: ["Welcome", "Content with keyword"],
query: "keyword"
) // → [SearchResult] with chapter_id
```
#### PDF/EPUB 元数据
```swift
// PDF
let pdfMeta = try readPdfMetadataFfi(filePath: "/path/to/doc.pdf")
print("Pages: \(pdfMeta.pageCount), Title: \(pdfMeta.title ?? "N/A")")
// EPUB
let epubMeta = try readEpubMetadataFfi(filePath: "/path/to/book.epub")
print("Chapters: \(epubMeta.chapterCount)")
let chapters = try readEpubChaptersFfi(filePath: "/path/to/book.epub")
for ch in chapters {
print(" \(ch.title)\(ch.path)")
}
// Office
let config = try getOfficePreviewConfigFfi(
materialType: .word,
fileSize: 1024
)
print("Strategy: \(config.strategy)") // PlatformPreview
```
## 6. 使用真实 XCFramework 的项目示例
`bindings/ios/demo/` 目录,包含最小 SwiftUI App 示例:
- `MaterialReaderDemo.swift` — 调用 detect_material_type 并展示结果
- `Info.plist` — App 配置
## 7. 常见问题
| 问题 | 解决 |
|------|------|
| `No such module 'zx_documentFFI'` | 确保 XCFramework 已添加到 Link Binary with Libraries |
| `library not found for -lzx_document_ffi` | 检查 Library Search Paths |
| Swift 类型不匹配 | 重新生成 `zx_document.swift`UDL 更新后必须重新生成) |
| 模拟器 crash | 确保用的是 `ios-arm64-simulator` slice |
| 文件路径错误 | 确保路径字符串是 OS 路径而非 Rust 路径 |
| 大文件解析慢 | 在后台线程调用 Rust 函数 |
## 8. 当前验证状态
-`cargo build --release --target aarch64-apple-ios` 通过
-`cargo build --release --target aarch64-apple-ios-sim` 通过
- ✅ XCFramework 已生成
- ✅ Swift bindings 已生成48KB, 所有类型)
-`cdylib``staticlib` 两种产物类型