传智播客第八天练习总结-QQ聊天UI界面

摘要:本练习是传智播客IOS UI基础班的第八天课程练习,对练习的知识点和重点进行总结。实现效果如图所示

QQ聊天界面UI练习

基本思路

  1. 导入素材,创建主界面
  2. 实现字典转模型(可以暂时不计算控件的frame),并实现懒加载
  3. 创建自定义Cell,实现其创建单元格各控件的类方法和对象方法,重写数据模型的set方法,在该方法中对各控件的内容和frame进行赋值
  4. 实现tableView的datasource方法
  5. 在frame模型中计算各控件的frame
  6. 修正tableView的细节:分割线、不可选中、背景色、正文的文字颜色和背景图片等(拉伸图片)
  7. 实现对自定义Cell中时间的判断,如果与上一条消息发送时间相同,则不显示
  8. 实现键盘弹出时整个View的随动效果(通知)
  9. 把键盘的return键变成send键,实现新的消息的发送和回复(UITextField的delegate方法)

1、导入素材,创建主界面

素材主要有一个的plist文件和相关的图片,plist文件总体是一个数组,每条消息是一个Dictionary,包含正文、事件和类型条信息,类型中0表示自己发送的消息,1表示别人发送的消息

plist文件

2、实现字典转模型和懒加载

由于要在懒加载时就计算控件的Frame和单元格行高,所以创建两个Model,一个存储消息模型,一个存储Frame信息

个人认为可以直接在一个model中存储message和messageFrame信息,但是结构不够清晰,维护相对复杂

由于消息的类型只有0和1两种类型,为了更加直观,可以将其存储为一个枚举形式

typedef enum {
    LJMessageTypeMe = 0,//表示自己发的消息
    LJMessageTypeOther = 1//表示对方发的消息
}LJMessageType;
@implementation LJMessage
-(instancetype)initWithDic:(NSDictionary *)dic
{
    if (self = [super init]) {
            //注意类成员的名称要与字典的键名称一致
        [self setValuesForKeysWithDictionary:dic];
    }
    return self;
}
+(instancetype)messageWithDic:(NSDictionary *)dic
{
    return [[self alloc] initWithDic:dic];
}
@end

Frame模型类中将message模型作为自己的成员之一,还包括时间、正文、头像的Frame,以及单元格的行高。.m文件中需要重写message的set方法,在这个方法中计算各控件的Frame和行高。

注意:计算Frame需要先引入<UIKit/UIKit.h>,否则无法计算文字高度

懒加载的实质是重写模型对象的get方法,当程序需要调用这个成员的get方法时,在方法内部判断模型对象是否为空,如果为空则加载对应的数据

懒加载通常的模式为:1、获取plist文件路径;2、根据文件路径创建一个由NSDictionary组成的NSArray;3、创建一个空的NSMutableArray;4、遍历每一个NSDictionary,使用模型对象的类方法或对象方法将其转换为模型对象;5、将该模型对象加入NSMutableArray;6、循环结束后将这个可变数组赋值给模型数据集;

注意:因为后续要实现消息的发送和自动回复,因此创建的模型数据集应该是一个可变数组

-(NSMutableArray *)messageFrames
{
    if (_messageFrames == nil) {
        NSString * path_plist = [[NSBundle mainBundle] pathForResource:@"messages.plist" ofType:nil];
        NSArray * messages_dic = [NSArray arrayWithContentsOfFile:path_plist];
        NSMutableArray * messageFrames_model = [NSMutableArray array];
        for (NSDictionary * dic in messages_dic) {
            LJMessageFrame * messageFrame_model_temp = [[LJMessageFrame alloc] init];
            LJMessage * currentMessage = [LJMessage messageWithDic:dic];
            messageFrame_model_temp.message= currentMessage;
            [messageFrames_model addObject:messageFrame_model_temp];
        }
        _messageFrames = messageFrames_model;
    }
    return _messageFrames;
}

3、创建自定义Cell

创建一个基于UITableViewCell的类,定义三个私有成员,lbl_time,img_icon,btn_text分别表示时间、头像和正文控件

重写自定义Cell的initWithStyle: reuseIdentifier:方法

#define myFont [UIFont systemFontOfSize:18]
-(instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
    if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
        //时间
        UILabel * lbl_time = [[UILabel alloc] init];
        lbl_time.font = myFont;
        lbl_time.textAlignment = NSTextAlignmentCenter;
        [self.contentView addSubview:lbl_time];
        self.lbl_time = lbl_time;
        
        //头像
        UIImageView * img_icon = [[UIImageView alloc] init];
        [self.contentView addSubview:img_icon];
        self.img_icon = img_icon;
        
        //正文
        UIButton * btn_text = [[UIButton alloc] init];
        btn_text.titleLabel.font = myFont;
        btn_text.titleLabel.numberOfLines = 0;
        [self.contentView addSubview:btn_text];
        self.btn_text = btn_text;
    }
    return self;
}

将整个自定义Cell的创建过程封装在一个类方法中,方便用户调用

+(instancetype)messageViewCellWithTableView:(UITableView *)tableView reuseidentifier:(NSString *)reuseID
{
    LJMessageViewCell * cell = [tableView dequeueReusableCellWithIdentifier:reuseID];
    if (!cell) {
        cell = [[LJMessageViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseID];
    }
    
    return cell;
}

自定义Cell中的控件信息必须是根据其模型数据来决定的,教程中的方法是为Cell添加一个Frame模型数据的成员变量,通过重写set方法的方式来为各控件赋值。这样比较容易理解,使用也方便。个人认为也可以为自定义Cell定义一个方法,该方法使用Frame模型作为参数,通过参数将模型数据信息传入,然后为各控件赋值。

@interface LJMessageViewCell : UITableViewCell
@property(nonatomic, strong)LJMessageFrame * messageFrameModel;
@property(nonatomic, strong)UILabel * lbl_time;
@property(nonatomic, strong)UIImageView * img_icon;
@property(nonatomic, strong)UIButton * btn_text;
@end
  
@implementation LJMessageViewCell
-(void)setMessageFrameModel:(LJMessageFrame *)messageFrameModel
{
    _messageFrameModel = messageFrameModel;
    //时间内容
    self.lbl_time.text = messageFrameModel.message.time;
    self.lbl_time.frame = messageFrameModel.timeFrame;
    
    //头像内容
    if (messageFrameModel.message.type == LJMessageTypeMe) {
        self.img_icon.image = [UIImage imageNamed:@"me"];
    }else{
        self.img_icon.image = [UIImage imageNamed:@"other"];
    }
    self.img_icon.frame = messageFrameModel.iconFrame;
    
    //正文内容
    [self.btn_text setTitle:messageFrameModel.message.text forState:UIControlStateNormal];
    //根据消息类型确定背景图名称和正文字体颜色
    NSString * nor,* highlight;
    if (messageFrameModel.message.type == LJMessageTypeMe) {
        nor = @"chat_send_nor";
        highlight = @"chat_send_press_pic";
        [self.btn_text setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
    } else {
        nor = @"chat_recive_nor";
        highlight = @"chat_recive_press_pic";
        [self.btn_text setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
    }
    UIImage * img_normal = [UIImage imageNamed:nor];
    UIImage * img_highlight = [UIImage imageNamed:highlight];
    //设置背景图
    [self.btn_text setBackgroundImage:img_normal forState:UIControlStateNormal];
    [self.btn_text setBackgroundImage:img_highlight forState:UIControlStateHighlighted];
    //设置frame
    self.btn_text.frame = messageFrameModel.textFrame;        
}
@end

4、实现tableView的datasource方法

主要需要实现四个方法:

  • 指定tableView有多少组

    -(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
    {return 1;}
    
  • 指定tableView每一组有多少行

    -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
    { return self.messageFrames.count;}
    
  • 指定tableView某一组某一行的Cell

    -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
        LJMessageViewCell * cell = [LJMessageViewCell messageViewCellWithTableView:tableView reuseidentifier:@"message"];
        LJMessageFrame * messageframe_current = self.messageFrames[indexPath.row];
        cell.messageFrameModel = messageframe_current;
        return  cell;
    }
    
  • 指定tableView某一组某一行的行高

    -(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
    {
        LJMessageFrame * messageframe_current = self.messageFrames[indexPath.row];
        return messageframe_current.rowHeight;
    }
    

5、计算各控件的Frame

实现各控件的frame的计算,主要的知识点在于对文字高度的计算,对文本高度的计算方法有很多种,教程中使用的是boundingRectWithSize:的方法:

  • 该方法返回的是一个CGRect类型

  • 第一个参数size传入一个CGSize,代表文本计算时的宽高方向的最大限制,如果无限制则可以写MAXFLOAT

  • 第二个参数options传入一个计算方法,它是一个枚举,可以相互组合

    enum { 
      //如果文字内容超出指定的矩形限制,文字将被截去并在最后一个字符后加上省略号
    NSStringDrawingTruncatesLastVisibleLine = 1 << 5, 
      //整个文本框将以每行组成的矩形为单位计算整个文字的尺寸
    NSStringDrawingUsesLineFragmentOrigin = 1 << 0, 
      //以字符的行距(leading,行距:从一行文字的底部到另一行文字底部的间距)来计算高度
    NSStringDrawingUsesFontLeading = 1 << 1, 
      //计算文本尺寸时将以每个字或字形为单位来计算
    NSStringDrawingUsesDeviceMetrics = 1 << 3,
    };typedef NSInteger NSStringDrawingOptions;
    
  • 第三个参数attributes传入应用于NSString字符串的文本属性对应的字典

  • 第四个参数Context用于控制如何调整字符的间距和缩放,可以为nil

#import <UIKit/UIKit.h>
#define myFont [UIFont systemFontOfSize:18]
-(void)setMessage:(LJMessage *)message
{
    _message = message;
    //获取屏幕宽度
    CGFloat screenW = [UIScreen mainScreen].bounds.size.width;
    static CGFloat margin = 5;
    NSDictionary * attr = @{NSFontAttributeName:myFont};
    NSString * text_string = message.text;
    CGSize text_size = [text_string boundingRectWithSize:CGSizeMake(200.0, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:attr context:nil].size;
        CGFloat textX = 0;
    CGFloat textY = iconY;
    CGFloat textW = text_size.width;
    CGFloat textH = text_size.height;
    if (message.type == LJMessageTypeOther) {
        textX = CGRectGetMaxX(self.iconFrame);
    } else {
        textX = iconX - textW;
    }
    self.textFrame = CGRectMake(textX, textY, textW, textH);
}

注意:计算Frame使用的文本属性应与实际显示的文本属性相同,否则会出现错误,另外文本框的numberOfLines应设置为0,否则无法换行。

6、修正tableView的细节

该部分主要包括:取消分割线,使单元格不可选中,设定tableview和cell的背景色,设定当拖动tableView时收回键盘,根据消息类型设置正文颜色和背景图。主要的知识点在于背景图的拉伸方式的设定,教程中使用的方法是

- (UIImage *)stretchableImageWithLeftCapWidth:(NSInteger)leftCapWidth topCapHeight:(NSInteger)topCapHeight;

该方法是通过设置左边不拉伸区域的宽度和上面不拉伸区域的高度来达到边角不拉伸的效果

leftCapWidth:左边不拉伸区域的宽度
topCapWidth:上面不拉伸区域的高度
扩展:
rightCapWidth = 图片宽度-leftCapWidth -1
bottomCapWidth = 图片高度-topCapWidth -1

而拉伸区域capInset实际上是(topCapHeight,leftCapWidth,bottomCapWidth,rightCapWidth)所以一般leftCapWidth取图片宽度的一般,topCapHeight取图片高度的一般,来获取拉伸区域1*1的矩阵来复制填充(UIImageResizingModeTile),保持外围的区域不变

与该方法类似的还有一个是resizableImageWithCapInsets

- (UIImage *)resizableImageWithCapInsets:(UIEdgeInsets)capInsets resizingMode:(UIImageResizingMode)resizingMode NS_AVAILABLE_IOS(6_0);

7、实现对自定义Cell中时间的判断

判断时间是否需要显示的关键在于判断当前消息的时间与前一条消息的事件是否相同,思路是在懒加载的for循环里,将当前的消息模型与上一条消息模型进行对比,上一条消息模型可以在当前模型尚未加入到NSMutableArray之前通过lastObject获取。然后在Frame模型中定义一个Bool成员变量,用于记录当前消息是否应该显示时间。

如果不需要显示时间,则可以直接不计算时间控件的frame

8、实现键盘弹出时整个View的随动效果

该效果实现的难点主要在于如何知道键盘的尺寸发生了改变,并且获取键盘改变之后的尺寸,教程中使用了通知机制

通知机制

每一个应用程序都有一个通知中心(NSNotificationCenter)实例,专门负责协调不同对象之间的消息通信;任何一个对象都可以向通知中心发布通知(NSNotification),描述自己在做什么。其它感兴趣的对象(Observer)可以申请在某个特定通知发布时(或在某个特定的对象发布通知时)收到这个通知。

  • 通知NSNotification

    属性

    - (NSString *)name;  //通知的名称
    - (id)object;  //通知发布者(是谁要发布通知)
    - (NSDictionary *)userInfo;  //一些额外的信息(通知发布者传递给通知接受者的信息内容)
    

    初始化

    + (instancetype)notificationWithName:(NSString *)aName object:(id)anObject;
    + (instancetype)notificationWithName:(NSString *)aName object:(id)anObject userInfo:(NSDictionary *)aUserInfo;
    + (instancetype)notificationWithName:(NSString *)aName object:(id)anObject userInfo:(NSDictionary *)aUserInfo;
    
  • 通知中心NSNotificationCenter

    发布通知

    - (void)postNotification:(NSNotification *)notification;
    

    发布一个notification通知,可在notification对象中设置通知的名称、通知发布者、额外信息等

    - (void)postNotification:(NSNotification *)notification;
    

    发布一个名称为aName的通知,anObject为这个通知的发布者

    - (void)postNotificationName:(NSString *)aName object:(id)anObject userInfo:(NSDictionary *)aUserInfo;
    

    发布一个名称为aName的通知,anObject为这个通知的发布者,aUserInfo为额外信息

    注册通知监听器

    - (void)addObserver:(id)observer selector:(SEL)aSelector name:(NSString *)aName object:(id)anObject;
    

    observer:监听器,即谁要接收这个通知;

    aSelector:收到通知后,回调监听器的这个方法,并且把通知对象当做参数传入

    aName:通知的名称。如果为nil,那么无论通知的名称是什么,监听器都能收到这个通知

    anObject:通知发布者。如果为nil,那么无论谁发布的这个名称的通知,监听器都能收到这个通知

    (id)addObserverForName:(NSString *)name object:(id)obj queue:(NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block;
    

    name:通知的名称

    obj:通知发布者

    block:收到对应的通知时,会回调这个block

    queue:决定了block在哪个操作队列中执行,如果传nil,默认在当前操作队列中同步执行

    取消注册通知监听器

    通知中心不会保留(retain)监听器对象,在通知中心注册过的对象,必须在该对象释放前取消注册。否则,当对应的通知再次出现时,通知中心仍然会向该监听器发送消息。因为相应的监听器对象已经被释放了,所以可能会导致应用崩溃。

    - (void)removeObserver:(id)observer;
    - (void)removeObserver:(id)observer name:(NSString *)aName object:(id)anObject;
    
    • 键盘相关的通知

    键盘状态改变的时候,系统会发出一些特定的通知,我们不需要关心这些通知的发送者是谁,只要知道通知的名称即可

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

    键盘通知发出时,通知NSNotification中会附带跟键盘有关的额外信息[字典],字典中常见的key如下:

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

    具体实现代码如下:

    -(void)ViewChangeWithKeyboard:(NSNotification *) NoteInfo
    {
        //获取通知内容中键盘的高度信息
        CGRect endSize = [NoteInfo.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
        CGFloat endSize_Y = endSize.origin.y;
        //获取通知内容中键盘的移动时间信息
        NSTimeInterval time = [NoteInfo.userInfo[UIKeyboardAnimationDurationUserInfoKey] floatValue];
        //获取当前屏幕的尺寸并计算整个view应移动到的Y值
        CGFloat screenH = [UIScreen mainScreen].bounds.size.height;
        CGFloat screenW = [UIScreen mainScreen].bounds.size.width;
        CGFloat moveToY = endSize_Y - screenH;
        //动画效果实现view的移动
        [UIView animateWithDuration:time animations:^{
            self.view.frame = CGRectMake(0, moveToY, screenW, screenH);
        }];
    }
    

9、实现新的消息的发送和回复

实现新消息的发送和回复主要的关键点在于监听文本框的输入操作,并更新tableView的数据源。实现思路如下:

  1. 使用UItextField的delegate方法textFieldShouldReturn:来监听发送按钮的点击操作
  2. 获取当前文本框的内容
  3. 获取当前时间并转换为指定的格式
  4. 根据消息正文和消息类型创建消息模型和Frame模型
  5. 将新的Frame模型加入到模型数据集和中
  6. 刷新tableView并将最后一行滚动到第一行
  7. 清空文本框

如果需要实现自动回复,将发送消息的功能进行封装即可,通过传入一个消息正文和消息类型实现自动发送

-(void)sendMessage:(NSString *)message WithType:(LJMessageType)type
{
    //获取当前时间
    NSDate * nowdate = [NSDate date];
    NSDateFormatter * dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setDateFormat:@"今天 HH:mm"];
    NSString * nowTime = [dateFormatter stringFromDate:nowdate];
    //根据文本框的内容创建LJMessageFrame对象
    LJMessage * message_send_model = [[LJMessage alloc] init];
    message_send_model.text = message;
    message_send_model.time = nowTime;
    message_send_model.type = type;
    LJMessageFrame * messageFrame_model = [[LJMessageFrame alloc] init];
    messageFrame_model.message = message_send_model;    
    //将创建的对象加入到模型数据集合中
    [self.messageFrames addObject:messageFrame_model];
    //刷新tableView
    [self.messageView reloadData];
}

以上就是QQ聊天UI界面的案例总结,有不足之处欢迎留言,一起学习一起进步!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249

推荐阅读更多精彩内容

  • UITableViewCell控件空间构造 cell的子控件是contentView,contentView的子控...
    CoderZXS阅读 637评论 0 1
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,028评论 1 32
  • scrollView: 介绍scrollView一些属性<1>.要想使用scrollView必须做两件事1).设置...
    騂跃神话阅读 1,523评论 0 0
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,551评论 4 58
  • 幸福是需要修出来的~每天进步1%~幸福实修08班~13-罗洁-余姚# 20170919(93/99) 【幸福三朵玫...
    幸福实修08班罗洁阅读 94评论 0 1