9.0 KiB
9.0 KiB
iOS Integration Guide
概述
本文档描述如何将 zhixi-document-runtime 集成到 iOS App 中。所有命令已验证可执行。
环境要求
- Rust 1.96+ (stable)
- Xcode 15+
- iOS 16+ deployment target
1. 构建 Rust 库
# 添加 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 混合模式:
# 通过 build-ios.sh 自动生成,或手动:
# 1. 编译 Rust(proc-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
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 项目
- 将
bindings/ios/ZxDocumentRuntime.xcframework拖入 Xcode 项目 - 将
bindings/ios/generated/zx_document.swift添加到项目 - Target → General → Frameworks, Libraries, and Embedded Content:确保
ZxDocumentRuntime.xcframework为Do Not Embed(静态库) - Build Settings → Library Search Paths:添加 framework 路径
5. Swift 调用 API
类型
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:
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 等完整能力。
创建阅读目标和会话
// 1. 创建 ReadingMaterialRef(Rust 不存储 readingTargetType,iOS 上传时补充)
let material = ReadingMaterialRef(materialId: "mat_001")
// 2. 启动阅读会话(返回 clientSessionId)
let timestamp = Int64(Date().timeIntervalSince1970 * 1000)
let sessionId = try startReadingSessionV2(material: material, timestampMs: timestamp)
推送 5 种事件
// 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 流程
// 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 启动恢复
// App 启动时调用,恢复上次未 ack 的事件
func applicationDidFinishLaunching() {
reloadStaleEventsV2() // 内部 Pending/Exported→Pending,可重新 export
}
搜索和笔记锚点
// 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 元数据
// 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两种产物类型