主题
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、测试userID和conversationID。
appID 在多租户部署时由服务端负责人提供;单租户可由 SDK 从 Server 自动 读取默认值。业务正式上线时只需再接一个账号层鉴权回调,见第 5 节,不影响 CocoaPods 安装和 Development 联调。
检查 CocoaPods:
bash
pod --version没有安装时,请按照 CocoaPods 官方安装指南 安装。XHIM 不要求全局安装其他构建工具。
2. 新建 App
- 在 Xcode 选择
File → New → Project...; - 选择
iOS → App; - Product Name 例如
XHIMExample; - Interface 选择
SwiftUI,Language 选择Swift; - Minimum Deployment 选择 iOS 15 或更高;
- 保存并关闭 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 XHIMSwiftUI5. 第一次连接
在账号会话对象中集中管理 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 默认值 |
storageURL | SDK 按 App ID + User ID 创建,可用 storageRootURL 覆盖根目录 |
| Bootstrap URL | server |
| Endpoint Key ID + Public Key | Bootstrap 公开配置 |
正式 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'
确认:
- 使用
.xcworkspace; - Podfile 中的 Target 名称与 Xcode 一致;
pod install没有报错;- 不要同时手工拖入另一份
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 例外;
- 验证会话/历史分页、文本/媒体/自定义消息、已读、编辑撤回和失败重试;
- 验证前后台、断网、账号切换、推送以及数据库升级恢复;
- 锁定
XHIM与XHIMSwiftUI的同一正式版本。
11. 升级和移除
升级到兼容版本:
bash
pod update XHIM XHIMSwiftUIXHIMSwiftUI 与 XHIM 使用相同版本发布,不能混用不同版本。
移除 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 协议兼容快照。