自定义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 周三