Skip to content

XHIM iOS/macOS 共享实现与发行维护

本目录不是客户接入入口,只保存 iOS 与 macOS 共同使用的 Swift Facade、 SwiftUI 组件、XCFramework 组包和签名工具。两个平台的公开产品文档、示例和 验收互相独立。

第一次在空白工程接入? iOS 请先看 iOS 从零快速接入;使用 CocoaPods 请直接看 iOS CocoaPods 从零接入;macOS 请看 macOS 从零快速接入。本页只面向 XHIM SDK 发行、 签名、安全和高级二次开发人员,不是客户端首次接入教程。服务端部署另见 XHIM Server 文档。 登录设备查询、踢设备、自撤销与只读策略 API 见 iOS QuickStartmacOS QuickStart 的“登录设备管理”。

2026-07-25 实测状态:仓库内 Apple 原生 Product Adapter 已使用 URLSession HTTP(S)/WS(S)、生产态系统 SecTrust、Endpoint Bundle Ed25519 校验和 Keychain 账号数据库 Key 接入统一 C++ Engine。iOS Device arm64 与 Simulator arm64/x86_64 的 SQLCipher 4.12.0 Development XCFramework、二进制 Swift Package 和真实服务端 Demo 均已完成构建;公司开发证书签名的 Demo 已安装到 登记 iPhone。模拟器 Bob 已通过局域网 Development 通道完成真实 send → 服务端提交 → sync → ServerAccepted,Alice/Bob 发送、同步和已读 闭环已通过。 这仍是受控 Development 证据,不等于面向客户的 Stable 商业发布。 2026-07-26 已把正式流水线收紧为 iOS Device、iOS Simulator、macOS arm64/x86_64 三个平台 Slice,并增加二进制 SHA-256、内嵌构建来源、 完整 ABI baseline、Mach-O Platform/Minimum OS 和空白消费者门禁;由于旧 Development 制品没有 macOS slice,它只是历史联调证据,必须用受审 SQLCipher 与 Product Adapter 重新构建后才能进入新门禁。

本机逐项证据见 Apple iOS 接入验证记录

1. 商用交付边界:先区分三种制品

制品用途网络数据库可对外售卖
previewABI/Wrapper 和本地流程测试不连接 XHIM Server编译进 SQLCipher,但不注入 Key
development真实服务器、模拟器和登记真机联调固定 Apple Product AdapterSQLCipher + Keychain 随机 Key仅受控测试
production客户正式登录、收发、同步和推送固定且审计过的 Apple Product AdapterSQLCipher + Keychain 随机 Key,缺失即失败完成全部正式门禁后

.localPreview 只用于自动化。developmentproduction 均关闭 Local Preview、要求数据库加密并嵌入真实 Adapter;production 另外强制:

  • Git 工作区干净;
  • XHIM_ENABLE_LOCAL_PREVIEW=OFF
  • XHIM_REQUIRE_DATABASE_ENCRYPTION=ON
  • 提供公司签名身份;
  • 显式提供经审核的 Privacy Manifest;
  • iOS Device、Simulator 与 macOS 均包含同一版本的 C ABI 和 SQLCipher;
  • Build Manifest 的制品 SHA-256、Commit、Toolchain、Product Adapter、 Local Preview 和数据库加密声明全部复核通过。

Apple 客户最终只依赖 Swift Package 或 CocoaPods,不编译 C++ 内核或管理 C handle。iOS 首发交付目录为:

text
XHIMSwift/
├── Package.swift
├── XHIM.podspec
├── XHIMSwiftUI.podspec
├── XHIMCore.xcframework        C ABI + Product Adapter + SQLCipher
├── XHIMCore.build-manifest.json 最终签名目录树与内嵌来源绑定
├── Sources/XHIM/              Swift Headless Facade
├── Sources/XHIMSwiftUI/       SwiftUI 组件,可选
├── Sources/XHIM/Resources/    PrivacyInfo.xcprivacy
├── Examples/XHIMDemo/         可运行 Xcode Demo
├── QUICKSTART_IOS.md          iOS 空白工程教程
├── QUICKSTART_COCOAPODS_IOS.md iOS CocoaPods 空白工程教程
├── QUICKSTART_MACOS.md        macOS 空白工程教程
├── UI_ASSETS.md               UI 资源版本与许可证说明
├── LICENSE
├── NOTICE
└── RELEASE-MANIFEST.json

XHIMCore.xcframework 至少包含:

  • iOS Device arm64;
  • iOS Simulator arm64 和当前支持的 Intel Simulator slice;
  • macOS arm64/x86_64,或按发布支持矩阵拆分;
  • 模块化 C 头 xhim/xhim_v1.h
  • 真实 Product Adapter,正式包关闭 Local Preview;
  • 同版本的符号文件、UUID、Commit SHA、ABI/Wire/DB Schema 版本。

当前核心是静态库 XCFramework,内部是 libXHIMCore.a,并不存在 .framework 子目录。签名单位是 XCFramework 外层 Bundle:签名前写入 XHIMBuildProvenance.json,再用一次 codesign 把 Info.plist、三组静态库、 Headers 和来源声明一起封装。禁止遍历并“签名每个 framework”,因为那会在 静态库布局下得到空集合,形成假通过或必失败门禁。

Apple 官方支持把 XCFramework 作为 Swift Package 的 binary target 分发,并用 checksum 校验远端 ZIP。参考: 创建 XCFramework通过 Swift Package 分发二进制 Framework

2. 当前公司 Apple 资源

  • Team:Bazhou Xihan Software Technology Co., Ltd (FG4922H85G);
  • Demo Bundle ID:com.xihansoftware.xhim.demo
  • App ID 已启用 Push Notifications;
  • 开发描述文件:XHIM Demo iOS Development
  • XCFramework 签名: Apple Distribution: Bazhou Xihan Software Technology Co., Ltd (FG4922H85G)
  • 测试 iPhone 已登记在该 Team。

证书私钥只保存在登录 Keychain,仓库只记录 identity 名称,不保存 .p12、私钥、 描述文件或 APNs 私钥。APNs Auth Key 属于服务端 Secret,不能放入 SDK、Demo、 App Bundle 或 Git。

3. 构建 Apple Development 包

3.1 构建 SQLCipher Apple Slice

使用经过法务和安全审查的 SQLCipher 4.12.0 amalgamation。社区版需要遵守其 许可证与用户可见归属要求;正式销售也可替换为 Zetetic 商业二进制,不能把本仓库 脚本当作商业授权。

bash
scripts/apple/build_sqlcipher_ios.sh \
  --source-dir /secure/vendor/sqlcipher-4.12.0-amalgamation \
  --output-dir build-ios-delivery/sqlcipher

脚本固定使用 Apple CommonCrypto,输出 iPhone arm64、Simulator arm64/x86_64 和 macOS arm64/x86_64,并记录源码 SHA-256、Xcode 版本及 iOS/macOS 最低版本。它会验证 sqlite3_key_v2sqlcipher_version 等符号,拒绝普通 SQLite。

3.2 接收服务端联调配置

Apple SDK 发行人员不负责部署 XHIM Server。开始 Development 制品验证前,由 服务端负责人按照 交付给客户端团队的联调信息 提供可访问的 Server URL、测试账号和测试会话。

发行流水线如需把公开 Endpoint 配置嵌入 Product Adapter,应从受保护的 CI 配置读取;服务端数据库、容器命令、管理凭证和签名私钥不能进入 Apple 构建 脚本或交付包。Development 明文网络只允许受控测试制品,Production 必须使用 系统可信 HTTPS/WSS,不能绕过 SecTrust

3.3 构建并签名 Development XCFramework

Development 与 Production 使用仓库内同一 Apple Adapter;区别是 Development 允许脏工作区和测试 Endpoint,不能发给客户:

bash
set -a
source /secure/xhim/apple-development-public.env
set +a

export XHIM_PRODUCT_ADAPTER_SOURCE_DIR="$PWD/adapters/apple_server"
export XHIM_PRODUCT_ADAPTER_OBJECT_TARGET=xhim_apple_product_adapter

scripts/apple/build_ios_xcframework.sh \
  --mode development \
  --sdk-version 0.1.0-beta.1 \
  --sqlcipher-dir build-ios-delivery/sqlcipher \
  --output-dir build-ios-development/apple \
  --signing-identity \
  "Apple Distribution: Bazhou Xihan Software Technology Co., Ltd (FG4922H85G)" \
  --expected-team-id FG4922H85G

apple-development-public.env 由发行 CI 管理,只允许包含 Product Adapter 所需 的公开 Endpoint 参数;它不是服务端运行环境文件,也不包含数据库或管理凭证。

产物:

text
build-ios-development/apple/
├── XHIMCore-development.xcframework
└── XHIMCore-development.manifest.json

签名验证:

bash
scripts/apple/verify_ios_xcframework.sh \
  --artifact build-ios-development/apple/XHIMCore-development.xcframework \
  --build-manifest build-ios-development/apple/XHIMCore-development.manifest.json \
  --mode development \
  --require-macos \
  --require-signature \
  --expected-team-id FG4922H85G

3.4 组装二进制 Swift Package 与 CocoaPods 包

bash
scripts/apple/assemble_ios_package.sh \
  --mode development \
  --artifact build-ios-development/apple/XHIMCore-development.xcframework \
  --build-manifest build-ios-development/apple/XHIMCore-development.manifest.json \
  --sqlcipher-license /secure/vendor/sqlcipher-4.12.0/LICENSE.md \
  --sdk-version 0.1.0-beta.1 \
  --expected-team-id FG4922H85G \
  --output-dir build-ios-development/distribution

脚本会生成本地二进制 Swift Package/CocoaPods 包、XCFramework ZIP、SwiftPM checksum、CocoaPods source ZIP/SHA-256、私有 Specs 目录、内部 RELEASE-MANIFEST.json、外部 Release Attestation、LICENSE、NOTICE、 SQLCipher/Monocypher 许可证归属和 Xcode Demo。它会在无证书条件下构建空白 SwiftPM/CocoaPods 的 iOS 与 macOS 消费者;没有匹配的 SQLCipher 许可证或 Build Manifest 时拒绝组包。

bash
open \
  build-ios-development/distribution/XHIMSwift-development/Examples/XHIMDemo/XHIMDemo.xcodeproj

选择 XHIMDemo Scheme。真机确认 Signing 使用 FG4922H85G Team 和 XHIM Demo iOS Development。如果仓库位于 iCloud/File Provider 目录,真机 构建应把 DerivedData 放到普通本地目录,例如 -derivedDataPath /tmp/xhim-derived-device,避免 Finder 元数据污染 .app

源码仓库中的 Demo 引用相邻源码 Package;面向客户验证应打开上述组装目录中的 Demo,这样依赖的是最终二进制 XCFramework,而不是本机临时 CMake Library。

3.5 Alice/Bob 双端互通

  1. iPhone 打开“晞晗IM Demo”,输入 Server 局域网地址和 alice
  2. 模拟器输入同一个 Server 地址和 bob
  3. 两端都显示 ready 后,Alice 发送一条消息;
  4. Bob 应在一次 WSS Hint + Sync 后看到该消息并回复;
  5. Alice 应看到 Bob 回复;服务端 xhim_messages.server_sequence 必须单调递增, 客户端本地状态必须进入 serverAccepted,不能只以“已进入 Outbox”判成功。

自动化启动时只需注入 Server 地址和账号。变量只存在于本次启动命令,不会写入 工程:

bash
XHIM_TEST_SERVER_URL="https://im-test.customer.com"

# Simulator 启动 Bob
SIMCTL_CHILD_XHIM_DEMO_ACCOUNT=bob \
SIMCTL_CHILD_XHIM_DEMO_SERVER_URL="$XHIM_TEST_SERVER_URL" \
xcrun simctl launch booted com.xihansoftware.xhim.demo

# 已解锁的登记真机启动 Alice;把 <CoreDevice-ID> 换成 devicectl 中的值
DEVICECTL_CHILD_XHIM_DEMO_ACCOUNT=alice \
DEVICECTL_CHILD_XHIM_DEMO_SERVER_URL="$XHIM_TEST_SERVER_URL" \
xcrun devicectl device process launch \
  --terminate-existing \
  --device <CoreDevice-ID> \
  com.xihansoftware.xhim.demo

Apple 侧验收状态、事件和时间线;消息是否已经持久化、账号成员关系和服务端审计 由服务端负责人按照 Server 文档独立验收,不在 Apple 工程中执行数据库命令。

Demo 已通过 projectionEvents 在投影提交后重新查询时间线,不再每秒轮询。 生产 App 同样由账号级 ViewModel/Repository 持有订阅,并只调用 SDK 查询 API, 不要直接读取 SQLite。

4. 客户项目接入

4.1 CocoaPods

本地试用包:

ruby
pod 'XHIM', :path => '../XHIMSwift-development'
pod 'XHIMSwiftUI', :path => '../XHIMSwift-development'

本轮生成的受控 Development 快照在上传并登记私有 Specs 后按下面方式精确锁定:

ruby
source 'git@git.xihansoftware.com:laowang/xhim-specs.git'
source 'https://cdn.cocoapods.org/'

pod 'XHIM', '= 0.1.0-dev.2'
pod 'XHIMSwiftUI', '= 0.1.0-dev.2'

该版本不是 Stable 销售制品;CocoaPods 的 ~> 0.1 不会选择 prerelease。 登记完成前使用发行负责人提供的同版本本地包。

正式版本由发行人员上传脚本生成的 *.pod-source.zip,再把 CocoaPodsSpecs-production/Specs 中的两个版本目录提交到私有 Specs 仓库。 完整发布顺序见 CocoaPods 私有发行。客户只需:

ruby
source 'git@git.xihansoftware.com:laowang/xhim-specs.git'
source 'https://cdn.cocoapods.org/'

pod 'XHIM', '~> 0.1'
pod 'XHIMSwiftUI', '~> 0.1'

只有首个通过商业门禁的稳定 0.1.x Podspec 存在后,上述稳定版本范围才有效。

详细步骤见 iOS CocoaPods 从零接入

4.2 Swift Package Manager

内测阶段可在 Xcode 使用 File → Add Package Dependencies → Add Local... 选择组装后的 XHIMSwift-development 目录。App Target 必选 XHIM;需要基础 会话/聊天视图时再选 XHIMSwiftUI

正式发布后,客户通过 Xcode 的 Package Dependencies 添加仓库地址,或在 Package.swift 中声明依赖。发行仓库内部使用类似配置:

swift
// 这是发行包 manifest 示例,不代表当前仓库已经产出该 URL 和 checksum。
.binaryTarget(
    name: "XHIMCore",
    url: "https://<your-cdn>/xhim/<version>/XHIMCore.xcframework.zip",
    checksum: "<swift-package-compute-checksum>"
)

App Target 只链接 Headless XHIM;需要当前基础页面时再增加 XHIMSwiftUI。未来的 UIKit 包也必须保持可选,不能成为登录、收发、同步或 媒体能力的前置依赖。

最低系统版本、Xcode/Swift 版本、模拟器架构和是否支持 Mac Catalyst 必须写入 每个版本的发布清单,不能只写“支持 iOS/macOS”。

业务 App 的推荐入口会自动发现部署、创建账号数据库、启动并登录:

swift
let client = try await XHIMClient.connect(
    server: "https://im.customer.com",
    userID: account.userID,
    authentication: .business {
        try await AccountAPI.current.xhimCredential()
    }
)

Development Server 开启 Easy Login 时省略 authentication 即可。底层显式 XHIMClient(configuration:)startloginupdateCredential 继续 保留,供迁移、发行验证和高级账号容器使用,不是空白项目的推荐路径。

.localPreview 不认证、不联网,只能用于开发包:

swift
let preview = try XHIMClient(configuration: .init(
    appID: "com.xihansoftware.xhim.demo",
    storageURL: previewDatabaseURL,
    mode: .localPreview
))

5. 构建 Production 包

仓库内 adapters/apple_server 是 Production 构建使用的 Apple Product Adapter。它负责:

  1. 通过 URLSession 和受系统 SecTrust 管理的 HTTPS/WSS 完成认证、 Endpoint 校验、收发和增量同步;
  2. 从 Keychain 创建/读取数据库路径隔离的 32 字节随机 Key,使用 kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,缺失或读取失败即 fail-closed;
  3. 把 Session Backend、Message Transport 和 Database Key Provider 作为同一个 Build Product Adapter 返回;
  4. 使用 DeploymentConfig 提供的客户 Endpoint 签名公钥,并固定 HTTPS/WSS scheme、帧与响应体上限、超时、取消、Epoch/Generation fence 和错误映射;客户域名只通过验签且未过期的 Endpoint Bundle 发现,同一 XCFramework 不需要因部署地址变化而重编;
  5. 使用 Monocypher 4.0.3 的 Ed25519 校验 Endpoint Bundle,避免依赖 Apple SDK 未公开的 Ed25519 Security.framework API;
  6. 缺 Token、Keychain Key、系统信任链或服务端配置时 fail-closed。

正式售卖前仍必须完成客户生产域名/KMS 配置、Keychain rekey/备份恢复矩阵、 APNs、正式隐私清单、全设备矩阵和发布门禁;Development 构建和一次真机联调 不能替代这些证据。

Production 组包还必须显式传入经隐私/法务复核的 --privacy-manifest /secure/reviewed/PrivacyInfo.xcprivacy。仓库自带的空收集 清单只描述不联网的 Preview,不能原样用于会上传账号标识、消息或媒体的生产包。

bash
set -a
source /secure/release/xhim-apple-client.env
set +a

export XHIM_PRODUCT_ADAPTER_SOURCE_DIR="$PWD/adapters/apple_server"
export XHIM_PRODUCT_ADAPTER_OBJECT_TARGET=xhim_apple_product_adapter

scripts/apple/build_ios_xcframework.sh \
  --mode production \
  --sdk-version 0.1.0 \
  --sqlcipher-dir /secure/build/sqlcipher-ios \
  --output-dir /secure/final/apple \
  --signing-identity \
  "Apple Distribution: Bazhou Xihan Software Technology Co., Ltd (FG4922H85G)" \
  --expected-team-id FG4922H85G

scripts/apple/assemble_ios_package.sh \
  --mode production \
  --artifact /secure/final/apple/XHIMCore-production.xcframework \
  --build-manifest /secure/final/apple/XHIMCore-production.manifest.json \
  --privacy-manifest /secure/reviewed/PrivacyInfo.xcprivacy \
  --privacy-review-id PRIVACY-2026-001 \
  --legal-review-id LEGAL-2026-001 \
  --sqlcipher-license /secure/vendor/sqlcipher-4.12.0/LICENSE.md \
  --sdk-version 0.1.0 \
  --cocoapods-source-url \
  "https://sdk.xihansoftware.com/xhim/swift/0.1.0/XHIMSwift-0.1.0-production.pod-source.zip" \
  --expected-team-id FG4922H85G \
  --output-dir /secure/final/distribution

scripts/release/stage_apple_packages.sh \
  0.1.0 \
  /secure/final/distribution \
  /secure/final-artifacts \
  FG4922H85G

adapters/reference_server 仍只用于桌面/服务端纵向联调,不能替代 adapters/apple_server。Production 构建要求干净 Git 工作区、正式签名、 数据库加密和显式 Product Adapter;任何缺项都应失败,不能改成明文、 Local Preview 或全信任 TLS 兜底。stage_apple_packages.sh 会把 Package、 私有 Specs、XCFramework ZIP、CocoaPods source ZIP 和 Release Attestation 原子复制到统一 apple/ 制品目录并复核全部 Hash。Apple 签名只允许发生在 build_ios_xcframework.sh;跨平台 sign_artifacts.sh 对 Apple 只验签, 绝不能在组包后重签并让 SwiftPM/CocoaPods 校验和失效。

6. 当前源码验证

macOS 主机仍可验证 C++ 内核和 CMake 安装包:

bash
cmake -S /path/to/xhim -B /path/to/build-xhim-apple \
  -G Ninja \
  -DCMAKE_BUILD_TYPE=Release \
  -DXHIM_BUILD_TESTS=ON \
  -DXHIM_WARNINGS_AS_ERRORS=ON
cmake --build /path/to/build-xhim-apple
ctest --test-dir /path/to/build-xhim-apple --output-on-failure

这条命令验证源码内核,不替代上述 Apple XCFramework、Swift Package、Demo、 签名和真机链路。新流水线会分别为 iOS Device、iOS Simulator 与 macOS 构建 目标文件,再由 xcodebuild -create-xcframework 组装;只允许同一平台的 arm64/x86_64 合并为 Universal Archive,不能把 iOS 与 macOS 目标文件直接 lipo 成伪平台 Slice。

每个 slice 都必须使用一致的 XHIM Commit、Product Adapter、C++ 标准库策略、 编译开关和协议版本。不要把 Device 与 Simulator 的目标文件直接用 lipo 伪装为一个平台 slice。

Apple 门禁自身的快速回归:

bash
python3 -m unittest discover \
  -s scripts/apple/tests -p 'test_*.py' -v
bash -n scripts/apple/*.sh

它只验证 Attestation/Manifest 的拒绝逻辑和脚本语法;正式发布仍必须运行完整 XCFramework 构建及两个空白消费者脚本。

7. Swift Facade API

推荐由一个 actor 或内部串行执行器持有唯一 Client handle:

swift
// 实际入口位于 Sources/XHIM/XHIMClient.swift。
let policy = XHIMRuntimePolicy(
    requestTimeoutMilliseconds: 15_000,
    syncPageSize: 200,
    reconnectInitialDelayMilliseconds: 1_000,
    reconnectMaxDelayMilliseconds: 30_000,
    maxReconnectAttempts: 8
)

public final class XHIMClient {
    public static func connect(
        server: String,
        userID: String,
        appID: String? = nil,
        authentication: XHIMAuthentication = .development,
        storageRootURL: URL? = nil
    ) async throws -> XHIMClient
    public static func connect(
        serverURL: URL,
        userID: String,
        appID: String? = nil,
        authentication: XHIMAuthentication = .development,
        storageRootURL: URL? = nil
    ) async throws -> XHIMClient
    public func start() async throws
    public func login(accountHint: String, accessToken: String) async throws
    public func updateCredential(accessToken: String) async throws
    public func logout() async throws
    public func sendText(
        conversationID: String,
        clientMessageID: String?,
        text: String
    ) async throws -> XHIMSendReceipt
    public func sendMessage(
        conversationID: String,
        clientMessageID: String? = nil,
        message: XHIMOutgoingMessage
    ) async throws -> XHIMSendReceipt
    public func retryMessage(
        clientMessageID: String
    ) async throws -> XHIMSendReceipt
    public func cancelMessage(
        clientMessageID: String
    ) async throws -> XHIMSendReceipt
    public func message(
        clientMessageID: String
    ) async throws -> XHIMMessage
    public func messages(
        conversationID: String,
        cursor: Data? = nil,
        limit: Int = 50
    ) async throws -> XHIMMessagePage
    public func conversations(
        cursor: Data? = nil,
        limit: Int = 50
    ) async throws -> XHIMConversationPage
    public func markConversationRead(
        conversationID: String,
        throughServerSequence: Int64
    ) async throws -> XHIMConversationReadReceipt
    public func shutdown() async
}

XHIMRuntimePolicyXHIMDeployment 会在 client_create 时复制并固定; 不要根据一次请求临时修改全局超时。客户可提供 Bootstrap URL、Endpoint Key ID 和 Ed25519 公钥;实际 API/WSS/上传/下载地址只能来自验签且未过期的 Endpoint Bundle。TLS Trust 仍由正式 Product Adapter fail-closed 管理,业务 不能关闭校验。

源码消费验证:

bash
swift build --package-path platforms/apple \
  -Xlinker -L -Xlinker /path/to/xhim-native-lib

业务初始化可直接参考 Examples/QuickStart.swift。SwiftUI 组件位于 Sources/XHIMSwiftUI,通过数组和动作闭包接入,不直接读取 SQLite。

事件流使用 AsyncStream<XHIMEvent> 或同等原生机制。Facade 公开 Swift struct/enum/Error,不公开 UnsafeMutablePointer<xhim_v1_client_t>、C callback、 SQLite 路径细节或 C++ 类型。

持久数据刷新使用独立的 AsyncStream<XHIMProjectionChange>client.projectionEventsmessageUpserted/messageStateChanged/ conversationChanged/socialChanged/syncApplied 只表示本地权威投影已经失效, 不是业务事件日志;订阅后先查询一次,之后按 scope/ID 重新调用 SDK。未知 kind/schema 会成为 projectionInvalidated,必须做宽范围重查。所有 C 借用内存 均在回调返回前复制,SwiftUI 可使用 MainActor XHIMProjectionRequeryController,UI 不直接访问 SQLite。

7.1 已读调用

messages 返回最新消息在前的 XHIMMessagePage,翻页时只能把 nextCursor 原样传回,不能解析、拼接或跨账号/会话复用。聊天页只有在一批 消息已经完成布局并对用户可见后,才能取这批消息中最大的正 serverSequence 调用:

swift
let page = try await client.messages(conversationID: conversationID)
let highestVisibleServerSequence = page.messages
    .compactMap(\.serverSequence)
    .max()

guard let highestVisibleServerSequence else { return }
let receipt = try await client.markConversationRead(
    conversationID: conversationID,
    throughServerSequence: highestVisibleServerSequence
)
// receipt.unreadCount 是服务端回包落库后的值,不是 UI 本地推算值。

本地 Pending/Sending 消息没有服务端序号,必须排除。调用完成前不要把角标 乐观清零;使用返回的 receipt 更新当前页面。监听 .conversationReadChanged(let conversationID) 时重新读取该会话摘要,它也会在 同账号其他设备推进已读后出现。同值或更低值允许重复调用并返回 no-op。

conversations 返回置顶优先、随后按持久活动排序的摘要页;收到 .conversationReadChanged 后用该 API 重新拉取对应摘要。业务层不得自己打开 SQLite、用数组下标或设备时间制造序号。

实时 Presence/Typing(非持久投影)

publishPresencepublishTyping 复用已登录会话,业务层不传 Credential。 服务端权威回显包含 sequenceexpiresAtMilliseconds;对端从 events 接收 presenceChanged/typingChanged。这两类状态不写 SQLite,也不进入 projectionEvents,UI 必须到期自动清除。输入状态只发布开始/停止转换,禁止 按每个按键上报;完整调用示例见 QuickStart 的“在线状态与正在输入”。

7.2 自定义消息与 UI Renderer

产品自定义消息使用自己的稳定命名空间和版本,例如 com.xihan.product-card@1。发送前先由业务插件校验,再把同一个不可变信封交给 Client:

swift
let plugins = XHIMMessagePluginRegistry()
try plugins.register(ProductCardPlugin())

let outgoing = XHIMOutgoingMessage(
    contentType: "com.xihan.product-card",
    contentVersion: 1,
    payload: cardJSON,
    fallbackText: "[商品卡片]"
)
try plugins.validate(outgoing)
try await client.sendMessage(
    conversationID: conversationID,
    message: outgoing
)

拉取消息后调用 plugins.presentation(for:) 生成会话摘要和通知摘要。未知 contentType、插件不支持的新版本、Payload 校验失败或插件抛错时,Registry 统一返回 fallbackText;原始 Data 仍由 Core 保存和分页,不能因为 UI 不认识而丢弃或阻塞 Cursor。

若插件返回 rendererKey,可在 XHIMMessageRendererRegistry 注册 XHIMMessageItemRenderer。Renderer 只接收复制后的 XHIMMessage 和展示摘要; 失败时退回基础文本气泡。不要在插件或 Renderer 中读取 SQLite、持有 C handle, 也不要用 Renderer 结果替换消息协议真相。完整示例见 Examples/QuickStart.swift

8. C ABI Bridge 规则

  • 创建所有 C 结构体前清零,填写 struct_sizeXHIM_V1_ABI_VERSION
  • Swift 字符串显式编码为 UTF-8 byte buffer;不能把字符数量当字节长度;
  • Callback 里的 xhim_v1_bytes_view_t、消息数组和 Cursor 在返回前深拷贝为 Data/String
  • C Callback 不在 MainActor。复制完成后再用受控 continuation、Actor 或 MainActor.run 投递;
  • 每个异步请求只恢复 continuation 一次;同步拒绝时立即释放 callback context;
  • Swift Task.cancel() 会调用 native cancel_request;收到 request_cancelled 后只结束该 Task,不假定已提交消息或已读事务被回滚;
  • XHIMNativeError 完整复制 domain/stableCode/nativeCode/retryable/retryAfterMilliseconds/userAction/operationID/traceID;业务不解析 message;
  • 收到 .credentialRequired 后只调用 updateCredential,不要销毁数据库或用 account hint 替换服务端确认的 canonical account;
  • cancelMessage 只取消尚未进入发送中的本地 Outbox,不等于服务端撤回;
  • markConversationRead 只提交 XHIM 时间线中已展示消息的服务端序号;等待 receipt 后再更新未读,不在 Swift 层维护第二份 read sequence;
  • Event subscription 与 Client 分开持有,先取消订阅,再 shutdown/destroy;
  • client_destroy 必须与全部 Client 调用串行化。不要依赖 Swift deinit 完成正常关闭;deinit 只做泄漏兜底;
  • Callback 内可以发起新请求,但不能阻塞等待另一个 XHIM Callback。

9. 生命周期和应用状态

建议由 App 的账号/会话容器拥有 XHIMClient,而不是由某个 ViewController 或 SwiftUI View 创建:

text
App/Scene 启动 → create/start
账号登录       → login,等待 Ready
进入后台       → 保持 Core;按策略暂停附件任务,不销毁数据库
账号退出       → logout,清理账号级 UI 状态
进程终止       → shutdown,再释放 Wrapper

多 Scene 共享一个账号 Client。不同账号使用不同 Client 和数据库目录,不能让 多个 Scene 或 Extension 同时打开同一 profile。Notification Service Extension 也不能直接复用主 App 数据库;通知解码应使用单独、明确版本化的轻量组件。

10. 存储和安全

  • 数据库放在 Application Support 下的账号隔离目录,不放 Bundle、Documents 公共导出区或 Caches;
  • 数据库的 -wal-shm 和媒体临时文件执行一致的数据保护与备份策略;
  • 若后台推送后需要在首次解锁之后访问数据,应由产品安全评审选择合适的 Data Protection 等级,不能为了后台方便直接关闭保护;
  • Token 和数据库/媒体密钥进入 Keychain,不进入 UserDefaults、日志或 Crash 附件;正式包把随机数据库密钥注入 SQLCipher Product Adapter;
  • storage profile 目录名使用不透明 ID,不直接使用手机号、邮箱或 Token;
  • App Group、Extension 和多进程访问必须单独设计,当前版本不支持共享连接。

11. 附件消息与可选实时音视频

  • XHIMAttachmentPickerButton 基于 PHPicker/UIImagePicker/DocumentPicker/ NSOpenPanel,返回沙盒可读 URL;
  • sendImage/sendVideo/sendAudio/sendFile 默认创建 Core 持久化上传任务; 后台计算 SHA-256、鉴权、断点续传、校验和依赖消息入队由 Core 负责;
  • API 返回只表示 durable admission。事件只给可查询的 task hint,完整状态通过 mediaTask(taskID:) 获取;cancelMediaTask 与取消一次 Swift Task 的 通用 request cancellation 语义不同;
  • acceptMediaDownload 使用稳定 MediaRef 和扁平 cacheKey 创建持久化 下载。不得从 storagePath 推导缓存路径,也不得把服务器文件名当 cache key;
  • 下载完成后只通过 XHIMMediaCacheReader 异步读取已校验字节;open/读取在 I/O 队列,单 Reader 串行且可晚于 Client 关闭,停止 chunk 循环即取消;
  • Apple Reference/Product Adapter 未提供安全 cache reader 时,open 明确返回 media_cache_unsupported,不会退化为路径拼接;
  • task、event、diagnostics() 和网络 Payload 都不包含本地路径、临时 URL、 bearer token 或签名 URL;diagnostics 是同步、只读、纯内存快照;
  • notifyNetworkAvailable() 仅把 NWPathMonitor 的“网络恢复”转成同步 best-effort 提示,无 callback/request ID;Core 重连定时器继续作为保底;
  • XHIMServerMediaUploader/XHIMMediaUploading 仅作为旧版兼容和定制迁移 接口,不是默认商用接入路径;
  • 实时音视频不属于纯 IM 首发包的 Stable 门禁。后续接入厂商时使用独立可选 XHIMCallKitBridge 和版本化 Provider SPI;
  • CallKit、PushKit、AVAudioSession、蓝牙路由和厂商 RTC 对象不得进入消息 Payload,也不得穿过稳定 C ABI。

12. UI Kit 与二次开发

建议拆分:

text
XHIM                 Headless Swift Facade
XHIMUIKit            UIKit 会话/聊天/联系人/媒体组件
XHIMSwiftUI          SwiftUI 包装与页面
XHIMCallKitBridge    系统通话桥,可选

UI Kit 通过协议注入主题、头像/图片加载、国际化、路由、菜单动作和自定义消息 Renderer。业务自定义 Cell 只读取平台消息模型,不读取 SQLite、不持有 C handle。

当前 XHIMSwiftUI 已内置同一套可商用 Lucide 矢量图标和 XHIMTheme。业务可直接使用 XHIMIconView(.send)XHIMIconView(.paperclip),也可通过 .environment(\.xhimTheme, customTheme) 覆盖颜色。资源来源、固定 Commit、 SHA-256 和许可证见 五端 UI 资源说明

13. 发布验收

  • 空白消费者分别只通过最终 Swift Package、CocoaPods 完成 iOS Device/Simulator 与 macOS 构建,SDK 验证阶段关闭 App 签名;
  • 发布者另行完成空白 macOS App arm64/x86_64 的 Developer ID/公证验证;
  • xhim_v1_abi_version() 与 Wrapper 编译时常量一致;
  • Instruments/Thread Sanitizer 下无 callback context、subscription 或 handle 泄漏;
  • 前后台、Scene 重建、锁屏、网络切换、进程重启和账号切换通过真机测试;
  • XCFramework ZIP checksum、外层 Bundle 签名、Build Manifest/Release Attestation、Privacy Manifest、符号文件、LICENSE、NOTICE 和 SBOM 齐全;
  • UI Kit 删除后 Headless SDK 仍可登录、收发和分页查询。

14. OfflineReader 平台契约

XHIMOfflineReader 是 iOS/macOS 共用的只读 facade,直接覆盖当前 C ABI 的消息、 消息搜索、会话以及好友申请/好友/群/群成员/黑名单/入群申请六类分页。

  • open、查询和 close 全部在私有 utility queue 执行;
  • 每次 C 调用返回前把 page、嵌套字符串、payload 和 opaque cursor 深拷贝为 Swift 值对象;
  • 句柄只在该串行队列访问,close() 幂等,关闭后返回 offline_reader_closed
  • 数据库 Key 只能由 XHIMOfflineDatabaseKeyProvider 在运行时注入。SDK 清理 临时 Data,不得把 Key 写进代码、plist、UserDefaults、日志或崩溃附件;
  • 未知 status 保留原始 code;消息和社交未知枚举同时保留 native* 值。

业务接入代码见 iOSmacOS。OfflineReader 不负责建库、 迁移、登录或网络同步,传入的必须是 Core 已创建且账号绑定一致的当前 Schema 数据库。

XHIM 客户端 SDK 与服务端文档