Skip to content

iOS 使用 CocoaPods 从零接入晞晗IM

这份文档只面向购买或试用 XHIM iOS SDK 的 iOS 开发者。服务端部署由运维 人员按 XHIM Server 文档 独立完成,本页只处理 CocoaPods 客户端工程。

跑通第一条消息后,所有公开方法、事件、模型和错误请查 SDK API Reference

完成接入后,Development 环境的业务代码只需要:

swift
import XHIM

XHIMClient.connect(
    server: "https://im-test.customer.com",
    userID: "alice",
    onConnecting: {
        print("正在连接")
    },
    onConnectSuccess: { client in
        print("连接成功", client)
    },
    onConnectFailure: { error in
        print("连接失败", error.code, error.message)
    }
)

你不需要向 XHIM 提供 Apple 证书,也不需要在 iOS 工程中配置数据库、对象 存储、服务端密钥或手工 Token。CocoaPods 安装完成后只填写 Server URL 和 当前 User ID。

1. 接入前准备

最低环境:

  • iOS 15;
  • Xcode 15;
  • Swift 5.9;
  • CocoaPods 1.11 或更高;
  • 服务端负责人提供的 server、测试 userIDconversationID

appID 在多租户部署时由服务端负责人提供;单租户可由 SDK 从 Server 自动 读取默认值。业务正式上线时只需再接一个账号层鉴权回调,见第 5 节,不影响 CocoaPods 安装和 Development 联调。

检查 CocoaPods:

bash
pod --version

没有安装时,请按照 CocoaPods 官方安装指南 安装。XHIM 不要求全局安装其他构建工具。

2. 新建 App

  1. 在 Xcode 选择 File → New → Project...
  2. 选择 iOS → App
  3. Product Name 例如 XHIMExample
  4. Interface 选择 SwiftUI,Language 选择 Swift
  5. Minimum Deployment 选择 iOS 15 或更高;
  6. 保存并关闭 Xcode 工程。

Apple Team 只用于运行或发布你的 App,接入 SDK 本身不需要把证书交给 XHIM。

3. 创建 Podfile

.xcodeproj 所在目录执行:

bash
pod init

打开 Podfile,写入:

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

platform :ios, '15.0'

target 'XHIMExample' do
  use_frameworks! :linkage => :static

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

XHIMExample 换成你的 App Target 名称。

  • XHIM 是必选的通信、消息、同步、媒体和本地数据库 SDK;
  • XHIMSwiftUI 是可选 UI 组件;
  • 完全自定义 UI 时,删除 XHIMSwiftUI 那一行即可。

0.1.0-dev.2 是本轮生成并通过隔离消费者构建的受控 Development 快照,不是 Stable 销售制品。发行负责人把制品上传并登记到私有 Specs 后,使用上面的远程 Podfile;登记前先使用下方“本地试用包”。必须使用 = 精确锁定,~> 0.1 不会匹配 prerelease。正式版本发布后,以其签名 Release Manifest 中的版本替换 这两行。

如果销售人员提供的是 HTTPS Specs 地址,把第一行替换成收到的地址。业务方不需要 访问 XHIM C++ 源码仓库。

使用本地试用包

需要验证当前源码对应的客户包,或该版本尚未发布到私有 Specs 时,把发行负责人 提供的同版本 XHIM 发行目录放在项目旁边:

text
Workspace/
├── XHIMSwift-development/
└── XHIMExample/
    ├── XHIMExample.xcodeproj
    └── Podfile

对应 Podfile:

ruby
source 'https://cdn.cocoapods.org/'

platform :ios, '15.0'

target 'XHIMExample' do
  use_frameworks! :linkage => :static

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

不要单独拖入 .a、头文件、SQLite 或 SQLCipher。它们已经封装在 XHIMCore.xcframework 中。

4. 安装 SDK

执行:

bash
pod install --repo-update

以后始终打开:

text
XHIMExample.xcworkspace

不要继续打开 .xcodeproj

验证 Headless SDK:

swift
import XHIM

使用 UI 组件时再增加:

swift
import XHIMSwiftUI

5. 第一次连接

在账号会话对象中集中管理 Client、请求句柄和监听 token。不要让每个页面各自 创建 Client:

swift
import XHIM

@MainActor
final class IMService {
    static let shared = IMService()

    private(set) var client: XHIMClient?
    private var connectRequest: XHIMRequest?
    private var eventToken: XHIMEventListenerToken?

    func connect(
        server: String,
        userID: String,
        onSuccess: @escaping () -> Void,
        onFailure: @escaping (XHIMCallbackError) -> Void
    ) {
        connectRequest = XHIMClient.connect(
            server: server,
            userID: userID,
            onConnecting: {
                print("XHIM 正在连接")
            },
            onConnectSuccess: { [weak self] client in
                self?.client = client
                self?.installListener(client)
                onSuccess()
            },
            onConnectFailure: onFailure
        )
    }

    private func installListener(_ client: XHIMClient) {
        eventToken = client.addEventListener { event in
            NotificationCenter.default.post(
                name: .xhimEvent,
                object: event
            )
        }
    }
}

extension Notification.Name {
    static let xhimEvent = Notification.Name("com.xhim.sdk.event")
}

Development 测试环境只需要服务地址和账号。SDK 自动完成公开配置发现、账号隔离 数据库、启动内核、测试登录、WebSocket 连接、增量同步和登录状态维护。

公开配置与底层最小配置的对应关系为:

Core 配置CocoaPods App 的来源
appID显式 appID,否则 Bootstrap 默认值
storageURLSDK 按 App ID + User ID 创建,可用 storageRootURL 覆盖根目录
Bootstrap URLserver
Endpoint Key ID + Public KeyBootstrap 公开配置

正式 App 已有自己的账号登录时,只需要把鉴权函数放在账号服务中:

swift
extension AccountService {
    func xhimCredential() async throws -> String {
        // 这里调用购买方自己的业务登录接口。
        try await api.fetchXHIMCredential()
    }
}

连接时把 AccountService.shared.xhimCredential 交给 authentication: .business(...)。普通页面不处理登录票据;需要续期时 SDK 会再次调用该函数并自动更新连接。 Production 必须使用 .business 与可信 HTTPS/WSS;Release 不允许 .development.localPreview 或 Easy Login。

6. 收发消息

连接成功后立即安装账号级监听,再查询首屏:

swift
eventToken = client.addEventListener { [weak self] event in
    switch event {
    case .stateChanged(let state):
        self?.updateConnectionState(state)
    case .messageUpserted(let change),
         .messageStateChanged(let change):
        self?.reloadMessages(conversationID: change.conversationID)
    case .conversationChanged:
        self?.reloadConversations()
    case .conversationReadChanged:
        self?.reloadUnreadCount()
    default:
        break
    }
}

client.conversations(
    limit: 50,
    onSuccess: { page in
        self.items = page.conversations
    },
    onFailure: { error in
        self.showError(error.message)
    }
)

和 Bob 建立单聊并发送文字:

swift
client.directConversation(
    with: "bob",
    onSuccess: { conversation in
        client.sendText(
            conversationID: conversation.conversationID,
            text: "你好,晞晗IM",
            onSuccess: { _ in
                print("消息已进入可靠发送队列")
            },
            onFailure: { error in
                print("发送失败", error.stableCode, error.message)
            }
        )
    },
    onFailure: { error in
        print("创建单聊失败", error.message)
    }
)

查询最近消息并上报已读:

swift
client.messages(
    conversationID: conversationID,
    limit: 50,
    onSuccess: { page in
        self.messages = page.messages

        guard let sequence = page.messages
            .compactMap(\.serverSequence)
            .max() else { return }

        client.markConversationRead(
            conversationID: conversationID,
            throughServerSequence: sequence,
            onSuccess: { receipt in
                self.unreadCount = receipt.unreadCount
            },
            onFailure: { error in
                print("已读上报失败", error.message)
            }
        )
    },
    onFailure: { error in
        self.showError(error.message)
    }
)

回调均在主线程执行。XHIMEventListenerToken 必须由账号容器强引用,释放 token 即停止监听。完整的回调方法、参数、返回值和错误字段见 iOS 回调式 API

6.1 Swift Concurrency 进阶写法

已经采用 async/await 架构的团队可以继续使用原生并发接口:

swift
let task = Task {
    for await event in client.events {
        await MainActor.run {
            viewModel.consume(event)
        }
    }
}
let conversations = try await client.conversations(limit: 50)
let page = try await client.messages(
    conversationID: conversationID,
    limit: 50
)

分页 Cursor 是不透明 Data,只能原样传回对应查询。发送失败时复用上面的 clientMessageID 调用 retryMessage,不要把重试当成一条新消息。

需要补拉服务端历史或修改当前账号自己的可见范围时:

swift
let history = try await client.getMessageHistory(
    conversationID: "xhim-demo-direct",
    limit: 50
)
if let continuation = history.continuation {
    _ = try await client.getMessageHistory(
        conversationID: "xhim-demo-direct",
        continuation: continuation,
        limit: 50
    )
}

if let serverMessageID = history.messages.first?.serverMessageID {
    _ = try await client.deleteMessageForSelf(
        conversationID: "xhim-demo-direct",
        serverMessageID: serverMessageID,
        mutationID: UUID().uuidString
    )
}

if history.latestServerSequence > 0 {
    let cleared = try await client.clearConversation(
        conversationID: "xhim-demo-direct",
        throughServerSequence: history.latestServerSequence,
        mutationID: UUID().uuidString,
        expectedRevision: history.view?.revision ?? 0
    )
    _ = try await client.hideConversation(
        conversationID: "xhim-demo-direct",
        mutationID: UUID().uuidString,
        expectedRevision: cleared.view.revision
    )
}

continuation 只能原样传回。删除、清空和隐藏只影响当前账号;同一次逻辑操作 失败重试时复用原 mutationID。后续 CAS 使用最新返回的 view.revision, 冲突时重新拉取历史。取消外层 Swift Task 会取消对应 native request。

编辑或撤回服务端已确认的消息:

swift
guard let message = page.messages.first,
      let serverMessageID = message.serverMessageID else { return }

let edited = try await client.editText(
    conversationID: message.conversationID,
    serverMessageID: serverMessageID,
    mutationID: UUID().uuidString,
    expectedRevision: message.mutationRevision,
    text: "修改后的内容"
)

_ = try await client.recall(
    conversationID: edited.conversationID,
    serverMessageID: serverMessageID,
    mutationID: UUID().uuidString,
    expectedRevision: edited.mutationRevision
)

mutationID 是一次操作的幂等键:同一次网络重试必须复用;新的用户操作才生成 新的 UUID。版本冲突时重新加载时间线,不要盲目覆盖服务端新版本。

7. 使用基础 UI

swift
import SwiftUI
import XHIMSwiftUI

struct ChatPage: View {
    @ObservedObject var viewModel: ChatViewModel

    var body: some View {
        XHIMChatView(
            messages: viewModel.messages,
            onSend: viewModel.send
        )
    }
}

UI 层不保存凭证,也不直接读取数据库。客户可以替换导航、主题、气泡和自定义消息 渲染器,不需要修改二进制内核。

8. 图片、视频、拍照和文件

使用系统附件入口:

swift
XHIMAttachmentPickerButton { attachment in
    viewModel.send(attachment)
} onFailure: { error in
    viewModel.show(error)
}

将附件保留在 App 的持久沙箱后直接提交给 Core,不创建 uploader:

swift
client.sendImage(
    conversationID: "xhim-demo-direct",
    fileURL: attachment.localURL,
    mimeType: attachment.mimeType,
    width: 1080,
    height: 1920,
    onSuccess: { task in
        viewModel.observeMediaTask(task.taskID)
    },
    onFailure: { error in
        viewModel.show(error.message)
    }
)

成功回调只表示持久任务已受理;监听 .mediaTaskUpdated 并按 task ID 查询 终态。取消外层 Swift Task 只取消当前请求等待,持久业务取消调用 cancelMediaTask(taskID:)。下载使用 acceptMediaDownload(XHIMMediaDownloadIntent),其中 cacheKey 是扁平私有 标识而不是路径。任务、事件和 try client.diagnostics() 均不包含本地路径、 用户凭证或签名 URL。

下载到达 .completed 后调用 client.openMediaCacheReader(cacheKey:mediaRef:),通过 reader.chunks() 异步消费已做大小和 SHA-256 校验的字节,结束时 await reader.close();不要 推导缓存路径。NWPathMonitor 报告 .satisfied 时可调用 try? client.notifyNetworkAvailable() 提前唤醒重连,原有退避定时器仍保底。

应用使用相机、照片或麦克风时,仍需按照自身功能在 Info.plist 中配置对应的系统 用途说明。XHIM 不会替客户自动修改 App 隐私文案。

8.1 位置、名片、回复、合并转发与本地搜索

这些标准消息使用 XHIM 固定的 Protobuf 契约,业务层不需要自己拼 Payload:

swift
import XHIM

let location = try XHIMStandardMessageFactory.location(
    latitude: 31.2304,
    longitude: 121.4737,
    name: "人民广场",
    address: "上海市黄浦区"
)
try await client.sendMessage(
    conversationID: conversationID,
    message: location
)

let card = try XHIMStandardMessageFactory.contactCard(
    userID: "bob",
    displayName: "Bob"
)
try await client.sendMessage(
    conversationID: conversationID,
    message: card
)

let result = try await client.searchMessages(
    XHIMMessageSearchQuery(
        text: "合同",
        conversationID: conversationID,
        contentTypes: ["text/plain", "xhim.media.file"]
    )
)
result.messages.forEach { print($0.fallbackText) }

搜索只读取当前账号本机已经同步的安全文本投影,不上传搜索词。继续翻页时把 result.nextCursor 原样传回,不能解析或改写 Cursor。

8.2 自定义消息

swift
import Foundation

struct OrderCard: Codable {
    let orderID: String
    let title: String
}

let custom = XHIMOutgoingMessage(
    contentType: "com.customer.message.order-card",
    contentVersion: 1,
    payload: try JSONEncoder().encode(
        OrderCard(orderID: "order-1001", title: "待付款订单")
    ),
    fallbackText: "[订单] 待付款订单"
)
_ = try await client.sendMessage(
    conversationID: conversationID,
    message: custom
)

OrderCard 是业务自己的 Codable 模型。接收端按 contentType + contentVersion 解码,未知版本展示 fallbackText;统一校验 和 UI Renderer 可通过 XHIMMessagePluginRegistry 注册,不能修改二进制内核。

9. 常见问题

No such module 'XHIM'

确认:

  1. 使用 .xcworkspace
  2. Podfile 中的 Target 名称与 Xcode 一致;
  3. pod install 没有报错;
  4. 不要同时手工拖入另一份 XHIMCore.xcframework

找不到 XHIM 私有 Specs 仓库

先确认你有仓库读取权限和正确的 SSH/HTTPS 地址,再执行:

bash
pod repo update
pod install

使用本地试用包时不需要连接私有 Specs 仓库。

图片图标没有显示

确认 XHIMSwiftUI 通过 CocoaPods 安装,不要只复制其中的 Swift 文件。 XHIMSwiftUIResources.bundle 必须存在于最终 App Bundle。

Passwordless login is disabled

当前服务地址没有开放 Development 测试账号直登。把错误、服务地址和账号交给 服务端负责人;不要在 iOS 工程中绕过业务登录。

局域网 HTTP 测试服务无法连接

仅在 Development App 的 Info.plist 配置局域网和 ATS 测试权限。Production 必须使用系统信任的 HTTPS/WSS。

连接错误按 XHIMConnectionError 分支,运行时错误按 XHIMNativeError.code/domain/stableCode/retryable 分支;不要解析可读 message 决定重试。

10. 登出与 Production 清单

swift
client.logout(
    onSuccess: {
        eventToken?.cancel()
        eventToken = nil
        self.client = nil
    },
    onFailure: { error in
        print("退出失败", error.message)
    }
)
  • 同一账号只创建一个 Client,换号前完整登出和销毁;
  • Production 使用 .business、可信 HTTPS/WSS 和签名正式 Pods;
  • Release 禁用 Local Preview、Easy Login 和开发 ATS 例外;
  • 验证会话/历史分页、文本/媒体/自定义消息、已读、编辑撤回和失败重试;
  • 验证前后台、断网、账号切换、推送以及数据库升级恢复;
  • 锁定 XHIMXHIMSwiftUI 的同一正式版本。

11. 升级和移除

升级到兼容版本:

bash
pod update XHIM XHIMSwiftUI

XHIMSwiftUIXHIM 使用相同版本发布,不能混用不同版本。

移除 SDK 时,从 Podfile 删除对应行并执行:

bash
pod install

客户端完整功能说明见 iOS 接入指南。服务端操作仅见 XHIM Server 文档

CocoaPods 中使用离线只读能力

XHIMOfflineReader 已包含在 XHIM Pod 内,不需要额外 Pod 或 linker 参数:

swift
import XHIM

let reader = try await XHIMOfflineReader.open(
    databaseURL: accountDatabaseURL,
    accountID: currentUserID,
    keyProvider: {
        try accountKeyStore.databaseKey(accountID: currentUserID)
    }
)
let page = try await reader.conversations()
await reader.close()

Key 必须由 Keychain/安全模块在运行时提供,不能写入 Podfile、.xcconfig、plist 或 Git。完整消息、搜索、会话和社交分页示例见 iOS 接入指南

CocoaPods 中使用登录设备管理

设备会话 API 已包含在 XHIM Pod,不需要附加 subspec:

swift
let sessions = try await client.listDeviceSessions()
if let target = sessions.sessions.first(where: {
    !$0.isCurrent && $0.isActive
}) {
    _ = try await client.revokeDeviceSession(
        sessionID: target.sessionID,
        mutationID: UUID().uuidString
    )
}
let policy = try client.deviceSessionPolicySnapshot()

同步策略快照不要求先调用列表;登录完成前或登出后会抛出原生 INVALID_STATE,且整个读取过程不执行网络或磁盘 I/O。

完整的幂等、自撤销、取消与日志脱敏约束见 iOS 登录设备管理

CocoaPods 中读取协议兼容快照(仅诊断/灰度)

无需新增 Pod 或配置项:

swift
let compatibility = try client.compatibilitySnapshot()
print(compatibility.serverProtocolVersion)

普通业务无需处理该值,登录已经对协议不兼容 fail-closed。同步 getter 仅用于 诊断、支持包和灰度观测,不访问网络或磁盘;登录前和登出后抛出原生 INVALID_STATE。不要记录版本字符串或 capability 值,完整约束见 iOS 协议兼容快照

XHIM 客户端 SDK 与服务端文档