Skip to content

晞晗IM iOS Demo

这是一个面向普通体验者和 iOS 开发者的 UIKit 聊天应用,不是 SDK 验收台。 打开后用手机号注册或登录,就能像普通 IM 一样使用“消息、通讯录、我的”三个 页面。

普通体验者:用 Workspace 直接运行

Demo 使用 CocoaPods Workspace 管理 XHIM 二进制 SDK 和 UI 依赖。Pods/ 是 本机生成目录,不随仓库提交;首次拉取代码必须先执行 pod install

  1. 安装 Xcode 16 或更高版本,以及 CocoaPods 1.16.2。
  2. 在仓库根目录执行:
bash
cd platforms/ios/Examples/XHIMUIKitDemo
pod install
../../../../scripts/apple/verify_ios_demo_package.py
open XHIMUIKitDemo.xcworkspace
  1. 在 Xcode 中选择 XHIMUIKitDemo Scheme 和模拟器后点击运行。
  2. 真机运行时,只需在 Signing & Capabilities 中选择你自己的 Apple Team。

以后始终打开 XHIMUIKitDemo.xcworkspace,不要打开 XHIMUIKitDemo.xcodeproj。SDK 是 Demo 内 Packages/XHIMSwift 交付的 预编译二进制 Pod;体验者不需要 XcodeGen、C++、Docker、服务端工具、私有 Specs 权限或晞晗软件的证书。工程不会引用 build-* 临时目录,清理构建产物后也不会 丢失 SDK。

No such module 'XHIM' 或 Pods 未同步

先完全退出 Xcode,再确认 Demo 目录中同时存在:

text
Podfile
Podfile.lock
Packages/XHIMSwift/XHIM.podspec
Packages/XHIMSwift/Package.swift
Packages/XHIMSwift/XHIMCore.xcframework

然后重新安装锁定依赖并打开 Workspace:

bash
cd platforms/ios/Examples/XHIMUIKitDemo
pod install
../../../../scripts/apple/verify_ios_demo_package.py
open XHIMUIKitDemo.xcworkspace

不要使用 pod update 随意升级版本,不要打开 .xcodeproj,也不要执行 Swift Package 的 Resolve PackagesReset Package Caches。如果 Xcode 仍显示旧错误,执行 Product → Clean Build Folder 后重新 Build;以当前 Workspace 的新 Build 结果为准。

只有维护人员修改 project.yml 或增删源码文件时,才需要重新生成工程。生成 .xcodeproj 后必须再次执行 pod install,让 Workspace 和 Pods 引用重新对齐:

bash
cd platforms/ios/Examples/XHIMUIKitDemo
./Scripts/sync_resources.sh
xcodegen generate
pod install
../../../../scripts/apple/verify_ios_demo_package.py

执行顺序固定为 同步资源 → xcodegen generate → pod install → 验证交付包xcodegen generate 会重建 .xcodeproj,如果跳过后面的 pod install,Workspace 中的 CocoaPods 配置会失效并出现 No such module 或 Package resolution 错误。

构建失败后手机仍显示旧行为

pod install 或 Workspace Build 失败时,不会替换手机上已经安装的 App。此时 从桌面启动的仍然是上一次成功安装的旧二进制;即使源码和服务端已经修复,旧 App 仍可能继续发送修复前的请求。

先确认当前 Workspace 的 Product → Build 成功,再覆盖安装到设备。也可以在 Demo 目录用下面的命令做无 UI 编译检查:

bash
pod install
xcodebuild \
  -workspace XHIMUIKitDemo.xcworkspace \
  -scheme XHIMUIKitDemo \
  -destination 'generic/platform=iOS Simulator' \
  build

Demo 的每次设备交付都应递增 CFBundleVersion。不要仅凭 Xcode 中已经打开了 新源码,就判断手机上的 App 已更新。

两台设备互相聊天

默认服务器是:

text
https://im.xihansoftware.com/xhim-api
  • 第一台设备输入一个手机号,切换到“注册”,设置密码并完成登录;
  • 第二台设备用另一个手机号完成注册登录;
  • 在通讯录点击“添加朋友”,默认输入对方注册时使用的完整手机号。Demo 会先 精确查询并展示对方的头像、昵称和公开用户 ID,确认无误后才发送申请,再由另一端 同意;
  • “我的”页面显示的是短公开用户 ID;协议使用的内部 userID 不在普通界面 展示;
  • 以后直接使用手机号和密码登录,不再依赖代码里预置的 A/B 账号。

昵称可以留空。Server 会为新账号生成稳定的四字中文昵称,手机号不会出现在 公开昵称里;用户之后仍可在“我的”页面修改。登录页默认开启“记住账号和密码”: 服务器地址和手机号保存在 App 设置中,密码只保存在当前设备的 iOS Keychain, 且仅在一次完整登录成功后写入。关闭开关并成功登录会清除已保存凭据。

手机号查找只支持单个完整号码的精确匹配,例如 13800138000+8613800138000;不支持手机号片段、昵称、模糊或批量搜索。查找成功后 SDK 返回内部 userID 和用于界面展示的 publicUserID,再由 Demo 把内部 ID 传给 sendFriendRequest(toUserID:)。普通页面不展示内部 ID,也不要把手机号或 publicUserID 直接当作 userID

Development Server 使用演示注册目录完成手机号查找。正式私有化部署必须接入 客户自己的用户目录,并配置“允许通过手机号被发现”的隐私开关、App 隔离、 鉴权、限流和防枚举策略;iOS App 与 SDK 不直接读取手机号身份表。

登录页的“服务器设置”只供私有化部署客户修改。普通体验无需填写 User ID、 Token、App ID 或管理员密钥。手机号演示账号只在 Development Server 开启; 正式产品接入客户自己的短信验证、风控和业务账号系统。

登录页会自动检测服务端能力。只有 Server 的公开配置同时声明能力协议 v1、 Development 环境和手机号认证已开启,手机号表单才可操作。网络临时失败可点击 “重新检测服务器”;提示“当前服务器未提供手机号注册登录”时,应先升级服务端, 而不是修改手机号、写死 Token 或切回预置 A/B 账号。

前后端发布必须按以下顺序完成:

  1. 服务端升级并完成数据库迁移;
  2. 服务端验证健康检查、手机号能力字段以及“注册 → 密码登录”闭环;
  3. 再向设备安装新版 Demo;
  4. Demo 登录页显示绿色“已支持手机号登录”后再注册。

如果提示“该手机号已经注册”,应切换到“登录”,而不是继续重复注册;如果提示 “手机号或密码不正确”,请核对原注册密码,Demo 不会泄露该手机号是否存在。 Server 的标准错误响应字段是 code。出现笼统的 http_401http_409 或 “操作未完成,错误 1”,说明设备仍在运行旧 Demo,应重新构建并覆盖安装。

手机号和密码只用于 Development Server 验证演示账号身份。验证成功后,Demo 使用 SDK 的 .development Credential Provider 建立 IM 会话并自动续期,不会 把密码长期保存在内存,也不会要求用户每 24 小时重新登录。正式产品必须改用 业务服务签发凭证并接入 .business(...);不要把 Development 返回的短期 Access Token 固定写入 App。

Demo 已包含的功能

个人资料与二维码

“我的”页点击“个人资料”会进入独立的“我的信息”页面,不再用一个大弹窗编辑所有 字段。主区域按 iOS 用户习惯分别展示头像、昵称、性别、生日、手机号码和邮箱; 更多资料区域展示真实姓名、个性签名、公开账号、部门和“我的二维码”。每一项都有 独立的编辑或查看流程,部门由企业组织架构管理,普通用户不能在本地随意改写。

二维码入口:

  • 我的 → 个人资料 → 我的二维码
  • 通讯录 → 右上角加号 → 扫一扫,支持相机和相册识别;
  • 群聊详情 → 群二维码

二维码只携带 Server 签发的不透明载荷。扫描个人二维码后进入用户资料, 非好友可发送带“二维码”来源的好友申请,已是好友可直接发消息。扫描群二维码后, 已在群内显示“进入群聊”;开放群加入成功后直接进群;需审批群提交申请并等待管理员处理。 协议和私有化配置见 二维码规范

  • 登录、退出、用户资料、头像上传、账号与安全和服务器切换;
  • 会话列表、未读数、单聊、消息搜索和草稿;
  • 文本、表情、相册图片/视频、拍照/短视频、文件、按住说话、带地图的位置卡片、名片、 红包和转账消息;
  • 图片安全下载后在气泡内直接显示;视频气泡显示首帧、中央播放按钮和右下角总 时长,点击后使用系统播放器全屏播放;文件、位置、名片、红包和转账使用独立 卡片,不把 [图片] 等会话摘要误当成聊天正文;语音使用“波纹图标 + 秒数”的 紧凑气泡,长度随时长增长且收发方向自动镜像,点击后直接在当前聊天页播放, 再次点击暂停/继续,不跳转 Quick Look 或独立播放页面;
  • 消息长按菜单从实际气泡/卡片触发,以最多五列的深色图标宫格定位在气泡附近, 支持复制、编辑、失败重试、取消发送、本地删除、图片/贴纸收藏、撤回、逐条 转发、引用回复和多选,不会高亮整块 TableView Cell;
  • 多选底栏支持批量删除、逐条转发和最多 100 条的合并转发;转发目标可一次选择 多个已有会话,收到合并转发后可进入独立聊天记录详情;
  • 引用回复会在输入栏上方显示原发送者和消息摘要,可随时关闭;发出后使用 SDK 标准引用消息合同,接收端通过公开强类型解码器渲染;
  • 群创建、邀请成员、移除成员和成员退群由 Server 写入不可伪造的系统消息, 在聊天中居中显示且没有头像和普通气泡;己方撤回文字可点“重新编辑”恢复到 输入框,其他类型只显示撤回提示;
  • 安全附件下载预览;
  • 好友申请的收发方向、同意/拒绝、好友备注、删除好友和黑名单;
  • 创建群聊时自动包含当前用户并设为群主,可从好友列表多选,也可通过完整手机号 精确添加陌生用户;另含入群审批、成员/管理员/禁言管理、群主转让、 退群和解散;
  • 勿扰模式、系统通知、新消息提示音和震动等面向普通用户的通知设置;
  • 群聊输入 @ 自动弹出可搜索的群成员列表,支持提醒指定成员;群主和管理员可 使用 @所有人。被提醒者即使开启该会话免打扰,仍会收到这条提醒和未读红色 角标;
  • 输入状态、服务端已读回执、实时消息更新和系统通知设备登记。

成功、失败、说明和进行中状态统一使用全局 XHHUD,例如 XHHUD.success("密码修改成功")XHHUD.loading("正在上传头像…")。表单输入、 危险操作确认仍使用系统 Alert;短暂操作结果不再由各页面重复创建底部 Label。

会话列表中每个会话的未读数使用固定圆形角标;底部“消息”Tab 的红色数字角标 等于当前账号的总未读消息数。“通讯录”Tab 的红色数字角标等于当前账号尚未处理 的来向好友申请与入群申请总数。数字变化由 SDK 投影事件触发重新查询,页面不在 本地自行加减。进入聊天、资料、申请处理、群详情等任意二级页面时会隐藏 TabBar,返回三个根页面后再恢复。

聊天页输入栏左侧的麦克风按钮会把输入框原位切换成全亮的“按住说话”按钮: 按住开始录音,松开发送,上滑超过提示阈值后松开会取消;键盘按钮可切回文字 输入。“+”只展开相册、拍摄、文件、位置、名片、红包和转账;相册入口可混选 图片和视频,拍摄入口可拍照或录制最长 30 秒短视频。技术性的 “自定义消息”入口不向普通用户展示;笑脸按钮会展开表情面板,点击表情后立即 插入输入框。

Demo 的“账号与安全”支持 Development Server 演示账号更换手机号和密码,并 要求验证当前密码。正式客户应由自己的短信验证、风控和账号服务实现该页面, 不要把演示账号接口当成生产身份系统。头像则使用正式 SDK 的 uploadCurrentUserAvatar,宿主无需接触对象存储签名。

聊天窗口使用一套统一的内容尺寸和视口规则:

  • 初次进入只读取最新 40 条,并在第一次布局事务内无动画定位到最后一条,不会 先显示最老消息再整屏滚到底部;用户向上滚动到列表顶部时,每次按 SDK nextCursor 继续读取 20 条更早消息;
  • 旧消息插入列表头部前先保存当前视口,插入后恢复原离底距离,因此正在阅读的 消息不会突然跳位;实时新消息只刷新、合并最新窗口,只有用户原本就在底部时 才继续贴住最新消息;
  • 短文本气泡按实际文字宽度收缩,只在长文本达到聊天区域约 72% 时换行,不会 因隐藏的图片、语音或卡片视图被撑成固定宽度;
  • 输入框按内容从 40pt 增长到 104pt,超过后只滚动输入框本身;右侧发送/附件 按钮始终保持同一尺寸、圆角和约束,文字变化不会触发按钮形状抖动;
  • 用户主动点击输入框、表情或“+”时,无论当前正在查看哪一条历史消息,都会先 取消历史位置恢复并立即滚到最新消息,再让键盘或面板与输入栏同步上移,保证 输入组件上方始终是最新一条消息;普通的非输入型布局变化才保持原离底距离;
  • 输入框在成为第一响应者之前保存列表位置,并在 keyboardWillChangeFrame 中同步更新布局,不把键盘通知转入延后的异步任务; 因此第一次点击输入框时,聊天内容会与键盘同时上移;
  • 点击聊天记录或空白区域会立即结束输入焦点并收起键盘/面板;收起手势不取消 TableView 的触摸事件,消息卡片点击、长按菜单和列表滚动仍可正常使用;
  • 接收气泡在浅色模式使用独立的次级分组底色和细分隔线,与聊天背景保持可见 层次;深色模式继续使用系统动态颜色。
  • 文字、图片、视频、语音、位置和通用卡片使用独立的 Cell 复用池。每次复用 都会清空旧媒体、固定尺寸和加载状态,避免滚动或展开附件面板时把位置/视频 卡片压成细条;图片本体与气泡同尺寸,不额外套品牌色边框。
  • 只要己方开始发送并刷新出新消息(包括引用历史消息后发送、批量图片和媒体 完成入队),列表都会定位到最新一条;收到对方消息且用户正在读历史时仍保持 原视口。

语音录制会在停止 AVAudioRecorder 前保存有效时长,停止后确认 M4A 文件已经 落盘,再直接进入现有音频消息上传与发送链路,不需要录完后再点一次“发送语音”。 少于 0.3 秒或空文件会提示重新录制,不会进入媒体上传队列。录音达到 60 秒会 自动结束并发送;页面退出、App 退到后台或系统音频中断会取消录音并删除临时 文件。

收到语音消息后,Demo 复用同一个 AVAudioPlayer:点击新的语音会停止上一条, 播放、暂停、下载和结束状态都按 clientMessageID 回写到对应气泡,Cell 复用不会 串状态。播放前仍调用 SDK 的 downloadMediaMessage 完成当前账号授权、大小和 SHA-256 校验;离开聊天页、App 退到后台、开始录音或发生系统音频中断时会停止 播放并释放音频会话。语音 Renderer 位于 Features/Chats/VoiceMessageContentView.swift;图标使用 iOS 15 原生 矢量资源,不依赖 SF Symbols。语音 Renderer 被隐藏时会同时释放自身宽、高 约束,重新显示后再恢复固定高度,避免与 UIStackView 的隐藏约束冲突而造成 气泡偶发变矮。

相册和拍摄由锁定版本的 ZLPhotoBrowser 提供。相册在一个入口中选择图片或 视频,拍摄最长录制 30 秒。选中视频后先读取时长、尺寸和首帧,并在创建本地 消息前按服务端当前 256 MiB 单对象上限完成只读预检;通过后在主线程插入 “发送中”的视频气泡,源资源复制、哈希、分片和上传在后台继续。发送中气泡 使用首帧、环形字节进度和暂停形态图标,服务端接收后切换为白色圆形播放按钮。 预检或发送失败会按 clientMessageID 移除对应临时气泡,不留下阻塞时间线的 伪消息。可直接读取的 AVURLAsset 不再做中等质量二次转码,只有非文件型组合 资源才使用 passthrough 导出。系统相册临时 URL 会在控制器退出时清理,SDK 持久任务始终使用 App 已接管的文件。

收到消息后,Demo 先按 contentType + contentVersion 解码。图片由 SDK 申请 当前账号的下载授权并校验大小与 SHA-256,原子写入私有缓存后交给 Kingfisher 从本地文件渲染;点击图片仍可进入系统安全预览。fallbackText 只用于会话摘要 和未知类型降级。若有效图片显示成 [图片],应检查消息 Renderer,而不是重复 修改相册选择或上传接口。

如果文字可发送但所有附件都返回 HTTP 500,应检查 Server 的 GET /health/ready,并确认媒体目录对运行 XHIM Server 的非 root UID/GID 可写。这是服务端对象存储故障,不应在 iOS 端重复更换相册权限或把失败吞掉。 服务端负责人应按 本地媒体存储权限迁移 先备份再检查 Named Volume 或 Bind Mount,并在非 root 写探针、健康检查和真实 附件收发都通过后恢复服务。

如果头像上传返回 HTTP 404,先请求同一域名下的 POST /v1/users/avatar/uploads:prepare。该路由需要携带 SDK Bearer Token; 401/403 说明路由存在但鉴权未通过,404 表示部署的 Server 版本仍未包含头像 路由。应更新并重启 Server,而不是在 iOS 端改 URL 或绕过 SDK 上传。

真实服务端自动化测试

普通构建不会访问网络。需要验证两个账号经远端服务互发消息时执行:

bash
pod install

xcrun simctl spawn booted launchctl setenv \
  XHIM_INTEGRATION_SERVER https://im.xihansoftware.com/xhim-api

xcodebuild \
  -workspace XHIMUIKitDemo.xcworkspace \
  -scheme XHIMUIKitDemo \
  -destination 'platform=iOS Simulator,name=iPhone 17 Pro' \
  test

不要给这条测试命令增加 CODE_SIGNING_ALLOWED=NO。Demo 的数据库密钥和“记住 密码”都使用 iOS Keychain;关闭模拟器本地签名会移除 Keychain entitlement, 并产生 Valet.KeychainError.missingEntitlement 或“Keychain database key lookup failed”,这不是 SDK 登录或服务端故障。

集成测试会通过 Development 手机号接口生成本轮独立的随机账号,并覆盖注册、 好友申请与处理、备注、黑名单、单聊、富媒体收发与校验、已读、消息修改、群组 治理和关系删除。测试不会依赖预置 A/B 账号,也不会在日志中输出手机号、密码或 登录凭证。

验证已经存在的两个账号是否能通过手机号双向精确查找时,可在 Test Scheme 或 CI 的私密环境变量中同时设置:

text
XHIM_INTEGRATION_USER_A
XHIM_INTEGRATION_PHONE_A
XHIM_INTEGRATION_USER_B
XHIM_INTEGRATION_PHONE_B

四项只用于测试进程,不应写入源码。测试会先执行 A 查 B、B 查 A,并核对返回的 User ID,然后再验证两端消息通信;两个手机号变量必须同时设置或同时省略。

APNs 的客户端登记链路可以在模拟器编译和单元测试中验证;真实通知到达仍需要 宿主 App 的 Push Notifications 能力、有效的 APNs entitlement,以及部署方在 Server 配置与 Bundle ID 匹配的 APNs 凭证。SDK 和 Demo 不内置晞晗软件或客户 的推送证书。用户点击消息通知后,Demo 会读取 xhim.resource_id(兼容 conversation_id)和 xhim.recipient_user_id,仅在通知所属账号与当前登录 账号一致时进入对应会话;这样退出 A 再登录 B 时不会错误打开 A 的通知。冷启动 时带账号身份的新通知会等待对应账号连接,无账号身份的旧通知不会跨账号保留。

项目结构

text
Application/       App 生命周期、登录/主界面路由
Core/              通用主题、图标与基础组件
Features/Auth/     登录
Features/Chats/    会话、聊天、表情与附件
Features/Contacts/ 通讯录、好友、群聊与黑名单
Features/Profile/  个人中心
Infrastructure/    SDK 会话、本地配置与 App 自有 HTTP

界面图标来自仓库锁定版本的 Lucide SVG,工程中保留 ISC 许可证和可编辑源文件。 返回、关闭、添加、更多、状态提示和列表箭头都使用 App 自带图标,不会在不同 iOS 版本上自动切换为 SF Symbols。XHIMNavigationController 同时使用不透明 导航栏;在 iOS 26 及以上通过公开的 hidesSharedBackgroundsharesBackground API 关闭导航按钮的 Liquid Glass 共享背景。交付检查会拒绝 重新引入系统图标或缺失 SVG 的改动。

Rx 事件监听约定

Demo 的全局事件、SDK 会话事件与 ViewModel 长生命周期输出统一使用 RxSwift/RxCocoa,二次开发者不需在页面中分散解析 Notification.NameuserInfo 或手动移除 Observer:

swift
private let disposeBag = DisposeBag()

session.events.socialDataChanged
    .emit(onNext: { [weak viewModel] _ in
        viewModel?.load()
    })
    .disposed(by: disposeBag)

viewModel.changes
    .emit(onNext: { [weak self] in
        self?.render()
    })
    .disposed(by: disposeBag)
  • 会重放当前值的 UI 状态用 Driver,例如加载状态和角标快照;
  • 只需发送一次的 UI 事件用 Signal,例如错误、数据变更和输入状态;
  • Relay 保持为 ViewModel/事件层内部实现,界面只依赖只读输出;
  • 长生命周订阅统一由 DisposeBag 回收。按钮点击、子控件回传和单次 async 完成回调仍保留闭包,它们不是可广播的应用状态,强行 Rx 化反而会 增加层级和阅读成本。

XHIMEventStream 仅是 Demo UI 层的类型化事件入口;XHIM 二进制 SDK 的 HTTP/WebSocket、消息同步、媒体传输和公开 API 没有改变,也不会向客户项目 传递 Rx 依赖。通讯录首次进入会显示结构一致的骨架屏;首次请求结束后 消失,之后的下拉刷新和实时社交事件刷新保留现有数据,不重新遮挡页面。

CocoaPods 依赖与许可证

Demo 通过 Podfile.lock 精确锁定:

  • XHIM 0.1.0-dev.10:本地二进制 SDK 交付包;
  • ZLPhotoBrowser 5.0.0:相册与拍摄体验;
  • Valet 5.1.0:基于 Keychain 的账号密码安全保存;
  • Kingfisher 8.11.0:头像下载与缓存;
  • Alamofire 5.12.0:仅用于 Demo 自有的服务配置、手机号注册和密码登录 REST 接口;
  • RxSwift / RxCocoa / RxRelay 6.9.0:Demo UI 和 ViewModel 的类型化 事件流;
  • ISEmojiView 0.3.5:表情键盘、最近使用和肤色选择。

这些第三方开源库的 NOTICE 和许可证原文位于 Demo 根目录的 NOTICEThirdPartyLicenses/。这些是 Demo 自身依赖,不会变成 XHIM Headless SDK 的 传递依赖。正式签名发布 Demo 时,仍需由仓库的商业发行流水线针对最终 Archive 生成 SPDX/CycloneDX SBOM。

Demo 自有 REST 统一经 Infrastructure/Networking/AppHTTPClient.swift 与类型化 Endpoint 发起,并 集中处理超时、重定向、响应大小、HTTP 状态和 JSON 解码。XHIM SDK 的消息发送、 同步、媒体、好友/群组、凭证续期和 WebSocket 实时通道仍完全由 SDK 管理; App 不用 Alamofire 重写或旁路这些协议。

XHIM 客户端 SDK 与服务端文档