之前写了豆瓣客户端(一)获取授权码和访问令牌后,就开始学习iOS7 TextKit方面的内容了,在前两天看Text Kit 看得无聊以后,又再回来写自己一直都很感兴趣的豆瓣客户端。途中遇到了各种各样的问题,先来小小吐槽一下:豆瓣的开发文档真是坑爹极了,基本上没有步骤可言,有时候就是一句带过,来个只能意会不能言传,难怪那么少人做豆瓣客户端,可能这个客户端赚不了什么钱吧。幸好以前有人也遇到过这些问题,在参考了他们的文章再结合一些实践和修改,最后终于掌握了调用豆瓣API的基本方法,下面让我仔细道来。
(一)通过access_token判断程序加载后的根视图页面
// 获取用NSUserDefaults保存的access_token
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
NSString *access_token = [userDefaults objectForKey:udAccess_token];
if (!access_token) {
self.window.rootViewController = self.authvc; // 如果未获得access_token,则转到授权页面
}
else {
self.window.rootViewController = self.uvc; // 否则转到个人主页页面
}
这里没有考虑到access_token过期的问题,我会改进的。
简单说一下NSUserDefaults。
NSUserDefaults用于存取程序的配置信息,使用起来非常简便,不用另外写存取的方法:
保存数据:
// 保存access_token
NSString *access_token = [rspDic objectForKey:@"access_token"];
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setObject:access_token forKey:udAccess_token]; // 设置key-object对并使用NSUserDefaults对象保存
[userDefaults synchronize]; // 同步方法,这个是必须的
获取数据:
// 获取用NSUserDefaults保存的access_token
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
NSString *access_token = [userDefaults objectForKey:udAccess_token];
如果要实现退出登录的功能,也非常简单,只需要设置NSUserDefaults中对应udAccess_token这个key的value为nil即可:
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setObject:nil forKey:udAccess_token];
(二)获取授权用户的简版信息
在(一)拿到access_token后,开发文档中介绍的接下来就是获取授权用户信息,参见:点击打开链接。
获取用户信息的API参见:点击打开链接。其权限必须包括scope: douban_basic_common。
这是从开发文档中摘取的内容:
豆瓣Connect通过以下两个步骤来完成认证授权.
- 请求用户授权,拿到授权过后的authorize_code
- 使用授权后的authorize_code,来获取access_token
在完成认证之后,您可以使用获取的Access Token,来获取已授权的用户信息,以便完成和您网站的用户对接工作.
获取当前授权用户信息
通过访问以下url来获取授权用户的信息
GET https://api.douban.com/v2/user/~me
例如:
curl "https://api.douban.com/v2/user/~me"
-H "Authorization: Bearer a14afef0f66fcffce3e0fcd2e34f6ff4"
说明:Bearer 后面的字符串就是你上一步获取到的access_token.
对于我们这些首次接触调用api的人来说,这里的curl -H真的不懂得是什么意思。查了资料:
curl是利用URL语法在命令行方式下工作的文件传输工具。
-H/--header <line>自定义头信息传递给服务器
那么怎样在代码中实现这种请求呢?在这里我使用了第三方类库ASIHTTPRequest类库提供的方法来实现。
简单说说第三方类库环境的配置方法:
1.在工程中添加文件。这个和平时在工程中添加文件一样。
2.在工程中添加类库需要的Framework。ASI类库需要的框架如下:
3.由于类库中的文件一般都采用手动内存管理,所以添加到使用arc的工程中会出现编译错误,解决方法是点击工程——Building Phases——在Complie sources选项中找到与ASI有关的源文件,双击Complier Flags,在弹出的框中填写-fno-objc-arc,如下所示:
4.接下来只要在需要时导入对应的头文件就可以使用类库中提供的方法了。
简单说一说ASIHttpRequest类库方法的使用:
目前我使用的包括ASIHttpRequest和ASIFormDataRequest两种类,两种类都可以发送http请求。当使用GET方式,可以直接在网址中设置参数时,一般使用ASIHttpRequest类。如果必须使用POST方式,或者参数较多,构造填写好参数的网址较为麻烦时,就使用ASIFormDataRequest类。
下面结合获取授权用户的信息代码看看ASIFormDataRequest类的使用方法:
// 获取当前授权用户的信息
/* 初始化ASIFormDataRequest请求 */
NSURL *url = [NSURL URLWithString:kGetAuthUserInfo]; // 设置请求网址url
__weak ASIFormDataRequest *formRequest = [ASIFormDataRequest requestWithURL:url];
/* 设置请求属性 */
[formRequest setDelegate:self]; // 异步请求需要设置委托
[formRequest setRequestMethod:@"GET"]; // 设置请求方式,默认为GET
[formRequest setTimeOutSeconds:kDefaultTimeoutSeconds]; // 设置请求超时时间
NSMutableDictionary *dic = [[NSMutableDictionary alloc] init];
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
NSString *access_token = [userDefaults objectForKey:udAccess_token]; // 从NSUserDefaults中获取之前保存的access_token
NSString *bear = [NSString stringWithFormat:@"Bearer %@", access_token]; // 设置请求头的value
[dic setObject:bear forKey:@"Authorization"]; // 请求头的key为Authorization
[formRequest setRequestHeaders:dic]; // 设置请求头字典
/* 设置请求完成后的处理动作 */
// 请求成功
[formRequest setCompletionBlock:^{
NSString *rspStr = [formRequest responseString]; // 获取返回的NSString
NSDictionary *rspDic = [rspStr objectFromJSONString]; // 将NSString转换为字典
if (rspDic) {
NSLog(@"rspDic:%@", rspDic);
[userDefaults setObject:rspDic forKey:udAuthUserInfoDic]; // 将用户信息字典用NSUserDefaults类保存
NSString *avatarStr = [rspDic objectForKey:ukAvatar]; // 获取用户头像缩略图网址字符串
NSURL *avaUrl = [NSURL URLWithString:avatarStr]; // 获取用户头像网址url
NSData *avaData = [NSData dataWithContentsOfURL:avaUrl]; // 通过url获得NSData数据
UIImage *avaImage = [UIImage imageWithData:avaData]; // 将NSData数据转换为UIImage图片
// 在GUI设置用户信息
_avatar_image.image = avaImage; // 用户头像缩略图
_name_label.text = [rspDic objectForKey:ukName]; // 用户名字
_id_label.text = [rspDic objectForKey:ukId]; // 用户id
_created_label.text = [rspDic objectForKey:ukCreated]; // 用户豆瓣帐号创建时间
_sign_label.text = [rspDic objectForKey:ukSignature]; // 用户签名
_desc_label.text = [rspDic objectForKey:ukDesc]; // 用户简述
}
else {
NSLog(@"rspDic == nil");
}
}];
// 请求失败
[formRequest setFailedBlock:^{
NSError *error = [formRequest error]; // 获取请求错误NSError
if (error) {
NSLog(@"Request failed:%@", error); // 输出错误信息
}
}];
/* 开始发送异步请求 */
[formRequest startAsynchronous];
主要有四个步骤:初始化ASIFormDataRequest对象——设置请求属性——设置请求完成后的处理动作——开始发送请求。
在请求完成后设置处理动作时有三种处理方式:
(1)如果请求是ASIHttpRequest,那么可以通过实现委托中的方法来实现:
@interface UsersViewController () <UISearchBarDelegate, ASIHTTPRequestDelegate> // 声明遵守ASIHttpRequestDelegate
-(void)requestFinished:(ASIHTTPRequest *)request // 请求成功
-(void)requestFailed:(ASIHTTPRequest *)request // 请求失败
(2)设置处理动作为自定方法:
[formRequest setDidFinishSelector:@selector(finishSearch:)]; // 请求成功
[formRequest setDidFailSelector: @selector(requestFailed:)]; // 请求失败
(3)设置请求完成后执行的代码块:
// 请求成功
[formRequest setCompletionBlock:^{
// 请求完成后的动作
}];
// 请求失败
[formRequest setFailedBlock:^{
// 请求失败后的动作
}];
方法(1)自带,不需要特别声明。
当有特别需要时(如在同一方法中多次发送请求)可以使用方法(2)。
如果希望代码简洁和编写方便,可以使用方法(3)。如果要在代码块中引用代码块之外的变量,为了避免形成retain cycle,所以要设置ASIFormRequest对象的属性为__weak。
其中发送请求时有同步请求(synchronus)和异步请求(asynchronus)两种方式,异步请求必须设置委托。
通过ASIFormDataRequest设置请求头并发送请求到网址https://api.douban.com/v2/user/~me。需要特别注意的是返回的json数据被转化化为NSString,而不是返回的NSDictionary,读者可以在控制台输出返回的字符串和字典对象看看。
控制台输出的字符串为:
2013-08-16 00:11:34.029 PurpleRuBy_Demo[18427:a0b] rspDic:{
alt = "http://www.douban.com/people/75816695/";
avatar = "http://img3.douban.com/icon/u75816695-2.jpg";
created = "2013-07-26 16:43:52";
desc = "DouBaning\U3002\U3002\U3002";
id = 75816695;
"is_suicide" = 0;
"large_avatar" = "http://img3.douban.com/icon/up75816695-2.jpg";
name = Luyu;
signature = "";
type = user;
uid = 75816695;
}
由于返回的数据是json数据格式的字符串,所以要将其转化为字典数据。在这里我又使用了便于处理由json数据生成的字符串数据的第三方类库——高效的JSONKit。这个类库对比起上一个类库非常简单,只需要简单地加入到工程和导入头文件就可以使用了:
#import "JSONKit.h"
(1)将请求返回的json数据生成的NSString对象转换为字典:
NSString *rspStr = [formRequest responseString]; // 获取返回的NSString
NSDictionary *rspDic = [rspStr objectFromJSONString]; // 将NSString转换为字典
(2)
将请求返回的json数据生成的NSString对象转换为数组:
NSString *str = [formRequest responseString]; // 返回由json格式数据生成的字符串
NSDictionary *dic = [str objectFromJSONString]; // 将以上字符串转换为字典
searchedUsers = [dic objectForKey:@"users"]; // json字典中对应key为users的value就是对应的用户数组
以上获取的是嵌套格式中的数组。简单来说将json格式数据生成的字符串转换为数组或字典就是使用以下方法:
- (id)objectFromJSONString
(三)通过用户uid或id搜索用户并获取完整信息
这是豆瓣api的说明:
*注意 :name 为用户uid或者数字id
获取用户信息 | GET | /v2/user/:name |
// 通过用户的uid或id获取指定用户的信息
NSString *str = [NSString stringWithFormat:@"%@/%@", kGetUserInfo, searchName]; // 通过用name获取指定用户的信息的api网址
NSURL *url = [NSURL URLWithString:str]; // 以上网址对应的url
/* 发送请求 */
__weak ASIHTTPRequest *httpRequest = [ASIHTTPRequest requestWithURL:url];
[httpRequest setDelegate:self];
[httpRequest setRequestMethod:@"GET"];
[httpRequest setTimeOutSeconds:kDefaultTimeoutSeconds];
[httpRequest setCompletionBlock:^{
NSString *rspStr = [httpRequest responseString];
NSDictionary *rspDic = [rspStr objectFromJSONString];
if (rspDic) {
NSLog(@"rpDic:%@", rspDic);
NSString *avatarStr = [rspDic objectForKey:ukAvatar];
NSURL *avaUrl = [NSURL URLWithString:avatarStr];
NSData *avaData = [NSData dataWithContentsOfURL:avaUrl];
UIImage *avaImage = [UIImage imageWithData:avaData];
_avatar_image.image = avaImage;
_name_label.text = [rspDic objectForKey:ukName];
_id_label.text = [rspDic objectForKey:ukId];
_created_label.text = [rspDic objectForKey:ukCreated];
_sign_label.text = [rspDic objectForKey:ukSignature];
_desc_label.text = [rspDic objectForKey:ukDesc];
}
else {
NSLog(@"rpDic == nil");
}
}];
[httpRequest setFailedBlock:^{
NSError *error = [httpRequest error];
if (error) {
NSLog(@"Request failed:%@", error);
}
}];
[httpRequest startAsynchronous];
只需要简单地设置请求网址为https://api.douban.com/v2/user/:name,设置name参数为用户uid或id对应的字符串(如
https://api.douban.com/v2/user/douban),发送请求并获取返回的数据即可。
由于请求方式比较简单,所以这里可以使用ASIHttpRequest类。
运行结果如下:
控制台输出为:
2013-08-16 00:24:26.420 PurpleRuBy_Demo[18427:a0b] rpDic:{
alt = "http://www.douban.com/people/douban/";
avatar = "http://img3.douban.com/icon/user_normal.jpg";
created = "2005-04-20 15:05:41";
desc = "";
id = 1000000;
"is_suicide" = 0;
"large_avatar" = "http://img3.douban.com/icon/user_large.jpg";
name = "\U516d\U96f6";
signature = "";
type = user;
uid = douban;
}
(四)搜索用户
先看看豆瓣api的说明:
GET https://api.douban.com/v2/user
请求参数
参数 | 意义 | 备注 |
q | 全文检索的关键词 | |
start | 起始元素 | |
count | 返回结果的数量 |
返回:
{
"start" = 0,
"count" = 10,
"total" = 34,
"users" = [User, ...]
}
起始元素从0开始,start和count参数的值必须为NSString对象。
在实现搜索用户信息模块中我使用了表格和搜索栏,如果掌握了(一)获取授权用户的简版信息,也没什么难度,就是设置一下请求的参数就可以了。
首先是跳转到搜索用户信息对应的表格视图控制器。
SearchUserTableViewController *sutvc = [[SearchUserTableViewController alloc] init]; // 搜索用户信息的表格视图控制器初始化
NavViewController *navc = [[NavViewController alloc] initWithRootViewController:sutvc]; // 表格视图对应的导航控制器初始化
[self presentViewController:navc animated:YES completion:^{ // 跳转到搜索用户信息的视图控制器
//
}];
然后是表格视图控制器的接口声明:
#import <UIKit/UIKit.h>
#import "DouBanAPI.h"
@interface SearchUserTableViewController : UITableViewController <UISearchBarDelegate> // 声明遵守搜索栏委托
@property (strong, nonatomic) UISearchBar *searchBar; // 搜索栏
@property (strong, nonatomic) NSMutableArray *searchedUsers; // 搜索结果中所有用户信息组成的数组
@property (strong, nonatomic) UIBarButtonItem *dismissButton; // 解散当前页面的按钮
@end
其中主要遇到了几个问题:
1.在运行到表格视图时出现了以下异常:
2013-08-16 00:35:24.697 PurpleRuBy_Demo[19001:a0b] *** Assertion failure in -[UITableView dequeueReusableCellWithIdentifier:forIndexPath:], /SourceCache/UIKit_Sim/UIKit-2817.7/UITableView.m:4965
2013-08-16 00:35:24.825 PurpleRuBy_Demo[19001:a0b] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'unable to dequeue a cell with identifier Cell - must register a nib or a class for the identifier or connect a prototype cell in a storyboard'
*** First throw call stack:
(0x1cb69b8 0x1a378b6 0x1cb6838 0x1614bc5 0x911506 0x52042 0x91b07f 0x91b153 0x901610 0x9133a2 0x89f53a 0x1a4981f 0x4235974 0x42297ee 0x422965a 0x419180a 0x4192b95 0x4193268 0x1c7e9ee 0x1c7e93f 0x1c5ccb0 0x1c5c10d 0x1c5bf3b 0x2bb1ff2 0x2bb1e19 0x8354eb 0x55fad 0x21f9725)
libc++abi.dylib: terminating with uncaught exception of type NSException
出错信息显示必须为表格单元设置一个可复用标识。
由于没有使用故事板,我的解决方法是首先创建一个表格单元对应的xib文件:
然后在代码中从xib文件中加载并注册标识:
// 在表格视图中注册表格单元并设置可复用标识
[self.tableView registerNib:[UINib nibWithNibName:@"TableViewCell" bundle:[NSBundle
mainBundle]] forCellReuseIdentifier:CellIdentifier]; // 从xib文件中加载
也可以从自定义的UITableViewCell子类中加载并注册:
// [self.tableView registerClass:CustomCell forCellReuseIdentifier:CellIdentifier];
在这里我使用了前者。
2.在设置请求完成后的动作时使用了以下方法:
[formRequest setDidFinishSelector:@selector(finishSearch:)]; // 请求成功
本来想向@selector(finishSearch:)传递两个参数,即ASIFormRequest对象和一个NSString对象,
但是没有找到方法,最后我使用了静态全局变量实现参数传递:
先是静态全局变量声明:
// 静态全局变量,用于保存并传递参数值
static NSString * CellIdentifier = @"Cell"; // 表格单元可复用标识
static NSString * startValue = @""; // start参数值
static NSString * countValue = @""; // count参数值
static NSString * nameValue = @""; // name 参数值
然后设置要传递的参数值:
startValue = [NSString stringWithFormat:@"%i", start]; // 设置start参数值
countValue = [NSString stringWithFormat:@"%i", count]; // 设置count参数值
nameValue = name; // 设置name 参数值
并设置请求完成后的动作:
[formRequest setDidFinishSelector:@selector(finishSearch:)];
[formRequest setDidFailSelector: @selector(requestFailed:)];
最后是在finishSearch:方法中使用参数值:
[formRequest setPostValue:nameValue forKey:pQ]; // 设置nameValue为参数q对
应的值
[formRequest setPostValue:startValue forKey:pStart]; // 设置startValue为参数
start对应的值
3.由于参数中count指定的是返回结果的数目,而我们一开始无法知道搜索结果的总数(由返回的json数据中total对应的键值给出)。
在这里我连续发送了两次请求,第一次请求获取搜索结果总数,第二次请求将搜索结果数据赋值给数组searchedUsers并重新加载表格数据。
首先是点击搜索后开始调用发送请求的方法:
#pragma - UISearchBarDelegate
// 在搜索栏输入文字完成后点击search后的动作
-(void)searchBarSearchButtonClicked:(UISearchBar *)aSearchBar
{
[aSearchBar resignFirstResponder]; // 解散键盘
NSString *searchName = aSearchBar.text; // 获取搜索栏中输入的文字
// 通过关键字搜索用户信息
[self searchForUsersRequestWithName:searchName Start:0 Count:0];
[self.tableView becomeFirstResponder]; //表格视图重新变成第一响应者
}
搜索方法:
-(void)searchForUsersRequestWithName:(NSString *)name Start:(NSInteger)start Count:
(NSInteger)count
{
// 发送第一次请求
__weak ASIFormDataRequest *formRequest = [ASIFormDataRequest requestWithURL:[NSURL
URLWithString:kGetUserInfo]];
[formRequest setDelegate:self];
[formRequest setRequestMethod:@"GET"];
[formRequest setTimeOutSeconds:kDefaultTimeoutSeconds];
startValue = [NSString stringWithFormat:@"%i", start]; // 设置start参数值
countValue = [NSString stringWithFormat:@"%i", count]; // 设置count参数值
nameValue = name; // 设置name 参数值
[formRequest setPostValue:startValue forKey:pStart]; // 通过post value设置请求时的
参数start值
[formRequest setPostValue:countValue forKey:pCount]; // 通过post value设置请求时的
参数count值,20是默认值
[formRequest setPostValue:name forKey:pQ]; // 通过post value设置请求时的
参数name 值
[formRequest setDidFinishSelector:@selector(finishSearch:)];
[formRequest setDidFailSelector:@selector(requestFailed:)];
[formRequest startAsynchronous];
}
第一次请求成功的处理动作(获取返回搜索结果的总数目并发送第二次请求):
-(void)finishSearch:(ASIFormDataRequest *)formRequest
{
if (![formRequest error]) {
NSString *rspStr = [formRequest responseString];
NSDictionary *dic = [rspStr objectFromJSONString];
NSLog(@"%@", dic);
// 获取返回搜索结果的总数目
NSString *searchedUsersCount = [dic objectForKey:@"total"];
// 发送第二次请求
__weak ASIFormDataRequest *formRequest = [ASIFormDataRequest requestWithURL:
[NSURL URLWithString:kGetUserInfo]];
[formRequest setDelegate:self];
[formRequest setRequestMethod:@"GET"];
[formRequest setTimeOutSeconds:kDefaultTimeoutSeconds];
// 设置api请求对应的参数值
[formRequest setPostValue:nameValue forKey:pQ]; // 设置nameValue为
参数q对应的值
[formRequest setPostValue:startValue forKey:pStart]; // 设置startValue
为参数start对应的值
[formRequest setPostValue:searchedUsersCount forKey:pCount]; // 设置
searchedCount为参数count对应的值
[formRequest setDidFinishSelector:@selector(getArray:)];
[formRequest setDidFailSelector:@selector(requestFailed:)];
[formRequest startAsynchronous];
}
}
获取搜索得到的用户数组:
// 获取搜索结果中所有用户组成的数组
-(void)getArray:(ASIFormDataRequest *)formRequest
{
NSString *str = [formRequest responseString]; // 返回由json格式数据生成的字符串
NSDictionary *dic = [str objectFromJSONString]; // 将以上字符串转换为字典
searchedUsers = [dic objectForKey:@"users"]; // json字典中对应key为users的
value就是对应的用户数组
if (searchedUsers) {
NSLog(@"searchedUsers:%@", searchedUsers);
}
else {
if ([formRequest error]) {
NSLog(@"Request failed:%@", [formRequest error]);
}
}
[self.tableView reloadData];
}
由于直接添加按钮的位置不理想,所以我直接在导航栏中设置该按钮:
// 在导航栏中设置解散按钮
dismissButton = [[UIBarButtonItem alloc] initWithTitle:@"Dismiss"
style:UIBarButtonItemStylePlain target:self action:@selector(dismiss:)];
self.navigationItem.rightBarButtonItem = dismissButton;
实现该按钮的方法:
-(void)dismiss:(id)sender
{
[self dismissViewControllerAnimated:YES completion:^{}]; // 解散当前视图
self.searchBar.hidden = YES; // 隐藏搜索栏
}
总的来说调用api时先设置api的url,然后创建ASIFormDataRequest请求,调用api所需的参数在请
求中通过postValue:forKey:方法设置,然后发送请求并获取数据就可以了。
最后小结一下:
1.调用api首先要设置好权限。
调用api的主要方法:
(1)GET url,带参数:
有两种方法:
方法一:可以建立ASIHttpRequest请求,初始化时设置url为包含参数及其值的完整url,如:https://api.douban.com/v2/user?q=douban&start=0&count=10
然后直接发送请求。
方法二:建立ASIFormDataRequest请求,初始化时设置的url为基本不带参数的url,如:https://api.douban.com/v2/user
设置参数值时,用setPostValue:forKey:方法设置。如:
[formRequest setPostValue:searchedUsersCount forKey:pCount]; // 设置searchedCount为参数count对应的值
设置请求方式是GET:
[formRequest setRequestMethod:@"GET"];
这个是默认的,可以不显式设置
(2)POST
POST方式一般都带参数,设置参数的方法和GET方式时相同。
必须设置请求的方式为POST,如:
[formRequest setRequestMethod:@"POST"];
这个一定要设置。
(3)curl -H,-F, GET/POST方式
curl -H 要设置好请求头(字典对象):
NSString *access_token = [userDefaults objectForKey:udAccess_token]; // 从NSUserDefaults中获取之前保存的access_token
NSString *bear = [NSString stringWithFormat:@"Bearer %@", access_token]; // 设置请求头的value
[dic setObject:bear forKey:@"Authorization"]; // 请求头的key为Authorization
[formRequest setRequestHeaders:dic]; // 设置请求头字典
curl -F 就是设置好参数,直接用setPostValue:forKey:方法设置。
方式为GET/POST。
主要豆瓣api网址参见http://developers.douban.com/wiki/?title=api_v2网址的底部。
2.正是因为豆瓣的api难用,所以在使用的过程中才能接触到更多知识并学到更多东西,凡事有利有弊,所以也不应该吐槽豆瓣太多。在摸到了方法以后基本就是千篇一律的重复了。