大家都知道做iOS開發本身的收入有三種來源:出售應用、內購和廣告。國內用戶通常很少直接購買應用,因此對於開發者而言(特別是個人開發者),內購和廣告收入就成了主要的收入來源。內購營銷模式,通常軟件本身是不收費的,但是要獲得某些特權就必須購買一些道具,而內購的過程是由蘋果官方統一來管理的,所以和Game Center一樣,在開發內購程序之前要做一些准備工作(下面的准備工作主要是針對真機的,模擬器省略Provisioning Profile配置過程):
前四步和Game Center基本完全一致,只是在選擇服務時不是選擇Game Center而是要選擇內購服務(In-App Purchase)。
到iTuens Connect中設置“App 內購買項目”,這裡仍然以上面的“KCTest”項目為例,假設這個足球競技游戲中有三種道具,分別為“強力手套”(增強防御)、“金球”(增加金球率)和“能量瓶”(提供足夠體力),前兩者是非消耗品只用一次性購買,後者是消耗品用完一次必須再次購買。In-App_Purchase_Config
到iTunes Connect中找到“協議、稅務和銀行業務”增加“iOS Paid Applications”協議,並完成所有配置後等待審核通過(注意這一步如果不設置在應用程序中無法獲得可購買產品)。
在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之後,下面看一下應用內購買的流程:
通過SKProductRequest獲得可購買產品SKProduct數組(SKProductRequest會根據程序的Bundle ID去對應的內購配置中獲取指定ID的產品對象),這個過程中需要知道產品標識(必須和iTuens Connect中的對應起來),可以存儲到沙盒中也可以存儲到數據庫中(下面的Demo中定義成了宏定義)。
請求完成後可以在SKProductRequest的-(void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response代理方法中獲得SKProductResponse對象,這個對象中保存了products屬性表示可用產品對象數組。
給SKPaymentQueue設置一個監聽者來獲得交易的狀態(它類似於一個代理),監聽者通過-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction方法反饋交易的變化狀態(通常在此方法中可以根據交易成功、恢復成功等狀態來做一些處理)。
一旦用戶決定購買某個產品(SKProduct),就可以根據SKProduct來創建一個對應的支付對象SKPayment,只要將這個對象加入到SKPaymentQueue中就會觸發購買行為(將訂單提交到蘋果服務器),一旦一個交易發生變化就會觸發SKPaymentQueue監聽者來反饋交易情況。
交易提交給蘋果服務器之後如果不出意外的話通常就會彈出一個確認購買的對話框,引導用戶完成交易,最終完成交易後(通常是完成交易,用戶點擊”好“)會調用交易監聽者-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction方法將此次交易的所有交易對象SKPaymentTransaction數組返回,可以通過交易狀態判斷交易情況。
通常一次交易完成後需要對本次交易進行驗證,避免越獄機器模擬蘋果官方的反饋造成交易成功假象。蘋果官方提供了一個驗證的URL,只要將交易成功後的憑證(這個憑證從iOS7之後在交易成功會會存儲到沙盒中)傳遞給這個地址就會給出交易狀態和本次交易的詳細信息,通過這些信息(通常可以根據交易狀態、Bundler ID、ProductID等確認)可以標識出交易是否真正完成。
對於非消耗品,用戶在完成購買後如果用戶使用其他機器登錄或者用戶卸載重新安裝應用後通常希望這些非消耗品能夠恢復(事實上如果不恢復用戶再次購買也不會成功)。調用SKPaymentQueue的restoreCompletedTransactions就可以完成恢復,恢復後會調用交易監聽者的paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction方法反饋恢復的交易(也就是已購買的非消耗品交易,注意這個過程中如果沒有非消耗品可恢復,是不會調用此方法的)。
下面通過一個示例程序演示內購和恢復的整個過程,程序界面大致如下:
主界面中展示了所有可購買產品和售價,以及購買情況。
選擇一個產品點”購買“可以購買此商品,購買完成後刷新購買狀態(如果是非消耗品則顯示已購買,如果是消耗品則顯示購買個數)。
程序卸載後重新安裝可以點擊”恢復購買“來恢復已購買的非消耗品。
In-App_Purchase_Layout
程序代碼:
// // KCMainTableViewController.m // kctest // // Created by Kenshin Cui on 14/4/5. // Copyright (c) 2015年 cmjstudio. All rights reserved. // #import "KCMainTableViewController.h" #import #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 () @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 運行效果(這是程序在卸載後重新安裝的運行效果,卸載前已經購買”強力手套“,因此程序運行後點擊了”恢復購買“): In_AppPurchase_Effect 擴展--廣告 上面也提到做iOS開發另一收益來源就是廣告,在iOS上有很多廣告服務可以集成,使用比較多的就是蘋果的iAd、谷歌的Admob,下面簡單演示一下如何使用iAd來集成廣告。使用iAd集成廣告的過程比較簡單,首先引入iAd.framework框架,然後創建ADBannerView來展示廣告,通常會設置ADBannerView的代理方法來監聽廣告點擊並在廣告加載失敗時隱藏廣告展示控件。下面的代碼簡單的演示了這個過程: // // ViewController.m // kctest // // Created by Kenshin Cui on 14/4/5. // Copyright (c) 2015年 cmjstudio. All rights reserved. // #import "ViewController.h" #import @interface ViewController () @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