在項目之前,最好下載該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);
}];
}