上节课我们已经用request token 换取到了access token,但是经过验证我们发现,每一次输入一样的账号和密码后,获取的access token 都是一样的,也就是我们不是每次都需要获取access token。所以今天我们就讲解下如何存储access token 以及如何用access token 获得打开存储微博数据的神秘盒子。
本节主要内容:
- 存储access token
- 授权界面的业务逻辑
- 获取微博数据
- 解决数据固定问题
- 添加刷新控件
本节资料
7.1 存储Access Token
到底access token 有什么意义呢?其实它保证了一个应用有权利获取一个用户的数据。因为access token 是和用户绑定在一起的,所以在相同的应用内,输入相同的用户,access token 也是固定的。我们不必每次都向服务器请求一个access token,只需要把其存储起来即可,这里稍微经过思考就知道,可以把获取到的含有access token 的信息转换为模型再存储,这也是我们第一次采用面向模型的思想。
首先依据上节课最后打印出的json数据创建一个YGAccount模型:
/*
"access_token" = "2.00tpZG4Guufa6C308aae9678ElqtPC";
"expires_in" = 157679999;
"remind_in" = 157679999;
uid = 5838220445;
*/
/**
* 获取数据的访问命令牌
*/
@property (nonatomic, copy) NSString *access_token;
/**
* 账号的有效期
*/
@property (nonatomic, copy) NSString *expires_in;
/**
* 用户唯一标识符
*/
@property (nonatomic, copy) NSString *uid;
/**
* 过期时间 = 当前保存时间+有效期
*/
@property (nonatomic, strong) NSDate *expires_date;
/**
* 账号的有效期
*/
@property (nonatomic, copy) NSString *remind_in;
+ (instancetype)accountWithDict:(NSDictionary *)dict;
在YGAccount的实现方法里,就要用到KVC赋值和归档解档了,KVC底层的实现原理就是遍历字典中所有的key,一个个获取到key后,就会去模型里查找有没有setKey方法,如果有就直接调用并赋值。如果没有setKey,就会寻找有没有带下划线的key,如果有就直接拿到并赋值,如果还没有找到,就查找有没有Key的属性,如果有就赋值,最后如果在模型里找不到对应的Key就会报错。 归档解档就需要模型遵守NSCoding协议,代码如下:
#define YGAccountTokenKey @"token"
#define YGUidKey @"uid"
#define YGExpires_inKey @"exoires"
#define YGExpires_dateKey @"date"
+ (instancetype)accountWithDict:(NSDictionary *)dict
{
YGAccount *account = [[self alloc] init];
[account setValuesForKeysWithDictionary:dict];
return account;
}
- (void)setExpires_in:(NSString *)expires_in
{
_expires_in = expires_in;
// 计算过期的时间 = 当前时间 + 有效期
_expires_date = [NSDate dateWithTimeIntervalSinceNow:[expires_in longLongValue]];
}
// 归档的时候调用:告诉系统哪个属性需要归档,如何归档
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeObject:_access_token forKey:YGAccountTokenKey];
[aCoder encodeObject:_expires_in forKey:YGExpires_inKey];
[aCoder encodeObject:_uid forKey:YGUidKey];
[aCoder encodeObject:_expires_date forKey:YGExpires_dateKey];
}
// 解档的时候调用:告诉系统哪个属性需要解档,如何解档
- (id)initWithCoder:(NSCoder *)aDecoder
{
if (self = [super init]) {
// 一定要记得赋值
_access_token = [aDecoder decodeObjectForKey:YGAccountTokenKey];
_expires_in = [aDecoder decodeObjectForKey:YGExpires_inKey];
_uid = [aDecoder decodeObjectForKey:YGUidKey];
_expires_date = [aDecoder decodeObjectForKey:YGExpires_dateKey];
}
return self;
}
现在我们有了Account模型,但当我们需要存储和获取account的时候,从哪里获取比较好呢。为了代码和逻辑的清晰,也为了以后维护的方便,我们决定再建一个类,专门处理存储和获取,这样我们只需要在这个类里直接调用合适的方法便可以得到account数据,让其他人不需要关心底层是怎么封装,怎么存储的。你可能现在不太明白我在说什么,但是当你仔细瞅瞅代码后,就知道这个设计方法多优雅美妙了。上代码:
@class YGAccount;
@interface YGAccountTool : NSObject
+ (void)saveAccount:(YGAccount *)account;
+ (YGAccount *)account;
可以看到,我们在YGAccountTool里,只设置里了两个方法,一个是存储account,一个是直接获取account ,如果以后直接就调用这两个方法,岂不是特别的方便,执行代码如下:
#define YGAccountFileName [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0] stringByAppendingPathComponent:@"account.data"]
// 类方法一般用静态变量代替成员属性
static YGAccount *_account;
+ (void)saveAccount:(YGAccount *)account
{
[NSKeyedArchiver archiveRootObject:account toFile:YGAccountFileName];
}
+ (YGAccount *)account
{
if (_account == nil) {
_account = [NSKeyedUnarchiver unarchiveObjectWithFile:YGAccountFileName];
// 判断下账号是否过期,如果过期直接返回Nil
// 2015 < 2017
if ([[NSDate date] compare:_account.expires_date] != NSOrderedAscending) { // 过期
return nil;
}
}
return _account;
}
所以下面我们就可以直接利用刚创建的类,在得到请求成功返回的数据后,把其转为account模型,并存储啦,代码如下:
// 字典转模型
YGAccount *account = [YGAccount accountWithDict:responseObject];
// 保存账号信息:
// 数据存储一般我们开发中会搞一个业务类,专门处理数据的存储
// 以后我不想归档,用数据库,直接改业务类
[YGAccountTool saveAccount:account];
7.2 授权界面的业务逻辑
亲们自己分析下就知道,微博应用应该首先进入授权界面,因为没有授权就没法显示东西,然后再进入新特性界面。但是如果我们直接把代码加到AppDelegate会发现,代码越来越多,越来越难找到,所以我们专门新建一个工具类,用来处理新特性界面的业务逻辑,也就是给代码搬搬家,使整个项目变得更整洁点。代码搬家的活,就不在这写出来了,请大家参考资料代码的YGRootVcTool类。AppDelegate 内的业务逻辑则变为:
// 判断下有没有授权
if ([YGAccountTool account]) { // 已经授权
// 选择根控制器
[YGRootVcTool chooseRootViewController:self.window];
}else{ // 进行授权
YGOAuthViewController *oauthVc = [[YGOAuthViewController alloc] init];
// 设置窗口的根控制器
self.window.rootViewController = oauthVc;
}
最后在AFNpost请求成功回调的方法内,添加如下一行代码,表示当我们存储完account数据后,便可以展示新特性界面或者这界面了。
// 进入主页或者新特性,选择窗口的根控制器
[YGRootVcTool chooseRootViewController:YGKeyWindow];
这样的代码是不是感觉很清爽,以后我们会创建其他的工具类来封装其他的代码,慢慢往下看吧。
7.3 获取微博数据
获取微博数据就需要再次查看微博API文档,文档内清楚的写明请求方法,请求所需参数,以及请求后返回的数据。当我们分析了其返回数据后,发现我们需要再次依据返回信息,创建合适的模型来接受数据。我们这里并不把所有的请求数据都存储到模型,只需要选择那些展示数据必要的参数即可。这里我们需要导入新的外部框架MJExtension,来给我们处理JSON数据转模型。截取的参数为:
//created_at string 微博创建时间
//idstr string 字符串型的微博ID
//text string 微博信息内容
//source string 微博来源
//user object 微博作者的用户信息字段 详细
//retweeted_status object 被转发的原微博信息字段,当该微博为转发微博时返回
//reposts_count int 转发数
//comments_count int 评论数
//attitudes_count int 表态数
//pic_urls 配图数组
我们创建的三个类来保存以上信息,因为user是个字典,且pic_urls是个数组,里面存放的是字典,需要新建它们各自的模型来存储。如下图
然后,我们转到YGHomeViewController执行文件内,创建一个名为loadNewStatus来请求最新的微博,代码如下:
#pragma mark - 请求最新的微博
- (void)loadNewStatus
{
// 创建请求管理者
AFHTTPRequestOperationManager *mgr = [AFHTTPRequestOperationManager manager];
// 创建一个参数字典
NSMutableDictionary *params = [NSMutableDictionary dictionary];
params[@"access_token"] = [YGAccountTool account].access_token;
// 发送get请求
[mgr GET:@"https://api.weibo.com/2/statuses/friends_timeline.json" parameters:params success:^(AFHTTPRequestOperation *operation, id responseObject) { // 请求成功的时候调用
NSLog(@"%@",responseObject);
// 获取到微博数据 转换成模型
// 获取微博字典数组
NSArray *dictArr = responseObject[@"statuses"];
// 把字典数组转换成模型数组
NSArray *statuses = (NSMutableArray *)[YGStatus mj_objectArrayWithKeyValuesArray:dictArr];
//保存模型数组
[self.statuses addObjectsFromArray:statuses];
// 刷新表格
[self.tableView reloadData];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
}];
}
可以看到我们在代码中分五步走:
第一 首先创建号GET请求所需要的参数字典params,
第二 在get请求成功后的回调函数内,把获取到的statuses 的JSON数组转化为NSArray,得到的是含有一个个字典的数组,
第三 再把字典数组通过MJExtension框架内的方法转化为含有YGStatuses模型的数组,
第四 然后把得到的微博模型数组保存到对象的数组实例中,
第五 最后刷新表格。
为了能把获取到的数据初步的展示出来,我们要设置tableView 的数据源代理如下:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.statuses.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *ID =@"cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
// Configure the cell...
if (cell ==nil) {
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:ID];
}
YGStatus *status =self.statuses[indexPath.row];
cell.textLabel.text =status.user.name;
[cell.imageView sd_setImageWithURL:status.user.profile_image_url placeholderImage:[UIImage imageNamed:@"timeline_image_placeholder"]];
cell.detailTextLabel.text = status.text;
return cell;
}
可以看到,以上代码里就需要导入一个新的专门处理图片下载的外部框架SDWebImage。这个框架有很多的优点,例如它是异步下载,不会卡死主线程,而且它内部做了数据缓存,不会重复下载同样的图片,节省用户流量,解决了循环利用的问题,不会让图片错乱。关于这个框架,如果你想深入学习,请百度或者谷歌吧,或者我以后也会出专题,好好讲一下各种框架的使用和优缺点。如果你此时运行项目就会发现,我们终于可以看到微博数据了。如下图:
如果你按部就班把代码敲到此时,是不是感觉有点激动呢?更激动更有趣的还在后面呢。
7.4 解决数据固定问题
可以发现,以上代码生成的微博数据只有二十条,而且不论怎么拖动都不会更新数据。怎么才能加载更多的数据呢?经过分析我们发现,每条微博都有一个idstr,且微博越新,idstr越大,所以要想加载更新的数据,解决一下两个问题:
1.更改代码,可以加载比以前获得微博的idstr更大的微博。
2.添加下拉刷新控件,下拉的时候刷新更多微博
我们首先更改loadNewStatus方法如下:
#pragma mark - 请求最新的微博
- (void)loadNewStatus
{
// 创建请求管理者
AFHTTPRequestOperationManager *mgr = [AFHTTPRequestOperationManager manager];
// 创建一个参数字典
NSMutableDictionary *params = [NSMutableDictionary dictionary];
if (self.statuses.count) {
params[@"since_id"] = [self.statuses[0] idstr];
}
params[@"access_token"] = [YGAccountTool account].access_token;
// 发送get请求
[mgr GET:@"https://api.weibo.com/2/statuses/friends_timeline.json" parameters:params success:^(AFHTTPRequestOperation *operation, id responseObject) { // 请求成功的时候调用
NSLog(@"%@",responseObject);
// 获取到微博数据 转换成模型
// 获取微博字典数组
NSArray *dictArr = responseObject[@"statuses"];
// 把字典数组转换成模型数组
NSArray *statuses = (NSMutableArray *)[YGStatus mj_objectArrayWithKeyValuesArray:dictArr];
NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, statuses.count)];
// 把最新的微博数插入到最前面
[self.statuses insertObjects:statuses atIndexes:indexSet];
// 刷新表格
[self.tableView reloadData];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
}];
}
请注意我们对代码做了怎么样的改动:首先我们在加载新数据前要判断是否已经有了微博,如果有我们就获取最前面微博的idstr赋给params参数,这样我们就可以请求比之前微博更新的微博了。然后在获取到新微博后,把新微博插入到微博数组的最前面,这样每次运行时都会刷新显示最新的微博。
7.5 添加刷新控件
可以发现以上只能每运行一次才能刷新一次微博,这岂不是很蛋疼,所以我们要搞一个下拉刷新控件,只需要拖拽下屏幕就可以刷新微博啦。这里就有需要导入一个叫MJRefresh的外部框架来专门给我们添加各种刷新控件。现在view DidLoad方法如下:
//添加下拉刷新控件,请求最新微博数据
self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingTarget:self refreshingAction:@selector(loadNewStatus)];
[self.tableView.mj_header beginRefreshing];
//添加上拉刷新旧微博控件
self.tableView.mj_footer = [MJRefreshAutoNormalFooter footerWithRefreshingTarget:self refreshingAction:@selector(loadMoreStatus)];
可见我们不仅仅增加了下拉刷新,也添加了上拉刷新,其实原理时一样的,只不过上拉刷新用的是maxid参数。loadMoreStatus代码如下:
#pragma mark - 请求更多旧的微博
- (void)loadMoreStatus
{
// 创建请求管理者
AFHTTPRequestOperationManager *mgr = [AFHTTPRequestOperationManager manager];
// 创建一个参数字典
NSMutableDictionary *params = [NSMutableDictionary dictionary];
if (self.statuses.count) { // 有微博数据,才需要下拉刷新
long long maxId = [[[self.statuses lastObject] idstr] longLongValue] - 1;
params[@"max_id"] = [NSString stringWithFormat:@"%lld",maxId];
}
params[@"access_token"] = [YGAccountTool account].access_token;
// 发送get请求
[mgr GET:@"https://api.weibo.com/2/statuses/friends_timeline.json" parameters:params success:^(AFHTTPRequestOperation *operation, id responseObject) { // 请求成功的时候调用
// 结束上拉刷新
[self.tableView.mj_footer endRefreshing];
// 获取到微博数据 转换成模型
// 获取微博字典数组
NSArray *dictArr = responseObject[@"statuses"];
// 把字典数组转换成模型数组
NSArray *statuses = (NSMutableArray *)[YGStatus mj_objectArrayWithKeyValuesArray:dictArr];
// 把数组中的元素添加进去
[self.statuses addObjectsFromArray:statuses];
// 刷新表格
[self.tableView reloadData];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
}];
}
注意在loadNewStatus内别忘了添加结束刷新代码,不然他就一直不停的显示正在刷新:
//结束下拉刷新
[self.tableView.mj_header endRefreshing];
这样我们再次运行项目就可以发现,更新微博和加载就微博都完美实现了。
今天的代码有点略多,但是总结起来中心思想就是建立模型,字典转换模型以及设计合适的工具类简化代码。下节课我们会封装优化我们的代码,让以后的开发更方便。