微博閱讀器demo實現的第二部分: 微博列表,這也是本demo的主要部分。
微博列表通過一個UITableView來實現,所要面臨的問題有如下幾個:
1.調用開放平臺的微博API,獲取json數據並解析;
2.UITableViewCell的佈局設計;
3.下拉刷新和上拉加載更多。
實現的效果如下:
一、調用開放平臺的微博API,獲取json數據並解析
本demo主要調用https://api.weibo.com/2/statuses/friends_timeline.json 這個API獲取當前登錄用戶及其所關注用戶的最新微博,
查閱api的介紹,這個接口採用get方法,參數如下:
必選 | 類型及範圍 | 說明 | |
---|---|---|---|
source | false | string | 採用OAuth授權方式不需要此參數,其他授權方式爲必填參數,數值爲應用的AppKey。 |
access_token | false | string | 採用OAuth授權方式爲必填參數,其他授權方式不需要此參數,OAuth授權後獲得。 |
since_id | false | int64 | 若指定此參數,則返回ID比since_id大的微博(即比since_id時間晚的微博),默認爲0。 |
max_id | false | int64 | 若指定此參數,則返回ID小於或等於max_id的微博,默認爲0。 |
count | false | int | 單頁返回的記錄條數,最大不超過100,默認爲20。 |
page | false | int | 返回結果的頁碼,默認爲1。 |
base_app | false | int | 是否只獲取當前應用的數據。0爲否(所有數據),1爲是(僅當前應用),默認爲0。 |
feature | false | int | 過濾類型ID,0:全部、1:原創、2:圖片、3:視頻、4:音樂,默認爲0。 |
trim_user | false | int | 返回值中user字段開關,0:返回完整user字段、1:user字段僅返回user_id,默認爲0。 |
我們只要指定access_token和page這兩個參數,其他默認。count默認爲20,也就是說我們一次能獲得20條微博。爲了不阻塞主線程,
訪問網絡的操作都採用GCD,
訪問成功之後得到NSData,並對其解析。
說到這裏,我要先介紹一下我的微博類(WBStatuses)的模型
#import <Foundation/Foundation.h>
#import "WBRetweetedStatus.h"
@interface WBStatuses : NSObject
@property NSString * profile_image; // 微博頭像地址
@property NSString * userName; // 用戶名
@property NSString * from; // 微博來源
@property NSString * text; // 微博正文
@property NSString * idstr; // 微博id
@property NSString * createAt; // 微博創建時間
@property NSMutableArray * thumbnailPictureUrls; // 縮略圖地址
@property NSMutableArray * bmiddlePictureUrls; // 中等大小圖片地址
@property NSMutableArray * largePictureUrls; // 原圖地址
@property WBRetweetedStatus * retweetedStatus; // 轉發的微博
@property bool hasRetweetedStatuses; // 是否含有轉發微博
@end
針對微博API所返回的json數據的格式,解析如下:
#import "WBStatusesImpl.h"
@implementation WBStatusesImpl
-(NSMutableArray *) httpRequestWithPage:(int) page
{
HttpHelper * httpHelper = [[HttpHelper alloc]init];
UrlHelper * urlHelper = [[UrlHelper alloc]init];
NSString * url = [NSString stringWithFormat:[urlHelper urlForKey:@"friends_timeline"],[urlHelper accessToken],page]; // 獲取完整地址
NSLog(@"%@",url);
NSData * data = [httpHelper SynchronousGetWithUrl:url]; // get 方法訪問網絡
NSMutableArray * statuses = [[NSMutableArray alloc]init]; // 存放20條微博的數組
if(data) // 獲取數據成功
{
NSDictionary * dic =[NSJSONSerialization JSONObjectWithData:data
options:NSJSONReadingMutableLeaves error:nil];
NSArray * statusesArray =[dic objectForKey:@"statuses"]; // 講NSData解析成數組
for(NSDictionary * d in statusesArray) // 循環解析每條微博
{
NSString * tem;
NSArray * urlTem;
NSDictionary * user = [d objectForKey:@"user"]; // 獲得用戶信息
NSDictionary * retweetedStatus = [d objectForKey:@"retweeted_status"]; // 轉發的微博
WBStatuses * wbstatuses = [[WBStatuses alloc]init];
wbstatuses.profile_image = [user objectForKey:@"profile_image_url"];
wbstatuses.userName =[user objectForKey:@"name"];
wbstatuses.text=[d objectForKey:@"text"];
wbstatuses.idstr = [d objectForKey:@"idstr"];
wbstatuses.createAt = [d objectForKey:@"created_at"];
wbstatuses.createAt = [DateHelper timePassedSinceDateString:wbstatuses.createAt]; // 計算髮微博的時間到現在的時間間隔
if(retweetedStatus!=nil) //解析轉發微博,方法與微博一致
{
wbstatuses.hasRetweetedStatuses = true;
wbstatuses.retweetedStatus.text = [retweetedStatus objectForKey:@"text"];
NSDictionary * retweetedUser = [retweetedStatus objectForKey:@"user"];
wbstatuses.retweetedStatus.userName = [retweetedUser objectForKey:@"name"];
wbstatuses.retweetedStatus.createAt = [retweetedStatus objectForKey:@"created_at"];
urlTem =[d objectForKey:@"pic_urls"];
for(NSDictionary * urlDic in urlTem)
{
NSString * str =[urlDic objectForKey:@"thumbnail_pic"];
[wbstatuses.retweetedStatus.thumbnailPictureUrls addObject:str];
[wbstatuses.retweetedStatus.bmiddlePictureUrls addObject:[str stringByReplacingOccurrencesOfString:@"sinaimg.cn/thumbnail/" withString:@"sinaimg.cn/bmiddle/"]];
[wbstatuses.retweetedStatus.largePictureUrls addObject:[str stringByReplacingOccurrencesOfString:@"sinaimg.cn/thumbnail/" withString:@"sinaimg.cn/large/"]];
// NSLog(@"%@",str);
}
}
urlTem =[d objectForKey:@"pic_urls"];
for(NSDictionary * urlDic in urlTem) // 解析微博圖片地址,縮略圖與其他圖地址只是前面部分不同
{
NSString * str =[urlDic objectForKey:@"thumbnail_pic"];
[wbstatuses.thumbnailPictureUrls addObject:str];
[wbstatuses.bmiddlePictureUrls addObject:[str stringByReplacingOccurrencesOfString:@"sinaimg.cn/thumbnail/" withString:@"sinaimg.cn/bmiddle/"]];
[wbstatuses.largePictureUrls addObject:[str stringByReplacingOccurrencesOfString:@"sinaimg.cn/thumbnail/" withString:@"sinaimg.cn/large/"]];
}
tem = [d objectForKey:@"source"];
wbstatuses.from=@"來自:";
tem = [[[tem substringToIndex:[tem length]-4] componentsSeparatedByString:@">"] lastObject];
wbstatuses.from = [wbstatuses.from stringByAppendingString:tem];
[statuses addObject:wbstatuses];
}
return statuses;
}
return nil;
}
@end
二、UITableViewCell的佈局設計
很明顯,cell需要採用自定義的模式。觀察發現每個微博cell就只有頭像,用戶名,時間,來源的位置和大小相對固定,可以在storyboard中先確定。
微博正文高度不定,圖片個數不定,需要動態加載。同時圖片的獲取需要採用GCD。
首先,每個cell的高度都不同,要在微博列表對應的控制類中實現UITableView的委託方法
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath 來確定每個cell的高度,這其中有一些要注意
的在我另一篇博客(UITableView中heightForRowAtIndexPath 產生 EXC_BAD_ACCESS 的原因)說明。此外,因爲UITableViewCell的重用機制,並且
每個cell都動態添加圖片和正文,這將導致cell中的內容錯位(即上一個cell的內容出現在下一個cell中)如下圖所示:
解決的辦法是給每個動態加載的圖片設置一個標記然後在
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath中遍歷cell的子視圖,
若爲動態加載的就將其remove,然後再重新加載新的子視圖:
static NSString *CellIdentifier = @"WBStatusesCell";
WBStatusesCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath]; // 重用
for(UIView * v in [cell subviews]) // 遍歷子視圖
{
if(v.tag == 1) // 標記爲1,是動態加載的,將其remove
{
[v removeFromSuperview];
}
}
WBStatuses *statuses = [self.statuses objectAtIndex:indexPath.row]; // 指定位置的微博數據
[cell contentWithWBStatuses:statuses]; // 重新加載
還有就是圖片的下載,因爲不能每一次加載都重新下載圖片,所以,下載過的圖片就存放在一個NSDictionary中以圖片地址作爲鍵值,
重新加載的時候,先查看NSDictionary中有沒有對應的圖片,有就直接用,沒有再下載:
UIImage *image = [self.imageDictionary objectForKey:statuses.profile_image];
cell.profile_image.image = image;
if(image==nil)
{
cell.profile_image.image = nil;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0),^{
cell.profile_image.image =[UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:statuses.profile_image]]];
[self.imageDictionary setValue:cell.profile_image.image forKey:statuses.profile_image];
});
}
三、下拉刷新和上拉加載更多
下拉刷新採用ios自帶的UIRefreshControl,UIScrollerView及其子類都有一個refreshControl屬性用於下拉刷新,只需在viewDidLoad中對refreshControl
進行設置即可:
self.refreshControl = [[UIRefreshControl alloc]init];
self.refreshControl.attributedTitle = [[NSAttributedString alloc]initWithString:@"下拉刷新"];
[self.refreshControl addTarget:self action:@selector(Refresh) forControlEvents:UIControlEventValueChanged];
將要執行的代碼放在Refresh方法中
-(void)Refresh
{
[self.refreshControl beginRefreshing];
self.refreshControl.attributedTitle = [[NSAttributedString alloc] initWithString:@"加載中..."];
[self performSelector:@selector(loadData) withObject:nil afterDelay:1.0f];
}
-(void)loadData
{
if (self.refreshControl.refreshing == true)
{
[self performHttpRequestWithPage:1];
}
}
加載完成後,修改refreshControl的屬性:
if(page==1&&self.refreshControl.refreshing)
{
[self.refreshControl endRefreshing];
self.refreshControl.attributedTitle = [[NSAttributedString alloc] initWithString:@"下拉刷新"];
}
上拉加載更多其實就是拉到最底部顯示一個按鈕,點擊按鈕就加載更多內容:
因爲每個cell的高度不固定,所以整個tableView的contentSize也就不固定,加載按鈕的位置也要動態變化。每次有新的cell加入時contenSize的高度就改變,
按鈕的位置(frame)就要隨之改變,相反,contenSize的高度不變,按鈕的位置就不應該變化。所以,我們通過tableView的tag來控制按鈕的位置是否需要改變。
viewDidLoad中設置並添加按鈕:
UIButton * btn =[[UIButton alloc]init];
[btn addTarget:self action:@selector(loadMore) forControlEvents:UIControlEventTouchDown];
[btn setTitle:@"加載更多" forState:UIControlStateNormal];
[btn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
self.loadMoreButton = btn;
[self.tableView addSubview:self.loadMoreButton];
self.tableView.tag = NEED_READD_BUTTON;
UITableView也是一個UIScrollView,可以實現方法- (void)scrollViewDidScroll:(UIScrollView *)scrollView判斷是否滑到底部:
- (void) scrollViewDidScroll:(UIScrollView *)scrollView
{
CGPoint contentOffsetPoint = self.tableView.contentOffset; // 偏移點
CGRect frame = self.tableView.frame;
if (contentOffsetPoint.y == self.tableView.contentSize.height - frame.size.height) // 滑到了底部
{
if(self.tableView.tag == NEED_READD_BUTTON) // 是否重新改變按鈕位置
{
self.tableView.contentSize = CGSizeMake(self.tableView.contentSize.width, self.tableView.contentSize.height+40); // 增加contenSize的高度,用於放置按鈕
self.loadMoreButton.frame = CGRectMake(0,self.tableView.contentSize.height-40,self.tableView.contentSize.width,40); // 改變按鈕的位置
self.tableView.tag = NOT_READD_BUTTON; // 將tableView 標記改爲不需改變
}
}
}
數據加載完成,回到主線程,改變tableView的標記,將其隱藏,並reloadData:
dispatch_async(dispatch_get_main_queue(), ^{
self.tableView.tag = NEED_READD_BUTTON;
self.loadMoreButton.frame = CGRectMake(0, 0, 0, 0);
[self.tableView reloadData];
});
微博閱讀器demo的主要部分就完成了,下面是我的源碼(注意將WBForWang-Prefix.pch文件中的REDIRECT_URI、 APP_KEY以及APP_SECRECT改爲自己對應的):