iOS 6 IAP入门
来自Ray: 这是iOS 6 Feast里的第三部教程!这篇由我们之前版本的教程更新而来的。现在它里面包含了最新的代码特征,比如说ARC,Storyboards和新的iOS 6
APIS。
这篇教程来自我们的新书iOS 6 by Tutorials里的Beginning In-App Purchase篇章, 书里面是讲的是另外一个app(iOS Hangman!),而且比现在这个要复杂的多,比如说用到了新的 iOS 6 Hosted Downloads特征。现在让我们开始这篇教程吧!
这篇教程由我们的站长Ray Wenderlich发布, 他是一名独立的应用开发者。
对于一名iOS开发者而言,最爽的事情就莫过于你可以通过多种方式来赚钱,比如说卖免费的应用(包含广告的),但是付费版的是没有广告的,或者你用IAP。
我觉得IAP是一个比较不错的方式,理由如下:
- 除了你赚应用的售价,有些用户可能会为了一些更强大的功能而买一些IAP,比如说小鸟里的神鹰。
- 你可以把你的应用发布为免费(对于大多数人而言,会无脑下载),然后如果他们喜欢它的话,会继续购买里面IAP。
- 一旦你用了IAP,以后你就可以继续在这个应用里添加内容(而不是另外做一个新的来赚钱!)
你能够使用IAP来开发不同的商业模式。比如说,在Wild Fables,我先把它做成一个只有三个故事的免费app,但是想要更多故事的话,则需要购买IAP了。在我即将升级的 Battle Map里,它将是一个付费应用并且里面有一个额外内容的IAP。
在这篇教程里,你将学到如何来通过IAP解锁你应用的其他内容,我也会指导你如何来处理棘手的IAP异步特征。
对于你阅读这篇教程,我假设你是有一定基础的iOS编程概念。如果你还是一个iOS新手,请先看看这个网站里的其他教程吧。
In App Rage
那么这篇教程我们将做一个什么例子呢?让我先介绍一下背景内容吧。。。
最近我对一个叫做rage comics的在线漫画有点痴迷。这些漫画主要描述的情节是,某个人首先如何碰到一个尴尬的情况,最后突然爆发出愤怒或者幽默。
所以我们将做一个 “In App Rage” 的应用,这个应用将卖一些这种漫画。在我们开始写代码前,你首先需要通过iOS Developer Center和iTunes Connect来创建一个应用程序接口。
我们的第一步是创建一个应用的App ID。所以先到iOS Developer Center里选择”App IDs”,然后点击”New App ID”:
先把页面里的description 和bundle identifier填完,填法类似下图:
记住在bundle identifier里应该填入你的特有前缀,可以使用你的域名(如果你有的话),如果它因为你名字不特殊而拒绝了的话,也可以使用其他的一些特殊的名字。
当你做完这些后,点击Submit,OK – 你已经拥有你自己的App ID了!现在你将使用它在iTunes Connect来创建你的新应用了。
登录 iTunes Connect, 点击”Manage Your Applications”,然后”Add New App”。如果你被提示选择iOS App还是Mac OSX App,显然应该选择iOS App。然后输入App名,SKU 号,选择你刚刚创建的Bundle ID:
你可能必须要修改App名,应该app名字应该是独一无二的。为这个应用,我添加了一个标记。对于你来说,只要把RW替换掉就行了。
接下来的两页将要求你填写你的应用信息。现在把空白处都填掉吧 – 这些你稍后也能修改。但不幸的是 – 你必须要每个带*的都填完(包括添加截图,我现在还没有讲到!)
下面是我对此的感受:
如果你得到类似以上的报错信息,那么请先写输入一些暂时性的假数据。现在我提供这些东西给你。有了它,你就能愉快地使用iTunes Connect了。
在你摆平所有的错误后,你应该创建好了你的空白应用了 !
处理IAP
那么为什么你先要创建一个空白应用呢,这是因为IAP代码必须在 iTunes Connect下才能设置。所以现在你有了一个空白应用,点击”Manage In App Purchases”按钮,看到如下所示:
然后在左上角点击 “Create New” :
你会得到一个让你选择IAP类型的页面。记住有两种IAP是经常用到的:
- Consumables. 这种IAP购买来的东西是应该在应用里被消费掉的,比如说游戏里主人公额外的生命数,主人公的现金,以及一些信春哥药物等等。
- Non-Consumables. 这种IAP购买来的东西就是你买了后,就能永久使用的。比如额外的关卡,为解锁的内容等等。
在In App Rage里,你将要贩卖漫画。也就是说,一旦用户购买了,那他们就应该永久拥有它们,所以我们选择Non-Consumable。
提示:任何Non-Consumable的IAP内容应该只适用于用户的当前设备。也就是说,如果用户有两台设备装了你的应用,并且有一台买了你的IAP,所以IAP内容只应该适用于那台买了你IAP的设备,而不是两台都兼顾!
我们待会会讲到如何允许用户得到他们在他们的其他设备上购买的non-consumable IAP内容,我们把这个放到恢复交易章节里面讨论。
尽管对于consumables 来说没有这种要求了。但如果你想要你的consumables 来支持跨设备,你就应该使用iCloud 或者其他的一些技术了。
接下来,你将到一个填写关于你IAP信息的界面。请根据下面截图来完成:
这里面每个fields都是些什么意思呢:
- Reference Name: 对于这个IAP如何显示在iTunes Connect里的名字。这里可以随意填因为这个名字在应用里是不可见的。
- Product ID: 就如在Apple文档里的”product identifier”一样,这个独一无二的字符串将鉴定你的IAP。通常来说,这里应该填写你的bundle id然后在后面加一个独特的名字。
- Cleared for Sale: 是否一旦你的应用在使用状态了,这些IAP对用户来说就变得可以购买了。
- Price Tier: 这个IAP应该卖的价钱。
在你把这些设完后,滚动到下方的Language 部分然后点击添加Language。根据以下信息来填完弹出的表单:
当你稍后在App Store里查询可用的IAP时这个信息将被显示:
你可能想知道为什么这一步是必要的(毕竟,你也可以在你的应用里包含这些信息嘛)呵呵,目前我们的Apple还需要知道我们IAP的价钱。App Store也需要这些信息(比如说当它在顶部显示IAP内容时)。结论是,这一步能让事情为你变得更简单,因为它节省了你把这些信息硬编码到你的应用里而且也能使你轻松激活/关闭IAP。
做完这些后,保存登记信息然后来创建另外一些IAP,结果如下图所示。你不要为descriptions 担心 – 在这篇教程里我们还用不到他们,目前你只能用他们的Display Name。
你可能觉得这过程有点长,我也能理解当你的应用里面有一万个IAP需要设置时,你的烦躁。还好现在你还不是那样,如果你的应用里有这种情况,那请给我画一张 暴走漫画吧:]
检索产品列表
在你允许用户购买你应用的任何IAP之前,你必须先向iTunes Connect服务器提交一个对你应用内包含的IAP的列表查询。
我们当然可以直接在view controller里面写这些代码来完成,但是这样做,我们的代码重复使用率很低。所以我们现在创建一个辅助类来帮我们处理所有的IAP,这样我们就能在整个项目里重复使用它了!
在我们的辅助类从服务器获取IAP列表的同时,它也要跟踪我们是否购买了的项目。它将在NSUserDefaults里对每一个项目保存它的product identifiers。
好的,那我们开始吧!在Xoce,创建一个新项目并选择iOSApplicationMaster-Detail Application模板。项目名设为InAppRage并且把设备选为iPhone,并确保 Use Storyboards 和Use Automatic Reference Counting是勾选了的。
接下来,在你项目的library库里面添加IAP必须要用到的StoreKit库。如图所示,如何操作:
最后一步配置是 – 打开你的Supporting FilesIn App Rage-Info.plist然后把Bundle identifier设为你的App ID:
现在让我们来写代码吧!先新建一个NSObject的子类,将之命名为IAPHelper。
打开IAPHelper.h然后用写入以下内容:
typedef void (^RequestProductsCompletionHandler)(BOOL success, NSArray * products); @interface IAPHelper : NSObject - (id)initWithProductIdentifiers:(NSSet *)productIdentifiers; - (void)requestProductsWithCompletionHandler:(RequestProductsCompletionHandler)completionHandler; @end |
这个类有两个方法:一个初始化方法是用来获取一系列的IAP product identifiers(如com.razeware.inapprage.nightlyrage),还有一个方法是用来从iTunes Connect服务器获取产品信息的。第二方法是异步进行的,所以它使用了一个block参数来通知呼叫者它完成信息获取了。
提示: blocks还困扰着你吗?请查阅这篇文章如何在iOS 5里面使用blocks.
回到IAPHelper.m先添加这些implementation:
// 1 #import "IAPHelper.h" #import <StoreKit/StoreKit.h> // 2 @interface IAPHelper () @end @implementation IAPHelper { // 3 SKProductsRequest * _productsRequest; // 4 RequestProductsCompletionHandler _completionHandler; NSSet * _productIdentifiers; NSMutableSet * _purchasedProductIdentifiers; } @end |
让我们来看看每步都做了什么。
- 首先你需要StoreKit库来连接IAP的APIS,所以你导入了StoreKit。.
- 如果要从StoreKit来获取一系列的产品信息,你需要执行SKProductsRequestDelegate 协议。这里你在类扩展里面标记我们的类会执行SKProductsRequestDelegate协议。
- 你创建了一个实例变量来储存用来提交一系列IAP产品信息查询的SKProductsRequest。当它激活的时候,你需要对此来做一个引用,为了一下目的a)你能知道是否你已经激活了一个请求。b) 当它激活时,你将会得到它已经占用内存的保证。
- 你也需要对completion处理器来进行跟踪,处理器里面会包含你查询的IAP产品列表和这列产品是否有已经购买了的。
接下来,添加initializer:
- (id)initWithProductIdentifiers:(NSSet *)productIdentifiers { if ((self = [super init])) { // Store product identifiers _productIdentifiers = productIdentifiers; // Check for previously purchased products _purchasedProductIdentifiers = [NSMutableSet set]; for (NSString * productIdentifier in _productIdentifiers) { BOOL productPurchased = [[NSUserDefaults standardUserDefaults] boolForKey:productIdentifier]; if (productPurchased) { [_purchasedProductIdentifiers addObject:productIdentifier]; NSLog(@"Previously purchased: %@", productIdentifier); } else { NSLog(@"Not purchased: %@", productIdentifier); } } } return self; } |
这一步会检查是否这个IAP已经购买了(基于查询储存在NSUserDefaults里面product identifiers值),如果检查到已经购买了则把它放入到一个本地的已购买列表里。
接下来,添加从iTunes Connect获取产品信息的方法:
- (void)requestProductsWithCompletionHandler:(RequestProductsCompletionHandler)completionHandler { // 1 _completionHandler = [completionHandler copy]; // 2 _productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:_productIdentifiers]; _productsRequest.delegate = self; [_productsRequest start]; } |
首先,我们copy了一个 compeletion block处理器复制到一个instance variable里面,这样当产品信息请求异步完成时它就能通知呼叫者了。
接着,它创建了一个SKProductsRequest的新实例,这是Apple提供的用来从iTunes Connect提取信息的类。它使用起来十分简单 – 你要同时指定它的一个delegate(根据与SKProductsRequestDelegate协议),然后在开始使用它。
我们把IAPHelper类设为它的delegate,这也就意味着当产品列表请求完成时它会启用它的回调函数,完成了就启用(productsRequest:didReceiveResponse),失败了就启用(request:didFailWithErorr)。
既然说到了delegate 回调函数,那就顺表把它也写了吧:
#pragma mark - SKProductsRequestDelegate - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response { NSLog(@"Loaded list of products..."); _productsRequest = nil; NSArray * skProducts = response.products; for (SKProduct * skProduct in skProducts) { NSLog(@"Found product: %@ %@ %0.2f", skProduct.productIdentifier, skProduct.localizedTitle, skProduct.price.floatValue); } _completionHandler(YES, skProducts); _completionHandler = nil; } - (void)request:(SKRequest *)request didFailWithError:(NSError *)error { NSLog(@"Failed to load list of products."); _productsRequest = nil; _completionHandler(NO, nil); _completionHandler = nil; } |
这里你执行了两个delegate回调函数 – 一个成功请求和一个失败请求。成功请求里,你log出了一些产品的相关信息,比如说产品的ID,本地语言的标题,以及售价。最后,你把_productsRequest这是实例变量设为nil了,因为你已经调用过它了。
让我们来 Build一下,你的项目应该编译起来没有错误了。
子类继承
IAPHelper辅助类已经写好了,现在你可以继承它来服务你的app,使product ID明确于你的app。许多人推荐你应该从一个网站服务器来获得除了product identifiers以外的更多的产品信息,这样你就能动态地添加一些新的IAP而不是只是要求你的应用更新。
这样做很对而且我也极力推荐,但是对于该教程,我们先把事情做的简单一点。对于这个应用,我们只在product identifiers方面编码。
iOSCocoa TouchObjective-C class模板创建一个新文件,然后点击Next,将之命名为RageIAPHelper,让它成为IAPHelper的一个子类,然后点击Next再点击Create。
打开RageIAPHelper.h然后写入下面代码:
#import "IAPHelper.h" @interface RageIAPHelper : IAPHelper + (RageIAPHelper *)sharedInstance; @end |
这里定义了一个静态方法来返回一个这个类的全局变量。
然后到RageIAPHelper.m写入以下代码:
#import "RageIAPHelper.h" @implementation RageIAPHelper + (RageIAPHelper *)sharedInstance { static dispatch_once_t once; static RageIAPHelper * sharedInstance; dispatch_once(&once, ^{ NSSet * productIdentifiers = [NSSet setWithObjects: @"com.razeware.inapprage.drummerrage", @"com.razeware.inapprage.itunesconnectrage", @"com.razeware.inapprage.nightlyrage", @"com.razeware.inapprage.studylikeaboss", @"com.razeware.inapprage.updogsadness", nil]; sharedInstance = [[self alloc] initWithProductIdentifiers:productIdentifiers]; }); return sharedInstance; } @end |
这个共享变量方式采用了Objective-C里的Singleton 模式来返回一个单独的,全局的RageIAPHelper类成员。这里它呼叫了它父类的initializer方法来传入所有你在 iTunes Connect里创建的product identifiers。
别忘了这些product identifiers是我的!你应该用你自己在iTunes Connect创建的product identifiers来替换掉我的!
再次ProductBuild一下,你的项目编译起来应该还是没有问题的。
展示产品
好了,现在各就各位了,是时候让我们在屏幕上看看效果了!
打开 MasterViewController.m 然后写入如下代码:
#import "MasterViewController.h" #import "DetailViewController.h" // 1 #import "RageIAPHelper.h" #import <StoreKit/StoreKit.h> // 2 @interface MasterViewController () { NSArray *_products; } @end @implementation MasterViewController // 3 - (void)viewDidLoad { [super viewDidLoad]; self.title = @"In App Rage"; self.refreshControl = [[UIRefreshControl alloc] init]; [self.refreshControl addTarget:self action:@selector(reload) forControlEvents:UIControlEventValueChanged]; [self reload]; [self.refreshControl beginRefreshing]; } // 4 - (void)reload { _products = nil; [self.tableView reloadData]; [[RageIAPHelper sharedInstance] requestProductsWithCompletionHandler:^(BOOL success, NSArray *products) { if (success) { _products = products; [self.tableView reloadData]; } [self.refreshControl endRefreshing]; }]; } #pragma mark - Table View - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } // 5 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return _products.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath]; SKProduct * product = (SKProduct *) _products[indexPath.row]; cell.textLabel.text = product.localizedTitle; return cell; } @end |
这里我们写了比较多的代码,让我们看看它们都做了什么。
- 首先导入了你写的RageIAPHelper类,还有StoreKit头文件,因为这里面要用到从iTunes Connect返回的SKProduct信息。
- 添加一个 instance variable来储存从iTunes Connect返回的SKProducts。现在tableview里的每个row将显示IAP产品的标题。
- 这pull-to-refresh control(下拉刷新)是我们iOS 6里面tableview一个极其好用的新特征。就像你看到的,它使用起来是如此的简单 – 你首先创建一个UIRefreshControl 的实例然后将它储存到内置的refreshControl 变量里(UITableViewController的一个成员)。然后你注册一个target来调用,当用户下拉table 刷新时 – 我们这里让他调用了reload 方法。当时当table view初次载入时,它默认是不会出现的,所以这里你通过手动调用reload来触发它。
- 当reload被调用时(无论它是初次你手动调用,或者用户下拉刷新调用),它里面调用了你之前在RageIAPHelper写的requestProductsWithCompletionHandler 方法来从iTunes Connect获得IAP 产品信息。当它完成时,里面的block会被调用。在block里它就把产品信息列表存入到本地的instance variable中去,然后刷新table view来展示它们,最后它通知refresh control来停止运动。
- 最后是标准的table view方程式来展示SKProduct 的本地语言标题。
编译并运行,现在你在table view里应该可以看到我们的产品列表信息了!
你看不到? 如果你看不到,请检查以下几样你是否都做对了(这个问题列表是我从论坛里搜集来的):
- 到SettingsiTunes & App Stores, 登出所有帐号,再次尝试登录并且确定你选了一个Sandbox account。
- 检查这个链接 – 如果它没有反应,那么可能iTunes sandbox可能已经down掉了。
- 是否真的已经在你App ID里启用了In-App Purchases?
- 是否你项目的plist 里的Bundle ID和你的App ID匹配?
- 当你使用SKProductRequest时候你是否用了 product ID的全名?
- 当你把你的产品加入iTunes Connect的时候你是否等了几个小时?
- 在iTunes Connect里你的银行信息是否填写完整了?
- 是否尝试过在你的设备里把应用删除然后重新安装呢?
所有都试过还是卡住啊?请在 old forum thread或者这篇教程的评论讨论里和其他读者讨论一下吧。
钱给我!
这篇教程现在已经很长了,但是我们还没到最重要的部分 – 买账收钱!
下面买账收钱的一个基本过程:
- 你创建一个SKPayment 对象,然后再确认用户买的那个产品的productIdentifier。你把它加入到你的payment队列里。
- StoreKit 会提示用户“你确定吗”,然后要求用户输入用户名/密码(如果需要的话),然后交易。然后在发送给你一个成功或者失败的提示。他们也会处理用户已经购买那个IAP产品或者只是重新下载的情况,同时系统也会发消息提示的。
- 你指定一个特别的对象来接受购买通知。这个对象需要处理下载IAP内容的进程(在你的情况下这没有必要,因为这很难写)然后解锁IAP购买的内容。(你的处理情况就是NSUserDefaults 里设置flag,然后把它存储到purchasedProducts arry里)。
不必担心 – 当你看到代码的时候你会发现这并不难。我们还是在IAPHelper 类里面来写它吧,为了整个app的方便使用。让我们开始编辑IAPHelper.h:
// Add to the top of the file #import <StoreKit/StoreKit.h> UIKIT_EXTERN NSString *const IAPHelperProductPurchasedNotification; // Add two new method declarations - (void)buyProduct:(SKProduct *)product; - (BOOL)productPurchased:(NSString *)productIdentifier; |
这里你声明了一个notification 来通知listeners当一个product被购买后。一个方法用来开始一个购买,另一个方法用来判断是否这个产品已经被购买。
接下来切换回 IAPHelper.m 然后添加如下代码:
- (BOOL)productPurchased:(NSString *)productIdentifier { return [_purchasedProductIdentifiers containsObject:productIdentifier]; } - (void)buyProduct:(SKProduct *)product { NSLog(@"Buying %@...", product.productIdentifier); SKPayment * payment = [SKPayment paymentWithProduct:product]; [[SKPaymentQueue defaultQueue] addPayment:payment]; } |
首先这里确认这个产品是否允许被购买,如果它允许则把这次购买标记为in-progress,然后把这个作为SKPayment添加到SKPaymentQueue里面。
这些就是能为你赚钱的代码了。但是你是否觉得,当你赚钱时你的用户会这么想“你Y给我拿钱然后闭嘴”。
我是这么想的,如果你赚了你用户的钱,你最好给它们一些好的回馈!(毕竟,你不是希腊政府出来的。)
所以你需要添加一些代码来判断是否这桩“交易”完成了,然后你适当处理。
要做到这效果也是非常简单的。首先,修改IAPHelper class extension来标记该类来执行SKPaymentTransactionObserver:
@interface IAPHelper () |
然后把这个方法添加到你initWithProductIdentifiers:方法的最下面(在if条件里哦):
[[SKPaymentQueue defaultQueue] addTransactionObserver:self]; |
现在当IAPHelper被初始化时,它会把它自己设为SKPaymentQueue交易的observer 。换句话说就是,当某个用户购买你的某些东西后Apple会通知你的!
关于此还有件重要的事需要提醒你。这种情况很有可能发生,当用户开始一次购买后(并且已经付钱了),但是苹果对他还没有回馈购买成功或者失败,用户就已经失去网络连接了(或者关掉你的app)。那这名用户是否算是购买成功了呢?很明显,这应该算是的,不然很多人要暴怒了!
幸运的是,苹果对此已经有了解决方案。这个方案是苹果会对你应用里的任何未完全处理的教程进程进行跟踪,并且会通知transaction observer 他们的信息。但为了我们有一个更好的效果,你应该在你的应用里尽早的注册你的transaction observer。
所以,让我们切到 AppDelegate.m然后 import:
#import "RageIAPHelper.h"
|
然后在application:didFinishLaunchingWithOptions:里写入如下代码:
[RageIAPHelper sharedInstance]; |
现在,一旦你的应用启动了,它就会创建一个singleton 的RageIAPHelper类。也就是说刚刚你修改的initWithProducts:会被调用,里面注册它自己transaction observer。那么你会被通知到所有还没被完全完成的交易。
这里,你还是需要执行SKPaymentTransactionObserver 协议,所以切回到IAPHelper.m 然后添加如下代码:
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { for (SKPaymentTransaction * transaction in transactions) { switch (transaction.transactionState) { case SKPaymentTransactionStatePurchased: [self completeTransaction:transaction]; break; case SKPaymentTransactionStateFailed: [self failedTransaction:transaction]; break; case SKPaymentTransactionStateRestored: [self restoreTransaction:transaction]; default: break; } }; } |
事实上,这是该协议唯一的一个required 方法。它会给你一系列实时的交易状况,你所要做的就是列举他们,然后根据他们的不同状态来各自处理。为了保持我们代码的简洁性,你会调用不同的方法来应对交易状态。如:completed ,failed ,或者successfully restored的。
Completed and failed我相信你能够理解,但是restored是啥玩意呢?还记得我们之前在选择“In-App Purchase Types”类型时,我提到过应该有一种方式来恢复用户的non-consumable内购。这点比较重要,当用户在不同设备里安装了同一个的app(或者删掉并重新安装了),然后他们想得到他们之前购买的IAP内容。稍后,你就会做到这个,现在你先需要有一个概念。
接下来到我们重要的内容了 – 执行completeTransaction,restoreTransaction和failedTransaction 方法。把这些加入到这个文件中:
- (void)completeTransaction:(SKPaymentTransaction *)transaction { NSLog(@"completeTransaction..."); [self provideContentForProductIdentifier:transaction.payment.productIdentifier]; [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; } - (void)restoreTransaction:(SKPaymentTransaction *)transaction { NSLog(@"restoreTransaction..."); [self provideContentForProductIdentifier:transaction.originalTransaction.payment.productIdentifier]; [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; } - (void)failedTransaction:(SKPaymentTransaction *)transaction { NSLog(@"failedTransaction..."); if (transaction.error.code != SKErrorPaymentCancelled) { NSLog(@"Transaction error: %@", transaction.error.localizedDescription); } [[SKPaymentQueue defaultQueue] finishTransaction: transaction]; } |
completeTransaction 和restoreTransaction 做了相同的事情 – 他们都呼叫了一个辅助方法来提供内容(接下来你会写到)。
failedTransaction 稍有一点不同。它呼叫了一个辅助方法来通知用户购买失败(接下来你也会写到),把该购买从标记中去掉,然后完成此次交易。
提示: 对于调用finishTransaction这一点是非常重要的,不然的话StoreKit 是不会知道你已经完成了交易处理过程,当你的应用启动时它还是会持续的提交交易!
最终,把如下改变添加如文件中:
// 加在文件内的上面 NSString *const IAPHelperProductPurchasedNotification = @"IAPHelperProductPurchasedNotification"; // 添加新方法 - (void)provideContentForProductIdentifier:(NSString *)productIdentifier { [_purchasedProductIdentifiers addObject:productIdentifier]; [[NSUserDefaults standardUserDefaults] setBool:YES forKey:productIdentifier]; [[NSUserDefaults standardUserDefaults] synchronize]; [[NSNotificationCenter defaultCenter] postNotificationName:IAPHelperProductPurchasedNotification object:productIdentifier userInfo:nil]; } |
当一个产品被购买后,这个方法把这个product identifier加到已购产品的array里面,并在NSUserDefaults并把它标记为已购买,然后发送一个通知,这样其他类就能注意到这次购买了。
你现在已经完成购买代码了 – 你所有需要做的就是把这些信息表现到用户界面里!首先打开MainStoryboard.storyboard,然后选中 prototype table view cell,并设它为Subtitle:
然后打开 MasterViewController.m写入如下代码:
// Add new instance variable to class extension NSNumberFormatter * _priceFormatter; // 在viewDidLoad最下面 _priceFormatter = [[NSNumberFormatter alloc] init]; [_priceFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4]; [_priceFormatter setNumberStyle:NSNumberFormatterCurrencyStyle]; // 在tableView:cellForRowAtIndexPath (在return cell之前)最下面 [_priceFormatter setLocale:product.priceLocale]; cell.detailTextLabel.text = [_priceFormatter stringFromNumber:product.price]; if ([[RageIAPHelper sharedInstance] productPurchased:product.productIdentifier]) { cell.accessoryType = UITableViewCellAccessoryCheckmark; cell.accessoryView = nil; } else { UIButton *buyButton = [UIButton buttonWithType:UIButtonTypeRoundedRect]; buyButton.frame = CGRectMake(0, 0, 72, 37); [buyButton setTitle:@"Buy" forState:UIControlStateNormal]; buyButton.tag = indexPath.row; [buyButton addTarget:self action:@selector(buyButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; cell.accessoryType = UITableViewCellAccessoryNone; cell.accessoryView = buyButton; } |
现在你通过一个price formatter的辅助把cell的subtitle来显示价钱了。同时,如果一个用户还没购买一个产品,你会把那个产品的accessory view设为一个button。如果用户已经购买了一个产品,你就把用checkmark来替代button。
把button的触发方法也加进去吧:
- (void)buyButtonTapped:(id)sender { UIButton *buyButton = (UIButton *)sender; SKProduct *product = _products[buyButton.tag]; NSLog(@"Buying %@...", product.productIdentifier); [[RageIAPHelper sharedInstance] buyProduct:product]; } |
这个方法判断了哪个产品购买按钮被点击了,然后呼叫你刚写的购买产品方法。
记住当一个购买完成后,它会发出一个通知。所以让我们来监听那个通知,通过通知我们在恰当的时候更新cell的accessoryView。
- (void)viewWillAppear:(BOOL)animated { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(productPurchased:) name:IAPHelperProductPurchasedNotification object:nil]; } - (void)viewWillDisappear:(BOOL)animated { [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (void)productPurchased:(NSNotification *)notification { NSString * productIdentifier = notification.object; [_products enumerateObjectsUsingBlock:^(SKProduct * product, NSUInteger idx, BOOL *stop) { if ([product.productIdentifier isEqualToString:productIdentifier]) { [self.tableView reloadRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:idx inSection:0]] withRowAnimation:UITableViewRowAnimationFade]; *stop = YES; } }]; } |
你还差一点就成功了!但首先,我们先来简短的说下Apple 帐号。
In App Purchases, Accounts, and the Sandbox
当你用XCode在运行你的app的时候,你不是在对着In-App Purchase服务器运行你的程序 – 你只是在对着sandbox服务器运行你的程序。
这也就意味着你可以毫无顾虑的购买东西,等。但你需要设置一个测试帐号,并且确认你在你的设备上已经登出store了,这样你就能看到整个过程了。
创建用户的话,先登录到iTunes Connect然后点”Manage Users”。再点击”Test User”,然后根据步骤来创建完成一个test user。这样你就能在你的sandbox服务器上做假购买了。
然后拿起你的iPhone并且确认你已经退出当前账户了。你可以到Settings里面然后点击“store”,再点击“Sign Out”。
最后,运行你的app,并且点击购买一个暴怒漫画。输入你的test user帐号信息,如果运行正常,这个购买了的产品cell旁边就应该有个check mark了。
等等 – 你的漫画呢?!因为你还没全部购买所以看不到呢!呵呵!
好吧,我知道你想画什么暴怒漫画了….
好了,我们这篇教程已经足够长了,把暴怒漫画加进去也不是我们这次的主题,所以就把它做为我们的课外作业留给你吧!:]
恢复交易
等等!最后一件事情要提醒的了。
这些日子来,对于任何一个有IAP的应用有一个需求,就是想要在里面放一个按钮,用户点了以后就能恢复他们的交易了。就如我之前提到的,如果用户想在他们的不同设备上来连接他们的IAP,这将变的非常有用。
好消息是,这种实现异常简单,因为你用的是一个健硕的库。
首先打开IAPHelper.h 然后声明如下方法:
- (void)restoreCompletedTransactions; |
然后在 IAPHelper.m 执行如下代码:
- (void)restoreCompletedTransactions { [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; } |
哈哈 – 能不能再简单一点!这个将与iTunes Connect取得连接然后再找出哪些non-consumable产品用户已经购买!在paymentQueue:updatedTransactions里的SKPaymentTransactionStateRestored情况下被调用!
这段代码将为你连接iTunes Connect然后找出用户已经买了哪些 non-consumable 产品。它是通过被paymentQueue:updatedTransactions里的SKPaymentTransactionStateRestored 情况下呼叫到,然后解锁你已经购买了的IAP!
接下来要做的就是我们要呼叫这个方法了。在MasterViewController.m写如下代码:
// 在viewDidLoad里的最后 self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Restore" style:UIBarButtonItemStyleBordered target:self action:@selector(restoreTapped:)]; // 添加新方法 - (void)restoreTapped:(id)sender { [[RageIAPHelper sharedInstance] restoreCompletedTransactions]; } |
现在在你的设备里把的app删掉重装,请注意到现在checkmark已经不在了,因为你把所有的NSUserDefaults都删除了。
现在点击Restore按钮,系统会用一点时间来加载数据,加载完后你可以看到checkmark又出现了 – 非常不错哦!
接下来去哪?
这是我们的 例子代码 包含了我们在这篇教程里所有的代码。
就如之前提到的,如果你想把漫画展示功能也放进去app里的话,你首先至少应该看看这些漫画 – 或者你也可以在线编辑一个:]
另外如果你想学到关于IAP的更多内容的话,你应该看看我们的新书 iOS 6 by Tutorials。这里面有巨详细的IAP教程包含以下方面:
- iOS 6 主办下载:你将学到如何在苹果官方服务器主办你的下载!
- 完全基于服务器的系统:你将学到如何添加新的IAP而不用通过你自己的服务器更新你的app!
- 发票验证:你将学到如何在你自己的服务器上验证IAP的发票!
- SKStoreProductViewController: 你将学到如何使用iOS 6的新方法在你的app里面贩卖来自iTunes Store的东西!
- iTunes 搜索API:你将学到如何使用iTunes Search API来动态显示你在store里搜索到的东西!
- Download progress and display: 你将学到如何来显示下载过程和状态,给用户展示一个不错的下载过程!
- 最后 最后的最后: 恕我直言,我这份教程是市面上最好最完成的!
所以妥妥地请查看iOS 6 by Tutorials如果你想要学的更多的话。与此同时,关于此教程如果你有任何问题,请参与下方的讨论!
最后给大家来一点娱乐的,下面这幅漫画来自Jayant C Varma! :]
作者:
这篇教程由我们的站长 Ray Wenderlich发布,他是一名独立的应用开发者。
译者:
我目前是一名自由iOS开发者,很高兴由我来为大家翻译这篇教程,有什么建议可以发信(xiekw2010@gmail.com)至我。