Skip to content

iOS 回调式 API

适用版本:0.1 Commercial Beta 适用模块:XHIM 最低版本:iOS 15、Swift 5.9

本页是 iOS 开发者的首选 API 入口。它采用与成熟 IM SDK 一致的 onSuccess / onFailure 形式,不要求接入者先理解 Swift async throws。 所有回调都在 MainActor 执行,可以直接更新 UIKit / SwiftUI 状态。

原有 Swift Concurrency API 完整保留,适合 Repository、批处理和高级并发控制。 两套入口调用的是同一个 XHIM Core,不会形成两份状态或两套数据库。

Completion 层不是示例专用的假包装。手写的高频 API 与生成的 类型安全重载共同覆盖全部公开异步/抛错入口;每个生成重载在 Swift 编译中直接调用原始强类型方法。消息工厂、取消标记等本来就是 同步且不失败的操作,仍保持直接调用,不为了形式统一增加多余回调。

1. 回调类型

swift
typealias XHIMSuccessCallback<Value> = (Value) -> Void
typealias XHIMFailureCallback = (XHIMCallbackError) -> Void
typealias XHIMEventCallback = (XHIMEvent) -> Void
类型用途
XHIMRequest一次异步请求的句柄;调用 cancel() 取消等待中的请求
XHIMEventListenerToken账号事件监听句柄;释放或调用 cancel() 即退订
XHIMCallbackError稳定错误对象,包含 code、stableCode、retryable 和 traceID

XHIMRequest.cancel() 只取消当前 API 请求。已经进入 Outbox 的消息用 cancelMessage,持久媒体任务用 cancelMediaTask

2. 连接并登录

方法原型

swift
XHIMClient.connect(
    server:userID:appID:authentication:storageRootURL:
    onConnecting:onConnectSuccess:onConnectFailure:
) -> XHIMRequest

参数

参数类型必填说明
serverStringXHIM Server 基地址,例如 https://im.example.com
userIDString当前业务用户 ID
appIDString?单租户可省略,由服务端公开配置返回
authenticationXHIMAuthenticationDevelopment 默认免 Token;Production 使用 business provider
storageRootURLURL?自定义账号数据库根目录,通常省略

回调

回调参数触发时机
onConnectingSDK 开始发现配置、启动 Core 并建立连接
onConnectSuccessXHIMClient登录、连接和首次同步完成
onConnectFailureXHIMCallbackError任一步骤失败

Development 示例

swift
private var client: XHIMClient?
private var connectRequest: XHIMRequest?

connectRequest = XHIMClient.connect(
    server: "https://im-test.example.com",
    userID: "alice",
    onConnecting: {
        print("XHIM 正在连接")
    },
    onConnectSuccess: { [weak self] client in
        self?.client = client
        print("XHIM 已连接")
    },
    onConnectFailure: { error in
        print("连接失败:", error.code, error.message)
    }
)

Development Server 显式启用免密测试登录时,客户端只填写 serveruserID。App 不需要手工申请、保存或续期测试 Token。

Production 示例

swift
connectRequest = XHIMClient.connect(
    server: "https://im.example.com",
    userID: account.userID,
    authentication: .business(
        AccountService.shared.xhimCredential
    ),
    onConnecting: {
        connectionState = .connecting
    },
    onConnectSuccess: { [weak self] client in
        self?.client = client
        self?.connectionState = .connected
    },
    onConnectFailure: { [weak self] error in
        self?.showConnectionError(error)
    }
)

AccountService.xhimCredential 属于 App 自己的账号层。页面不接触 Token; XHIM 需要续期时会自动再次调用它。

3. 添加事件监听

方法原型

swift
client.addEventListener(_:) -> XHIMEventListenerToken

示例

swift
private var eventToken: XHIMEventListenerToken?

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()

    case .presenceChanged(let update):
        self?.updatePresence(update)

    case .typingChanged(let update):
        self?.updateTyping(update)

    case .customSignalReceived(let event):
        self?.handleOnlineCollaborationSignal(event.signal)

    default:
        break
    }
}

必须先保存监听 token,再查询首屏。退出账号时:

swift
eventToken?.cancel()
eventToken = nil

事件是“本地投影已变化”的通知,不是完整消息副本。收到消息或会话事件后,调用 查询 API 刷新对应页面。

customSignalReceived 是唯一例外:它携带经过 Core 会话 Fence 与服务端 TTL 校验的在线瞬时载荷,不写消息历史,也不会离线补发。需要离线可靠到达、审计或 重放的业务必须发送版本化自定义消息,不能把该事件用于订单、OA 审批或支付。

4. 获取或创建单聊

方法原型

swift
client.directConversation(
    with:onSuccess:onFailure:
) -> XHIMRequest

示例

swift
client.directConversation(
    with: "bob",
    onSuccess: { conversation in
        self.openChat(conversationID: conversation.conversationID)
    },
    onFailure: { error in
        self.showError(error.message)
    }
)

不要在客户端自行拼接单聊会话 ID。

5. 发送文字消息

方法原型

swift
client.sendText(
    conversationID:clientMessageID:text:
    onSuccess:onFailure:
) -> XHIMRequest

参数

参数类型必填说明
conversationIDString服务端返回的会话 ID
clientMessageIDString?幂等 ID;省略时由 SDK 生成
textString文字内容

示例

swift
let messageID = UUID().uuidString.lowercased()

client.sendText(
    conversationID: conversationID,
    clientMessageID: messageID,
    text: "你好,晞晗IM",
    onSuccess: { receipt in
        print("消息已进入发送队列:", receipt.payload)
    },
    onFailure: { error in
        if error.retryable {
            self.showRetryButton(clientMessageID: messageID)
        } else {
            self.showError(error.message)
        }
    }
)

成功表示可靠 Outbox 已接受消息,不等于对端已读。最终发送状态通过消息查询和 messageStateChanged 事件刷新。

6. 发送自定义消息

方法原型

swift
client.sendMessage(
    conversationID:clientMessageID:message:
    onSuccess:onFailure:
) -> XHIMRequest

示例

swift
let payload = Data(
    #"{"orderID":"20260728001","amount":199}"#.utf8
)
let message = XHIMOutgoingMessage(
    contentType: "com.example.order.card",
    contentVersion: 1,
    payload: payload,
    fallbackText: "[订单] 20260728001"
)

client.sendMessage(
    conversationID: conversationID,
    message: message,
    onSuccess: { _ in
        print("自定义消息已入队")
    },
    onFailure: { error in
        print(error.stableCode, error.message)
    }
)

自定义类型必须使用自有命名空间、正整数版本和安全 fallback。配套解析与 UI Renderer 见二次开发扩展点

7. 查询会话列表

方法原型

swift
client.conversations(
    cursor:limit:onSuccess:onFailure:
) -> XHIMRequest

示例

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

下一页把 page.nextCursor 原样传回 cursor。收到 conversationChanged 后重新查询第一页。

8. 查询消息列表

方法原型

swift
client.messages(
    conversationID:cursor:limit:onSuccess:onFailure:
) -> XHIMRequest

示例

swift
client.messages(
    conversationID: conversationID,
    limit: 50,
    onSuccess: { page in
        self.messages = page.messages
        self.nextCursor = page.nextCursor
    },
    onFailure: { error in
        self.showError(error.message)
    }
)

查询更早的服务端历史使用:

swift
client.getMessageHistory(
    conversationID: conversationID,
    continuation: historyContinuation,
    limit: 50,
    onSuccess: { page in
        self.prepend(page.messages)
        self.historyContinuation = page.continuation
    },
    onFailure: { error in
        self.showError(error.message)
    }
)

9. 已读和未读

标记会话已读

swift
client.markConversationRead(
    conversationID: conversationID,
    throughServerSequence: latestServerSequence,
    onSuccess: { receipt in
        self.unreadCount = receipt.unreadCount
    },
    onFailure: { error in
        self.showError(error.message)
    }
)

批量上报群消息已读

swift
client.markGroupMessagesRead(
    conversationID: groupConversationID,
    serverMessageIDs: visibleServerMessageIDs,
    onSuccess: { receipt in
        self.unreadCount = receipt.unreadCount
    },
    onFailure: { error in
        self.showError(error.message)
    }
)

查询逐消息已读成员

swift
client.groupMessageReaders(
    conversationID: groupConversationID,
    serverMessageIDs: [serverMessageID],
    peerLimit: 1_000,
    onSuccess: { page in
        self.readerUserIDs = page.details.first?.readerUserIDs ?? []
    },
    onFailure: { error in
        self.showError(error.message)
    }
)

peerReadsTruncatedtrue 时,readCount 只是已投影的下界。 当前用户不包含在 reader IDs 内。

查询总未读

swift
client.totalUnreadCount(
    onSuccess: { count in
        UIApplication.shared.applicationIconBadgeNumber = Int(count)
    },
    onFailure: { error in
        print(error.message)
    }
)

全部标记已读

swift
client.markAllConversationsRead(
    mutationID: UUID().uuidString,
    onSuccess: { result in
        self.reloadConversations()
        print("已更新会话数:", result.changedConversationCount)
    },
    onFailure: { error in
        self.showError(error.message)
    }
)

10. 用户、好友与群组查询

这些查询具有相同的 onSuccess / onFailure 结构:

swift
client.currentUserProfile(
    onSuccess: { profile in self.profile = profile },
    onFailure: { error in self.showError(error.message) }
)

client.userProfiles(
    userIDs: ["alice", "bob"],
    onSuccess: { batch in self.consume(batch) },
    onFailure: { error in self.showError(error.message) }
)

phoneLookupRequest = client.resolveUser(
    phoneNumber: "13800138000",
    onSuccess: { profile in
        self.showFriendConfirmation(profile)
    },
    onFailure: { error in
        self.showLookupError(code: error.stableCode)
    }
)

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

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

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

resolveUser(phoneNumber:) 只接受一个完整手机号并做精确匹配,返回的 profile.userID 才能传给好友申请或单聊 API,普通页面展示 profile.publicUserID,不直接暴露内部 userID。它不支持手机号片段、昵称、模糊 或批量搜索。页面退出或输入改变时调用 phoneLookupRequest?.cancel();取消会 返回稳定错误 request_cancelled,UI 应静默处理。

用户更新头像或昵称后,资料事件会到达本人、有效好友以及共享会话的成员。 已经打开的聊天页应在事件到达后重新调用 userProfiles 获取最新资料并刷新可见 消息;远程头像加载成功时必须隐藏文字占位,无需让用户退出聊天页后重新进入。

同类回调式查询还包括 friendRequestsblocksgroupJoinRequests

Presence 显式订阅与 Typing 快照

swift
presenceRequest = client.subscribePresence(
    userIDs: [peerUserID],
    onSuccess: { [weak self] snapshots in
        self?.renderPresence(snapshots)
    },
    onFailure: { [weak self] error in
        self?.showError(error.message)
    }
)

typingRequest = client.queryTyping(
    conversationID: conversationID,
    userID: peerUserID,
    onSuccess: { [weak self] snapshot in
        self?.setTypingVisible(snapshot.isTyping)
    },
    onFailure: { [weak self] error in
        self?.showError(error.message)
    }
)

subscribePresence 先向服务端执行整批授权,成功后才原子更新 本地订阅。页面离开时调用回调式 unsubscribePresence;需要检查 当前集合时使用 listPresenceSubscriptions(onSuccess:onFailure:)。 空退订仅会进入显式过滤模式,不会清空已有 ID。页面持续变化仍从 presenceChanged / typingChanged 事件监听,快照 API 不代替长期事件。

11. 媒体任务

图片、视频、语音和文件由 SDK 建立持久媒体任务。创建任务成功不代表上传完成, 最终状态通过 mediaTaskUpdated 监听并用 mediaTask 查询。

swift
client.sendImage(
    conversationID: conversationID,
    fileURL: attachment.localURL,
    mimeType: attachment.mimeType,
    width: 1080,
    height: 1920,
    onSuccess: { task in
        self.observeMediaTask(task.taskID)
    },
    onFailure: { error in
        self.showError(error.message)
    }
)

client.mediaTask(
    taskID: taskID,
    onSuccess: { task in
        self.updateProgress(task)
    },
    onFailure: { error in
        self.showError(error.message)
    }
)

同样提供 sendVideosendAudiosendFile 回调接口;需要完全自定义 持久任务参数时使用 acceptMediaUpload

头像使用独立的账号媒体流程,不需要会话 ID,也不会暴露上传签名:

swift
avatarRequest = client.uploadCurrentUserAvatar(
    fileURL: resizedJPEG,
    mimeType: "image/jpeg",
    progress: { progress in
        Task { @MainActor in
            self.progressView.progress =
                Float(progress.fractionCompleted)
        }
    },
    onSuccess: { profile in
        self.avatarView.setURL(profile.avatarURL)
    },
    onFailure: { error in
        self.showError(error.message)
    }
)

页面退出时可调用 avatarRequest.cancel() 取消当前 HTTP 请求;已完成的服务端 审核或资料更新不会被回滚。头像只接受 JPEG、PNG、HEIC 或 WebP,最大 5 MiB。

取消持久媒体任务:

swift
client.cancelMediaTask(
    taskID: taskID,
    onSuccess: { task in self.updateProgress(task) },
    onFailure: { error in self.showError(error.message) }
)

12. 失败回调与错误处理

swift
func handle(_ error: XHIMCallbackError) {
    switch error.userAction {
    case .retry:
        showRetryButton()
    case .updateCredential:
        accountService.refreshLogin()
    case .checkNetwork:
        showNetworkNotice()
    case .contactSupport:
        showSupport(traceID: error.traceID)
    case .none:
        showError(error.message)
    }
}
字段用途
code平台稳定整数错误码
domain错误子系统
stableCode业务分支和统计的稳定字符串
retryable是否允许使用同一幂等 ID 重试
retryAfterMilliseconds建议的最短等待时间
userAction推荐的用户或账号层动作
operationID / traceID客服与服务端排障
message日志和用户提示,不用于业务判断

13. 退出登录

swift
client.logout(
    onSuccess: { [weak self] in
        self?.eventToken?.cancel()
        self?.eventToken = nil
        self?.client = nil
    },
    onFailure: { error in
        print("退出失败:", error.message)
    }
)

账号容器销毁时还应调用 shutdown() 完成不可逆资源释放。应用层默认沿用本页 Completion 写法即可;已有统一并发封装的团队可在 Repository 内部使用异步重载, 不要把异常处理细节散落到页面代码。

XHIM 客户端 SDK 与服务端文档