效果图

自定义View是一项非常有成就感的实践,如果有Android基础的同学接触起来应该会比较顺手,基本思路都差不多。项目采用原生的布局方式,实现仿微信消息列表效果,主要是学习自定义Cell,展示头像、标题、消息概要、时间和未读数View。下图左边是样例,右边是实现的效果图。

项目结构

项目采用MVC模式,Model处理数据,View提供自定义的Cell和Cell的位置大小,ViewController负责获取数据、创建View和实现UITableView的代理和数据源方法,结构如图所示。

数据源

MsgModel是消息的数据源,包括头像、标题、消息概要、时间和未读数。DataManager负责解析本地的json文件并封装数据,对消息列表数据增删改查操作。

#import "DataManager.h"
#import "MsgModel.h"
#import "MessageItemFrame.h"@implementation DataManager
- (NSMutableArray *)onParseJson
{NSString *jsonStr = nil;NSMutableArray *dataArr = nil;//获取项目json文件路径NSString *filePath = [[NSBundle mainBundle] pathForResource:@"msgdatas" ofType:@"json"];NSFileManager *fileMgr = [NSFileManager defaultManager];//    NSError *error;if ([fileMgr fileExistsAtPath:filePath]) {//可以通过流的形式解析NSInputStream *is = [[NSInputStream alloc] initWithFileAtPath:filePath];[is open];id jsonObj = [NSJSONSerialization JSONObjectWithStream:is options:NSJSONReadingAllowFragments error:nil];[is close];if ([jsonObj isKindOfClass:[NSArray class]]) {dataArr = [[NSMutableArray alloc] init];for (NSDictionary *dict in (NSArray *) jsonObj) {MsgModel *model = [[MsgModel alloc] init];model.msgId = [dict objectForKey:@"_id"];model.pic = [dict objectForKey:@"picture"];model.name = [dict objectForKey:@"name"];model.msg = [dict objectForKey:@"message"];model.time = [dict objectForKey:@"time"];model.unreadCount = [(NSNumber *) [dict objectForKey:@"unreadCount"] integerValue];MessageItemFrame *itemFrame = [[MessageItemFrame alloc] init];itemFrame.msgModel = model;[dataArr addObject:itemFrame];}}} else {NSLog(@"文件不存在!");return nil;}return dataArr;
}
@end

考虑到刚刚接触ISO学习的同学对于Json解析还不是很熟悉,所以补充一下Json解析方法,需要根据不同的数据结构进行解析,核心是通过苹果提供的NSJSONSerialization解析,例如数据是NSDictionary格式:

NSString *jsonArr = @"[{\"_id\":\"5d1edda6710720fb68360a89\",\"name\":\"Daisy Pugh\"},{\"_id\":\"5d1edda6710720fb68360a89\",\"name\":\"DaisyPugh\"}]";
NSData *jsonData = [jsonArr dataUsingEncoding:NSUTF8StringEncoding];
id jsonObj = [NSJSONSerialization JSONObjectWithData:jsonData
options:NSJSONReadingAllowFragments error:nil];
if ([jsonObj isKindOfClass:[NSDictionary class]])
{NSDictionary *dict = (NSDictionary *) jsonObj;NSString *_id = [dict objectForKey:@"_id"];NSString *_pic = [dict objectForKey:@"picture"];NSLog(@"id: %@  picture: %@", _id, _pic);
}

解析JSON文件有很多种方式,下面主要介绍三种:
第一种根据规范的字符串格式:

jsonStr = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
NSData *jsonData = [jsonStr dataUsingEncoding:NSUTF8StringEncoding];
id jsonObj = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingAllowFragments error:nil];

第二种是通过直接NSData的initWithContentsOfFile解析文件:

NSData *jsonData = [[NSData alloc] initWithContentsOfFile:filePath];
id jsonObj = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingAllowFragments error:nil];

第三种是流的形式解析文件:

NSInputStream *is = [[NSInputStream alloc] initWithFileAtPath:filePath];
[is open];
id jsonObj = [NSJSONSerialization JSONObjectWithStream:is options:NSJSONReadingAllowFragments error:nil];
[is close];

View

LoadingView

采用IOS原生的View-UIActivityIndicatorView,通过startAnimating和stopAnimating启动和停止动画。

- (void)initLoadingView
{self.indicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];CGFloat loadingViewWidth = 60;CGFloat loadingViewHeight = 60;CGFloat loadingViewX = (SCREENW / 2) - (loadingViewWidth / 2);CGFloat loadingViewY = (SCREENH / 2) - (loadingViewHeight / 2);self.indicatorView.frame = CGRectMake(loadingViewX, loadingViewY, loadingViewWidth, loadingViewHeight);self.indicatorView.color = [UIColor lightGrayColor];self.indicatorView.hidesWhenStopped = YES;[self.view addSubview:self.indicatorView];
}

WCConversationTableViewCell

通过重写initWithStyle方法创建并初始化显示头像、标题、消息概要、时间和未读数View,提供cellWithTableView创建Cell并调用initWithStyle方法完成子View的创建和初始化。提供setter方法(setMessageItemFrame)设置Cell子View的数据和位置大小。注:由于代码比较长,没有全部展示。

//  WCConversationTableViewCell.h
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@class MessageItemFrame;
@interface WCConversationTableViewCell : UITableViewCell
@property (nonatomic, strong) MessageItemFrame *messageItemFrame;
+ (instancetype)cellWithTableView:(UITableView *)tableView;
@end
NS_ASSUME_NONNULL_END//  WCConversationTableViewCell.m
#import "WCConversationTableViewCell.h"
#import "UIImageView+ImageViewLoader.h"
#import "MsgModel.h"
#import "MessageItemFrame.h"
#import "TimeUtils.h"
@interface WCConversationTableViewCell ()
@end
@implementation WCConversationTableViewCell
+ (instancetype)cellWithTableView:(UITableView *)tableView
{static NSString *cellId = @"cellID";//到缓存池中找cell,没有再创建并加入缓存池WCConversationTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellId];if (cell == nil) {cell = [[WCConversationTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellId];}return cell;
}
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];if (self) {UIImageView *avatarView = [[UIImageView alloc] init];avatarView.layer.cornerRadius = 4;[self.contentView addSubview:avatarView];self.avatarView = avatarView;UILabel *titleLabel = [[UILabel alloc] init];titleLabel.font = [UIFont boldSystemFontOfSize:16];titleLabel.numberOfLines = 1;[self.contentView addSubview:titleLabel];self.titleLabel = titleLabel;...}return self;
}
/**初始化数据和布局@param messageItemFrame 位置数据*/
- (void)setMessageItemFrame:(MessageItemFrame *)messageItemFrame
{_messageItemFrame = messageItemFrame;[self setItemViewData];[self setItemViewFrame];
}
- (void)setItemViewData
{MsgModel *model = self.messageItemFrame.msgModel;if (model.pic) {[self.avatarView setOnlineImage:model.pic placeholderImage:[UIImage imageNamed:@"icon_touxiang_non"]];}self.titleLabel.text = model.name;self.msgLabel.text = model.msg;self.timeLabel.text = [TimeUtils getSessionTimePretty:model.time];if (model.unreadCount <= 0) {self.unReadCountLable.hidden = YES;} else {self.unReadCountLable.hidden = NO;if (model.unreadCount > 99) {self.unReadCountLable.text = @"99+";} else {self.unReadCountLable.text = [NSString stringWithFormat:@"%i", model.unreadCount];}}
}
- (void)setItemViewFrame
{self.avatarView.frame = self.messageItemFrame.iconF;self.titleLabel.frame = self.messageItemFrame.nameF;self.msgLabel.frame = self.messageItemFrame.msgF;self.timeLabel.frame = self.messageItemFrame.timeF;self.unReadCountLable.frame = self.messageItemFrame.unReadCountF;
}
@end

MessageItemFrame

为了方便管理Cell的布局,所以抽了一个类负责计算Cell子View的位置和大小,思路是先计算头像的frame,然后根据头像作为参考点,以此布局其他的View。

@implementation MessageItemFrame
- (void)setMsgModel:(MsgModel *)msgModel
{_msgModel = msgModel;// 间隙CGFloat padding = 10;// 设置头像的frameCGFloat iconViewX = padding;CGFloat iconViewY = padding;CGFloat iconViewW = 40;CGFloat iconViewH = 40;self.iconF = CGRectMake(iconViewX, iconViewY, iconViewW, iconViewH);//标题CGFloat titleLabelX = CGRectGetMaxX(self.iconF) + padding;//根据文字的长度和字体计算view的宽度和高度CGSize titleSize = [self sizeWithString:_msgModel.time font:[UIFont systemFontOfSize:14] maxSize:CGSizeMake(250, MAXFLOAT)];self.nameF = CGRectMake(titleLabelX, padding, 150, titleSize.height);CGSize timeSize = [self sizeWithString:[TimeUtils getSessionTimePretty:_msgModel.time] font:[UIFont systemFontOfSize:12] maxSize:CGSizeMake(250, MAXFLOAT)];//时间CGFloat timeViewY = padding;CGFloat timeLabelH = timeSize.height;CGFloat timeLabelW = timeSize.width;CGFloat timeViewX = SCREENW - timeLabelW - padding;self.timeF = CGRectMake(timeViewX, timeViewY, timeLabelW, timeLabelH);//消息摘要CGFloat textLabelY = titleSize.height + 1.5 * padding;CGSize textSize = [self sizeWithString:_msgModel.msg font:NJTextFont maxSize:CGSizeMake([[UIScreen mainScreen] bounds].size.width, 30)];CGFloat textLabelW = textSize.width - 4 * padding;CGFloat textLabelH = textSize.height;self.msgF = CGRectMake(titleLabelX, textLabelY, textLabelW, textLabelH);//设置红点CGFloat unReadCountW = 16;CGFloat unReadCountH = 16;CGFloat unReadCountX = titleLabelX - padding - unReadCountW / 2;CGFloat unReadCountY = timeViewY - unReadCountH / 2;self.unReadCountF = CGRectMake(unReadCountX, unReadCountY, unReadCountW, unReadCountH);//cell行高self.cellHeight = CGRectGetMaxY(self.iconF) + padding;
}
/**根据文字测量大小@param string 数据@param font 字体@param maxSize 限定宽高@return 测量宽高大小*/
- (CGSize)sizeWithString:(NSString *)string font:(UIFont *)font maxSize:(CGSize)maxSize
{NSDictionary *dict = @{NSFontAttributeName: font};// 如果将来计算的文字的范围超出了指定的范围,返回的就是指定的范围// 如果将来计算的文字的范围小于指定的范围, 返回的就是真实的范围CGSize size = [string boundingRectWithSize:maxSize options:NSStringDrawingUsesLineFragmentOrigin attributes:dict context:nil].size;return size;
}
@end

Controller

ViewController承担Controller的工作,负责通过DataManager获取数据并创建和初始化UITableView并实现数据源方法numberOfRowsInSection和代理方法heightForRowAtIndexPath。

- (void)viewDidLoad
{[super viewDidLoad];[self initLoadingView];[self.indicatorView startAnimating];DataManager *dataMgr = [[DataManager alloc] init];self.data = [dataMgr onParseJson];_tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];_tableView.dataSource = self;_tableView.delegate = self;[self.indicatorView stopAnimating];[self.view addSubview:_tableView];
}- (void)initLoadingView
{self.indicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];CGFloat loadingViewWidth = 60;CGFloat loadingViewHeight = 60;CGFloat loadingViewX = (SCREENW / 2) - (loadingViewWidth / 2);CGFloat loadingViewY = (SCREENH / 2) - (loadingViewHeight / 2);self.indicatorView.frame = CGRectMake(loadingViewX, loadingViewY, loadingViewWidth, loadingViewHeight);self.indicatorView.color = [UIColor lightGrayColor];self.indicatorView.hidesWhenStopped = YES;[self.view addSubview:self.indicatorView];
}#pragma mark - 数据源方法
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{return self.data.count;
}- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{WCConversationTableViewCell *cell = [WCConversationTableViewCell cellWithTableView:tableView];cell.messageItemFrame = self.data[indexPath.row];return cell;
}#pragma mark - 代理方法
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{MessageItemFrame *itemF = self.data[indexPath.row];return itemF.cellHeight;
}- (BOOL)prefersStatusBarHidden
{return YES;
}

优化

图片缓存

消息列表每条Cell都要展示图片,而且图片是从网络中下载,如果在主线程加载大量数据和网络请求必定会耗时阻塞主线程并可能导致界面卡顿,所以需要实现图片异步加载和二级缓存。二级缓存分别是内存缓存和磁盘缓存,使用NSData dataWithContentsOfURL的方式请求下载图片没有网络缓存。后续有时间可以通过NSUrlSession的方式设定网络缓存加载图片,实现三级缓存。下面说下实现思路:
ImageCacheQueue通过维护一个NSMutableDictionary进行图片内存缓存,设定的缓存大小是50张图片,注意不推荐这样做,一般情况下是限定存储大小,可以通过NSCache,常用的SDWebImage使用NSCache实现缓存,提供了自动删除策略并且操作是线程安全,本文是希望对于新人可以多练习写代码能力,所以不采用NSCache。如下是内存缓存核心代码:

#pragma mark - 内存缓存图片,最多缓存50条,采用FIFO算法
- (void)cacheImageToMemory:(NSDictionary *)info
{if (memoryCache.count >= 50) {[memoryCache removeObjectForKey:[memoryCache.allKeys objectAtIndex:0]];}[memoryCache setObject:[info objectForKey:@"image"] forKey:[info objectForKey:@"key"]];
}

磁盘缓存是通过UIImageJPEGRepresentation将图片转成NSData,以图片链接的MD5值作为存储路径写入沙盒文件,核心代码如下:

#pragma mark - 磁盘缓存
- (void)cacheImageToDisk:(NSDictionary *)info
{NSString *key = [info objectForKey:@"key"];UIImage *image = [info objectForKey:@"image"];NSString *localPath = [diskCachePath stringByAppendingPathComponent:[key MD5ForLower32Bate]];NSData *localData = UIImageJPEGRepresentation(image, 1.0f);if ([localData length] <= 1) {return;}if (![[NSFileManager defaultManager] fileExistsAtPath:localPath]) {[[NSFileManager defaultManager] createFileAtPath:localPath contents:localData attributes:nil];}
}

加载图片是先判断内存是否有缓存,没有则去磁盘查找是否有缓存,都没有再通过网络请求下载图片,图片下载成功就加入内存缓存,然后写入磁盘。

- (UIImage *)getImageFromDiskByKey:(NSString *)key
{NSString *localPath = [diskCachePath stringByAppendingPathComponent:[key MD5ForLower32Bate]];if (![[NSFileManager defaultManager] fileExistsAtPath:localPath]) {return nil;}UIImage *image = [[UIImage alloc] initWithContentsOfFile:localPath];if (nil != image) {NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys:image, @"image", key, @"key", nil];[self performSelector:@selector(cacheImageToMemory:) withObject:info];return image;}return nil;
}

笔者对ImageCacheQueue进行封装,业务调用很简单,仅仅是如下一行代码即可:

[self.avatarView setOnlineImage:model.pic placeholderImage:[UIImage imageNamed:@"icon_touxiang_non"] withRow:@(self.row)];

图片加载错乱

因为UITableViewCell复用机制,所以只有创建了一屏的Cell,当移出屏幕的Cell会被循环使用,而图片是异步加载的,当第个Cell发起图片异步下载请求或者下载到某个进度了,突然移出屏幕,这会是第二Cell,第一个Cell这会如果图片也下载成功了,那么第二个Cell首先会展示第一个Cell的图片,在性能或者体验效果方面非常不好,因此需要解决这个问题。
解决方法: 首先在创建Cell之后为子控件UIImageView设置tag标识,tag的值简单为当前cell的行数,当然不推荐这样做,应该设定一个唯一值消息列表的ID或者其他,上述是为了简便操作。

+ (instancetype)cellWithTableView:(UITableView *)tableView withRow:(NSInteger)row
{...cell.avatarView.tag = row;return cell;
}

然后是为Cell绑定图片数据的时候异步请求带上当前Cell的行数,

- (void)setItemViewData
{MsgModel *model = self.messageItemFrame.msgModel;if (model.pic) {[self.avatarView setOnlineImage:model.pic placeholderImage:[UIImage imageNamed:@"icon_touxiang_non"] withRow:@(self.row)];}
}

在异步下载图片结果返回时比较UIImageView的tag和行数一致才加载图片即可。

- (void)imageDownloader:(AsyncImageDownLoader *)downloader onSuccessWithImage:(UIImage *)image withRow:(NSInteger)row
{WeakSelf;dispatch_async(dispatch_get_main_queue(), ^{StrongSelf;if (self.tag == row) {self.image = image;}});
}

block内存泄漏

图片下载完成回调的方法需要通过block子线程到主线程的通信,使用block容易出现循环引用的问题,传统且优雅的方法就是通过weakSelf、strongSelf结合使用。

#define WeakSelf __weak typeof(self) weakSelf = self
#define StrongSelf __strong typeof(weakSelf) self = weakSelf
- (void)imageDownloader:(AsyncImageDownLoader *)downloader onSuccessWithImage:(UIImage *)image withRow:(NSInteger)row
{WeakSelf;dispatch_async(dispatch_get_main_queue(), ^{StrongSelf;NSLog(@"tag main %d,roe is %d", (int) self.tag, (int) row);//标识cell加载的图片位置是否正确,解决图片混乱问题if (self.tag == row) {self.image = image;}});
}

总结

总体而言是实现了效果还是可以的,不足的地方还有很多,有很大的优化空间,比如下载图片任务管理情况、解析时间耗时等情况,这篇文章demo适合新手练练手,熟悉UITableView的使用和图片异步加载等,工作繁忙还没有时间将代码放到GitHub,后续有空了会上传并补充链接,如果有疑惑的同学可以先评论留言,笔者会尽快回复。如果OC语言基础还不是很熟悉的同学推荐看这篇文章–看一篇就够入门Objective-C,干货满满。

手把手教学IOS自定义cell-仿微信消息列表相关推荐

  1. iOS Swift 高仿微信

    LXFWeChat Swift 3.0 高仿微信 两个测试账号: lxf lqr  密码都是123456 源码地址 码云 http://git.oschina.net/LinXunFeng/LXFWe ...

  2. 【uniapp前端组件】仿微信通讯录列表组件

    仿微信通讯录列表组件 示例图 前言 仿微信通讯录列表组件,可实现通讯列表以及选择多个联系人功能. 组件介绍 本组件有三个自定义组件构成,都已经集成在bugking7-contact-list中,该组件 ...

  3. iOS——自定义cell

    iOS--自定义cell 在写自定义cell怎么实现之前,先来看一下自定义cell的作用和用法,这一点远远比怎么实现有用的多,在进行了两天的网易云仿写后,才发现自己对自定义cell的理解完全是错的,按 ...

  4. android 微信朋友圈 全功能,Android仿微信朋友圈文字展开全文功能 Android自定义TextView仿微信朋友圈文字展开全文功能...

    Android自定义TextView仿微信朋友圈文字信息,展开全文功能 代码及注释如下: 首先写一个xml文件 showmore.xml: android:orientation="vert ...

  5. android的实现关注好友功能,android仿微信好友列表功能

    android studio实现微信好友列表功能,注意有一个jar包我没有放上来,请大家到MainActivity中的那个网址里面下载即可,然后把pinyin4j-2.5.0.jar复制粘贴到项目的a ...

  6. android 微信朋友圈 全功能,Android自定义TextView仿微信朋友圈文字展开全文功能

    Android自定义TextView仿微信朋友圈文字信息,展开全文功能 代码及注释如下: 首先写一个xml文件 showmore.xml: android:orientation="vert ...

  7. iOS - Swift 高仿微信

    LXFWeChat Swift 3.0 高仿微信 源码地址 码云 https://git.oschina.net/coderlxf/LXFWeChat GitHub https://github.co ...

  8. android仿微信点击好友,安卓开发仿微信联系人列表-机器人列表视图仿微通道聊天多久最底部滑动...

    楼主你好!根据你的描述,让我给你答案! :新内容加进来,列表视图重新为setSelection后,定位结束后,拉起一个页面放. . 希望你能有所帮助,如果满意,请记得采纳像下拉条为微信好友如何实现 简 ...

  9. android旋转不重绘,Android自定义view仿微信刷新旋转小风车

    本文实例为大家分享了Android仿微信刷新旋转小风车 具体代码,供大家参考,具体内容如下 不太会录像,没办法,智能截图了 不多说了,直接上代码 package com.shipneg.demoysp ...

最新文章

  1. 蓝桥杯 【基础练习】 十六进制转八进制
  2. Node.js建站笔记-使用react和react-router取代Backbone
  3. 把图片保存到数据库的实现
  4. 第十六节: EF的CodeFirst模式通过Fluent API修改默认协定
  5. ImPan免费版 百度云网盘第三方不限速下载工具
  6. wenbao与最短路(Floyd)
  7. 单片机c语言曲普两只蝴蝶,51曲谱网_51单片机简谱编码
  8. AI新星丨普林斯顿陈丹琦
  9. 从oracle到mysql模型转换的自动化实现
  10. 获取计算机主机mac地址的命令有,Mac系统获取远程电脑MAC地址的两种简单方法
  11. android拷机工具,Android 3DMark大更新:无敌拷机神器
  12. RINEX 3.02 版本导航文件格式说明
  13. 华为云GaussDB(for Redis)GaussDB(for Redis)全面对比Codis
  14. zxr10交换机配置手册vlan_中兴ZXR10配置说明
  15. android系统底层的updater 命令,Android ROM 刷机脚本 updater-script 的基本流程和初级语句...
  16. 记一次Electron+vue实现动态打印小票
  17. 中国最强AI超级服务器问世,每秒提供AI计算2000万亿次
  18. 送快递的,收快递的电话
  19. Arduino Cloud 现已支持乐鑫 ESP32-S2、S3 和 C3
  20. 纯电动汽车Matlab/Simulink软件模型,纯电动汽车动力性、经济性仿真模型

热门文章

  1. 维修RC-DS10K东芝IH电饭煲-温度保险丝断开了-维修笔记
  2. 月亮与六便士,诗与远方
  3. eplan连接定义点不显示_最新电气绘图软件EPLAN,附超详细安装教程
  4. fisher算法 matlab实现(分类线、投影方向、取点)
  5. 怎样把c语言软件卸载干净,系统软件怎样操作才能彻底卸载删除干净软件程序...
  6. suse enterprise linux 10 安装及配置svn(使用svnserve)
  7. 建筑艺术与数据科技完美融合 全球最美的十大数据中心
  8. FBEC2020 | 最后1天,第五届金陀螺奖参评报名明日截止!
  9. PaddleX助力无人驾驶:基于YOLOv3的车辆检测和车道线分割实战
  10. 计算机软件流控制com,电脑控(com.pw.pccontrol) - 2.7.1 - 应用 - 酷安