微博阅读器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改为自己对应的):