在项目之前,最好下载该App或者GitHub源码跑一下看一下效果,该项目旨在练习UI及网络数据的处理,推荐初学者边参考笔记边进行代码的编写
分析项目,确定 UI 框架
- 导航栏可以左右拖动,可以点击,优先考虑
UICollectionView
,cell重用方面可以减小内存,点击对应的项目的时候字体放大,变为红色
- 新闻信息栏既可以上下拖动来查看该类别不同新闻(显然是UITableView),也可以左右拖动来切换不同新闻频道(UICollectionView),则需要把
N 个tableView 放到一个 collectionView
上
- 数据方面,先获取新闻频道的json数据,根据其tid值获取对应频道的新闻数据网址,网址内包括新闻的图片、标题等信息
一. 新闻频道
WPFChannelView: 承载整个新闻频道的collectionView
WPFChannelCell: 每个新闻频道的collectionViewCell
WPFChannel: 每个新闻频道对象
1. 创建WPFChannel
- 新闻频道名称-->tname
- 新闻频道的标识符,用来加载对应新闻类别的栏目-->tid
- 快速创建方法-->字典转模型
+ (instancetype)channalWithDict:(NSDictionary *)dict {
WPFChannal *channal = [[WPFChannal alloc] init];
[channal setValuesForKeysWithDictionary:dict];
return channal;
}
#warning 当只使用字典中部分键值对的时候,最好加上这个方法
// kvc,防止找不到对应的key值而崩溃
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
// 什么都不用写
}
2. 创建 WPFChannalCell 类,继承UICollectionCell
- 每一个频道cell 都绑定一个WPFChannel对象
@property (nonatomic, strong) WPFChannal *channal;
- 有一个 label 显示对应频道文字
- 重写构造方法的时候实例化label控件
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
// 初始化label
self.lblName = [[UILabel alloc] init];
// 设置文本框字号
self.lblName.font = [UIFont systemFontOfSize:16];
// 设置文本框文字居中
self.lblName.textAlignment = NSTextAlignmentCenter;
// 将label 添加到cell中
[self.contentView addSubview:self.lblName];
}
return self;
}
- 重写 WPFChannel 对象的 set 方法进行数据的传递,通过判断:在选择状态下显示红字大字号,非选择状态下显示黑色普通字号
- (void)setChannal:(WPFChannal *)channal {
_channal = channal;
// 进行名称的赋值
self.lblName.text = channal.tname;
// 如果当前cell 处于被选状态,放大字号(20),红色
if (self.isSelected) {
// 获取当前view 的父view
UICollectionView *collectionView = (UICollectionView *)self.superview;
self.lblName.font = [UIFont systemFontOfSize:20];
self.lblName.textColor = [UIColor redColor];
// 如果不是被选状态,正常字号(16),黑色
} else {
self.lblName.font = [UIFont systemFontOfSize:16];
self.lblName.textColor = [UIColor blackColor];
}
// 在这句代码之后,lblName 才有frame
[self.lblName sizeToFit];
// 改变文本框中心点
self.lblName.center = CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2);
}
sizeToFit方法快速计算label 的长度(也可以通过字数及字号确定,但是略麻烦)
3. 创建 WPFChannalView 类,继承UICollectionView
- 重写其构造方法:除了常规的设置数据源和代理对象,取消滚动条,注册cell,还要在这里进行数据的加载,采用异步+主队列的方式来保证加载完 UI 界面后再进行网络数据的加载
- (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(nonnull UICollectionViewLayout *)layout {
// 首先要执行父类的构造方法
if (self = [super initWithFrame:frame collectionViewLayout:layout]) {
// 设置导航栏背景颜色
self.backgroundColor = [UIColor grayColor];
// 设置数据源对象和代理对象
self.dataSource = self;
self.delegate = self;
// 取消横向滚动条
self.showsHorizontalScrollIndicator = NO;
// 注册cell
[self registerClass:[WPFChannalCell class] forCellWithReuseIdentifier:kIdentifier];
// 异步+主队列:保证执行顺序,在加载完毕UI界面后再加载数据
dispatch_async(dispatch_get_main_queue(), ^{
[self loadServerDataWithUrlString:@"http://localhost/topic_news.json"]; // 自定义方法
});
}
return self;
}
- 加载服务器数据,使用第三方框架(AFNetworking),NSURLSession也可以,在这里将获取到的网络数据转化为模型对象,再放到模型数组中
- (void)loadServerDataWithUrlString:(NSString *)urlString {
// 利用第三方框架请求服务器数据
[[AFHTTPSessionManager manager] GET:urlString parameters:nil progress:^(NSProgress * _Nonnull downloadProgress) {
// 不需要写东西
} success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
#wanring 在这里需要打印一下,确定数据具体形式!!!!
// NSLog(@"responseObject-->%@", responseObject);
/*
打印结果--》是一个字典,里面包含一个名为tList的数组,数组内部是一个个字典,打印头部分如下:
{
tList = (
{
*/
// 接受获取的网络数据
NSDictionary *channalDict = responseObject;
NSArray *channalArray = channalDict[@"tList"];
// 遍历数组
[channalArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
// 数组元素是字典类型
NSDictionary *dict = obj;
// 进行字典转模型
WPFChannal *channal = [WPFChannal channalWithDict:dict];
// 将模型对象添加到模型数组中
// 注意该数组的懒加载
[self.channals addObject:channal];
}];
// 刷新数据,先加载UICollectionViewDelegate,再加载viewDidLoad
[self reloadData];
// 必须有数据之后,选中第一个cell 才有意义
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0];
[self selectItemAtIndexPath:indexPath animated:NO scrollPosition:UICollectionViewScrollPositionNone];
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"channalView: error-->%@", error);
}];
}
注意先打印一遍数据,再根据数据类型的层级关系转化为具体对象
- 动态改变 UIlabel 大小的方法.
// 一旦实现了下面的代理方法, layout.itemSize 就是失效.
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(nonnull NSIndexPath *)indexPath {
// 1. 获取当前行的模型对象的文字
WPFChannal *channal = self.channals[indexPath.row];
NSString *name = channal.tname;
// 2. 返回对应文字的label的尺寸
return [self getLabelSizeWithTname:name];
}
// 获取对应文字的label的尺寸
- (CGSize)getLabelSizeWithTname:(NSString *)name {
UILabel *label = [[UILabel alloc] init];
label.text = name;
label.font = [UIFont systemFontOfSize:16];
[label sizeToFit];
return label.frame.size;
}
- 每组有几个cell,每个cell 具体内容,在这里就不赘述了
二. 具体新闻信息
WPFMainData: 每一条具体新闻的对象
WPFMainTableViewCell: 存放每个新闻的tableViewCell
WPFMainTableView: 存放一个频道所有新闻消息的tableView
WPFMainCollectionViewCell: 存放一个频道的tableView 的collectionViewCell
WPFMainCollectionView: 存放所有频道新闻消息的collectionView
1. 创建 WPFMainData,每个新闻信息对象
-
需要绑定的属性
- title --> 新闻标题
- digest --> 新闻摘要,副标题
- imgsrc --> 新闻配图
- replyCount --> 回帖数(使用NSNumber,如果是null可以识别,NSInteger则不可以,会报错)
快速进行字典转模型的创建,方法同WPFChannel 新闻频道对象的创建
+ (instancetype)mainDataWithDict:(NSDictionary *)dict {
WPFMainData *data = [[WPFMainData alloc] init];
[data setValuesForKeysWithDictionary:dict];
return data;
}
// 有些变量名没有定义,防止崩溃
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
// 什么都不用写
}
2. 创建 WPFMainTableViewCell
重写构造方法中实例化UI控件
重写绑定对象的set方法中进行数据的添加
layoutSubViews 中进行控件frame的布局
- 外部绑定:一个WPFMainData 对象
@property (nonatomic, strong) WPFMainData *data;
- 内部绑定:一个图片框、三个label(标题/副标题/跟帖数)
/** 图片框 */
@property (nonatomic, strong) UIImageView *imgViewIcon;
/** 标题label */
@property (nonatomic, strong) UILabel *lblTitle;
/** 副标题(摘要)label */
@property (nonatomic, strong) UILabel *lblDigest;
/** 跟帖数label */
@property (nonatomic, strong) UILabel *lblReplyCount;
- 重写cell构造方法,实例化UI控件及分割线,注意:重写tableViewCell 的构造方法一定要用 -initWithStyle reuseIdentifier:!!
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
#warning 重写cell 的构造方法一定要用这个!!
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
// 实例化UI控件
self.imgViewIcon = [[UIImageView alloc] init];
self.imgViewIcon.backgroundColor = [UIColor orangeColor];
self.lblDigest = [[UILabel alloc] init];
self.lblDigest.font = [UIFont systemFontOfSize:13];
self.lblDigest.numberOfLines = 2;
self.lblReplyCount = [[UILabel alloc] init];
self.lblReplyCount.backgroundColor = [UIColor lightGrayColor];
self.lblReplyCount.font = [UIFont systemFontOfSize:12];
self.lblTitle = [[UILabel alloc] init];
self.lblTitle.font = [UIFont systemFontOfSize:16];
// 将UI控件添加到当前cell 中
[self.contentView addSubview:self.imgViewIcon];
[self.contentView addSubview:self.lblTitle];
[self.contentView addSubview:self.lblReplyCount];
[self.contentView addSubview:self.lblDigest];
// cell 分割线
UIView *separateLine = [[UIView alloc] initWithFrame:CGRectMake(0, 79, self.frame.size.width, 1)];
separateLine.backgroundColor = [UIColor blackColor];
[self.contentView addSubview:separateLine];
}
return self;
}
- 对控件进行布局,一定在layoutsubviews方法
- (void)layoutSubviews {
// 一定要记得调用父类的该方法
[super layoutSubviews];
CGFloat imgX = 8;
CGFloat imgY = 8;
CGFloat imgW = 100;
CGFloat imgH = 64;
self.imgViewIcon.frame = CGRectMake(imgX, imgY, imgW, imgH);
self.lblTitle.frame = CGRectMake(imgW + 2*imgX, imgY, self.frame.size.width - 3*imgX - imgW, 15);
self.lblDigest.frame = CGRectMake(self.lblTitle.frame.origin.x, CGRectGetMaxY(self.lblTitle.frame) + 3, self.lblTitle.frame.size.width, 40);
// sizeToFit快速得出label实际大小
[self.lblReplyCount sizeToFit];
// 即 label 右下角位置不变
CGFloat replyX = self.frame.size.width - self.lblReplyCount.frame.size.width - imgX;
CGFloat replyY = self.frame.size.height - self.lblReplyCount.frame.size.height - imgY;
self.lblReplyCount.frame = CGRectMake(replyX, replyY, self.lblReplyCount.bounds.size.width, self.lblReplyCount.bounds.size.height);
}
- 重写 data 对象的 set 方法,进行数据的赋值,下载图片需要用到第三方框架(SDWebImage)并导入头文件
UIImageView+WebCache.h
- (void)setData:(WPFMainData *)data {
_data = data;
// 设置数据
self.lblTitle.text = data.title;
self.lblDigest.text = data.digest;
self.lblReplyCount.text = [NSString stringWithFormat:@"回帖数:%@", data.replyCount];
// 自动下载并显示图片
[self.imgViewIcon sd_setImageWithURL:[NSURL URLWithString:data.imgsrc]];
}
3. 创建 WPFMainTableView
- 重写该类 构造方法
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
// 设置数据源和代理对象
self.delegate = self;
self.dataSource = self;
// 注册cell
[self registerClass:[WPFMainTableViewCell class] forCellReuseIdentifier:kMainTableViewCell];
// 隐藏cell 分割线
self.separatorStyle = UITableViewCellSeparatorStyleNone;
}
return self;
}
- 重写channal对象的set方法
- (void)setChannal:(WPFChannal *)channal {
_channal = channal;
// 初始值为10,表示滚动到第十条新闻的时候,开始加载第二十条信息
self.index = 10;
// 清空数据源
[self.newsData removeAllObjects];
// 刷新数据
[self reloadData];
// 根据tid 值获取当前页面的数据
[self getMainDataWithTid:channal.tid];
}
- 根据tid 值获取当前页面的数据
- (void)getMainDataWithTid:(NSString *)tid {
// 数据加载原则:
// 1. 单词加载的数据量能够铺满一个屏幕
// 2. 给用户预留一个屏幕的数据量作为滚动使用
// 小菊花妈妈课堂开课了!
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
// 1. 拼接网址字符串
NSString *urlString = [NSString stringWithFormat:@"http://c.m.163.com/nc/article/headline/%@/0-20.html", tid];
// 2. 发送请求
[[AFHTTPSessionManager manager] GET:urlString parameters:nil progress:^(NSProgress * _Nonnull downloadProgress) {
//
} success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
// NSLog(@"responseObject-->%@", responseObject);
/*
打印结果:返回的整体是一个字典,下面是以tid值为名称的数组,数组内部是一个个字典,则根据其类型进行字典转模型
tid-->T1370583240249
responseObject-->{
T1370583240249 = (
{
*/
// 1. 获取整体的字典
NSDictionary *mainDict = responseObject;
// 2. 获取字典下面的数组
NSArray *mainArray = mainDict[tid];
// 3. 遍历数组元素
[mainArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
// 3.0 转化为正确的类型
NSDictionary *dict = obj;
// 3.1 进行字典转模型
WPFMainData *data = [WPFMainData mainDataWithDict:dict];
// 3.2 将模型添加到模型数组中
[self.newsData addObject:data];
}];
dispatch_async(dispatch_get_main_queue(), ^{
// 小菊花隐藏
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
[self reloadData];
});
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"mainTableView: error-->%@", error);
}];
}
- 每行有几个、每个cell的具体内容、cell单元格高度,在此不一一赘述
3. 创建 WPFMainCollectionViewCell
- 重写其构造方法:实例化 WPFMainTableView,并添加到contentView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
WPFMainTableView *tableView = [[WPFMainTableView alloc] initWithFrame:self.bounds];
self.tableView = tableView;
[self.contentView addSubview:tableView];
}
return self;
}
3. 创建 WPFMainCollectionView
- (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout {
if (self = [super initWithFrame:frame collectionViewLayout:layout]) {
// 注册cell
[self registerClass:[WPFMainCollectionViewCell class] forCellWithReuseIdentifier:kMainCollectionViewCell];
// 设置其代理对象和数据源对象
self.delegate = self;
self.dataSource = self;
// 去向横向滚动条
self.showsHorizontalScrollIndicator = NO;
// 设置翻页效果
self.pagingEnabled = YES;
// 取消弹簧效果
self.bounces = NO;
}
return self;
}
三. 数据的传递
不同类之间进行信息的传递最好用通知
注意添加监听者的代码执行越早越好,一般都是重写类的创建方法的时候就添加了,还有不要忘记在dealloc 方法中移除监听者
1. 显示数据:将channelView 中的channels数组传递给mainCollectionView
- 在WPFChannalView 中加载服务器数据的方法 loadServerDataWithUrlString 中加载数据并成功转化为模型对象后
// 将加载到的新闻数据传递给主界面
[[NSNotificationCenter defaultCenter] postNotificationName:@"NewsChannelDataLoadSuccess" object:self.channals];
- 在WPFMainCollectionView 中重写该类的构建方法中接受通知:接受频道数据加载完毕的通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(loadChannalDataWithNoti:) name:@"NewsChannelDataLoadSuccess" object:nil];
- 在WPFMainCollectionView接受到通知加载完毕后实现的方法,刷新数据后就可以加载数据了
- (void)loadChannalDataWithNoti:(NSNotification *)noti {
self.channals = noti.object;
[self reloadData];
}
2. 实现点击新闻频道,就会切换到对应的新闻信息板块
点击上面的小collectionView,自动切换下面的大collectionView
- 在WPFChannalView 中,cell被选择的方法中进行信息的传递
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
// 1. 获取被选中的cell
WPFChannalCell *cell = (WPFChannalCell *)[collectionView cellForItemAtIndexPath:indexPath];
// 2. 重新设置模型对象,在其set 方法会自动调整文字格式
cell.channal = self.channals[indexPath.row];
// 3. 发送通知,改变新闻控制器
[[NSNotificationCenter defaultCenter] postNotificationName:@"MainCollectionViewChangeToIndexPath" object:indexPath];
}
- 同理:cell 的未被选择方法,调整文字模式恢复为黑色正常字号
- (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath {
// 获取被选中的cell
WPFChannalCell *cell = (WPFChannalCell *)[collectionView cellForItemAtIndexPath:indexPath];
// 重新设置模型对象,在其set 方法会自动调整文字格式
cell.channal = self.channals[indexPath.row];
}
- 在MainCollectionView中,重写该类的构建方法添加监听者
// 接受改变新闻频道的方法
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(changeToIndexPathWithNoti:) name:@"MainCollectionViewChangeToIndexPath" object:nil];
- 在MainCollectionView中,接受到新闻频道改变后执行的方法
- (void)changeToIndexPathWithNoti:(NSNotification *)noti {
// animated: 表面上是是否以动画方式显现
// YES: 滚动经过的所有界面都会被加载
// NO: 只加载最后停留的界面
// 一般为了节省客户的流量,都使用 NO
[self scrollToItemAtIndexPath:noti.object atScrollPosition:UICollectionViewScrollPositionNone animated:NO];
}
3. 拖动新闻信息板块,上面的新闻频道索引也会自动切换
滑动下面的大collectionView,自动切换上面的小collectionView
- 在WPFMainCollectionView 中监听减速方法
// 减速结束的代理方法
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
// 算出页数:注意这里需要手动计算,不能直接获取,因为当前页面大多情况下只被拖动没被选择
// 道理同新闻频道的collectionView,拖动但是没有选择
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:self.contentOffset.x / self.frame.size.width inSection:0];
[[NSNotificationCenter defaultCenter] postNotificationName:@"changeChannalToIndexPath" object:indexPath];
}
- 在WPFChannelView 中重写该类构建方法的时候添加监听者
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(changeToIndexWithNoti:) name:@"changeChannalToIndexPath" object:nil];
- 在WPFChannelView 监听到该消息执行的方法
- UICollectionViewScrollPositionCenteredHorizontally 可以自动将备选的新闻频道跳转到界面中央
- (void)changeToIndexWithNoti:(NSNotification *)noti {
[self reloadData];
// 获取当前页数并选择
[self selectItemAtIndexPath:noti.object animated:YES scrollPosition:UICollectionViewScrollPositionCenteredHorizontally];
}
补充,如果想联系编程数学,加深对collectionView理解的同仁,可以参考下面的代码
- 如果不用UICollectionViewScrollPositionCenteredHorizontally,就在WPFChannelCell的setChannal方法中,如果该cell被选择代码块中,添加以下代码
// 中间部分
if (self.center.x < collectionView.contentSize.width - kWidth/2 && self.center.x > kWidth / 2) {
offsetX = self.center.x - kWidth / 2;
// 前半屏幕
} else if(self.center.x < kWidth/2) {
offsetX = 0;
// 最后半屏幕
} else {
offsetX = collectionView.contentSize.width - kWidth;
}
[UIView animateWithDuration:0.5 animations:^{
collectionView.contentOffset = CGPointMake(offsetX, 0);
}];
4. 加载更多信息
- 添加一个属性
@property (nonatomic, assign) NSInteger index;
- 重写channal对象的set方法的时候对该属性进行初始赋值
// 初始值为10,表示滚动到第十条新闻的时候,开始加载第二十条信息
self.index = 10;
- 在代理方法 tableView: cellForRowAtIndexPath: 即每行的具体内容中
// 如果当前加载的行 = 需要加载数据的行索引,就加载更多数据
if (indexPath.row == self.index) {
// 表示再往下拖十条数据,再次加载
self.index += 10;
[self loadMoreDataWithTid:self.channal.tid startIndex:self.index];
}
- 加载更多数据的方法(可以和第一次加载数据的方法进行合并)
- (void)loadMoreDataWithTid:(NSString *)tid startIndex:(NSInteger)index {
// 小菊花转起来
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
// 1. 拼接网址字符串,%ld-10表示从index 开始往后加载10条数据
NSString *urlString = [NSString stringWithFormat:@"http://c.m.163.com/nc/article/headline/%@/%ld-10.html", tid, index];
// 2. 发送请求
[[AFHTTPSessionManager manager] GET:urlString parameters:nil progress:^(NSProgress * _Nonnull downloadProgress) {
//
} success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
// 1. 获取整体的字典
NSDictionary *mainDict = responseObject;
// 2. 获取字典下面的数组
NSArray *mainArray = mainDict[tid];
// 3. 遍历数组元素
[mainArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
// 3.0 转化为正确的类型
NSDictionary *dict = obj;
// 3.1 进行字典转模型
WPFMainData *data = [WPFMainData mainDataWithDict:dict];
// 3.2 将模型添加到模型数组中
[self.newsData addObject:data];
}];
dispatch_async(dispatch_get_main_queue(), ^{
// 小菊花不转
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
[self reloadData];
});
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"mainTableView: error-->%@", error);
}];
}