0%

NIMKit浅析

最近把公司项目的聊天模块从”XMPP”转到”网易云信”官网github。在转的过程中,上手很快,基本上没遇到什么难题,很多程度上感谢云信的NIMKit。以前也接触过几个IM SDK服务商的代码,那个时候看他们的代码根本没什么欲望,但这次看云信的代码有种被吸引的感觉,恨不得一下子把它的代码全部看完,封装的很好,扩展性很强(ps:就我目前的水平只能说出这些优点)。对于一般的聊天UI完全可以满足,就算不用网易的IM SDK,但他们的代码真的值得一下(尽管他们的UIKit代码注释比较少)。

在看本文之前,请先看一下他们官方的github简介

##Base tips
####cell的组成结构
对于聊天”MessageCell”的介绍,一定要记住下面这张图片,以及相关参数的解释。
nimkit_cell

  • 蓝色区域:为具体内容,如文字 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值。

好了,现在到了一个我当时比较蛋疼的地方了,请看下图
Image2
看到很多的方法,仔细看看,除了layoutConfig的配置方法以外,还有很多Attachment(Attachment 是属于自定义消息的配置协议)结尾的方法,其实这里应该只会提示NIMCellLayoutDefaultConfigNTESSessionCustomLayoutConfigNTESChatroomCellLayoutConfig聊天室的布局配置请忽略)才对,它们只是方法名相同,应该是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么?我将会在说自定义消息类型的时候谈谈我简单的处理方式。
这里我个人觉得有两点可以改变一下。

  • NIMBaseSessionContentConfigNIMMessage对象应该改为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类型的;方法名前面加上attachmentInfoNIMSessionContentConfig协议的相关方法作为区分。

######复杂自定义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方法调用时,我就将相应的临时变量赋值给例变量。