#前言
iOS开发过程中有时候难免会使用iOS内置的一些应用软件和服务,例如QQ通讯录、微信电话本会使用iOS的通讯录,一些第三方软件会在应用内发送短信等。今天将和大家一起学习如何使用系统应用、使用系统服务:
#目录
1 . 社交
2 . Game Center
3 应用内购买
4. iCloud
5 Passbook

1. 社交

现在很多应用都内置“社交分享”功能,可以将看到的新闻、博客、广告等内容分享到微博、微信、QQ、空间等,其实从iOS6.0开始苹果官方就内置了Social.framework专门来实现社交分享功能,利用这个框架开发者只需要几句代码就可以实现内容分享。下面就以一个分享到新浪微博的功能为例来演示Social框架的应用,整个过程分为:创建内容编辑控制器,设置分享内容(文本内容、图片、超链接等),设置发送(或取消)后的回调事件,展示控制器。

程序代码:

#import "ViewController.h"
#import <Social/Social.h>@interface ViewController ()@end@implementation ViewController
#pragma mark - 控制器视图事件
- (void)viewDidLoad {[super viewDidLoad];}#pragma mark - UI事件
- (IBAction)shareClick:(UIBarButtonItem *)sender {[self shareToSina];
}#pragma mark - 私有方法
-(void)shareToSina{//检查新浪微博服务是否可用if(![SLComposeViewController isAvailableForServiceType:SLServiceTypeSinaWeibo]){NSLog(@"新浪微博服务不可用.");return;}//初始化内容编写控制器,注意这里指定分享类型为新浪微博SLComposeViewController *composeController=[SLComposeViewController composeViewControllerForServiceType:SLServiceTypeSinaWeibo];//设置默认信息[composeController setInitialText:@"Kenshin Cui's Blog..."];//添加图片[composeController addImage:[UIImage imageNamed:@"stevenChow"]];//添加连接[composeController addURL:[NSURL URLWithString:@"http://www.cnblogs.com/kenshincui"]];//设置发送完成后的回调事件__block SLComposeViewController *composeControllerForBlock=composeController;composeController.completionHandler=^(SLComposeViewControllerResult result){if (result==SLComposeViewControllerResultDone) {NSLog(@"开始发送...");}[composeControllerForBlock dismissViewControllerAnimated:YES completion:nil];};//显示编辑视图[self presentViewController:composeController animated:YES completion:nil];
}@end

运行效果:

发送成功之后:

在这个过程中开发人员不需要知道新浪微博的更多分享细节,Social框架中已经统一了分享的接口,你可以通过ServiceType设置是分享到Facebook、Twitter、新浪微博、腾讯微博,而不关心具体的细节实现。那么当运行上面的示例时它是怎么知道用哪个账户来发送微博呢?其实在iOS的设置中有专门设置Facebook、Twitter、微博的地方:


必须首先在这里设置微博账户才能完成上面的发送,不然Social框架也不可能知道具体使用哪个账户来发送。

###第三方框架

当然,通过上面的设置界面应该可以看到,苹果官方默认支持的分享并不太多,特别是对于国内的应用只支持新浪微博和腾讯微博(事实上从iOS7苹果才考虑支持腾讯微博),那么如果要分享到微信、人人、开心等等国内较为知名的社交网络怎么办呢?目前最好的选择就是使用第三方框架,因为如果要自己实现各个应用的接口还是比较复杂的。当前使用较多的就是友盟社会化组件、ShareSDK,而且现在百度也出了社会化分享组件。今天无法对所有组件都进行一一介绍,这里就以友盟社交化组件为例简单做一下介绍:

  1. 注册友盟账号并新建应用获得AppKey。
  2. 下载友盟SDK并将下载的文件放到项目中(注意下载的过程中可以选择所需要的分享服务)
  3. 在应用程序中设置友盟的AppKey。
  4. 分享时调用presentSnsIconSheetView: appKey: shareText: shareImage: shareToSnsNames: delegate:方法或者presentSnsController: appKey: shareText: shareImage: shareToSnsNames: delegate:方法显示分享列表(注意这个过程中要使用某些服务需要到对应的平台去申请并对应扩展框架进行设置,否则分享列表中不会显示对应的分享按钮)。

下面是一个简单的示例:

#import "ViewController.h"
#import "UMSocial.h"
#import "UMSocialWechatHandler.h"@interface ViewController ()<UMSocialUIDelegate>@end@implementation ViewController
#pragma mark - 控制器视图事件
- (void)viewDidLoad {[super viewDidLoad];}#pragma mark - UI事件
- (IBAction)shareClick:(UIBarButtonItem *)sender {//设置微信AppId、appSecret,分享url
//    [UMSocialWechatHandler setWXAppId:@"wx30dbea5d5a258ed3" appSecret:@"cd36a9829e4b49a0dcac7b4162da5a5" url:@"http://www.cmj.com/social-UM"];//微信好友、微信朋友圈、微信收藏、QQ空间、QQ好友、来往好友等都必须经过各自的平台集成否则不会出现在分享列表,例如上面是设置微信的AppId和appSecret[UMSocialSnsService presentSnsIconSheetView:self appKey:@"54aa0a0afd98c5209f000efa" shareText:@"Kenshin Cui's Blog..." shareImage:[UIImage imageNamed:@"stevenChow"] shareToSnsNames:@[UMShareToSina,UMShareToTencent,UMShareToRenren,UMShareToDouban] delegate:self];}#pragma mark - UMSocialSnsService代理
//分享完成
-(void)didFinishGetUMSocialDataInViewController:(UMSocialResponseEntity *)response{//分享成功if(response.responseCode==UMSResponseCodeSuccess){NSLog(@"分享成功");}
}
@end

运行效果:

2. Game Center

Game Center是由苹果发布的在线多人游戏社交网络,通过它游戏玩家可以邀请好友进行多人游戏,它也会记录玩家的成绩并在排行榜中展示,同时玩家每经过一定的阶段会获得不同的成就。这里就简单介绍一下如何在自己的应用中集成Game Center服务来让用户获得积分、成就以及查看游戏排行和已获得成就。

由于Game Center是苹果推出的一项重要服务,苹果官方对于它的控制相当严格,因此使用Game Center之前必须要做许多准备工作。通常需要经过以下几个步骤(下面的准备工作主要是针对真机的,模拟器省略Provisioning Profile配置过程):

  1. 在苹果开发者中心创建支持Game Center服务的App ID并指定具体的Bundle ID,假设是“com.cmjstudio.kctest”(注意这个Bundle ID就是日后要开发的游戏的Bundle ID)。

  2. 基于“com.cmjstudio.kctest”创建开发者配置文件(或描述文件)并导入对应的设备(创建过程中选择支持Game Center服务的App ID,这样iOS设备在运行指定Boundle ID应用程序就知道此应用支持Game Center服务)。

  3. 在iTunes Connect中创建一个应用(假设叫“KCTest”,这是一款足球竞技游戏)并指定“套装ID”为之前创建的“com.cmjstudio.kctest”,让应用和这个App关联(注意这个应用不需要提交)。

  4. 在iTunes Connect的“用户和职能”中创建沙盒测试用户(由于在测试阶段应用还没有正式提交到App Store,所以只有沙盒用户可以登录Game Center)。

  5. 在iOS“设置”中找到Game Center允许沙盒,否则真机无法调试

    有了以上准备就可以在应用程序中增加积分、添加成就了,当然在实际开发过程积分和成就都是基于玩家所通过的关卡来完成的,为了简化这个过程这里就直接通过几个按钮手动触发这些事件。Game Center开发需要使用GameKit框架,首先熟悉一下常用的几个类:

GKLocalPlayer:表示本地玩家,在GameKit中还有一个GKPlayer表示联机玩家,为了保证非联网用户也可以正常使用游戏功能,一般使用GKLocalPlayer。

GKScore:管理游戏积分,例如设置积分、排名等。

GKLeaderboard:表示游戏排行榜,主用用于管理玩家排名,例如加载排行榜、设置默认排行榜、加载排行榜图片等。

GKAchievement:表示成就,主用用于管理玩家成就,例如加载成就、提交成就,重置成就等。

GKAchievementDescription:成就描述信息,包含成就的标题、获得前描述、获得后描述、是否可重复获得成就等信息。

GKGameCenterViewController:排行榜、成就查看视图控制器。如果应用本身不需要自己开发排行榜、成就查看试图可以直接调用此控制器。

下面就以一个简单的示例来完成排行榜、成就设置和查看,在这个演示程序中通过两种方式来查看排行和成就:一种是直接使用框架自带的GKGameCenterViewContrller调用系统视图查看,另一种是通过API自己读取排行榜、成就信息并显示。此外在应用中有两个添加按钮分别用于设置得分和成就。应用大致布局如下(图片较大可点击查看大图):

1.首先看一下主视图控制器KCMainTableViewController:

主视图控制器调用GKLeaderboard的loadLeaderboardsWithCompletionHandler:方法加载了所有排行榜,这个过程需要注意每个排行榜(GKLeaderboard)中的scores属性是没有值的,如果要让每个排行榜的scores属性有值必须调用一次排行榜的loadScoresWithCompletionHandler:方法。

调用GKAchievement的loadAchievementsWithCompletionHandler:方法加载加载成就,注意这个方法只能获得完成度不为0的成就,如果完成度为0是获得不到的;然后调用GKAchievementDesciption的loadAchievementDescriptionsWithCompletionHandler:方法加载了所有成就描述,这里加载的是所有成就描述(不管完成度是否为0);紧接着调用了每个成就描述的loadImageWithCompletionHandler:方法加载成就图片。

将获得的排行榜、成就、成就描述、成就图片信息保存,并在导航到详情视图时传递给排行榜视图控制器和成就视图控制器以便在子控制器视图中展示。

在主视图控制器左上方添加查看游戏中心控制按钮,点击按钮调用GKGameCenterViewController来展示排行榜、成就、玩家信息,这是系统自带的一个游戏中心视图方便和后面我们自己获得的信息对比。

程序如下

#import "KCMainTableViewController.h"
#import <GameKit/GameKit.h>
#import "KCLeaderboardTableViewController.h"
#import "KCAchievementTableViewController.h"@interface KCMainTableViewController ()<GKGameCenterControllerDelegate>@property (strong,nonatomic) NSArray *leaderboards;//排行榜对象数组
@property (strong,nonatomic) NSArray *achievements;//成就
@property (strong,nonatomic) NSArray *achievementDescriptions;//成就描述
@property (strong,nonatomic) NSMutableDictionary *achievementImages;//成就图片@property (weak, nonatomic) IBOutlet UILabel *leaderboardLabel; //排行个数
@property (weak, nonatomic) IBOutlet UILabel *achievementLable; //成就个数@end@implementation KCMainTableViewController#pragma mark - 控制器视图事件
- (void)viewDidLoad {[super viewDidLoad];[self authorize];
}#pragma mark - UI事件
- (IBAction)viewGameCenterClick:(UIBarButtonItem *)sender {[self viewGameCenter];
}#pragma mark - GKGameCenterViewController代理方法
//点击完成
-(void)gameCenterViewControllerDidFinish:(GKGameCenterViewController *)gameCenterViewController{NSLog(@"完成.");[gameCenterViewController dismissViewControllerAnimated:YES completion:nil];
}#pragma mark -导航
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{//如果是导航到排行榜,则将当前排行榜传递到排行榜视图if ([segue.identifier isEqualToString:@"leaderboard"]) {UINavigationController *navigationController=segue.destinationViewController;KCLeaderboardTableViewController *leaderboardController=[navigationController.childViewControllers firstObject];leaderboardController.leaderboards=self.leaderboards;}else if ([segue.identifier isEqualToString:@"achievement"]) {UINavigationController *navigationController=segue.destinationViewController;KCAchievementTableViewController *achievementController=[navigationController.childViewControllers firstObject];achievementController.achievements=self.achievements;achievementController.achievementDescriptions=self.achievementDescriptions;achievementController.achievementImages=self.achievementImages;}
}#pragma mark - 私有方法
//检查是否经过认证,如果没经过认证则弹出Game Center登录界面
-(void)authorize{//创建一个本地用户GKLocalPlayer *localPlayer= [GKLocalPlayer localPlayer];//检查用于授权,如果没有登录则让用户登录到GameCenter(注意此事件设置之后或点击登录界面的取消按钮都会被调用)[localPlayer setAuthenticateHandler:^(UIViewController * controller, NSError *error) {if ([[GKLocalPlayer localPlayer] isAuthenticated]) {NSLog(@"已授权.");[self setupUI];}else{//注意:在设置中找到Game Center,设置其允许沙盒,否则controller为nil[self  presentViewController:controller animated:YES completion:nil];}}];
}
//UI布局
-(void)setupUI{//更新排行榜个数[GKLeaderboard loadLeaderboardsWithCompletionHandler:^(NSArray *leaderboards, NSError *error) {if (error) {NSLog(@"加载排行榜过程中发生错误,错误信息:%@",error.localizedDescription);}self.leaderboards=leaderboards;self.leaderboardLabel.text=[NSString stringWithFormat:@"%i",leaderboards.count];//获取得分,注意只有调用了loadScoresWithCompletionHandler:方法之后leaderboards中的排行榜中的scores属性才有值,否则为nil[leaderboards enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {GKLeaderboard *leaderboard=obj;[leaderboard loadScoresWithCompletionHandler:^(NSArray *scores, NSError *error) {}];}];}];//更新获得成就个数,注意这个个数不一定等于iTunes Connect中的总成就个数,此方法只能获取到成就完成进度不为0的成就[GKAchievement loadAchievementsWithCompletionHandler:^(NSArray *achievements, NSError *error) {if (error) {NSLog(@"加载成就过程中发生错误,错误信息:%@",error.localizedDescription);}self.achievements=achievements;self.achievementLable.text=[NSString stringWithFormat:@"%i",achievements.count];//加载成就描述(注意,即使没有获得此成就也能获取到)[GKAchievementDescription loadAchievementDescriptionsWithCompletionHandler:^(NSArray *descriptions, NSError *error) {if (error) {NSLog(@"加载成就描述信息过程中发生错误,错误信息:%@",error.localizedDescription);return ;}self.achievementDescriptions=descriptions;//加载成就图片_achievementImages=[NSMutableDictionary dictionary];[descriptions enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {GKAchievementDescription *description=(GKAchievementDescription *)obj;[description loadImageWithCompletionHandler:^(UIImage *image, NSError *error) {[_achievementImages setObject:image forKey:description.identifier];}];}];}];}];
}//查看Game Center
-(void)viewGameCenter{if (![GKLocalPlayer localPlayer].isAuthenticated) {NSLog(@"未获得用户授权.");return;}//Game Center视图控制器GKGameCenterViewController *gameCenterController=[[GKGameCenterViewController alloc]init];//设置代理gameCenterController.gameCenterDelegate=self;//显示[self presentViewController:gameCenterController animated:YES completion:nil];
}
@end

2.然后看一下排行榜控制器视图KCLeaderboardTableViewController:

在排行榜控制器视图中定义一个leaderboards属性用于接收主视图控制器传递的排行榜信息并且通过一个UITableView展示排行榜名称、得分等。

在排行榜控制器视图中通过GKScore的reportScores: withCompletionHandler:设置排行榜得分,注意每个GKScore对象必须设置value属性来表示得分(GKScore是通过identifier来和排行榜关联起来的)。
程序如下

#import "KCLeaderboardTableViewController.h"
#import <GameKit/GameKit.h>
//排行榜标识,就是iTunes Connect中配置的排行榜ID
#define kLeaderboardIdentifier1 @"Goals"@interface KCLeaderboardTableViewController ()
@end@implementation KCLeaderboardTableViewController- (void)viewDidLoad {[super viewDidLoad];
}
#pragma mark - UI事件
//添加得分(这里指的是进球数)
- (IBAction)addScoreClick:(UIBarButtonItem *)sender {[self addScoreWithIdentifier:kLeaderboardIdentifier1 value:100];
}
#pragma mark - UITableView数据源方法
-(NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index{return 1;
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{return self.leaderboards.count;
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{static NSString *identtityKey=@"myTableViewCellIdentityKey1";UITableViewCell *cell=[self.tableView dequeueReusableCellWithIdentifier:identtityKey];if(cell==nil){cell=[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:identtityKey];}GKLeaderboard *leaderboard=self.leaderboards[indexPath.row];GKScore *score=[leaderboard.scores firstObject];NSLog(@"scores:%@",leaderboard.scores);cell.textLabel.text=leaderboard.title;//排行榜标题cell.detailTextLabel.text=[NSString stringWithFormat:@"%lld",score.value]; //排行榜得分return cell;
}#pragma mark - 属性#pragma mark - 私有方法
/***  设置得分**  @param identifier 排行榜标识*  @param value      得分*/
-(void)addScoreWithIdentifier:(NSString *)identifier value:(int64_t)value{if (![GKLocalPlayer localPlayer].isAuthenticated) {NSLog(@"未获得用户授权.");return;}//创建积分对象GKScore *score=[[GKScore alloc]initWithLeaderboardIdentifier:identifier];//设置得分score.value=value;//提交积分到Game Center服务器端,注意保存是异步的,并且支持离线提交[GKScore reportScores:@[score] withCompletionHandler:^(NSError *error) {if(error){NSLog(@"保存积分过程中发生错误,错误信息:%@",error.localizedDescription);return ;}NSLog(@"添加积分成功.");}];
}
@end

3.最后就是成就视图控制器KCAchievementTableViewController:

在成就视图控制器定义achievements、achievementDescriptions、achievementImages三个属性分别表示成就、成就描述、成就图片,这三个属性均从主视图控制器中传递进来,然后使用UITableView展示成就、成就图片、成就进度。

创建GKAchievemnt对象(通过identifier属性来表示具体的成就)并指定完成度,通过调用GKAchievement的reportAchievements: withCompletionHandler:方法提交完成度到Game Center服务器。
程序如下

#import "KCAchievementTableViewController.h"
#import <GameKit/GameKit.h>
//成就标识,就是iTunes Connect中配置的成就ID
#define kAchievementIdentifier1 @"AdidasGoldenBall"
#define kAchievementIdentifier2 @"AdidasGoldBoot"@interface KCAchievementTableViewController ()@end@implementation KCAchievementTableViewController
#pragma mark - 控制器视图方法
- (void)viewDidLoad {[super viewDidLoad];}#pragma mark - UI事件
//添加成就
- (IBAction)addAchievementClick:(UIBarButtonItem *)sender {[self addAchievementWithIdentifier:kAchievementIdentifier1];
}#pragma mark - UITableView数据源方法
-(NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index{return 1;
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{return self.achievementDescriptions.count;
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{static NSString *identtityKey=@"myTableViewCellIdentityKey1";UITableViewCell *cell=[self.tableView dequeueReusableCellWithIdentifier:identtityKey];if(cell==nil){cell=[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:identtityKey];}GKAchievementDescription *desciption=[self.achievementDescriptions objectAtIndex:indexPath.row];cell.textLabel.text=desciption.title ;//成就标题//如果已经获得成就则加载进度,否则为0double percent=0.0;GKAchievement *achievement=[self getAchievementWithIdentifier:desciption.identifier];if (achievement) {percent=achievement.percentComplete;}cell.detailTextLabel.text=[NSString stringWithFormat:@"%3.2f%%",percent]; //成就完成度//设置成就图片cell.imageView.image=[self.achievementImages valueForKey:desciption.identifier];return cell;
}#pragma mark - 私有方法
//添加指定类别的成就
-(void)addAchievementWithIdentifier:(NSString *)identifier{if (![GKLocalPlayer localPlayer].isAuthenticated) {NSLog(@"未获得用户授权.");return;}//创建成就GKAchievement *achievement=[[GKAchievement alloc]initWithIdentifier:identifier];achievement.percentComplete=100;//设置此成就完成度,100代表获得此成就NSLog(@"%@",achievement);//保存成就到Game Center服务器,注意保存是异步的,并且支持离线提交[GKAchievement reportAchievements:@[achievement] withCompletionHandler:^(NSError *error) {if(error){NSLog(@"保存成就过程中发生错误,错误信息:%@",error.localizedDescription);return ;}NSLog(@"添加成就成功.");}];
}//根据标识获得已取得的成就
-(GKAchievement *)getAchievementWithIdentifier:(NSString *)identifier{for (GKAchievement *achievement in self.achievements) {if ([achievement.identifier isEqualToString:identifier]) {return achievement;}}return nil;
}
@end

运行效果:

3. 应用内购买

大家都知道做iOS开发本身的收入有三种来源:出售应用、内购和广告。国内用户通常很少直接购买应用,因此对于开发者而言(特别是个人开发者),内购和广告收入就成了主要的收入来源。内购营销模式,通常软件本身是不收费的,但是要获得某些特权就必须购买一些道具,而内购的过程是由苹果官方统一来管理的,所以和Game Center一样,在开发内购程序之前要做一些准备工作(下面的准备工作主要是针对真机的,模拟器省略Provisioning Profile配置过程):

  1. 前四步和Game Center基本完全一致,只是在选择服务时不是选择Game Center而是要选择内购服务(In-App Purchase)。
  2. 到iTuens Connect中设置“App 内购买项目”,这里仍然以上面的“KCTest”项目为例,假设这个足球竞技游戏中有三种道具,分别为“强力手套”(增强防御)、“金球”(增加金球率)和“能量瓶”(提供足够体力),前两者是非消耗品只用一次性购买,后者是消耗品用完一次必须再次购买。
  3. 到iTunes Connect中找到“协议、税务和银行业务”增加“iOS Paid Applications”协议,并完成所有配置后等待审核通过(注意这一步如果不设置在应用程序中无法获得可购买产品)。
  4. 在iOS“设置”中找到”iTunes Store与App Store“,在这里可以选择使用沙盒用户登录或者处于注销状态,但是一定注意不能使用真实用户登录,否则下面的购买测试不会成功,因为到目前为止我们的应用并没有真正通过苹果官方审核只能用沙盒测试用户(如果是模拟器不需要此项设置)。

有了上面的设置之后保证应用程序Bundle ID和iTunes Connect中的Bundle ID(或者说App ID中配置的Bundle ID)一致即可准备开发。

开发内购应用时需要使用StoreKit.framework,下面是这个框架中常用的几个类:

SKProduct:可购买的产品(例如上面设置的能量瓶、强力手套等),其productIdentifier属性对应iTunes Connect中配置的“产品ID“,但是此类不建议直接初始化使用,而是要通过SKProductRequest来加载可用产品(避免出现购买到无效的产品)。

SKProductRequest:产品请求类,主要用于加载产品列表(包括可用产品和不可用产品),通常加载完之后会通过其-(void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response代理方法获得响应,拿到响应中的可用产品。

SKPayment:产品购买支付类,保存了产品ID、购买数量等信息(注意与其对应的有一个SKMutablePayment对象,此对象可以修改产品数量等信息)。

SKPaymentQueue:产品购买支付队列,一旦将一个SKPayment添加到此队列就会向苹果服务器发送请求完成此次交易。注意交易的状态反馈不是通过代理完成的,而是通过一个交易监听者(类似于代理,可以通过队列的addTransactionObserver来设置)。

SKPaymentTransaction:一次产品购买交易,通常交易完成后支付队列会调用交易监听者的-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction方法反馈交易情况,并在此方法中将交易对象返回。

SKStoreProductViewController:应用程序商店产品展示视图控制器,用于在应用程序内部展示此应用在应用商店的情况。(例如可以使用它让用户在应用内完成评价,注意由于本次演示的示例程序没有正式提交到应用商店,所以在此暂不演示此控制器视图的使用)。

了解了以上几个常用的开发API之后,下面看一下应用内购买的流程:

  1. 通过SKProductRequest获得可购买产品SKProduct数组(SKProductRequest会根据程序的Bundle ID去对应的内购配置中获取指定ID的产品对象),这个过程中需要知道产品标识(必须和iTuens Connect中的对应起来),可以存储到沙盒中也可以存储到数据库中(下面的Demo中定义成了宏定义)。
  2. 请求完成后可以在SKProductRequest的-(void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response代理方法中获得SKProductResponse对象,这个对象中保存了products属性表示可用产品对象数组。
  3. 给SKPaymentQueue设置一个监听者来获得交易的状态(它类似于一个代理),监听者通过-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction方法反馈交易的变化状态(通常在此方法中可以根据交易成功、恢复成功等状态来做一些处理)。
  4. 一旦用户决定购买某个产品(SKProduct),就可以根据SKProduct来创建一个对应的支付对象SKPayment,只要将这个对象加入到SKPaymentQueue中就会触发购买行为(将订单提交到苹果服务器),一旦一个交易发生变化就会触发SKPaymentQueue监听者来反馈交易情况。
  5. 交易提交给苹果服务器之后如果不出意外的话通常就会弹出一个确认购买的对话框,引导用户完成交易,最终完成交易后(通常是完成交易,用户点击”好“)会调用交易监听者-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction方法将此次交易的所有交易对象SKPaymentTransaction数组返回,可以通过交易状态判断交易情况。
  6. 通常一次交易完成后需要对本次交易进行验证,避免越狱机器模拟苹果官方的反馈造成交易成功假象。苹果官方提供了一个验证的URL,只要将交易成功后的凭证(这个凭证从iOS7之后在交易成功会会存储到沙盒中)传递给这个地址就会给出交易状态和本次交易的详细信息,通过这些信息(通常可以根据交易状态、Bundler ID、ProductID等确认)可以标识出交易是否真正完成。
  7. 对于非消耗品,用户在完成购买后如果用户使用其他机器登录或者用户卸载重新安装应用后通常希望这些非消耗品能够恢复(事实上如果不恢复用户再次购买也不会成功)。调用SKPaymentQueue的restoreCompletedTransactions就可以完成恢复,恢复后会调用交易监听者的paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction方法反馈恢复的交易(也就是已购买的非消耗品交易,注意这个过程中如果没有非消耗品可恢复,是不会调用此方法的)。

下面通过一个示例程序演示内购和恢复的整个过程,程序界面大致如下:

主界面中展示了所有可购买产品和售价,以及购买情况。

选择一个产品点”购买“可以购买此商品,购买完成后刷新购买状态(如果是非消耗品则显示已购买,如果是消耗品则显示购买个数)。

程序卸载后重新安装可以点击”恢复购买“来恢复已购买的非消耗品。

程序代码:

#import "KCMainTableViewController.h"
#import <StoreKit/StoreKit.h>
#define kAppStoreVerifyURL @"https://buy.itunes.apple.com/verifyReceipt" //实际购买验证URL
#define kSandboxVerifyURL @"https://sandbox.itunes.apple.com/verifyReceipt" //开发阶段沙盒验证URL//定义可以购买的产品ID,必须和iTunes Connect中设置的一致
#define kProductID1 @"ProtectiveGloves" //强力手套,非消耗品
#define kProductID2 @"GoldenGlobe" //金球,非消耗品
#define kProductID3 @"EnergyBottle" //能量瓶,消耗品@interface KCMainTableViewController ()<SKProductsRequestDelegate,SKPaymentTransactionObserver>@property (strong,nonatomic) NSMutableDictionary *products;//有效的产品
@property (assign,nonatomic) int selectedRow;//选中行
@end@implementation KCMainTableViewController
#pragma mark - 控制器视图方法
- (void)viewDidLoad {[super viewDidLoad];[self loadProducts];[self addTransactionObjserver];
}#pragma mark - UI事件
//购买产品
- (IBAction)purchaseClick:(UIBarButtonItem *)sender {NSString *productIdentifier=self.products.allKeys[self.selectedRow];SKProduct *product=self.products[productIdentifier];if (product) {[self purchaseProduct:product];}else{NSLog(@"没有可用商品.");}}
//恢复购买
- (IBAction)restorePurchaseClick:(UIBarButtonItem *)sender {[self restoreProduct];
}#pragma mark - UITableView数据源方法- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {return 1;
}- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {return self.products.count;
}- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {static NSString *identtityKey=@"myTableViewCellIdentityKey1";UITableViewCell *cell=[self.tableView dequeueReusableCellWithIdentifier:identtityKey];if(cell==nil){cell=[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:identtityKey];}cell.accessoryType=UITableViewCellAccessoryNone;NSString *key=self.products.allKeys[indexPath.row];SKProduct *product=self.products[key];NSString *purchaseString;NSUserDefaults *defaults=[NSUserDefaults standardUserDefaults];if ([product.productIdentifier isEqualToString:kProductID3]) {purchaseString=[NSString stringWithFormat:@"已购买%i个",[defaults integerForKey:product.productIdentifier]];}else{if([defaults boolForKey:product.productIdentifier]){purchaseString=@"已购买";}else{purchaseString=@"尚未购买";}}cell.textLabel.text=[NSString stringWithFormat:@"%@(%@)",product.localizedTitle,purchaseString] ;cell.detailTextLabel.text=[NSString stringWithFormat:@"%@",product.price];return cell;
}
#pragma mark - UITableView代理方法
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{UITableViewCell *currentSelected=[tableView cellForRowAtIndexPath:indexPath];currentSelected.accessoryType=UITableViewCellAccessoryCheckmark;self.selectedRow=indexPath.row;
}
-(void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath{UITableViewCell *currentSelected=[tableView cellForRowAtIndexPath:indexPath];currentSelected.accessoryType=UITableViewCellAccessoryNone;
}#pragma mark - SKProductsRequestd代理方法
/***  产品请求完成后的响应方法**  @param request  请求对象*  @param response 响应对象,其中包含产品信息*/
-(void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{//保存有效的产品_products=[NSMutableDictionary dictionary];[response.products enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {SKProduct *product=obj;[_products setObject:product forKey:product.productIdentifier];}];//由于这个过程是异步的,加载成功后重新刷新表格[self.tableView reloadData];
}
-(void)requestDidFinish:(SKRequest *)request{NSLog(@"请求完成.");
}
-(void)request:(SKRequest *)request didFailWithError:(NSError *)error{if (error) {NSLog(@"请求过程中发生错误,错误信息:%@",error.localizedDescription);}
}#pragma mark - SKPaymentQueue监听方法
/***  交易状态更新后执行**  @param queue        支付队列*  @param transactions 交易数组,里面存储了本次请求的所有交易对象*/
-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions{[transactions enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {SKPaymentTransaction *paymentTransaction=obj;if (paymentTransaction.transactionState==SKPaymentTransactionStatePurchased){//已购买成功NSLog(@"交易\"%@\"成功.",paymentTransaction.payment.productIdentifier);//购买成功后进行验证[self verifyPurchaseWithPaymentTransaction];//结束支付交易[queue finishTransaction:paymentTransaction];}else if(paymentTransaction.transactionState==SKPaymentTransactionStateRestored){//恢复成功,对于非消耗品才能恢复,如果恢复成功则transaction中记录的恢复的产品交易NSLog(@"恢复交易\"%@\"成功.",paymentTransaction.payment.productIdentifier);[queue finishTransaction:paymentTransaction];//结束支付交易//恢复后重新写入偏好配置,重新加载UITableView[[NSUserDefaults standardUserDefaults]setBool:YES forKey:paymentTransaction.payment.productIdentifier];[self.tableView reloadData];}else if(paymentTransaction.transactionState==SKPaymentTransactionStateFailed){if (paymentTransaction.error.code==SKErrorPaymentCancelled) {//如果用户点击取消NSLog(@"取消购买.");}NSLog(@"ErrorCode:%i",paymentTransaction.error.code);}}];
}
//恢复购买完成
-(void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue{NSLog(@"恢复完成.");
}#pragma mark - 私有方法
/***  添加支付观察者监控,一旦支付后则会回调观察者的状态更新方法:-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions*/
-(void)addTransactionObjserver{//设置支付观察者(类似于代理),通过观察者来监控购买情况[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
/***  加载所有产品,注意产品一定是从服务器端请求获得,因为有些产品可能开发人员知道其存在性,但是不经过审核是无效的;*/
-(void)loadProducts{//定义要获取的产品标识集合NSSet *sets=[NSSet setWithObjects:kProductID1,kProductID2,kProductID3, nil];//定义请求用于获取产品SKProductsRequest *productRequest=[[SKProductsRequest alloc]initWithProductIdentifiers:sets];//设置代理,用于获取产品加载状态productRequest.delegate=self;//开始请求[productRequest start];
}
/***  购买产品**  @param product 产品对象*/
-(void)purchaseProduct:(SKProduct *)product{//如果是非消耗品,购买过则提示用户NSUserDefaults *defaults=[NSUserDefaults standardUserDefaults];if ([product.productIdentifier isEqualToString:kProductID3]) {NSLog(@"当前已经购买\"%@\" %i 个.",kProductID3,[defaults integerForKey:product.productIdentifier]);}else if([defaults boolForKey:product.productIdentifier]){NSLog(@"\"%@\"已经购买过,无需购买!",product.productIdentifier);return;}//创建产品支付对象SKPayment *payment=[SKPayment paymentWithProduct:product];//支付队列,将支付对象加入支付队列就形成一次购买请求if (![SKPaymentQueue canMakePayments]) {NSLog(@"设备不支持购买.");return;}SKPaymentQueue *paymentQueue=[SKPaymentQueue defaultQueue];//添加都支付队列,开始请求支付
//    [self addTransactionObjserver];[paymentQueue addPayment:payment];
}/***  恢复购买,对于非消耗品如果应用重新安装或者机器重置后可以恢复购买*  注意恢复时只能一次性恢复所有非消耗品*/
-(void)restoreProduct{SKPaymentQueue *paymentQueue=[SKPaymentQueue defaultQueue];//设置支付观察者(类似于代理),通过观察者来监控购买情况
//    [paymentQueue addTransactionObserver:self];//恢复所有非消耗品[paymentQueue restoreCompletedTransactions];
}/***  验证购买,避免越狱软件模拟苹果请求达到非法购买问题**/
-(void)verifyPurchaseWithPaymentTransaction{//从沙盒中获取交易凭证并且拼接成请求体数据NSURL *receiptUrl=[[NSBundle mainBundle] appStoreReceiptURL];NSData *receiptData=[NSData dataWithContentsOfURL:receiptUrl];NSString *receiptString=[receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];//转化为base64字符串NSString *bodyString = [NSString stringWithFormat:@"{\"receipt-data\" : \"%@\"}", receiptString];//拼接请求数据NSData *bodyData = [bodyString dataUsingEncoding:NSUTF8StringEncoding];//创建请求到苹果官方进行购买验证NSURL *url=[NSURL URLWithString:kSandboxVerifyURL];NSMutableURLRequest *requestM=[NSMutableURLRequest requestWithURL:url];requestM.HTTPBody=bodyData;requestM.HTTPMethod=@"POST";//创建连接并发送同步请求NSError *error=nil;NSData *responseData=[NSURLConnection sendSynchronousRequest:requestM returningResponse:nil error:&error];if (error) {NSLog(@"验证购买过程中发生错误,错误信息:%@",error.localizedDescription);return;}NSDictionary *dic=[NSJSONSerialization JSONObjectWithData:responseData options:NSJSONReadingAllowFragments error:nil];NSLog(@"%@",dic);if([dic[@"status"] intValue]==0){NSLog(@"购买成功!");NSDictionary *dicReceipt= dic[@"receipt"];NSDictionary *dicInApp=[dicReceipt[@"in_app"] firstObject];NSString *productIdentifier= dicInApp[@"product_id"];//读取产品标识//如果是消耗品则记录购买数量,非消耗品则记录是否购买过NSUserDefaults *defaults=[NSUserDefaults standardUserDefaults];if ([productIdentifier isEqualToString:kProductID3]) {int purchasedCount=[defaults integerForKey:productIdentifier];//已购买数量[[NSUserDefaults standardUserDefaults] setInteger:(purchasedCount+1) forKey:productIdentifier];}else{[defaults setBool:YES forKey:productIdentifier];}[self.tableView reloadData];//在此处对购买记录进行存储,可以存储到开发商的服务器端}else{NSLog(@"购买失败,未通过验证!");}
}
@end

运行效果(这是程序在卸载后重新安装的运行效果,卸载前已经购买”强力手套“,因此程序运行后点击了”恢复购买“):

###扩展–广告

上面也提到做iOS开发另一收益来源就是广告,在iOS上有很多广告服务可以集成,使用比较多的就是苹果的iAd、谷歌的Admob,下面简单演示一下如何使用iAd来集成广告。使用iAd集成广告的过程比较简单,首先引入iAd.framework框架,然后创建ADBannerView来展示广告,通常会设置ADBannerView的代理方法来监听广告点击并在广告加载失败时隐藏广告展示控件。下面的代码简单的演示了这个过程:

#import "ViewController.h"
#import <iAd/iAd.h>@interface ViewController ()<ADBannerViewDelegate>
@property (weak, nonatomic) IBOutlet ADBannerView *advertiseBanner;//广告展示视图@end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];//设置代理self.advertiseBanner.delegate=self;
}#pragma mark - ADBannerView代理方法
//广告加载完成
-(void)bannerViewDidLoadAd:(ADBannerView *)banner{NSLog(@"广告加载完成.");
}
//点击Banner后离开之前,返回NO则不会展开全屏广告
-(BOOL)bannerViewActionShouldBegin:(ADBannerView *)banner willLeaveApplication:(BOOL)willLeave{NSLog(@"点击Banner后离开之前.");return YES;
}
//点击banner后全屏显示,关闭后调用
-(void)bannerViewActionDidFinish:(ADBannerView *)banner{NSLog(@"广告已关闭.");
}
//获取广告失败
-(void)bannerView:(ADBannerView *)banner didFailToReceiveAdWithError:(NSError *)error{NSLog(@"加载广告失败.");self.advertiseBanner.hidden=YES;
}@end

运行效果:

4. iCloud

iCloud是苹果提供的云端服务,用户可以将通讯录、备忘录、邮件、照片、音乐、视频等备份到云服务器并在各个苹果设备间直接进行共享而无需关心数据同步问题,甚至即使你的设备丢失后在一台新的设备上也可以通过Apple ID登录同步。当然这些内容都是iOS内置的功能,那么对于开放者如何利用iCloud呢?苹果已经将云端存储功能开放给开发者,利用iCloud开发者可以存储两类数据:用户文档和应用数据、应用配置项。前者主要用于一些用户文档、文件的存储,后者更类似于日常开放中的偏好设置,只是这些配置信息会同步到云端。

要进行iCloud开发同样需要一些准备工作(下面的准备工作主要是针对真机的,模拟器省略Provisioning Profile配置过程):

1、2步骤仍然是创建App ID启用iCloud服务、生成对应的配置(Provisioning Profile),这个过程中Bundle ID可以使用通配符(Data Protection、iCloud、Inter-App Audio、Passbook服务在创建App ID时其中的Bundle ID是可以使用通配ID的)。

3.在Xcode中创建项目(假设项目名称为“kctest”)并在项目的Capabilities中找到iCloud并打开。这里需要注意的就是由于在此应用中要演示文档存储和首选项存储,因此在Service中勾选“Key-value storae”和“iCloud Documents”:


在项目中会自动生成一个”kctest.entitlements”配置文件,这个文档配置了文档存储容器标识、键值对存储容器标识等信息。

4.无论是真机还是模拟器都必须在iOS“设置”中找到iCloud设置登录账户,注意这个账户不必是沙盒测试用户。

A.首先看一下如何进行文档存储。文档存储主要是使用UIDocument类来完成,这个类提供了新建、修改(其实在API中是覆盖操作)、查询文档、打开文档、删除文档的功能。

UIDocument对文档的新增、修改、删除、读取全部基于一个云端URL来完成(事实上在开发过程中新增、修改只是一步简单的保存操作),对于开发者而言没有本地和云端之分,这样大大简化了开发过程。这个URL可以通过NSFileManager的URLForUbiquityContainerIdentifier:方法获取,identifier是云端存储容器的唯一标识,如果传入nil则代表第一个容器(事实上这个容器可以通过前面生成的“kctest.entiements”中的Ubiquity Container Identifiers来获取。如上图可以看到这是一个数组,可以配置多个容器,例如我们的第一个容器标识是“iCloud.(CFBundleIdentifier)”,其中(CFBundleIdentifier)”,其中(CFBundleIdentifier)”,其中(CFBundleIdentifier)是Bundle ID,那么根据应用的Bundle ID就可以得知第一个容器的标识是“iCloud.com.cmjstudio.kctest”。)。下面是常用的文档操作方法:

-(void)saveToURL:forSaveOperation:completionHandler::将指定URL的文档保存到iCloud(可以是新增或者覆盖,通过saveOperation参数设定)。

-(void)openWithCompletionHandler::打开当前文档。

注意:删除一个iCloud文档是使用NSFileManager的removeItemAtURL:error:方法来完成的。

由于实际开发过程中数据的存储和读取情况是复杂的,因此UIDocument在设计时并没有提供统一的存储方式来保存数据,而是希望开发者自己继承UIDocument类并重写-(id)contentsForType:(NSString *)typeName error:(NSError *__autoreleasing *)outError和-(BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError *__autoreleasing *)outError方法来根据不同的文档类型自己来操作数据(contents)。这两个方法分别在保存文档(-(void)saveToURL:forSaveOperation:completionHandler:)和打开文档(-(void)openWithCompletionHandler:)时调用。通常在子类中会定义一个属性A来存储文档数据,当保存文档时,会通过-(id)contentsForType:(NSString *)typeName error:(NSError *__autoreleasing *)outError方法将A转化为NSData或者NSFileWrapper(UIDocument保存数据的本质就是保存转化得到的NSData或者NSFileWrapper);当打开文档时,会通过-(BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError *__autoreleasing *)outError方法将云端下载的NSData或者NSFileWrapper数据转化为A对应类型的数据。为了方便演示下面简单定义一个继承自UIDocument的KCDocument类,在其中定义一个data属性存储数据:

#import "KCDocument.h"@interface KCDocument()@end@implementation KCDocument#pragma mark - 重写父类方法
/***  保存时调用**  @param typeName <#typeName description#>*  @param outError <#outError description#>**  @return <#return value description#>*/
-(id)contentsForType:(NSString *)typeName error:(NSError *__autoreleasing *)outError{if (self.data) {return [self.data copy];}return [NSData data];
}/***  读取数据时调用**  @param contents <#contents description#>*  @param typeName <#typeName description#>*  @param outError <#outError description#>**  @return <#return value description#>*/
-(BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError *__autoreleasing *)outError{self.data=[contents copy];return true;
}
@end

如果要加载iCloud中的文档列表就需要使用另一个类NSMetadataQuery,通常考虑到网络的原因并不会一次性加载所有数据,而利用NSMetadataQuery并指定searchScopes为NSMetadataQueryUbiquitousDocumentScope来限制查找iCloud文档数据。使用NSMetadataQuery还可以通过谓词限制搜索关键字等信息,并在搜索完成之后通过通知的形式通知客户端搜索的情况。

大家都知道微软的OneNote云笔记本软件,通过它可以实现多种不同设置间的笔记同步,这里就简单实现一个基于iCloud服务的笔记软件。在下面的程序中实现笔记的新增、修改、保存、读取等操作。程序界面大致如下,点击界面右上方增加按钮增加一个笔记,点击某个笔记可以查看并编辑。

在主视图控制器首先查询所有iCloud保存的文档并在查询通知中遍历查询结果保存文档名称和创建日期到UITableView展示;其次当用户点击了增加按钮会调用KCDocument完成文档添加并导航到文档详情界面编辑文档内容。

#import "KCMainTableViewController.h"
#import "KCDocument.h"
#import "KCDetailViewController.h"
#define kContainerIdentifier @"iCloud.com.cmjstudio.kctest" //容器id,可以从生产的entitiements文件中查看Ubiquity Container Identifiers(注意其中的$(CFBundleIdentifier)替换为BundleID)@interface KCMainTableViewController ()
@property (strong,nonatomic) KCDocument *document;//当前选中的管理对象
@property (strong,nonatomic) NSMutableDictionary *files; //现有文件名、创建日期集合
@property (strong,nonatomic) NSMetadataQuery *dataQuery;//数据查询对象,用于查询iCloud文档@end@implementation KCMainTableViewController
#pragma mark - 控制器视图方法
- (void)viewDidLoad {[super viewDidLoad];[self loadDocuments];
}#pragma mark - UI事件
//新建文档
- (IBAction)addDocumentClick:(UIBarButtonItem *)sender {UIAlertController *promptController=[UIAlertController alertControllerWithTitle:@"KCTest" message:@"请输入笔记名称" preferredStyle:UIAlertControllerStyleAlert];[promptController addTextFieldWithConfigurationHandler:^(UITextField *textField) {textField.placeholder=@"笔记名称";}];UIAlertAction *okAction=[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {UITextField *textField= promptController.textFields[0];[self addDocument:textField.text];}];[promptController addAction:okAction];UIAlertAction *cancelAction=[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {}];[promptController addAction:cancelAction];[self presentViewController:promptController animated:YES completion:nil];}
#pragma mark - 导航
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {if ([segue.identifier isEqualToString:@"noteDetail"]) {KCDetailViewController *detailController= segue.destinationViewController;detailController.document=self.document;}
}#pragma mark - 属性
-(NSMetadataQuery *)dataQuery{if (!_dataQuery) {//创建一个iCloud查询对象_dataQuery=[[NSMetadataQuery alloc]init];_dataQuery.searchScopes=@[NSMetadataQueryUbiquitousDocumentsScope];//注意查询状态是通过通知的形式告诉监听对象的[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(metadataQueryFinish:) name:NSMetadataQueryDidFinishGatheringNotification object:_dataQuery];//数据获取完成通知[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(metadataQueryFinish:) name:NSMetadataQueryDidUpdateNotification object:_dataQuery];//查询更新通知}return _dataQuery;
}
#pragma mark - 私有方法
/***  取得云端存储文件的地址**  @param fileName 文件名,如果文件名为nil则重新创建一个url**  @return 文件地址*/
-(NSURL *)getUbiquityFileURL:(NSString *)fileName{//取得云端URL基地址(参数中传入nil则会默认获取第一个容器)NSURL *url= [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:kContainerIdentifier];//取得Documents目录url=[url URLByAppendingPathComponent:@"Documents"];//取得最终地址url=[url URLByAppendingPathComponent:fileName];return url;
}/***  添加文档到iCloud**  @param fileName 文件名称(不包括后缀)*/
-(void)addDocument:(NSString *)fileName{//取得保存URLfileName=[NSString stringWithFormat:@"%@.txt",fileName];NSURL *url=[self getUbiquityFileURL:fileName];/**创建云端文档操作对象*/KCDocument *document= [[KCDocument alloc]initWithFileURL:url];[document saveToURL:url forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {if (success) {NSLog(@"保存成功.");[self loadDocuments];[self.tableView reloadData];self.document=document;[self performSegueWithIdentifier:@"noteDetail" sender:self];}else{NSLog(@"保存失败.");}}];
}/***  加载文档列表*/
-(void)loadDocuments{[self.dataQuery startQuery];
}
/***  获取数据完成后的通知执行方法**  @param notification 通知对象*/
-(void)metadataQueryFinish:(NSNotification *)notification{NSLog(@"数据获取成功!");NSArray *items=self.dataQuery.results;//查询结果集self.files=[NSMutableDictionary dictionary];//变量结果集,存储文件名称、创建日期[items enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {NSMetadataItem *item=obj;NSString *fileName=[item valueForAttribute:NSMetadataItemFSNameKey];NSDate *date=[item valueForAttribute:NSMetadataItemFSContentChangeDateKey];NSDateFormatter *dateformate=[[NSDateFormatter alloc]init];dateformate.dateFormat=@"YY-MM-dd HH:mm";NSString *dateString= [dateformate stringFromDate:date];[self.files setObject:dateString forKey:fileName];}];[self.tableView reloadData];
}-(void)removeDocument:(NSString *)fileName{NSURL *url=[self getUbiquityFileURL:fileName];NSError *error=nil;//删除文件[[NSFileManager defaultManager] removeItemAtURL:url error:&error];if (error) {NSLog(@"删除文档过程中发生错误,错误信息:%@",error.localizedDescription);}[self.files removeObjectForKey:fileName];//从集合中删除
}#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {return 1;
}- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {return self.files.count;
}- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {static NSString *identtityKey=@"myTableViewCellIdentityKey1";UITableViewCell *cell=[self.tableView dequeueReusableCellWithIdentifier:identtityKey];if(cell==nil){cell=[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:identtityKey];cell.accessoryType=UITableViewCellAccessoryDisclosureIndicator;}NSArray *fileNames=self.files.allKeys;NSString *fileName=fileNames[indexPath.row];cell.textLabel.text=fileName;cell.detailTextLabel.text=[self.files valueForKey:fileName];return cell;
}- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {if (editingStyle == UITableViewCellEditingStyleDelete) {UITableViewCell *cell=[self.tableView cellForRowAtIndexPath:indexPath];[self removeDocument:cell.textLabel.text];[tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];} else if (editingStyle == UITableViewCellEditingStyleInsert) {// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view}
}#pragma mark - UITableView 代理方法
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{UITableViewCell *cell=[self.tableView cellForRowAtIndexPath:indexPath];NSURL *url=[self getUbiquityFileURL:cell.textLabel.text];self.document=[[KCDocument alloc]initWithFileURL:url];[self performSegueWithIdentifier:@"noteDetail" sender:self];
}@end

当新增一个笔记或选择一个已存在的笔记后可以查看、保存笔记内容。

#import "KCDetailViewController.h"
#import "KCDocument.h"
#define kSettingAutoSave @"com.cmjstudio.kctest.settings.autosave"@interface KCDetailViewController ()
@property (weak, nonatomic) IBOutlet UITextView *textView;@end@implementation KCDetailViewController
#pragma mark - 控制器视图方法
- (void)viewDidLoad {[super viewDidLoad];[self setupUI];
}-(void)viewWillDisappear:(BOOL)animated{[super viewWillDisappear:animated];//根据首选项来确定离开当前控制器视图是否自动保存BOOL autoSave=[[NSUbiquitousKeyValueStore defaultStore] boolForKey:kSettingAutoSave];if (autoSave) {[self saveDocument];}
}#pragma mark - 私有方法
-(void)setupUI{UIBarButtonItem *rightButtonItem=[[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemSave target:self action:@selector(saveDocument)];self.navigationItem.rightBarButtonItem=rightButtonItem;if (self.document) {//打开文档,读取文档[self.document openWithCompletionHandler:^(BOOL success) {if(success){NSLog(@"读取数据成功.");NSString *dataText=[[NSString alloc]initWithData:self.document.data encoding:NSUTF8StringEncoding];self.textView.text=dataText;}else{NSLog(@"读取数据失败.");}}];}
}
/***  保存文档*/
-(void)saveDocument{if (self.document) {NSString *dataText=self.textView.text;NSData *data=[dataText dataUsingEncoding:NSUTF8StringEncoding];self.document.data=data;[self.document saveToURL:self.document.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:^(BOOL success) {NSLog(@"保存成功!");}];}
}@end

到目前为止都是关于如何使用iCloud来保存文档的内容,上面也提到过还可以使用iCloud来保存首选项,这在很多情况下通常很有用,特别是对于开发了iPhone版又开发了iPad版的应用,如果用户在一台设备上进行了首选项配置之后到另一台设备上也能使用是多么优秀的体验啊。相比文档存储,首选项存储要简单的多,在上面“kctest.entitlements”中可以看到首选项配置并非像文档一样可以包含多个容器,这里只有一个Key-Value Store,通常使用NSUbiquitousKeyValueStore的defaultStore来获取,它的使用方法和NSUserDefaults几乎完全一样,当键值对存储发生变化后可以通过NSUbiquitousKeyValueStoreDidChangeExternallyNotification等获得对应的通知。在上面的笔记应用中有一个”设置“按钮用于设置退出笔记详情视图后是否自动保存,这个选项就是通过iCloud的首选项来存储的。

#import "KCSettingTableViewController.h"
#define kSettingAutoSave @"com.cmjstudio.kctest.settings.autosave"@interface KCSettingTableViewController ()
@property (weak, nonatomic) IBOutlet UISwitch *autoSaveSetting;@end@implementation KCSettingTableViewController- (void)viewDidLoad {[super viewDidLoad];[self setupUI];
}#pragma mark - UI事件
- (IBAction)autoSaveClick:(UISwitch *)sender {[self setSetting:sender.on];
}#pragma mark - 私有方法
-(void)setupUI{//设置iCloud中的首选项值NSUbiquitousKeyValueStore *defaults=[NSUbiquitousKeyValueStore defaultStore];self.autoSaveSetting.on= [defaults boolForKey:kSettingAutoSave];//添加存储变化通知[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyValueStoreChange:) name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification object:defaults];
}
/***  key-value store发生变化或存储空间不足**  @param notification 通知对象*/
-(void)keyValueStoreChange:(NSNotification *)notification{NSLog(@"Key-value store change...");
}/***  设置首选项**  @param value 是否自动保存*/
-(void)setSetting:(BOOL)value{//iCloud首选项设置NSUbiquitousKeyValueStore *defaults=[NSUbiquitousKeyValueStore defaultStore];[defaults setBool:value forKey:kSettingAutoSave];[defaults synchronize];//同步
}
@end

运行效果:

5 Passbook

Passbook是苹果推出的一个管理登机牌、会员卡、电影票、优惠券等信息的工具。Passbook就像一个卡包,用于存放你的购物卡、积分卡、电影票、礼品卡等,而这些票据就是一个“Pass”。和物理票据不同的是你可以动态更新Pass的信息,提醒用户优惠券即将过期;甚至如果你的Pass中包含地理位置信息的话当你到达某个商店还可以动态提示用户最近商店有何种优惠活动;当用户将一张团购券添加到Passbook之后,用户到了商店之后Passbook可以自动弹出团购券,店员扫描之后进行消费、积分等等都是Passbook的应用场景。Passbook可以管理多类票据,苹果将其划分为五类:

  1. 登机牌(Boarding pass)
  2. 优惠券(Coupon)
  3. 活动票据、入场券(Event ticket)
  4. 购物卡、积分卡(Store Cards)
  5. 普通票据(自定义票据)(Generic pass)
    苹果的划分一方面出于不同票据功能及展示信息不同,另一方面也是为了统一票据的设计,下面是苹果官方关于五种票据的布局设计布局:


既然一个票据就是一个Pass,那么什么是Pass呢?在iOS中一个Pass其实就是一个.pkpass文件,事实上它是一个Zip压缩包,只是这个压缩包要按照一定的目录结构来设计,下面是一个Pass包的目录结构(注意不同的票据类型会适当删减):
Pass Package

├── icon.png

├── icon@2x.png

├── logo.png

├── logo@2x.png

├── thumbnail.png

├── thumbnail@2x.png

├── background.png

├── background@2x.png

├── strip.png

├── strip@2x.png

├── manifest.json

├── fr.lproj

│ └── pass.strings

├── de.lproj

│ └── pass.strings

├── pass.json

└── signature

也就是说在Passbook应用中显示的内容其实就是一个按照上面文件列表来组织的一个压缩包。在.pkpass文件中除了图标icon、缩略图thumbnail和logo外最重要的就是pass.json、manifest.json和signature。
1.pass.json

这个文件描述了Pass的布局、颜色设置、文本描述信息等,也就是说具体Pass包如何展示其实就是通过这个JSON文件来配置的,关于pass.json的具体配置项在此不再一一介绍,大家可以查看苹果官方帮助文档“Pass Design and Creation”。这里主要说一下其中关键的几个配置项:

passTypeIdentifier:pass唯一标识,这个值类似于App ID,需要从开发者中心创建,并且这个标识必须以“pass”开头(例如下面的示例中取名为“pass.com.cmjstudio.mypassbook”)。

teamIdentifier:团队标识,申请苹果开发者账号时会分配一个唯一的团队标识(可以在苹果开发者中心–查看账户信息中查看”Team ID“)。

barcode:二维码信息配置,主要指定二维码内容、类型、编码格式。

locations:地理位置信息,可以配置相关位置的文本信息。

2.manifest.json

manifest.json从名称可以看出这个文件主要用来描述当前Pass包中的文件目录组织结构。这个文件记录了除“manifest.json”、“signature”外的文件和对应的sha1哈希值(注意:哈希值可以通过”openssl sha1 [ 文件路径]“命令获得)。

3.signature

signature是一个签名文件。虽然manifest.json存储了哈希值,但是大家都知道hash算法是公开的,如何保证一个pass包是合法的,未经修改的呢?那就是使用一个签名文件来验证。

了解了以上内容后基本上对于如何定义一个pass包有了简单的概念。有了pass包之后对于添加pass到passbook应用是比较简单的。但事实上通常大家看到的passbook应用中添加的pass包并不是手动组织的,而是通过程序来完成pass包制作的。举例来说:如果你在美团上购买一张电影票之后,会告诉你一个优惠码,这个优惠码会显示到pass中。由于这个优惠码是动态生成的,所以直接手动制作出一个pass包是不现实的。通常情况下pass包的生成都是通过后台服务器动态生成,然后返回给iOS客户端来读取和添加的,手动制作pass包的情况是比较少的,除非你的票据信息是一成不变的。当然为了演示Passbook应用,这里还是会以手动方式演示一个pass包的生成过程,了解了这个过程之后相信在服务器端通过一些后台程序生成一个pass包也不在话下(下面的生成过程均可通过服务器端编程来实现)。

同其他Apple服务开发类似,做Passbook开发同样需要一些准备工作:

  1. 在苹果开发者中心新建Pass Type ID(例如这里新建一个“pass.com.cmjstudio.mypassbook”),并且基于这个Pass Type ID创建一个Passbook证书(在mac上找到钥匙串,选择”从证书颁发机构请求证书“,生成一个证书请求文件;将此文件上传到对应的Pass Type ID下生成证书文件)如下图:

    下载证书后,将此证书导入Mac中(此处配置的Pass Type ID对应pass.json中的”passTypeIdentitifier“,此证书用于生成签名文件signature。)。
  2. 在Xcode中-Targets-Capabilities启用Pasbook服务,这里需要注意的是”Allow all team pass types“选项,如果勾选了这一项,那么pass.json中的passTypeIdentifier和teamIdentifier就可以是任何团队创建的任何Pass项目了,这里使用前面创建的项目,所以选择”Allow subset of pass types“。

    有了上面的准备工作,下面看一下如何制作一个Pass:
  3. 根据所选择的Passbook类型准备图片素材,由于这里以一个Store Card举例,所以需要准备icon、logo和strip三类图片。
  4. 配置pass.json,这里还是强调一下passTypeIdentifier和teamIdentifier,前者就是上面在开发者中心创建的Pass Type ID(”pass.com.cmjstudio.mypassbook“),后者是对应的团队标识,其他信息根据实际情况配置。
{"formatVersion":1,"passTypeIdentifier":"pass.com.cmjstudio.mypassbook","serialNumber":"54afe978584e3","teamIdentifier":"JB74M3J7RY","authenticationToken":"bc83dde3304d766d5b1aea631827f84c","barcode":{"message":"userName KenshinCui","altText":"会员详情见背面","format":"PKBarcodeFormatQR","messageEncoding":"iso-8859-1"},"locations":[{"longitude":-122.3748889,"latitude":37.6189722},{"longitude":-122.03118,"latitude":37.33182}],"organizationName":"CMJ Coffee","logoText":"CMJ Coffee","description":"","foregroundColor":"rgb(2,2,4)","backgroundColor":"rgb(244,244,254)","storeCard":{"headerFields":[{"key":"date","label":"余额","value":"¥8888.50"}],"secondaryFields":[{"key":"more","label":"VIP会员","value":"Kenshin Cui"}],"backFields":[{"key":"records","label":"消费记录(最近10次)","value":" 9/23    ¥107.00     无糖冰美式\n 9/21    ¥58.00      黑魔卡\n 8/25    ¥44.00      魔卡\n 8/23    ¥107.00     无糖冰美式\n 8/18    ¥107.00     无糖冰美式\n 7/29    ¥58.00      黑魔卡\n 7/26    ¥44.00      魔卡\n 7/13    ¥58.00      黑魔卡\n 7/11    ¥44.00      魔卡\n 6/20    ¥44.00      魔卡\n"},{"key":"phone","label":"联系方式","value":"4008-888-88"},{"key":"terms","label":"会员规则","value":"(1)本电子票涉及多个环节,均为人工操作,用户下单后,1-2个工作日内下发,电子票并不一定能立即收到,建议千品用户提前1天购买,如急需使用,请谨慎下单; \n(2)此劵为电子劵,属特殊产品,一经购买不支持退款(敬请谅解); \n(3)特别注意:下单时请将您需要接收电子票的手机号码,填入收件人信息,如号码填写错误,损失自负;购买成功后,商家于周一至周五每天中午11点和下午17点发2维码/短信到您手机(周六至周日当天晚上发1次),请用户提前购买,凭此信息前往影院前台兑换即可; \n(4)订购成功后,(您在购买下单后的当天,给您发送电子券,系统会自动识别;如果您的手机能接收二维码,那收到的就是彩信,不能接收二维码的话,系统将会自动转成短信发送给您),短信为16位数,如:1028**********; 每个手机号码只可购买6张,如需购买6张以上的请在订单附言填写不同的手机号码,并注明张数(例如团购10张,1350755****号码4张,1860755****号码6张);\n(5)电子票有效期至2016年2月30日,不与其他优惠券同时使用"},{"key":"support","label":"技术支持","value":"http://www.cmjstudio.com\n\n                                            \n                                            \n                                          "}]},"labelColor":"rgb(87,88,93)"
}
  1. 根据pass所需文件创建manifest.json文件,可以通过”openssl sha1 [文件路径]“分别计算出所有文件的哈希值:
{"pass.json":"3292f96c4676aefe7122abb47f86be0d95a6faaf","icon@2x.png":"83438c13dfd7c4a5819a12f6df6dc74b71060289","icon.png":"83438c13dfd7c4a5819a12f6df6dc74b71060289","logo@2x.png":"83438c13dfd7c4a5819a12f6df6dc74b71060289","logo.png":"83438c13dfd7c4a5819a12f6df6dc74b71060289","strip@2x.png":"885ff9639c90147a239a7a77e7adc870d5e047e2","strip.png":"885ff9639c90147a239a7a77e7adc870d5e047e2"
}
  1. 接下来下来准备生成signature文件:
    a.通过前面导入的Pass Type证书(Pass Type ID:pass.com.cmjstudio.mypassbook)导出个人信息交换(.p12)文件并指定密码(假设密码为456789),保存成”mypassbook.p12“(注意是导出证书而不是导出证书下的专用秘钥)。
    b.在钥匙串中找到”Apple Worldwide Developer Relations Certification Authority“证书导出增强保密邮件(.pem),保存成”AWDRCA.pem“。
    c.将.p12证书转化为.pem证书mypassbook.pem(需要输入导出时设置的密码456789),输入如下命令:
openssl pkcs12 -in mypassbook.p12 -clcerts -nokeys -out mypassbook.pem -passin pass:456789

d.从.p12导出秘钥文件mypassbookkey.pem(这里设置密码为123456):

openssl pkcs12 -in mypassbook.p12 -nocerts -out mypassbookkey.pem -passin pass:456789 -passout pass:123456

e.根据AWDRCA.pem、mypassbook.pem、mypassbookkey.pem、manifest.json生成signature文件(按照提示输入mypassbookkey.pem导出时设置的密码123456):

openssl smime -binary -sign -certfile AWDRCA.pem -signer mypassbook.pem -inkey mypassbookkey.pem -in manifest.json -out signature -outform DER
  1. 将icon.png、icon@2x.png、logo.png、logo@2x.png、strip.png、strip@2x.png 、pass.json、manifest.json、signature压缩成pass包(这里命名为”mypassbook.pkpass“)。
zip -r mypassbook.pkpass manifest.json pass.json signature logo.png logo@2x.png icon.png icon@2x.png strip.png strip@2x.png

到这里一个pass制作完成了,此处可以在mac中打开预览:

到这里一个Pass就只做完成了,下面就看一下在iOS中如何添加这个Pass到Passbook,这里直接将上面制作完成的Pass放到Bundle中完成添加。当然这些都是一步步手动完成的,前面也说了实际开发中这个Pass是服务器端来动态生成的,在添加时会从服务器端下载,这个过程在示例中就不再演示。iOS中提供了PassKit.framework框架来进行Passbook开发,下面的代码演示了添加Pass到Passbook应用的过程:

#import "ViewController.h"
#import <PassKit/PassKit.h>@interface ViewController ()<PKAddPassesViewControllerDelegate>
@property (strong,nonatomic) PKPass *pass;//票据
@property (strong,nonatomic) PKAddPassesViewController *addPassesController;//票据添加控制器
@end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];
}
#pragma mark - UI事件
- (IBAction)addPassClick:(UIBarButtonItem *)sender {//确保pass合法,否则无法添加[self addPass];
}#pragma mark - 属性
/***  创建Pass对象**  @return Pass对象*/
-(PKPass *)pass{if (!_pass) {NSString *passPath=[[NSBundle mainBundle] pathForResource:@"mypassbook.pkpass" ofType:nil];NSData *passData=[NSData dataWithContentsOfFile:passPath];NSError *error=nil;_pass=[[PKPass alloc]initWithData:passData error:&error];if (error) {NSLog(@"创建Pass过程中发生错误,错误信息:%@",error.localizedDescription);return nil;}}return _pass;
}/***  创建添加Pass的控制器**  @return <#return value description#>*/
-(PKAddPassesViewController *)addPassesController{if (!_addPassesController) {_addPassesController=[[PKAddPassesViewController alloc]initWithPass:self.pass];_addPassesController.delegate=self;//设置代理}return _addPassesController;
}#pragma mark - 私有方法
-(void)addPass{if (![PKAddPassesViewController canAddPasses]) {NSLog(@"无法添加Pass.");return;}[self presentViewController:self.addPassesController animated:YES completion:nil];
}#pragma mark - PKAddPassesViewController代理方法
-(void)addPassesViewControllerDidFinish:(PKAddPassesViewController *)controller{NSLog(@"添加成功.");[self.addPassesController dismissViewControllerAnimated:YES completion:nil];//添加成功后转到Passbook应用并展示添加的PassNSLog(@"%@",self.pass.passURL);[[UIApplication sharedApplication] openURL:self.pass.passURL];
}
@end

注意:如果大家对于Pass不是太熟悉,刚开始可以使用一些制作工具来完成Pass制作,例如:PassSource、国内的PassQuan等都支持在线制作Pass包。

iOS开发之--内购、GameCenter、iCloud、Passbook功能开发汇总相关推荐

  1. 关于iOS订阅型内购开发

    ####由于公司项目里面有一个类似购买一个时期的产品,原本使用消耗式内购来做,但是被苹果审核拒绝了,苹果建议(要求)使用订阅式内购来做这个,于是就来研究一下 #####1.第一步添加内购产品 首先还是 ...

  2. iOS:苹果内购实践

    iOS 苹果的内购 一.介绍 苹果规定,凡是虚拟的物品(例如:QQ音乐的乐币)进行交易时,都必须走苹果的内购通道,苹果要收取大约30%的抽成,所以不允许接入第三方的支付方式(微信.支付宝等),当然开发 ...

  3. iOS通讯录,蓝牙,内购等开发系列

    –系统应用与系统服务 iOS开发过程中有时候难免会使用iOS内置的一些应用软件和服务,例如QQ通讯录.微信电话本会使用iOS的通讯录,一些第三方软件会在应用内发送短信等.今天将和大家一起学习如何使用系 ...

  4. 1、ios开发之 内购

    大家都知道做iOS开发本身的收入有三种来源:出售应用.内购和广告.国内用户通常很少直接购买应用,因此对于开发者而言(特别是个人开发者),内购和广告收入就成了主要的收入来源.内购营销模式,通常软件本身是 ...

  5. iOS开发支付 — 内购(IAP)

    为什么要使用内购? 如果你购买的商品,是在本App中使用和消耗的,就一定要用内购,否则会被拒绝上线,例如:游戏币.在线书籍.直播中用来打赏用的金币.app中使用的道具等.如果是直接购买商城之类的快递包 ...

  6. iOS IAP应用内购详细步骤和问题总结指南

    最近公司在做APP内购会员功能 遇到了很多问题 总结记录一下 首先一定要区分Apple pay 和IAP内购的区别 可以先去看一下官方文档地址 有每个步骤的详细解释 本篇文章分为:1. 内购支付流程: ...

  7. 【iOS】苹果内购调研

    参考文章 官方文档 iOS开发内购全套图文教程 App Store上架指导 苹果不允许 iOS 应用内置购买(IAP)使用第三方支付方式,那么跨平台的电子书阅读器怎么解决这个问题? 应用内购(In-A ...

  8. IOS OC IPA内购流程

    IOS 内购分为四种商品类型: 消耗品项目 非消耗品项目 自动续期订阅 非续期订阅 基本实现流程 添加支付监听 [[SKPaymentQueue defaultQueue] addTransactio ...

  9. [iOS]swift版内购

    //内购Demo,看代码说话吧 class IAPTestViewController: UIViewController ,SKProductsRequestDelegate, SKPaymentT ...

  10. iOS订阅型内购要点

    订阅型内购, 有一套完整的销售体系, 这一点非常重要. 以往的内购app, 一般上都使用我们自己的销售体系, 然后跟苹果的内购配合起来, 尤其是消耗性内购, 在我们自己的商品体系中, 加上一个ID对应 ...

最新文章

  1. Asynctask源码分析
  2. 求100之内的自然数中能被13整除的最大数
  3. 全球首例猪心移植人体手术:57岁晚期心脏病患者术后状况良好
  4. [Linux] Linux smaps接口文件结构
  5. 百分点大数据技术团队:数据治理“PAI”实施方法论
  6. php后台无法登入,PHP magento后台无法登录问题解决方法
  7. [转]Java 关闭线程的安全方法
  8. cnn之将原始图像转换成矩阵
  9. ionic安装插件常用命令
  10. HDU_oj_2047 阿牛的EOF牛肉面
  11. iOS中的XML解析
  12. IPv4中IP地址分类
  13. 全新 ENVI Modeler 遥感建模工具
  14. 多源多目标统计信息融合 目标跟踪 信息融合 贝叶斯滤波总结
  15. python 爬虫。爬取小说--斗破苍穹
  16. 《玩透嵌入式C的角角落落》当你需要循环体至少执行一次时,选择do
  17. T02 - 005、上海微创软件股份有限公司
  18. 《深入理解Android 卷III》第八章深入理解Android壁纸
  19. 推荐一款不错的嵌入式GUI(玲珑GUI)及在嵌入式linux上的移植
  20. 二叉树的遍历-先序遍历、中序遍历、后序遍历

热门文章

  1. Mac安装双系统的那些坑
  2. 恩智浦智能车主控板分享
  3. 购物网站流程图(收藏)
  4. sql 脚本 昨天的日期获取,今天的前一天的数据获取, 前两个月的今天
  5. mac为什么不支持ntfs,mac读取ntfs移动硬盘软件有哪些
  6. matlab 双均线,一辈子坚持使用双均线
  7. 留学生Essay写作没思路的解决方案
  8. PTA 乙级 1002 写出这个数 (20 分) C++
  9. dijkstra 路径搜索算法的c++简单实现
  10. 快到期的域名如何防止被抢注?