- UI组件
- 即时通讯
【免费下载链接】JSQMessagesViewController
An elegant messages UI library for iOS
导读
本文基于 JSQMessagesViewController 官方 FAQ 整理而成,围绕 iOS 聊天界面开发中最常遇到的五类问题展开:UITabBarController/UITabBar兼容性、实验性的弹性气泡(springy bubbles)、头像移除、消息 Cell 的两种定制路线、输入工具栏按钮的换位与替换。文中所有结论均与当前仓库源码(JSQMessagesCollectionViewFlowLayout、JSQMessagesViewController、JSQMessagesInputToolbar)及 Demo(DemoMessagesViewController.m)相互印证。读完本文,你将掌握上述五类问题的可直接复制的 Objective-C 解决方案,并理解每段代码背后的布局、委托与工具栏机制。
一、在 UITabBar / UITabBarController 中使用本库
1.1 问题背景
FAQ 明确指出:库与UITabBarController/UITabBar的兼容性是"是又不是"(yes and no),存在历史性的布局争议。核心原因在于JSQMessagesViewController在viewDidLoad阶段会主动将自身 view 扩展到整个屏幕(edgesForExtendedLayout的默认行为),当嵌入 TabBar 容器时,聊天视图底部会被 TabBar 遮挡,导致最后一条消息或输入工具栏显示不全。
1.2 官方推荐 Workaround
FAQ 给出的最稳妥方案是在JSQMessagesViewController子类的viewDidLoad中关闭边缘延伸:
- (void)viewDidLoad { [super viewDidLoad]; self.edgesForExtendedLayout = UIRectEdgeNone; }1.3 原理补充(源码依据)
viewDidLoad必须调用[super viewDidLoad]:该方法是 JSQMessagesViewController.h 中标注为NS_REQUIRES_SUPER的生命周期方法之一(viewWillAppear:、viewDidAppear:等同样如此),跳过 super 调用会导致内部布局逻辑失效。- 关闭
edgesForExtendedLayout后,控制器视图的自动布局将基于安全区域之外的内容矩形(即 TabBar 顶部)来计算,聊天 collection view 与输入工具栏即可完整落在 TabBar 之上。
提示:若你在
viewDidAppear:中开启了弹性气泡(见下文第二节),请一并注意该时序与布局属性的配合。
二、开启"弹性气泡"(Springy Bubbles)——实验特性
2.1 开启方式
FAQ 给出了最小启用代码,并标注该特性仍处于实验阶段:
- (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; self.collectionView.collectionViewLayout.springinessEnabled = YES; }关键时序约束:springinessEnabled必须在viewDidAppear:中设置,而不是viewDidLoad。原因见下节源码分析。
2.2 底层原理(源码证据)
弹性气泡由布局对象 JSQMessagesCollectionViewFlowLayout 实现,它继承自UICollectionViewFlowLayout,并在内部使用UIDynamicAnimator驱动:
- 属性声明见 JSQMessagesCollectionViewFlowLayout.h:
springinessEnabled默认值为NO;springResistanceFactor(阻力系数)默认值为1000,数值越大阻力越大、气泡越"不弹",调小则更"弹"。 - 初始化时这两个默认值在
jsq_configureFlowLayout中设定(见 JSQMessagesCollectionViewFlowLayout.m)。 - 布局通过
UIDynamicAnimator+UIAttachmentBehavior(吸附行为)模拟弹簧:prepareLayout会为可见区域内的 item 创建/移除吸附行为(JSQMessagesCollectionViewFlowLayout.m),并在滚动时依据手指位置与springResistanceFactor动态调整每个 item 的 center(jsq_adjustSpringBehavior:forTouchLocation:,见 JSQMessagesCollectionViewFlowLayout.m)。 - 关闭
springinessEnabled时布局会移除所有动力学行为并清空可见 indexPath 缓存(JSQMessagesCollectionViewFlowLayout.m)。 - Demo 中同样在
viewDidAppear:里根据用户设置开启/关闭该特性(DemoMessagesViewController.m),并在注释中强调"必须在viewDidAppear:中设置,且此特性大多稳定但仍是实验性的"。
为什么不建议在
viewDidLoad开启?此时 collection view 的 bounds 尚未完成布局,UIDynamicAnimator无法正确计算可见 item 集合,会出现抖动或行为失效。
三、移除头像(Avatars)
3.1 两步移除法
FAQ 要求同时完成两件事:把布局中的入站/出站头像尺寸清零,并在数据源方法中返回nil:
- (void)viewDidLoad { [super viewDidLoad]; self.collectionView.collectionViewLayout.incomingAvatarViewSize = CGSizeZero; self.collectionView.collectionViewLayout.outgoingAvatarViewSize = CGSizeZero; } - (id<JSQMessageAvatarImageDataSource>)collectionView:(JSQMessagesCollectionView *)collectionView avatarImageDataForItemAtIndexPath:(NSIndexPath *)indexPath { return nil; }3.2 源码依据与隐藏细节
- 两个属性定义于 JSQMessagesCollectionViewFlowLayout.h:
incomingAvatarViewSize与outgoingAvatarViewSize默认值均为(30.0f, 30.0f),文档明确说明"设为CGSizeZero即移除头像";也可使用常量kJSQMessagesCollectionViewAvatarSizeDefault(值为30.0f,见 JSQMessagesCollectionViewFlowLayout.m)来恢复默认尺寸。 - 修改尺寸会触发布局失效:setter 内部调用
invalidateLayoutWithContext:(JSQMessagesCollectionViewFlowLayout.m),因此放在viewDidLoad中即可生效,无需额外刷新。 - Demo 正是用这一模式按用户偏好开关入站/出站头像(DemoMessagesViewController.m)。
- 数据源方法返回
nil是第二步:即使尺寸已归零,若仍返回头像对象,一些复用场景下可能出现残留视图;两者配合才能彻底移除。
布局属性负责"留不留空间",数据源返回
nil负责"提不提供内容",二者缺一不可。
四、定制消息 Cell:两种路线
FAQ 将定制 cell 归纳为两种路线,按需求复杂度选择:
- 定制现有 cell 的外观与行为(简单,推荐多数场景);
- 提供完全自定义的 cell 原型(复杂,需要增删 cell 子视图时使用)。
4.1 路线一:定制现有 cell(Easy)
仅需重写cellForItemAtIndexPath:,拿到基类JSQMessagesCollectionViewCell的实例后即可访问其全部属性:
- (UICollectionViewCell *)collectionView:(JSQMessagesCollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { JSQMessagesCollectionViewCell *cell = (JSQMessagesCollectionViewCell *)[super collectionView:collectionView cellForItemAtIndexPath:indexPath]; // Customize the shit out of this cell // See the docs for JSQMessagesCollectionViewCell return cell; }可操作属性一览(声明于 JSQMessagesCollectionViewCell.h):
| 属性 | 说明 |
|---|---|
cellTopLabel | 钉在 cell 顶部的标签,常用于时间戳 |
messageBubbleTopLabel | 气泡上方的标签,常用于发送者名字 |
cellBottomLabel | cell 底部的标签,常用于送达状态 |
textView | 承载消息正文的JSQMessagesCellTextView |
messageBubbleImageView | 气泡背景图片视图 |
messageBubbleContainerView | 气泡容器(textView 与气泡图的父视图) |
avatarImageView/avatarContainerView | 头像视图及容器 |
accessoryButton | cell 的附件按钮 |
mediaView | 媒体消息内容视图(非空时textView与messageBubbleImageView为 nil) |
delegate | 遵守JSQMessagesCollectionViewCellDelegate的委托,回调头像/气泡/cell 点击 |
三个重要雷区(Demo 源码注释明确标注,见 DemoMessagesViewController.m):
- 不要直接设置
cell.textView.font!字体应通过self.collectionView.collectionViewLayout.messageBubbleFont在viewDidLoad中统一设置,否则尺寸计算(JSQMessagesBubblesSizeCalculator)与实际渲染不一致,导致气泡高度错误。messageBubbleFont默认取系统UIFontTextStyleBody首选字体(JSQMessagesCollectionViewFlowLayout.m)。 - 不要手动改 cell 的布局信息(frame 等),应通过布局属性定制。
- 设置正文颜色、链接颜色等是安全的;Demo 中即按消息方向设置
cell.textView.textColor与linkTextAttributes(DemoMessagesViewController.m)。
4.2 路线二:提供自定义 cell 原型(Hard)
此路线给予最大自由度,适合需要增删 cell 子视图的场景。FAQ 给出五步流程:
- 提供自己的 cell 子类,仿照库内置的
JSQMessagesCollectionViewCell、JSQMessagesCollectionViewCellIncoming、JSQMessagesCollectionViewCellOutgoing(后两者见 JSQMessagesCollectionViewCellIncoming.h 与 JSQMessagesCollectionViewCellOutgoing.h)。 - 在
JSQMessagesViewController子类上设置如下属性(声明见 JSQMessagesViewController.h):outgoingCellIdentifier—— 出站文本消息 cell 复用标识(默认[JSQMessagesCollectionViewCellOutgoing cellReuseIdentifier])outgoingMediaCellIdentifier—— 出站媒体消息 cell 复用标识(默认[JSQMessagesCollectionViewCellOutgoing mediaCellReuseIdentifier])incomingCellIdentifier—— 入站文本消息 cell 复用标识(默认[JSQMessagesCollectionViewCellIncoming cellReuseIdentifier])incomingMediaCellIdentifier—— 入站媒体消息 cell 复用标识(默认[JSQMessagesCollectionViewCellIncoming mediaCellReuseIdentifier])
- 用上述标识把自定义 cell 类/nib 注册到 collection view。
- 重写
collectionView:cellForItemAtIndexPath:,且不要调用super——因为是自己提供的 cell,调用 super 会执行大量无用工作。 - (可选)模型对象可实现
JSQMessageData协议(见 JSQMessageData.h)或继承JSQMessage扩展需求。
注意:这 4 个 cell 标识属性的默认值不建议在未提供自定义 cell 时覆盖;只有走路线二才需要修改它们。
五、定制输入工具栏按钮
5.1 替换 / 移除左右按钮
FAQ 提供了在viewDidLoad中定制工具栏的完整代码:
- (void)viewDidLoad { [super viewDidLoad]; // This button will call the `didPressAccessoryButton:` selector on your JSQMessagesViewController subclass self.inputToolbar.contentView.leftBarButtonItem = /* custom button or nil to remove */ // This button will call the `didPressSendButton:` selector on your JSQMessagesViewController subclass self.inputToolbar.contentView.rightBarButtonItem = /* custom button or nil to remove */ // Swap buttons, move send button to the LEFT side and the attachment button to the RIGHT // For RTL language support self.inputToolbar.contentView.leftBarButtonItem = [JSQMessagesToolbarButtonFactory defaultSendButtonItem]; self.inputToolbar.contentView.rightBarButtonItem = [JSQMessagesToolbarButtonFactory defaultAccessoryButtonItem]; // The library will call the correct selector for each button, based on this value self.inputToolbar.sendButtonOnRight = NO; }leftBarButtonItem/rightBarButtonItem是 JSQMessagesToolbarContentView 上的属性:置nil即可移除对应按钮;按钮高度被忽略(由工具栏高度决定),宽度保留,可用leftBarButtonItemWidth/rightBarButtonItemWidth显式指定宽度,左右留白由leftContentPadding/rightContentPadding控制(默认8.0f)。- 若想用库内置样式,直接使用工厂类 JSQMessagesToolbarButtonFactory 的
defaultSendButtonItem(文字 "Send"、无图标、蓝色)与defaultAccessoryButtonItem(回形针图标、无文字)。其实现见 JSQMessagesToolbarButtonFactory.m:send 按钮文本取自本地化字符串"send",颜色使用jsq_messageBubbleBlueColor;accessory 图标取自UIImage jsq_defaultAccessoryImage(即 Assets 中的 clip.png 系列)。
5.2 关于sendButtonOnRight的说明
FAQ 示例中的sendButtonOnRight属于 7.x 早期 API。在当前仓库源码中,该语义已演进为 JSQMessagesInputToolbar 的枚举属性sendButtonLocation:
typedef NS_ENUM(NSUInteger, JSQMessagesInputSendButtonLocation) { JSQMessagesInputSendButtonLocationNone, // 无发送按钮(或自行接管) JSQMessagesInputSendButtonLocationRight, // 发送按钮在右侧(默认) JSQMessagesInputSendButtonLocationLeft // 发送按钮在左侧 };- 默认值为
JSQMessagesInputSendButtonLocationRight(见 JSQMessagesInputToolbar.m)。 - 关键语义:该属性只决定"左右两个按钮中哪个是发送按钮/哪个是附件按钮",从而决定触发哪个回调——并不会物理移动按钮位置(头文件注释明确说明)。你仍需要自己把按钮放到对应的一侧。
- 回调分派逻辑在 JSQMessagesViewController.m:按下左侧按钮时,若
sendButtonLocation == JSQMessagesInputSendButtonLocationLeft则触发didPressSendButton:withMessageText:senderId:senderDisplayName:date:,否则触发didPressAccessoryButton:;右侧按钮同理。 - 当输入框有文本时,发送按钮的启用/禁用也依据
sendButtonLocation自动更新(JSQMessagesInputToolbar.m),由enablesSendButtonAutomatically(默认YES)控制;若关闭自动管理,需自行控制按钮可用状态。
实战建议:做 RTL(从右到左)语言适配时,按 FAQ 的做法把发送按钮放到左侧并同步把
sendButtonLocation设为Left,即可保证点击回调仍被正确路由到didPressSendButton:。若当前仓库版本不支持sendButtonOnRight,请改用self.inputToolbar.sendButtonLocation = JSQMessagesInputSendButtonLocationLeft;。
六、延伸阅读
- 从零集成本库,参见 getting_started.md;版本迁移注意点参见 migration.md。
- 头像工厂与气泡工厂:JSQMessagesAvatarImageFactory.h、JSQMessagesBubbleImageFactory.h。
- 相关布局与委托协议:JSQMessagesCollectionViewDelegateFlowLayout.h、JSQMessagesCollectionViewDataSource.h。
- 单元测试覆盖了本 FAQ 涉及的关键行为,可作为行为契约参考:JSQMessagesInputToolbarTests.m(验证
sendButtonLocation默认值)、JSQMessagesCollectionViewFlowLayoutTests.m(布局尺寸与失效行为)、JSQMessagesCollectionViewCellTests.m。
- UI组件
- 即时通讯
【免费下载链接】JSQMessagesViewController
An elegant messages UI library for iOS
相关推荐
告别手动抢购:i茅台自动预约系统完整指南
告别手动抢购:i茅台自动预约系统完整指南 还在为每天手动抢购茅台而烦恼吗?你是否曾经因为错过预约时间、操作速度慢而错失购买机会?Campus iMaoTai自动
后端前端任务调度工作流自动化PyPTO 泳道图性能分析 FAQ 深度解读:文件定位、气泡含义与 TileShape 选择实战指南
PyPTO 泳道图性能分析 FAQ 深度解读:文件定位、气泡含义与 TileShape 选择实战指南 泳道图(Swimlane)是 PyPTO 算子深度性能调优
人工智能大模型算子库模型优化AI 技能CANNAscend虚拟摄像头开源项目指南及常见问题解答
虚拟摄像头开源项目指南及常见问题解答 项目基础介绍 虚拟摄像头是一款基于Xposed框架的安卓应用模块,它允许用户通过替换方式模拟摄像头输出,适用于Androi
移动开发音视频
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考