# 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. 编译 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 ```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. 创建 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 种事件 ```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` 两种产物类型