UI022: UITableView实现聊天界面效果

聊天界面效果

自定义Cell+通知:

聊天界面。手写实现自定义Cell。

UIViewController,上半区使用TableView,低部分使用UIView

// CZMessage.h
// 定义枚举类。表示自己、对方
typedef enum {
    CZMessageTypeMe = 0,
    CZMessageTypeOther = 1
} CZMessageType;

@interface CZMessage :NSObject
@property(nonatomic, copy) NSString *text;
@property(nonatomic, copy) NSString *time;
 // 消息发送方(对方发送的,还是自己发送的消息)
@property(nonatomic, assign) CZMessageType type;
// 记录是否需要显示时间label
@property(nonatomic, assign) BOOL hideTime;

- (instancetype)initWithDict:(Dictionary *)dict;
+ (instancetype)messageWithDict:(Dictionary *)dict;

@end

//----------------
// CZMessage.m
@implementation CZMessage
- (instancetype)initWithDict:(Dictionary *)dict
{
    if(self = [super init]) {
        [self setValuesForKeysWithDictionary:dict];
    }
    return self;
}
+ (instancetype)messageWithDict:(Dictionary *)dict
{
    return [[self alloc] initWithDict:dict];
}
@end


// CZMessageFrame.h
#import <Foundation/Foundation.h>
#import <CoreGraphics/CoreGraphics.h>
// 定义宏。
#define textFont [UIFont systemFontOfSize:13] 


@class CZMessage
@interface CZMessageFrame : NSObject
@property(nonatomic, strong) CZMessage *message;
// 时间,头像,正文内容的frame,行高
@property(nanotomic, assign, readonly) CGRect timeFrame;
@property(nanotomic, assign, readonly) CGRect iconFrame;
@property(nanotomic, assign, readonly) CGRect textFrame;
@property(nanotomic, assign, readonly) CGFloat rowHeight;
@end

//----------------
// CZMessageFrame.m
#import "CZMessageFrame.h"
#import <UIKit/UIKit.h>
#import <CZMessage.h>
#import <NSString+CZNSStringExt.h>

@implementation CZMessageFrame
// 重写message的set方法,计算各个控件的frame的值。
- (void)setMessage:(CZMessage *) message
{
    _message = message;
    // 屏幕的宽度
    CGFloat screenW = [UIScreen mainScreen].bounds.size.width;
    CGFloat margin = 5;

    // 计算各控件的frame,行高。时间label
    CGFloat tiemX = 0;
    CGFloat tiemY = 0;
    CGFloat tiemW = screenW;
    CGFloat tiemH = 15;
    if(!message.hiddenTime) { // 要显示时间,才计算frame。
        _timeFrame = CGRectMake(tiemX, tiemY, tiemW, tiemH);
    }
    // 计算头像的frame
    CGFloat iconW = 30;
    CGFloat iconH = 30;
    CGFloat iconY = CGRectGetMaxY(_timeFrame) + margin;
    CGFloat iconX = message.type == CZMessageTypeOther 
        ? margin 
        : screenW -margin - iconW;
    _iconFrame = CGRectMake(iconX, iconY, iconW, iconH);

    // 计算内容文本text的frame,先计算正文的大小。
    // 计算正文的大小,在计算x、y
    CGSize textSize = message.text sizeOfTextWithMaxSize:CGSizeMake(200, MAXFLOAT font:textFont)
    CGFloat textW = textSize.width +40; // 按钮背景变大+40
    CGFloat textH = textSize.height +30; // +30
    CGFloat textY = iconY;
    CGFloat textX = message.type == CZMessageTypeOther 
        ? CGRectGetMaxX(_iconFrame)
        : screenW -margin- iconW - textW;
    _textFrame = CGRectMake(textX, textY, textW, textH);

    // 计算行高。头像与正文的最大Y值比较 + margin
    CGFloat maxY = MAX(CGRectGetMaxY(_textFrame), CGRectGetMaxY(_iconFrame));
    _rowHeight = maxY + margin;
}
@end
给字符串,写1个分类。计算大小。

在Others分组中,新增Categorys分组。
新建--iOS---Source---【Object-C File】--- Next---FileType选Category
---文件名:CZNsstringExt, Class选NSString。Next。
新建好分类:NSString+CZNSStringExt

// NSString+CZNSStringExt.h
#import <Foundation/Foundation.h>
@interface NSString (CZNSStringExt)
// 对象方法,类方法
- (CGSize)sizeOfTextWithMaxSize:(CGSize)maxSize font:(UIFont *)font;

+ (CGSize)sizeWithText:(NSString *)text maxSize:(CGSize)maxSize 
font:(UIFont *)font;
@end

//--------------
// NSString+CZNSStringExt.m
@implementation NSString (CZNSStringExt)
// 对象方法。
- (CGSize)sizeOfTextWithMaxSize:(CGSize)maxSize font:(UIFont *)font
{
    NSDictionary *attrs = @{NSFontAttributeName: font};
    return [self boundingRectWithSize:maxSize 
    options:NSStringDrawingUsesLineFragmentOrigin
    attributes:attrs content:nil].size;
}
// 类方法
+ (CGSize)sizeWithText:(NSString *)text maxSize:(CGSize)maxSize 
font:(UIFont *)font
{
    return [text sizeOfTextWithMaxSize:maxSize font:font];
}
@end

自定义Cell: CZMessageCell


#import <UIKit/UIKit.h>
@class CZMessageFrame;
@interface CZMessageCell : UITableViewCell
// 自定义cell 持有frame模型属性。messageFrame
@property (nanotomic, strong)CZMessageFrame *messageFrame;

// 封装1个创建自定义Cell的方法。
+ (instancetype)messageCellWithTableView:(UITableView *)tableView;
@end

//-----------------
#import "CZMessageCell.h"
#import "CZMessage.h"
#import "CZMessageFrame.h"

@interface CZMessageCell ()
// 引用cell中的控件,在initWithStyle中赋值。
@property (nanotomic, weak) UILabel *lblTime;
@property (nanotomic, weak) UIImageView *imgViewIcon;
@property (nanotomic, weak) UIButton *btnText;

@end


@implementation CZMessageCell
// 重写messageFrame模型的set方法
- (void)setMessageFrame:(CZMessageFrame *)messageFrame 
{
    _messageFrame = messageFrame;
    CZMessage *message = messageFrame.message;
    // 分别设置每个控件的数据,frame信息。
    self.lblTime.text = message.time;
    self.lblTime.frame = messageFrame.timeFrame;
    self.lblTime.hidden = message.hidden;

    NSString *iconImg = message.type == CZMessageTypeMe ? @"me" : @"other";
    self.imgViewIcon.image = [UIImage imageNamed:iconImg];
    self.imgViewIcon.frame = messageFrame.iconFrame;
    // 设置消息正文。
    [self.btnText setTitle:message.text forState:UIControlStateNormal];
    self.btnText.frame = messageFrame.textFrame;
   
    // 设置正文背景图。
    NSString *imgNor, *imgHighlighted;
    if(message.type == CZMessageTypeMe) {
        imgNor = @"chat_send_nor";  
        imgHighlighted = @"chat_send_press_pic";
        // 设置按钮文字为'白色' 
        [self.btnText setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
    } else { // 对方发的
        imgNor = @"chat_receive_nor";  
        imgHighlighted = @"chat_receive_press_pic"; 
        // 设置按钮文字为'黑色' 
        [self.btnText setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
    }
    // 加载图片,设置背景图,背景图的拉伸方式为平铺。
    UIImage *imageNormal = [UIImage imageNamed:imgNor];
    UIImage *imageHighlighted = [UIImage imageNamed:imgHighlighted];
    // 平铺的方式,加载。
    imgNormal = [imageNormal 
        stretchableImgeWidthLeftCapWidth:imageNormal.size.width/2
        topCapHeight:imageNormal.size.height/2];
    imageHighlighted = [imageHighlighted 
        stretchableImgeWidthLeftCapWidth:imageHighlighted.size.width/2
        topCapHeight:imageHighlighted.size.height/2];
    [self.btnText setBackgroundImage:imageNormal forState:UIControlStateNormal];
    [self.btnText setBackgroundImage:imageHighlighted forState:UIControlStateHighlighted];

}



// 重写initWithStyle方法。
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
    if(self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
       // 创建子控件:时间、头像、正文UIButton
       UILabel *lblTime = [[UILabel alloc] init];   
       // 设置字体,居中显示。
       lblTime.font = [UIFont systemFontOfSize:12];
       lblTime.textAlignment = NSTextAlignmentCenter;
       [self.contentView addSubview:lblTime];
       self.lblTime = lblTime;

       UIImageView *imgViewIcon = [[UIImageView alloc] init];   
       [self.contentView addSubview:imgViewIcon];
       self.imgViewIcon = imgViewIcon;

        // 正文按钮,设置文字体大小。
       UIButton *btnText = [[UIButton alloc] init];   
       // 文字大小,颜色,numberOfLines=0可换行。
        btnText.titleLabel.font = textFont;
       [btnText setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
       btnText.titleLabel.numberOfLines = 0; 
       
       // 设置按钮的背景色。按钮的背景放大,
       // btnText.backgoundColor = [UIColor purpleColor];
       // 设置按钮中titleLabel的背景色
       // btnText.titleLabel.backgroundColor = [UIColor greenColor];
       // 设置按钮内容的内边距。同时按钮放大。
       btnText.contentEdgeInsets = UIEdgeInsetsMake(15, 20, 15, 20); 

       [self.contentView addSubview:btnText];
       self.btnText = btnText;
    }
    // 设置单元格颜色clearColor
    self.backgroundColor = [UIColor clearColor];

    return self;
}

// 创建自定义cell的方法
+ (instancetype)messageCellWithTableView:(UITableView *)tableView
{
    static NSString *ID = @"message_cell";
    CZMessageCell *cell = [tableView dequeueReusableCellWIthIdentifier:ID];
    if(cell == nil) {
        cell = [[CZMessageCell alloc] initWithWtyle:UITableViewCellStyleDefault
            reuseIdentifier:ID];    
    }
    return cell;
}

- (void)awakeFromNib {

}

@end

操作步骤:

Main.storyboard 拖入UITableView, 间隔底部44。
底部放入UIView,高度44.作为底部Bar。
设置背景图,拽入UIImageView,设置宽高等于底部Bar,设置背景。
拖入Button,发声音的按钮,设置按钮的图片Image属性。设置background属性,会拉伸图片。
拖入Button,表情按钮。
拖入Button,+号按钮。
拖入文本框,作为聊天内容输入框,设置背景图作为文本框的效果。
设置UITableView的数据源对象,dataSource。dalegate。

// 设置文本框的leftView属性,实现焦点距离左边,有一定间隙。
// 点击输入框,弹出键盘。底部栏平移到键盘的上面。
监听键盘弹出事件。获取键盘的高度,平移。
系统时间监听:用通知来监听。

当键盘弹出的时候,拖动列表,键盘自动隐藏收起。
键盘弹出时候,上部分的列表,滚动到列表的底。
让最后一行内容,滚动到最上面。
键盘的右下角,默认显示send。
storyboard中设置文本框的属性,ReturnKey:选择【Send】。
监听send的点击事件,
通过文本框的代理,监听【Send】的点击事件。
给文本框设置代理(控制器 拖线)

ViewController的代码:

#import "ViewController.h"
#import "Message.h"
#import "MessageFrames.h"
#import "CZMessageCell.h"


@interface ViewController () <UITableViewDataSource, UITableViewDelegate, 
 UITextFieldDelegate>
// 消息回增加,可变数组. 保存消息的Frame模型对象。
@property(nonatomic, strong)NSMutableArray *messageFrames;

@property(weak, nonatomic)IBOutlet UITableViwe *tableView;
// 用户输入的文本框。
@property(weak, nonatomic)IBOutlet UITextField *txtInput;

@end

@implementation ViewController

#pragma mark ------懒加载数据------
- (NSMutableArray *)messageFrames 
{
    if(_messageFrames == nil) {
        NSString *path = [[NSBundle mainBundle] pathForResource:@"messages.plist" ofType:nil];
        NSArray *arrayDict = [NSArray arrayWithContentsOfFile:path];

        NSMutableArray *arrayModels = [NSMutableArray array];
        for(NSDictionary *dict in arrayDict) {
            // 创建数据模型
            CZMessage *model = [CZMessage mesageWithDict:dict];
            // 判断当前item的时间与上一条item的时间,是否一致。一致做个标记。
            // 从arrayModels中,取出最后一条记录。来比较
            CZMessage *lastMessage = (CZMessage *)[[arrayModels lactObject] message];
            if([model.time isEqualToString: lastMessage.time]) {
                model.hideTime = YES;
            }
                 
            // 创建数据模型的frame模型。
            MessageFrames *modelFrame = [[MessageFrames alloc] init];
            modelFrame.message = model;

            [arrayModels addObject:modelFrame];
        }
        _messageFrames = arrayModels;
    }
    return _messageFrames;
}

#pragma mark ------UITableView的代理方法------
- (void)scrollViewWillBeginDraging:(UIScrollView *)scrollView
{
    // 让键盘缩回去。
    [self.view endEditing:YES];
}

#pragma mark ------文本框的代理方法------
// UITextFieldDelegate: textFieldShouldReturn 键盘点击Return键,回调方法。
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
    // 1.获取输入内容
    NSString *input = textField.text;
    // 2.发送用户消息
    [self sendMessage:input withType:CZMessageTypeMe];  
    // 3.发送系统消息 
    [self sendMessage:@"我是机器人" withType:CZMessageTypeOther];
    // 清空文本框
    textField.text = nil; 
    return YES;
}

// 封装发消息的代码
- (void)sendMessage:(NSString *)msg withType:(CZMessageType)type
{
    // 2.创建模型和 数据的frame模型,
    CZMessage *model = [[CZMessage alloc] init];
    NSDate *nowDate = [NSDate date];
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    formatter.dateFormat = @"今天 HH:mm"; // 设置时间格式
    model.time = [formatter stringFromDate:nowDate];
    model.type = type; // 发送方
    model.text = input;

    CZMessageFrame *modelFrame = [[CZMessageFrame alloc] init];
    modelFrame.message = model;
    // 比较与上一条记录,是否要隐藏时间label。
    CZMessageFrame *lastMessageFrame = [self.messageFrames lastObject];
    NSString *lastTime = lastMessageFrame.message.time;
    if([model.time isEqualToString:lastTime]) {
        model.hideTime = YES;
    }
    // 3.frame模型添加到集合中,
    [self.messageFrames addObject:modelFrame];
    // 4.刷新列表
    [self.tableView reloadData];
    // 5.最后一行,滚动到最上面
    NSIndexPath *lastRowIdxPath = [NSIndexPath indexPathForRow:self.messageFrames.count-1  inSection:0];
    [slef.tableView scrollToRowAtIndexPath: lastRowIdxPath
        atScrollPosition:UITableViewScrollPositionTop  animated:YES];
}



#pragma mark ------数据源方法------
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger) section
{
    return self.messageFrames.count;
}
- (UITableVIewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *) indexPath
{
    // 1.获取数据模型
    CZMessageFrame *modelFrame = self.messageFrames[indexPath.row];
    // 2.创建单元格,传入tableView,内部处理复用cell的代码。
    CZMessageCell *cell = [CZMessageCell messageCellWithTableView:tableView]; 

    // 3.把模型设置给单元格对象
    cell.messageFrame = modelFrame;
    // 4.返回单元格
    return cell;
}
// 返回每一行的   行高的方法。
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    CZMessageFrame *messageFrame = self.messageFrames[indexPath.row];
    return messageFrame.rowHeight;
}

#pragma mark ------其他方法------
- (void)viewDidLoad {
    [super viewDidLoad];
    // 取消分割线:
    self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; 
    // 设置TableView背景色, 颜色传入百分比。
    self.tableView.backgroundColor = [UIColor colorWithRed:236/255.0 
    green:236/255.0  blue:236/255.0  alpha:1.0]; 
    // 让cell不允许被选中
    self.tableView.allowsSelection = NO;

    // 设置文本框,最左侧有一定间距。让有自定义背景的输入框的光标,距离左边有间距。
    UIView *leftVw = [[UIView alloc] init];
    leftVw.frame = CGRectMake(0, 0, 5, 1);
    self.txtInput.leftView = leftVw; // 设置leftView
    self.txtInput.leftViewMode = UITextFieldViewModeAlways;

    // 监听键盘的弹出事件。
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    // 监听键盘的弹出事件。UIKeyboardWillChangeFrameNotification
    [center addObserver:self 
        selector:@selector(keyboardWillChangeFrame:) 
        name: UIKeyboardWillChangeFrameNotification
        object: nil];
}

// 监听键盘弹出和收起
- (void)keyboardWillChangeFrame:(NSNotification *)noteInfo 
{
    // 平移动画:处理键盘的Y值。
    // 1.获取键盘的Y值
    CGRect rectEndP = [noteInfo.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectVlaue];
    CGFloat keyboardY = rectEndP.origin.y; // 执行完毕的键盘的Y值。

    CGFloat transformValueY = keyboardY - self.view.frame.size.height;
    [UIView animatedWithDuration:0.25 animations:^{
        // self.view 整体执行向上或向下平移动画
        self.view.transform = CGAffineTransformMakeTranslation(0, transformValueY);
    }];


    // 让UITableView的最后一行,滚动到最上面
    NSIndexPath *lastRowIdxPath = [NSIndexPath indexPathForRow:self.messageFrames.count-1  inSection:0];
    [slef.tableView scrollToRowAtIndexPath: lastRowIdxPath
        atScrollPosition:UITableViewScrollPositionTop  animated:YES];
}
// ******注意:监听通知后,一定要在监听通知的对象的dealloc方法中,移除监听。******
- (void)dealloc
{
    // 移除通知
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

@end


2023/05/31 周三

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

推荐阅读更多精彩内容

  • Enlightening and Awakening Indeed, if kindness and tolera...
    欣悦Xinyue阅读 45评论 0 0
  • 最近一直在看关于形象设计的视频,从发型到穿着,想要打造一个美美的自己。 晚上要和几个新邻居一起吃个饭,第一次见面,...
    江小米米阅读 46评论 0 1
  • 昨天刚提醒员工严格按图面尺寸管控,而且安排老员工去测量尺寸。今天参加玩培训的我,刚进车间,检验人员告诉我,...
    树叶的春天阅读 118评论 1 5
  • 昨晚我哭了很久。因为他说的一些话,我觉得我可以改正,但是有些话很让人难过。 今天早上我很早就起来了依旧不高兴。 他...
    9ae5e31b7ad0阅读 33评论 0 0
  • 王总很久没有给我打电话了,中午吃饭的时候突然间来电,我还以为有什么事呢,结果他说公司开会的时候看了我的周报,知道我...
    春敏阅读 26评论 0 0