032-UITableView(3)-iOS笔记

/ 0

学习目标

1.【理解】QQ界面搭建

2.【理解】通知中心NSNotificationCenter

3.【理解】实现发布信息和自动回复

一、QQ界面搭建

手机QQ聊天软件应该大多数人都接触过,就不细说了,需求是自定义cell搭建手机QQ聊天界面。

分析:顶部、底部是单独UIView控件,中间是tableview。所以可以一眼看出顶部、底部不是在tableview中的,因为他们不会随着tableview而滑动。效果图如下:点击这里查看动态图

Snip20150817_1还是和上一篇文章的微博案例一样,先封装数据模型和cell的frame模型。导入素材和plist文件,创建模型类。素材下载地址

Snip20150817_2

创建模型类注意:

type建议定义为枚举类型,虽然也不是必须要这样,但是这样看起来必须形象些吧。还有需要定义一个时间是否隐藏的属性,当多条消息时间相同的时候,只显示第一条消息的时间,而隐藏之后消息的时间。我们每天都在用QQ,应该都很熟悉这个了,如下图所示:

Snip20150817_3

JFMessage.h

#import <Foundation/Foundation.h>

//定义一个消息类型枚举,0表示我发的消息,1表示别人发的消息
typedef enum {
    JFMessageSelf = 0,
    JFMessageOther
} JFMessageType;

@interface JFMessage : NSObject

@property (copy, nonatomic) NSString *text;
@property (copy, nonatomic) NSString *time;
@property (assign, nonatomic) JFMessageType type;

//是否隐藏时间显示
@property (assign, nonatomic, getter=isHideTime) BOOL hideTime;

//快速创建模型的方法
- (instancetype)initWithDictionary:(NSDictionary *)dict;
+ (instancetype)messageWithDictionary:(NSDictionary *)dict;

//返回一个模型数组
+ (NSMutableArray *)messages;

@end

 JFMessage.m

#import "JFMessage.h"

@implementation JFMessage

//快速创建模型的方法
- (instancetype)initWithDictionary:(NSDictionary *)dict {
    if (self = [super init]) {
        [self setValuesForKeysWithDictionary:dict];
    }
    return self;
}
+ (instancetype)messageWithDictionary:(NSDictionary *)dict {
    return [[self alloc] initWithDictionary:dict];
}

//返回一个模型数组
+ (NSMutableArray *)messages {
    NSArray *array = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"messages.plist" ofType:nil]];
    NSMutableArray *arrayM = [NSMutableArray array];
    
    for (NSDictionary *dict in array) {
        JFMessage *message = [JFMessage messageWithDictionary:dict];
        
        //第一次笔记可变数组最后一个元素是nil,所以和当前模型的时间不同。
        //第二次可变数组最后一个元素是上一次存入模型,和当前模型进行比较。
        //如果相同则当前模型的时间隐藏,不同则显示
        if ([message.time isEqualToString:[[arrayM lastObject] time]]) {
            
            //如果当前时间和上一条消息时间相同,则隐藏当前消息的时间显示
            message.hideTime = YES;
        }
        
        //将封装的模型存入数组
        [arrayM addObject:message];
    }
    return arrayM;
}
@end

封装好数据模型后再继续封装frame模型,和微博案例类似,frame模型类中有模型对象属性、cell子控件的frame和cell的高度。因为封装frame的最终目的就是为了在创建cell之前计算出cell的高度,而计算cell的高度又必须根据cell子控件的frame来计算。最后提供一个返回frame模型数组的类方法,就可以将懒加载中的部分代码封装到模型类中,减少控制器中的代码量。

JFMessageFrame.h

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@class JFMessage;

//定义时间字体、消息字体的宏,写到这里是为了让其他类引入该头文件也能使用此宏
#define timeFont [UIFont systemFontOfSize:13]
#define textFont [UIFont systemFontOfSize:13]

@interface JFMessageFrame : NSObject

//模型对象属性
@property (strong, nonatomic) JFMessage *message;

//cell高度
@property (assign, nonatomic) CGFloat cellHeight;

//时间、头像、消息的frame
@property (assign, nonatomic) CGRect timeFrame;
@property (assign, nonatomic) CGRect iconFrame;
@property (assign, nonatomic) CGRect textFrame;

//返回一个frame模型数组
+ (NSMutableArray *)messageFrames;

@end

 JFMessageFrame.m

#import "JFMessageFrame.h"
#import "JFMessage.h"
#import "NSString+JFFontSize.h"
#define margin 10 //每个子控件之间的间距

@implementation JFMessageFrame

//返回一个frame模型数组
+ (NSMutableArray *)messageFrames {
    
    //模型数组
    NSMutableArray *arrayModel = [JFMessage messages];
    
    //frame模型数组
    NSMutableArray *arrayFrameModel = [NSMutableArray array];
    
    for (JFMessage *message in arrayModel) {
        //创建frame模型
        JFMessageFrame *messageFrame = [[JFMessageFrame alloc] init];
        
        //为frame模型赋值,这个步骤会调用我们重写后的set方法,计算各种frame和cell高度
        messageFrame.message = message;
        
        //将带数据的frame模型存入数组
        [arrayFrameModel addObject:messageFrame];
    }
    return arrayFrameModel;
}

//重写set方法为frame模型赋值
- (void)setMessage:(JFMessage *)message {
    
    _message = message;
    
    //取出屏幕宽度
    CGFloat screenW = [UIScreen mainScreen].bounds.size.width;
    
    //计算frame
    //时间
    CGFloat timeViewW = screenW;
    CGFloat timeViewH = 20;    //时间高度先定死
    CGFloat timeViewX = 0;     //x、y坐标也设为原点,创建View时让文字居中显示
    CGFloat timeViewY = 0;
    _timeFrame = CGRectMake(timeViewX, timeViewY, timeViewW, timeViewH);
    
    //头像
    CGFloat iconViewW = 30;    //头像尺寸先定死
    CGFloat iconViewH = 30;
    CGFloat iconViewX = 0;     //需要判断消息类型确定x
    CGFloat iconViewY = CGRectGetMaxY(_timeFrame) + margin;
    //根据消息类型计算iconViewX的值
    if (self.message.type == JFMessageSelf) {
        //自己发的信息
        iconViewX = screenW - (iconViewW + margin);
    } else {
        //别人发的信息
        iconViewX = margin;
    }
    _iconFrame = CGRectMake(iconViewX, iconViewY, iconViewW, iconViewH);
    
    //文本
    //调用方法计算文本的宽、高
    CGSize textViewSize = [self.message.text getFontSizeWithMaxSize:CGSizeMake(250, MAXFLOAT) andFont:textFont];
    CGFloat textViewW = textViewSize.width + 40;  //这里增加的宽、高是为了让消息框中文字显示在背景图中。
    CGFloat textViewH = textViewSize.height + 30; //消息框frame增大、并添加内间距(contentEdgeInsets属性),就能让文字向中间挤,从而达到文字在背景图中居中的效果。
    CGFloat textViewX = 0;   //要根据消息类型计算
    CGFloat textViewY = CGRectGetMaxY(_iconFrame) - margin;//比头像的底部高一个margin
    //根据消息类型计算textViewX的值
    if (self.message.type == JFMessageSelf) {
        //自己发的信息
        textViewX = screenW - (textViewW + iconViewW + 2 * margin);
    } else {
        //别人发的信息
        textViewX = CGRectGetMaxX(_iconFrame) + margin;
    }
    _textFrame = CGRectMake(textViewX, textViewY, textViewW, textViewH);
    
    //计算cell高度
    _cellHeight = CGRectGetMaxY(_textFrame) + margin;
}

@end

这里用到了一个方法,不是NSString自带的方法,而是我们给NSString添加的分类。分类的代码如下:

- (CGSize)getFontSizeWithMaxSize:(CGSize)maxSize andFont:(UIFont *)font;

NSString+JFFontSize.h

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@interface NSString (JFFontSize)

//根据传入最大Size和字体,返回一个Size
- (CGSize)getFontSizeWithMaxSize:(CGSize)maxSize andFont:(UIFont *)font;
@end

NSString+JFFontSize.m

#import "NSString+JFFontSize.h"

@implementation NSString (JFFontSize)

//根据传入最大Size和字体,返回一个Size
- (CGSize)getFontSizeWithMaxSize:(CGSize)maxSize andFont:(UIFont *)font {
    return [self boundingRectWithSize:maxSize options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName : font} context:nil].size;
}
@end

封装好数据模型和frame模型后,就可以开始封装cell。方法和微博案例也是类似,只不过需要根据消息发送时间判断时间是否显示。并且要用到一个新的方法来拉升背景图片,从而达到背景图放大而不失真的效果。

 JFMessageCell.h

#import <UIKit/UIKit.h>
@class JFMessageFrame;

@interface JFMessageCell : UITableViewCell

//数据模型属性,用于为cell子控件赋值数据
@property (strong, nonatomic) JFMessageFrame *messageFrame;

//创建cell
+ (instancetype)messageCellWithTableView:(UITableView *)tableView;

@end

 JFMessageCell.m

#import "JFMessageCell.h"
#import "JFMessage.h"
#import "JFMessageFrame.h"
#import "UIImage+JFImageSize.h"

//添加扩展,封装类内部属性,禁止外界访问
@interface JFMessageCell ()

@property (weak, nonatomic) UILabel *timeView;
@property (weak, nonatomic) UIButton *iconButtonView;
@property (weak, nonatomic) UIButton *textButtonView;

@end

@implementation JFMessageCell

//创建cell
+ (instancetype)messageCellWithTableView:(UITableView *)tableView {
    
    static NSString *ID = @"qq";
    
    //从缓存中创建cell
    JFMessageCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
    
    //缓存中没有就新创建
    if (cell == nil) {
        cell = [[JFMessageCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:ID];
    }
    return cell;
}

//重写initWithStyle:reuseIdentifier:方法创建cell的子控件
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    
    //重写父类构造方法需要先调用父类的构造方法,让父类先初始化完毕子类再初始化
    if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
        
        //创建cell的所有子控件
        //----------时间不能点击,使用UILabel----------
        UILabel *timeView = [[UILabel alloc] init];
        
        //将创建好的控件添加到cell中
        [self addSubview:timeView];
        
        //因为这里创建的控件是局部的,不能被类中其他方法访问到,所以定义控件属性指向这个控件
        self.timeView = timeView;
        
        //----------头像可点击,使用按钮-----------
        UIButton *iconButtonView = [[UIButton alloc] init];
        [self addSubview:iconButtonView];
        self.iconButtonView = iconButtonView;
        
        //----------消息可点击,使用按钮----------
        UIButton *textButtonView = [[UIButton alloc] init];
        [self addSubview:textButtonView];
        self.textButtonView = textButtonView;
        
        //将cell的背景颜色设为透明
        self.backgroundColor = [UIColor clearColor];
        
    }
    return self;
}

//重写set方法为cell的子控件赋值
- (void)setMessageFrame:(JFMessageFrame *)messageFrame {
    
    _messageFrame = messageFrame;
    
    //----------时间----------
    //设置时间文字文字字体
    self.timeView.font = timeFont;
    
    //让时间居中显示
    self.timeView.textAlignment = NSTextAlignmentCenter;
    
    //时间隐藏属性非YES则显示时间,否则不显示
    if (!self.messageFrame.message.hideTime) {
        
        //为时间控件赋值数据
        self.timeView.text = self.messageFrame.message.time;

        //为时间控件设置frame
        self.timeView.frame = self.messageFrame.timeFrame;
    }
    
    //----------头像----------
    //判断消息发送对象,再赋值头像数据
    if (self.messageFrame.message.type == JFMessageSelf) {
        
        //为头像控件赋值数据,头像直接写死
        [self.iconButtonView setBackgroundImage:[UIImage imageNamed:@"me"] forState:UIControlStateNormal];
    } else {
        
        //为头像控件赋值数据,头像直接写死
        [self.iconButtonView setBackgroundImage:[UIImage imageNamed:@"other"] forState:UIControlStateNormal];
    }
    //为头像设置frame
    self.iconButtonView.frame = self.messageFrame.iconFrame;
    
    //----------消息----------
    //设置消息文字字体
    self.textButtonView.titleLabel.font = textFont;
    
    //按钮文字自动换行
    self.textButtonView.titleLabel.numberOfLines = 0;
    
    //为消息控件赋值数据
    [self.textButtonView setTitle:self.messageFrame.message.text forState:UIControlStateNormal];
    
    //设置消息控件的frame
    self.textButtonView.frame = self.messageFrame.textFrame;
    
    //设置文字的内边距
    self.textButtonView.contentEdgeInsets = UIEdgeInsetsMake(20, 20, 20, 20);
    
    //调用分类方法,拉升背景图片
    UIImage *selfbgNor = [UIImage getReSizeingImageWithName:@"chat_send_nor"];
    UIImage *selfbgLigh = [UIImage getReSizeingImageWithName:@"chat_send_press_pic"];
    UIImage *otherbgNor = [UIImage getReSizeingImageWithName:@"chat_recive_nor"];
    UIImage *otherbgLigh = [UIImage getReSizeingImageWithName:@"chat_recive_press_pic"];
    
    //判断消息发送对象,设置背景图片
    if (self.messageFrame.message.type == JFMessageSelf) {
        //改变按钮文字颜色
        [self.textButtonView setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
        [self.textButtonView setBackgroundImage:selfbgNor forState:UIControlStateNormal];
        [self.textButtonView setBackgroundImage:selfbgLigh forState:UIControlStateHighlighted];
    } else {
        //改变按钮文字颜色
        [self.textButtonView setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
        [self.textButtonView setBackgroundImage:otherbgNor forState:UIControlStateNormal];
        [self.textButtonView setBackgroundImage:otherbgLigh forState:UIControlStateHighlighted];
    }
}

@end

上面用到的拉升图片的方法,是一个自定义的UIImage分类的方法。分类的代码如下:

UIImage+JFImageSize.h

#import <UIKit/UIKit.h>

@interface UIImage (JFImageSize)

//传入一张图片名称返回一张拉升后的图片对象
+ (UIImage *)getReSizeingImageWithName:(NSString *)name;
@end

 UIImage+JFImageSize.m

#import "UIImage+JFImageSize.h"

@implementation UIImage (JFImageSize)

//传入一张图片名称返回一张拉升后的图片对象
+ (UIImage *)getReSizeingImageWithName:(NSString *)name {
    return [[UIImage imageNamed:name] resizableImageWithCapInsets:UIEdgeInsetsMake(30, 20, 18, 30) resizingMode:UIImageResizingModeStretch];
}
@end

封装好cell后,就可以在控制器调用并展示数据了。首先在Main.storyboard中拖拽好基本界面,并对tableview和textField进行属性连线。

Snip20150817_1在控制器中遵守数据源协议、代理协议,并指定当前控制器为tableview的数据源对象、代理对象。并实现对应的方法。需要注意的是这里设置背景图的前提是cell的背景色是clearColor,否则会被cell自带的背景色给覆盖住。

ViewController.m

#import "ViewController.h"
#import "JFMessage.h"
#import "JFMessageCell.h"
#import "JFMessageFrame.h"

@interface ViewController () <UITableViewDataSource,UITableViewDelegate>

@property (weak, nonatomic) IBOutlet UITableView *tableView;
@property (weak, nonatomic) IBOutlet UITextField *textFieldView;

//frame模型数组
@property (strong, nonatomic) NSMutableArray *messageFrames;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //指定数据源对象、代理对象
    self.tableView.dataSource = self;
    self.tableView.delegate = self;
    
    //设置tableview背景图
    UIImageView *bg = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"1"]];
    self.tableView.backgroundView = bg;
    
    //取消tableView分割线
    self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
    
    //取消垂直滚动条
    self.tableView.showsVerticalScrollIndicator = NO;
    
    //禁止tableView被点击
    self.tableView.allowsSelection = NO;
    
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}

//懒加载数据
- (NSMutableArray *)messageFrames {
    if (_messageFrames == nil) {
        _messageFrames = [JFMessageFrame messageFrames];
    }
    return _messageFrames;
}

//一共有多少行
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.messageFrames.count;
}

//创建每行的cell
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    //获取frame模型数据
    JFMessageFrame *messageFrame = self.messageFrames[indexPath.row];
    
    //创建cell
    JFMessageCell *cell = [JFMessageCell messageCellWithTableView:tableView];
    
    //为cell赋值数据
    cell.messageFrame = messageFrame;
    
    return cell;

}

//返回每个cell的高度,这个方法的执行优先权比创建cell的优先权更高
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    //获取frame模型数据
    JFMessageFrame *messageFrame = self.messageFrames[indexPath.row];
    
    //返回cell高度
    return messageFrame.cellHeight;
}
@end

最终效果图:

Snip20150817_5

二、通知中心NSNotificationCenter

通知中心是程序内部提供的消息广播的一种机制。实际上就是一个消息传递者,把接收到的消息,根据内部的一个消息转发表,来将消息转发给需要的对象。通知中心是基于观察者模式的,它允许注册、删除观察者。

一个NSNotificationCenter可以有许多的通知消息NSNotification,对于每一个NSNotification可以有很多的观察者Observer来接收通知。

创建通知中心

一个应用程序内部只有一个通知中心实例对象(单例对象):

NSNotificationCenter *center=[NSNotificationCenter defaultCenter];

发布通知

/*
参数说明
postNotificationName: 通知的名称
object: 通知发布者
userInfo: 通知的额外信息
*/

[center postNotificationName:<#(NSString *)#> object:<#(id)#> userInfo:<#(NSDictionary *)#>]

[center postNotificationName:<#(NSString *)#> object:<#(id)#>]

 注册通知监听器

/*
参数说明:
addObserver:监听器:即谁要接收这个通知,这里传入需要接收通知的对象
selector:接收者接到通知后调用这个回调函数进行处理,这埋在注意要将当前通知对象做为参数传入
name:所接收的通知的名称,只接收这个名称所对应的通知,如果为nil,则接收所有通知
object:通知发布者,只接收这个名称的对象所发布的通知,如果为nil,则接收任何对象所发布的通知
*/

[center addObserver:<#(id)#> selector:<#(SEL)#> name:<#(NSString *)#> object:<#(id)#>]

 取消注册通知监听器

//取消指定对象的所有注册监听
[[NSNotificationCenter defaultCenter] removeObserver:<#(id)#>];

//取消指定对象的所有注册监听
[[NSNotificationCenter defaultCenter] removeObserver:<#(id)#> name:<#(NSString *)#> object:<#(id)#>];

 键盘通知

键盘状态发生改变的时候,系统会发出一些特定的通知

UIKeyboardWillShowNotification // 键盘即将显示
UIKeyboardDidShowNotification // 键盘显示完毕
UIKeyboardWillHideNotification // 键盘即将隐藏
UIKeyboardDidHideNotification // 键盘隐藏完毕
UIKeyboardWillChangeFrameNotification // 键盘的位置尺寸即将发生改变
UIKeyboardDidChangeFrameNotification // 键盘的位置尺寸改变完毕

 注册键盘通知监听器

我们经常需要在键盘弹出或者隐藏的时候做一些特定的操作,因此需要监听键盘的状态。让控件器监听系统自动发出的键盘的frame将要发生改变事件,调用控制器的方法进行处理。代码如下:

//创建通知中心
NSNotificationCenter *center=[NSNotificationCenter defaultCenter];

//注册键盘通知
[center addObserver:self selector:@selector(keyBoardWillChangeFrame:) name:UIKeyboardWillChangeFrameNotification object:nil];

 键盘通知的额外信息

系统发出键盘通知时,会附带一下跟键盘有关的额外信息(字典),字典常见的key:

UIKeyboardFrameBeginUserInfoKey // 键盘刚开始的frame
UIKeyboardFrameEndUserInfoKey // 键盘最终的frame(动画执行完毕后)
UIKeyboardAnimationDurationUserInfoKey // 键盘动画的时间
UIKeyboardAnimationCurveUserInfoKey // 键盘动画的执行节奏(快慢)

 实现键盘监听事件处理

获取键盘的开始和最终的frame,计算出偏移值,让tableView以动画的形式也进行相应的偏移。

//处理监听键盘事件
- (void) keyBoardWillChangeFrame:(NSNotification *)note {

    //获取额外信息
    NSDictionary *info = note.userInfo;

    //获取动画时间,将来以相同的时间以动画的形式移动view
    CGFloat duration = [info[UIKeyboardAnimationDurationUserInfoKey] floatValue];
    
    //获取键盘完全出现后的frame
    CGRect endFrame = [info[@"UIKeyboardFrameEndUserInfoKey"] CGRectValue];

    //计算Y轴偏移值
    CGFloat offsetY = endFrame.origin.y - self.view.frame.size.height;

    //动画移动整个view
    [UIView animateWithDuration:duration animations:^{
        //进行y方向的偏移
        self.view.transform = CGAffineTransformMakeTranslation(0, offsetY);
    }];
}

通知和代理的区别:

代理 :一对一关系 (1个对象只能告诉另1个对象发生了什么事情)

通知:多对多关系(1个对象能告诉N个对象发生了什么事情, 1个对象能得知N个对象发生了什么事情)

通知和代理的选择:

如果是自定义的操作,一般使用代理。

如果是系统组件事件,就先考虑通知。

三、实现发布信息和自动回复

QQ界面已经搭建完毕,接下来实现键盘的弹出和收起,发布消息和自动回复功能。先设置UITextField的Return Key选项为Send,并勾选Auto-enable Return Key,让发送键在未输入内容的情况下禁止点击。

Snip20150817_6

然后创建通知中心单例对象,注册通知监听事件并监听键盘的状态。在键盘弹出后,屏幕内容也跟着向上偏移,在拖动tableview的时候,收起键盘。

ViewController.m

//
//  ViewController.m
//  QQ案例练习
//
//  Created by itcast on 15/8/17.
//  Copyright © 2015年 itcast. All rights reserved.
//

#import "ViewController.h"
#import "JFMessage.h"
#import "JFMessageCell.h"
#import "JFMessageFrame.h"

@interface ViewController () <UITableViewDataSource,UITableViewDelegate>

@property (weak, nonatomic) IBOutlet UITableView *tableView;
@property (weak, nonatomic) IBOutlet UITextField *textFieldView;

//frame模型数组
@property (strong, nonatomic) NSMutableArray *messageFrames;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //指定数据源对象、代理对象
    self.tableView.dataSource = self;
    self.tableView.delegate = self;
    
    //设置tableview背景图
    UIImageView *bg = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"1"]];
    self.tableView.backgroundView = bg;
    
    //取消tableView分割线
    self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
    
    //取消垂直滚动条
    self.tableView.showsVerticalScrollIndicator = NO;
    
    //禁止tableView被点击
    self.tableView.allowsSelection = NO;
    
    //创建通知中心
    NSNotificationCenter *center=[NSNotificationCenter defaultCenter];
    
    //注册键盘通知
    [center addObserver:self selector:@selector(keyBoardWillChangeFrame:) name:UIKeyboardWillChangeFrameNotification object:nil];
    
}

//处理监听键盘事件
- (void) keyBoardWillChangeFrame:(NSNotification *)note {
    
    //获取额外信息
    NSDictionary *info = note.userInfo;
    
    //获取动画时间,将来以相同的时间以动画的形式移动view
    CGFloat duration = [info[UIKeyboardAnimationDurationUserInfoKey] floatValue];
    
    //获取键盘完全出现后的frame
    CGRect endFrame = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue];
    
    //计算Y轴偏移值
    CGFloat offsetY = endFrame.origin.y - self.view.frame.size.height;
    
    //动画移动整个view
    [UIView animateWithDuration:duration animations:^{
        //进行y方向的偏移
        self.view.transform = CGAffineTransformMakeTranslation(0, offsetY);
    }];
    
    //让tableview滑到底部
    NSIndexPath *path = [NSIndexPath indexPathForRow:self.messageFrames.count - 1 inSection:0];
    [self.tableView scrollToRowAtIndexPath:path atScrollPosition:UITableViewScrollPositionBottom animated:YES];
}

// 移除通知
- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

//拖拽tableview的时候执行这个方法,收起键盘
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    //收起键盘
    [self.view endEditing:YES];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    
}

//懒加载数据
- (NSMutableArray *)messageFrames {
    if (_messageFrames == nil) {
        _messageFrames = [JFMessageFrame messageFrames];
    }
    return _messageFrames;
}

//一共有多少行
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.messageFrames.count;
}

//创建每行的cell
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    //获取frame模型数据
    JFMessageFrame *messageFrame = self.messageFrames[indexPath.row];
    
    //创建cell
    JFMessageCell *cell = [JFMessageCell messageCellWithTableView:tableView];
    
    //为cell赋值数据
    cell.messageFrame = messageFrame;
    
    return cell;

}

//返回每个cell的高度,这个方法的执行优先权比创建cell的优先权更高
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    //获取frame模型数据
    JFMessageFrame *messageFrame = self.messageFrames[indexPath.row];
    
    //返回cell高度
    return messageFrame.cellHeight;
}
@end

实现发布信息和自动回复功能,监听Send键的点击。当点击键盘上的Send键后创建模型,添加到frame模型数组中,并重新加载tableview中的数据。

ViewController.m

#import "ViewController.h"
#import "JFMessage.h"
#import "JFMessageCell.h"
#import "JFMessageFrame.h"

@interface ViewController () <UITableViewDataSource,UITableViewDelegate,UITextFieldDelegate>

@property (weak, nonatomic) IBOutlet UITableView *tableView;
@property (weak, nonatomic) IBOutlet UITextField *textFieldView;

//frame模型数组
@property (strong, nonatomic) NSMutableArray *messageFrames;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //指定数据源对象、代理对象
    self.tableView.dataSource = self;
    self.tableView.delegate = self;
    self.textFieldView.delegate = self;
    
    //设置tableview背景图
    UIImageView *bg = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"1"]];
    self.tableView.backgroundView = bg;
    
    //取消tableView分割线
    self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
    
    //取消垂直滚动条
    self.tableView.showsVerticalScrollIndicator = NO;
    
    //禁止tableView被点击
    self.tableView.allowsSelection = NO;
    
    //创建通知中心
    NSNotificationCenter *center=[NSNotificationCenter defaultCenter];
    
    //注册键盘通知
    [center addObserver:self selector:@selector(keyBoardWillChangeFrame:) name:UIKeyboardWillChangeFrameNotification object:nil];
    
}

//处理监听键盘事件
- (void) keyBoardWillChangeFrame:(NSNotification *)note {
    
    //获取额外信息
    NSDictionary *info = note.userInfo;
    
    //获取动画时间,将来以相同的时间以动画的形式移动view
    CGFloat duration = [info[UIKeyboardAnimationDurationUserInfoKey] floatValue];
    
    //获取键盘完全出现后的frame
    CGRect endFrame = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue];
    
    //计算Y轴偏移值
    CGFloat offsetY = endFrame.origin.y - self.view.frame.size.height;
    
    //动画移动整个view
    [UIView animateWithDuration:duration animations:^{
        //进行y方向的偏移
        self.view.transform = CGAffineTransformMakeTranslation(0, offsetY);
    }];
    
    //让tableview滑到底部
    NSIndexPath *path = [NSIndexPath indexPathForRow:self.messageFrames.count - 1 inSection:0];
    [self.tableView scrollToRowAtIndexPath:path atScrollPosition:UITableViewScrollPositionBottom animated:YES];
}

// 移除通知
- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

//拖拽tableview的时候执行这个方法,收起键盘
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    //收起键盘
    [self.view endEditing:YES];
}

// 当键盘上的return键被单击的时候触发
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
    
    //获取用户输入的文本
    NSString *text = textField.text;
    
    //发送用户的消息
    [self sendMessage:text withType:JFMessageSelf];
    
    //发送一个系统消息
    [self sendMessage:@"不认识!" withType:JFMessageOther];
    
    //清空文本框
    textField.text = nil;
    
    return YES;
}

// 发送消息
- (void)sendMessage:(NSString *)msg withType:(JFMessageType)type {
    
    //获取当前系统时间
    NSDate *nowDate = [NSDate date];
    //创建一个日期时间格式化器
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    //设置格式
    formatter.dateFormat = @"今天 HH:mm";
    
    JFMessageFrame *modelFrame = [[JFMessageFrame alloc] init];
    
    JFMessage *message = [[JFMessage alloc] init];
    
    //设置模型的属性
    message.time = [formatter stringFromDate:nowDate];
    message.type = type;
    message.text = msg;
    
    //为frame模型赋值
    modelFrame.message = message;
    
    //根据当前消息的时间和上一条消息的时间, 来设置是否需要隐藏时间Label
    JFMessageFrame *lastMessageFrame = [self.messageFrames lastObject];
    
    NSString *lastTime = lastMessageFrame.message.time;
    //判断时间是否需要隐藏
    if ([message.time isEqualToString:lastTime]) {
        modelFrame.message.hideTime = YES;
    }
    
    //添加frame模型到模型数组
    [self.messageFrames addObject:modelFrame];
    
    //刷新tableview中所有数据
    [self.tableView reloadData];
    
    //把最后一行滚动到最上面
    NSIndexPath *idxPath = [NSIndexPath indexPathForRow:self.messageFrames.count - 1 inSection:0];
    [self.tableView scrollToRowAtIndexPath:idxPath atScrollPosition:UITableViewScrollPositionTop animated:YES];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    
}

//懒加载数据
- (NSMutableArray *)messageFrames {
    if (_messageFrames == nil) {
        _messageFrames = [JFMessageFrame messageFrames];
    }
    return _messageFrames;
}

//一共有多少行
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.messageFrames.count;
}

//创建每行的cell
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    //获取frame模型数据
    JFMessageFrame *messageFrame = self.messageFrames[indexPath.row];
    
    //创建cell
    JFMessageCell *cell = [JFMessageCell messageCellWithTableView:tableView];
    
    //为cell赋值数据
    cell.messageFrame = messageFrame;
    
    return cell;

}

//返回每个cell的高度,这个方法的执行优先权比创建cell的优先权更高
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    //获取frame模型数据
    JFMessageFrame *messageFrame = self.messageFrames[indexPath.row];
    
    //返回cell高度
    return messageFrame.cellHeight;
}
@end

 案例易错总结:

1.注册的监听一定要在对象销毁的时候取消,不然系统还会一直向当前注册监听的对象发送通知,但是对象已经不存在,就会造成野指针的错误。

2.这里不能直接使用UITableViewController,因为当前界面还有其他控件。

3.忘记设置代理对象。