最近把公司项目的聊天模块从”XMPP”转到”网易云信”官网、github。在转的过程中,上手很快,基本上没遇到什么难题,很多程度上感谢云信的NIMKit。以前也接触过几个IM SDK服务商的代码,那个时候看他们的代码根本没什么欲望,但这次看云信的代码有种被吸引的感觉,恨不得一下子把它的代码全部看完,封装的很好,扩展性很强(ps:就我目前的水平只能说出这些优点)。对于一般的聊天UI完全可以满足,就算不用网易的IM SDK,但他们的代码真的值得一下(尽管他们的UIKit代码注释比较少)。
在看本文之前,请先看一下他们官方的github简介。
##Base tips
####cell的组成结构
对于聊天”MessageCell”的介绍,一定要记住下面这张图片,以及相关参数的解释。
- 蓝色区域:为具体内容,如文字 UILabel ,图片 UIImageView 等 。(对应的
NIMMessageModel对象的contentSize属性)。注:NIMMessageModel为消息(NIMMessage) 在NIMKit中的封装。这个封装主要是为了对计算结果和布局配置进行缓存,以避免反复的计算和读取相同的信息,从而提高应用性能。
- 绿色区域:为消息的气泡,具体的内容和气泡之间会有一定的内间距,这里为 contentViewInsets 。(对应的
NIMMessageModel对象的contentViewInsets属性)
- 紫色区域:为整个 UITableViewCell ,具体的气泡和整个cell会有一定的内间距,这里为 cellInsets 。(对应的
NIMMessageModel对象的bubbleViewInsets属性)
####config配置协议
在聊天界面有几个config配置代理,先熟悉一下。
- NIMSessionConfig:消息对应的session配置。如:录音、输入框、表情、更多等操作的选择;点击”+”号出来的多媒体按钮;是否禁用输入控件;输入控件的最大长度;输入控件的placeholder;一次最多消息的消息内容;间隔多久显示时间;语音红点是否禁用;是否自动切换成听筒模式;是否自动获取历史消息;消息数据提供器;消息的排版配置等。可以说这个是贯穿整个聊天模块的配置,修改聊天界面一般就得调整这里。
- NIMCellLayoutConfig:消息对应的布局配置。我们可以在这个config里面根据消息类型是否显示头像、姓名、头像与姓名之间的margin等;然后你会在项目里面看到自定义消息类型对应的NTESSessionCustomLayoutConfig,以及default默认的配置NIMCellLayoutDefaultConfig;
- NIMSessionContentConfig:消息内容配置。这个配置主要是为
NIMSessionMessageContentView(请看下面对 聊天 NIMMessageCell.h 的介绍)对象为设置的。
####聊天 NIMMessageCell.h
先来看看头文件定义的属性
1 2 3 4 5 6
| @property (nonatomic, strong) NIMAvatarImageView *headImageView; @property (nonatomic, strong) UILabel *nameLabel; //姓名(群显示 个人不显示) @property (nonatomic, strong) NIMSessionMessageContentView *bubbleView; //内容区域 @property (nonatomic, strong) UIActivityIndicatorView *traningActivityIndicator; //发送loading @property (nonatomic, strong) UIButton *retryButton; //重试 @property (nonatomic, strong) NIMBadgeView *audioPlayedIcon; //语音未读红点
|
NIMSessionMessageContentView,顾名思义就是MessageCell的内容View(包括下面的bubble气泡View)。而 NIMSessionContentConfig 配置主要是配置 contentSize、contentViewInsets以及这个配置所应的 messageContentView 类名(NIMSessionMessageContentView的子类,每种聊天类型对应一个messageContentView)。注意,这里并没有提到 bubbleViewInsets,因为气泡隔cell的距离不会因不同类型而改变,我们只需在 cellLayoutConfig 里面处理即可,当然想要做到不同的话,也可以在 NIMSessionContentConfig 配置里面增加一个协议方法。注意 NIMSessionMessageContentView 是继承自 UIControl,这样不仅能处理点击事件,还能很好的处理点击高亮的效果。
##NIMSessionViewController(聊天回话控制器基类)
先来看看最重要的计算高度方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { CGFloat cellHeight = 0; id modelInArray = [[_sessionDatasource modelArray] objectAtIndex:indexPath.row]; if ([modelInArray isKindOfClass:[NIMMessageModel class]]) { NIMMessageModel *model = (NIMMessageModel *)modelInArray; NSAssert([model respondsToSelector:@selector(contentSize)], @"config must have a cell height value!!!"); [self layoutConfig:model]; CGSize size = model.contentSize; UIEdgeInsets contentViewInsets = model.contentViewInsets; UIEdgeInsets bubbleViewInsets = model.bubbleViewInsets; cellHeight = size.height + contentViewInsets.top + contentViewInsets.bottom + bubbleViewInsets.top + bubbleViewInsets.bottom; } else if ([modelInArray isKindOfClass:[NIMTimestampModel class]]) { cellHeight = [modelInArray height]; } else { NSAssert(0, @"not support model"); } return cellHeight; }
|
某个数据源所对应的高度就是三个颜色区域的高度之和(contentSize.height + contentViewInsets.top + contentViewInsets.bottom + bubbleViewInsets.top + bubbleViewInsets.bottom)。
然后我们在来看看layoutConfig:方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| - (void)layoutConfig:(NIMMessageModel *)model{ model.sessionConfig = self.sessionConfig; if (model.layoutConfig == nil) { id<NIMCellLayoutConfig> layoutConfig = nil; if ([self.sessionConfig respondsToSelector:@selector(layoutConfigWithMessage:)]) { layoutConfig = [self.sessionConfig layoutConfigWithMessage:model.message]; } if (!layoutConfig) { layoutConfig = [NIMDefaultValueMaker sharedMaker].cellLayoutDefaultConfig; } model.layoutConfig = layoutConfig; } [model calculateContent:self.tableView.nim_width]; }
|
其实这里就是,先配置model的sessionConfig,然后配置layoutConfig,配置完后就去计算该model所对应内容的contentSize。注:layoutConfigWithMessage:方法是自定义消息类型需要处理,还有记得在写代码中做好判nil的处理,如果为nil的话给default值。
好了,现在到了一个我当时比较蛋疼的地方了,请看下图

看到很多的方法,仔细看看,除了layoutConfig的配置方法以外,还有很多Attachment(Attachment 是属于自定义消息的配置协议)结尾的方法,其实这里应该只会提示NIMCellLayoutDefaultConfig和NTESSessionCustomLayoutConfig(NTESChatroomCellLayoutConfig聊天室的布局配置请忽略)才对,它们只是方法名相同,应该是Xcode抽风而导致的。
####NIMCellLayoutDefaultConfig计算contentSize
先看三个相关部分的代码
######NIMCellLayoutDefaultConfig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| - (CGSize)contentSize:(NIMMessageModel *)model cellWidth:(CGFloat)cellWidth{ id<NIMSessionContentConfig>config = [[NIMSessionContentConfigFactory sharedFacotry] configBy:model.message]; return [config contentSize:cellWidth]; }
- (NSString *)cellContent:(NIMMessageModel *)model{ id<NIMSessionContentConfig>config = [[NIMSessionContentConfigFactory sharedFacotry] configBy:model.message]; NSString *cellContent = [config cellContent]; return cellContent ? : @"NIMSessionUnknowContentView"; }
- (UIEdgeInsets)contentViewInsets:(NIMMessageModel *)model{ id<NIMSessionContentConfig>config = [[NIMSessionContentConfigFactory sharedFacotry] configBy:model.message]; return [config contentViewInsets]; }
|
######NIMSessionContentConfigFactory
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| - (instancetype)init { if (self = [super init]) { _dict = @{@(NIMMessageTypeText) : [NIMTextContentConfig new], @(NIMMessageTypeImage) : [NIMImageContentConfig new], @(NIMMessageTypeAudio) : [NIMAudioContentConfig new], @(NIMMessageTypeVideo) : [NIMVideoContentConfig new], @(NIMMessageTypeFile) : [NIMFileContentConfig new], @(NIMMessageTypeLocation) : [NIMLocationContentConfig new], @(NIMMessageTypeNotification) : [NIMNotificationContentConfig new], @(NIMMessageTypeTip) : [NIMTipContentConfig new]}; } return self; }
- (id<NIMSessionContentConfig>)configBy:(NIMMessage *)message { NIMMessageType type = message.messageType; id<NIMSessionContentConfig>config = [_dict objectForKey:@(type)]; if (config == nil) { config = [NIMUnsupportContentConfig sharedConfig]; } if ([config isKindOfClass:[NIMBaseSessionContentConfig class]]) { [(NIMBaseSessionContentConfig *)config setMessage:message]; } return config; }
|
######NIMImageContentConfig
1 2 3 4 5
| #import "NIMBaseSessionContentConfig.h"
@interface NIMTextContentConfig : NIMBaseSessionContentConfig<NIMSessionContentConfig>
@end
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| @interface NIMTextContentConfig()
@property (nonatomic,strong) NIMAttributedLabel *label;
@end
@implementation NIMTextContentConfig
- (CGSize)contentSize:(CGFloat)cellWidth { NSString *text = self.message.text; [self.label nim_setText:text]; CGFloat msgBubbleMaxWidth = (cellWidth - 130); CGFloat bubbleLeftToContent = 14; CGFloat contentRightToBubble = 14; CGFloat msgContentMaxWidth = (msgBubbleMaxWidth - contentRightToBubble - bubbleLeftToContent); return [self.label sizeThatFits:CGSizeMake(msgContentMaxWidth, CGFLOAT_MAX)]; }
- (NSString *)cellContent { return @"NIMSessionTextContentView"; }
- (UIEdgeInsets)contentViewInsets { return self.message.isOutgoingMsg ? UIEdgeInsetsMake(11,11,9,15) : UIEdgeInsetsMake(11,15,9,9); }
- (NIMAttributedLabel *)label { if (_label) { return _label; } _label = [[NIMAttributedLabel alloc] initWithFrame:CGRectZero]; _label.font = [UIFont systemFontOfSize:NIMKit_Message_Font_Size]; return _label; } @end
|
这里就是实现了NIMSessionContentConfig配置协议,我举例一个文本消息类型的sessionContentView的处理方式,其他类型是一样的处理方法,实现相关配置协议方法即可。你可能会想到如果某个sessionContentView上面的元素有很多时该怎么处理,我该不会把某个sessionContentView的元素都定义一次,然后全部赋值再计算contentSize么?我将会在说自定义消息类型的时候谈谈我简单的处理方式。
这里我个人觉得有两点可以改变一下。
NIMBaseSessionContentConfig的NIMMessage对象应该改为NIMMessageModel对象比较好。因为我需要用到contentSize,根据contentSize来设置控件的宽度适应屏幕。所以我在自定义的消息里面,将NTESCustomAttachmentInfo协议需要传入的NIMMessage对象改为NIMMessageModel对象。
- 在返回
contentView类名时,改为NSStringFromClass([NIMSessionTextContentView Class])会好点,怕输入字符串时时产生错误嘛。
在NIMSessionContentConfigFactory类里面定义了基本消息类型所对应的contentConfig配置协议(注意,在云信demo里面,每个sessionContentView都对应一个sessionContentConfig)。请看NIMUnsupportContentConfig判nil处理,如果没有这段判断处理,你在添加自定义消息时候,忘记在 NTESSessionCustomLayoutConfig类的supportAttachmentType方法里面添加你的自定义消息,程序就会崩溃。ps:防止崩溃,请从细节做起,谢谢!
那NIMCellLayoutDefaultConfig计算contentSize就简单明了了,就是调用相关 sessionContentConfig 的方法嘛。
####NNTESSessionCustomContentConfig计算contentSize
当我们看到NTESSessionCustomLayoutConfig类时,有两个地方是值得我们注意,也是与NIMCellLayoutDefaultConfig不同的地方。
- 一个
NTESSessionCustomContentConfig类的属性
supportAttachmentType 内部方法,用来获取customLayoutConfig直接的类型。
######NTESSessionCustomContentConfig
它有一个NIMMessage类型的public属性,而在介绍NIMBaseSessionContentConfig配置协议时,我有说过建议将它的NIMMessage对象改为NIMMessageModel对象,在这里我也同样建议,原因上面有提过。
请看它的.m文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| @interface NTESSessionCustomContentConfig()
@property (nonatomic, strong) id<NTESCustomAttachmentInfo> attachmentInfo;
@end
@implementation NTESSessionCustomContentConfig
- (void)setMessage:(NIMMessage *)message { NIMCustomObject *object = message.messageObject; _message = message; _attachmentInfo = (id<NTESCustomAttachmentInfo>)object.attachment; }
- (CGSize)contentSize:(CGFloat)cellWidth { return [self.attachmentInfo contentSize:self.message cellWidth:cellWidth]; }
- (NSString *)cellContent { return [self.attachmentInfo cellContent:self.message]; }
- (UIEdgeInsets)contentViewInsets { return [self.attachmentInfo contentViewInsets:self.message]; }
@end
|
attachmentInfo对象代表不同类型的自定义消息,只要它遵守NTESCustomAttachmentInfo协议即可。(ps:其实NTESCustomAttachmentInfo协议就相当于上面基本消息类型所对应的NIMSessionContentConfig协议;注:这里所谓的基本消息类型,即云信SDK已定义的消息类型,相对于自定义消息类型而言而已。)
下面来看看 NTESCustomAttachmentInfo 协议(已添加注释)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @protocol NTESCustomAttachmentInfo <NSObject>
@optional /// contentView类名 - (NSString *)cellContent:(NIMMessage *)message; /// contentSize - (CGSize)contentSize:(NIMMessage *)message cellWidth:(CGFloat)width; /// 内容距离bubble气泡的相关距离 - (UIEdgeInsets)contentViewInsets:(NIMMessage *)message; /// 格式化消息 某些消息需要在最近回话列表特殊文字 如:收到一段文字,但是需要显示[系统消息] - (NSString *)formatedMessage; /// 封面图片 如果一个视频 得显示一张图片在界面 - (UIImage *)showCoverImage; /// 设置封面图片 - (void)setShowCoverImage:(UIImage *)image;
|
在这里我提出两点建议
NIMMessage对象改为NIMMessageModel对象;
cellContent:、contentSize: cellWidth:、contentViewInsets:这三个方法改为@required类型的;方法名前面加上attachmentInfo与NIMSessionContentConfig协议的相关方法作为区分。
######复杂自定义sessionContentView的简单处理方式
上面在介绍NIMBaseSessionContentConfig配置协议时,我有提到如果某个sessionContentView上面的元素有很多时该怎么处理。下面我说说我的处理方式。
- 把计算contentSize和contentViewInsets的方法丢到contentView里面,这样一来,那么只要在attachMentInfo里面调用所属contentView的计算方法。
- 我会在
NIMSessionMessageContentView类里面增加两个方法
1 2
| - (CGSize)attachmentInfoViewContentSize:(NIMMessageModel *)messageModel cellWidth:(CGFloat)width; - (UIEdgeInsets)attachmentInfoViewcontentViewInsets:(NIMMessageModel *)messageModel;
|
- 在具体的contentView里面,我定义方法,它有一个Bool类型的isInit(是否初始化)入参,在这个方法里面我创建和实例变量一样的临时变量,当
attachmentInfoViewContentSize: cellWidth方法调用它时,我只是为了方便计算contentSize,如果是initSessionMessageContentView方法调用时,我就将相应的临时变量赋值给例变量。