iOS開發過程中有時候難免會使用iOS內置的一些應用軟件和服務,例如QQ通訊錄、微信電話本會使用iOS的通訊錄,一些第三方軟件會在應用內發送短信等。今天將和大家一起學習如何使用系統應用、使用系統服務:
在開發某些應用時可能希望能夠調用iOS系統內置的電話、短信、郵件、浏覽器應用,此時你可以直接使用UIApplication的OpenURL:方法指定特定的協議來打開不同的系統應用。常用的協議如下:
打電話:tel:或者tel://、telprompt:或telprompt://(撥打電話前有提示)
發短信:sms:或者sms://
發送郵件:mailto:或者mailto://
啟動浏覽器:http:或者http://
下面以一個簡單的demo演示如何調用上面幾種系統應用:
// // ViewController.m // iOSSystemApplication // // Created by Kenshin Cui on 14/04/05. // Copyright (c) 2014年 cmjstudio. All rights reserved. // #import "ViewController.h" @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; } #pragma mark - UI事件 //打電話 - (IBAction)callClicK:(UIButton *)sender { NSString *phoneNumber=@"18500138888"; // NSString *url=[NSString stringWithFormat:@"tel://%@",phoneNumber];//這種方式會直接撥打電話 NSString *url=[NSString stringWithFormat:@"telprompt://%@",phoneNumber];//這種方式會提示用戶確認是否撥打電話 [self openUrl:url]; } //發送短信 - (IBAction)sendMessageClick:(UIButton *)sender { NSString *phoneNumber=@"18500138888"; NSString *url=[NSString stringWithFormat:@"sms://%@",phoneNumber]; [self openUrl:url]; } //發送郵件 - (IBAction)sendEmailClick:(UIButton *)sender { NSString *mailAddress=@"[email protected]"; NSString *url=[NSString stringWithFormat:@"mailto://%@",mailAddress]; [self openUrl:url]; } //浏覽網頁 - (IBAction)browserClick:(UIButton *)sender { NSString *url=@"http://www.cnblogs.com/kenshincui"; [self openUrl:url]; } #pragma mark - 私有方法 -(void)openUrl:(NSString *)urlStr{ //注意url中包含協議名稱,iOS根據協議確定調用哪個應用,例如發送郵件是“sms://”其中“//”可以省略寫成“sms:”(其他協議也是如此) NSURL *url=[NSURL URLWithString:urlStr]; UIApplication *application=[UIApplication sharedApplication]; if(![application canOpenURL:url]){ NSLog(@"無法打開\"%@\",請確保此應用已經正確安裝.",url); return; } [[UIApplication sharedApplication] openURL:url]; } @end
不難發現當openURL:方法只要指定一個URL Schame並且已經安裝了對應的應用程序就可以打開此應用。當然,如果是自己開發的應用也可以調用openURL方法來打開。假設你現在開發了一個應用A,如果用戶機器上已經安裝了此應用,並且在應用B中希望能夠直接打開A。那麼首先需要確保應用A已經配置了Url Types,具體方法就是在plist文件中添加URL types節點並配置URL Schemas作為具體協議,配置URL identifier作為這個URL的唯一標識,如下圖:
然後就可以調用openURL方法像打開系統應用一樣打開第三方應用程序了:
//打開第三方應用 - (IBAction)thirdPartyApplicationClick:(UIButton *)sender { NSString *url=@"cmj://myparams"; [self openUrl:url]; }
就像調用系統應用一樣,協議後面可以傳遞一些參數(例如上面傳遞的myparams),這樣一來在應用中可以在AppDelegate的-(BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation代理方法中接收參數並解析。
-(BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation{ NSString *str=[NSString stringWithFormat:@"url:%@,source application:%@,params:%@",url,sourceApplication,[url host]]; NSLog(@"%@",str); return YES;//是否打開 }
調用系統內置的應用來發送短信、郵件相當簡單,但是這麼操作也存在著一些弊端:當你點擊了發送短信(或郵件)操作之後直接啟動了系統的短信(或郵件)應用程序,我們的應用其實此時已經處於一種掛起狀態,發送完(短信或郵件)之後無法自動回到應用界面。如果想要在應用程序內部完成這些操作則可以利用iOS中的MessageUI.framework,它提供了關於短信和郵件的UI接口供開發者在應用程序內部調用。從框架名稱不難看出這是一套UI接口,提供有現成的短信和郵件的編輯界面,開發人員只需要通過編程的方式給短信和郵件控制器設置對應的參數即可。
在MessageUI.framework中主要有兩個控制器類分別用於發送短信(MFMessageComposeViewController)和郵件(MFMailComposeViewController),它們均繼承於UINavigationController。由於兩個類使用方法十分類似,這裡主要介紹一下MFMessageComposeViewController使用步驟:
下面自定義一個發送短信的界面演示MFMessageComposeViewController的使用:
用戶通過在此界面輸入短信信息點擊“發送信息”調用MFMessageComposeViewController界面來展示或進一步編輯信息,點擊MFMessageComposeViewController中的“發送”來完成短信發送工作,當然用戶也可能點擊“取消”按鈕回到前一個短信編輯頁面。
實現代碼:
// // KCSendMessageViewController.m // iOSSystemApplication // // Created by Kenshin Cui on 14/04/05. // Copyright (c) 2014年 cmjstudio. All rights reserved. // #import "KCSendMessageViewController.h" #import@interface KCSendMessageViewController () @property (weak, nonatomic) IBOutlet UITextField *receivers; @property (weak, nonatomic) IBOutlet UITextField *body; @property (weak, nonatomic) IBOutlet UITextField *subject; @property (weak, nonatomic) IBOutlet UITextField *attachments; @end @implementation KCSendMessageViewController #pragma mark - 控制器視圖方法 - (void)viewDidLoad { [super viewDidLoad]; } #pragma mark - UI事件 - (IBAction)sendMessageClick:(UIButton *)sender { //如果能發送文本信息 if([MFMessageComposeViewController canSendText]){ MFMessageComposeViewController *messageController=[[MFMessageComposeViewController alloc]init]; //收件人 messageController.recipients=[self.receivers.text componentsSeparatedByString:@","]; //信息正文 messageController.body=self.body.text; //設置代理,注意這裡不是delegate而是messageComposeDelegate messageController.messageComposeDelegate=self; //如果運行商支持主題 if([MFMessageComposeViewController canSendSubject]){ messageController.subject=self.subject.text; } //如果運行商支持附件 if ([MFMessageComposeViewController canSendAttachments]) { /*第一種方法*/ //messageController.attachments=...; /*第二種方法*/ NSArray *attachments= [self.attachments.text componentsSeparatedByString:@","]; if (attachments.count>0) { [attachments enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { NSString *path=[[NSBundle mainBundle]pathForResource:obj ofType:nil]; NSURL *url=[NSURL fileURLWithPath:path]; [messageController addAttachmentURL:url withAlternateFilename:obj]; }]; } /*第三種方法*/ // NSString *path=[[NSBundle mainBundle]pathForResource:@"photo.jpg" ofType:nil]; // NSURL *url=[NSURL fileURLWithPath:path]; // NSData *data=[NSData dataWithContentsOfURL:url]; /** * attatchData:文件數據 * uti:統一類型標識,標識具體文件類型,詳情查看:幫助文檔中System-Declared Uniform Type Identifiers * fileName:展現給用戶看的文件名稱 */ // [messageController addAttachmentData:data typeIdentifier:@"public.image" filename:@"photo.jpg"]; } [self presentViewController:messageController animated:YES completion:nil]; } } #pragma mark - MFMessageComposeViewController代理方法 //發送完成,不管成功與否 -(void)messageComposeViewController:(MFMessageComposeViewController *)controller didFinishWithResult:(MessageComposeResult)result{ switch (result) { case MessageComposeResultSent: NSLog(@"發送成功."); break; case MessageComposeResultCancelled: NSLog(@"取消發送."); break; default: NSLog(@"發送失敗."); break; } [self dismissViewControllerAnimated:YES completion:nil]; } @end
這裡需要強調一下:
其實只要熟悉了MFMessageComposeViewController之後,那麼用於發送郵件的MFMailComposeViewController用法和步驟完全一致,只是功能不同。下面看一下MFMailComposeViewController的使用:
// // KCSendEmailViewController.m // iOSSystemApplication // // Created by Kenshin Cui on 14/04/05. // Copyright (c) 2014年 cmjstudio. All rights reserved. // #import "KCSendEmailViewController.h" #import@interface KCSendEmailViewController () @property (weak, nonatomic) IBOutlet UITextField *toTecipients;//收件人 @property (weak, nonatomic) IBOutlet UITextField *ccRecipients;//抄送人 @property (weak, nonatomic) IBOutlet UITextField *bccRecipients;//密送人 @property (weak, nonatomic) IBOutlet UITextField *subject; //主題 @property (weak, nonatomic) IBOutlet UITextField *body;//正文 @property (weak, nonatomic) IBOutlet UITextField *attachments;//附件 @end @implementation KCSendEmailViewController - (void)viewDidLoad { [super viewDidLoad]; } #pragma mark - UI事件 - (IBAction)sendEmailClick:(UIButton *)sender { //判斷當前是否能夠發送郵件 if ([MFMailComposeViewController canSendMail]) { MFMailComposeViewController *mailController=[[MFMailComposeViewController alloc]init]; //設置代理,注意這裡不是delegate,而是mailComposeDelegate mailController.mailComposeDelegate=self; //設置收件人 [mailController setToRecipients:[self.toTecipients.text componentsSeparatedByString:@","]]; //設置抄送人 if (self.ccRecipients.text.length>0) { [mailController setCcRecipients:[self.ccRecipients.text componentsSeparatedByString:@","]]; } //設置密送人 if (self.bccRecipients.text.length>0) { [mailController setBccRecipients:[self.bccRecipients.text componentsSeparatedByString:@","]]; } //設置主題 [mailController setSubject:self.subject.text]; //設置內容 [mailController setMessageBody:self.body.text isHTML:YES]; //添加附件 if (self.attachments.text.length>0) { NSArray *attachments=[self.attachments.text componentsSeparatedByString:@","] ; [attachments enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { NSString *file=[[NSBundle mainBundle] pathForResource:obj ofType:nil]; NSData *data=[NSData dataWithContentsOfFile:file]; [mailController addAttachmentData:data mimeType:@"image/jpeg" fileName:obj];//第二個參數是mimeType類型,jpg圖片對應image/jpeg }]; } [self presentViewController:mailController animated:YES completion:nil]; } } #pragma mark - MFMailComposeViewController代理方法 -(void)mailComposeController:(MFMailComposeViewController *)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error{ switch (result) { case MFMailComposeResultSent: NSLog(@"發送成功."); break; case MFMailComposeResultSaved://如果存儲為草稿(點取消會提示是否存儲為草稿,存儲後可以到系統郵件應用的對應草稿箱找到) NSLog(@"郵件已保存."); break; case MFMailComposeResultCancelled: NSLog(@"取消發送."); break; default: NSLog(@"發送失敗."); break; } if (error) { NSLog(@"發送郵件過程中發生錯誤,錯誤信息:%@",error.localizedDescription); } [self dismissViewControllerAnimated:YES completion:nil]; } @end
運行效果:
iOS中帶有一個Contacts應用程序來管理聯系人,但是有些時候我們希望自己的應用能夠訪問或者修改這些信息,這個時候就要用到AddressBook.framework框架。iOS中的通訊錄是存儲在數據庫中的,由於iOS的權限設計,開發人員是不允許直接訪問通訊錄數據庫的,必須依靠AddressBook提供的標准API來實現通訊錄操作。通過AddressBook.framework開發者可以從底層去操作AddressBook.framework的所有信息,但是需要注意的是這個框架是基於C語言編寫的,無法使用ARC來管理內存,開發者需要自己管理內存。下面大致介紹一下通訊錄操作中常用的類型:
由於通訊錄操作的關鍵是對ABRecordRef的操作,首先看一下常用的操作通訊錄記錄的方法:
ABPersonCreate():創建一個類型為“kABPersonType”的ABRecordRef。
ABRecordCopyValue():取得指定屬性的值。
ABRecordCopyCompositeName():取得聯系人(或群組)的復合信息(對於聯系人則包括:姓、名、公司等信息,對於群組則返回組名稱)。
ABRecordSetValue():設置ABRecordRef的屬性值。注意在設置ABRecordRef的值時又分為單值屬性和多值屬性:單值屬性設置只要通過ABRecordSetValue()方法指定屬性名和值即可;多值屬性則要先通過創建一個ABMutableMultiValueRef類型的變量,然後通過ABMultiValueAddValueAndLabel()方法依次添加屬性值,最後通過ABRecordSetValue()方法將ABMutableMultiValueRef類型的變量設置為記錄值。
ABRecordRemoveValue():刪除指定的屬性值。
注意:
由於聯系人訪問時(讀取、設置、刪除時)牽扯到大量聯系人屬性,可以到ABPerson.h中查詢或者直接到幫助文檔“Personal Information Properties”
通訊錄的訪問步驟一般如下:
下面就通過一個示例演示一下如何通過ABAddressBook.framework訪問通訊錄,這個例子中通過一個UITableViewController模擬一下通訊錄的查看、刪除、添加操作。
主控制器視圖,用於顯示聯系人,修改刪除聯系人:
KCContactViewController.h
// // KCTableViewController.h // AddressBook // // Created by Kenshin Cui on 14/04/05. // Copyright (c) 2014年 cmjstudio. All rights reserved. // #import/** * 定義一個協議作為代理 */ @protocol KCContactDelegate //新增或修改聯系人 -(void)editPersonWithFirstName:(NSString *)firstName lastName:(NSString *)lastName workNumber:(NSString *)workNumber; //取消修改或新增 -(void)cancelEdit; @end @interface KCContactTableViewController : UITableViewController @end
KCContactViewController.m
// // KCTableViewController.m // AddressBook // // Created by Kenshin Cui on 14/04/05. // Copyright (c) 2014年 cmjstudio. All rights reserved. // #import "KCContactTableViewController.h" #import #import "KCAddPersonViewController.h" @interface KCContactTableViewController ()@property (assign,nonatomic) ABAddressBookRef addressBook;//通訊錄 @property (strong,nonatomic) NSMutableArray *allPerson;//通訊錄所有人員 @property (assign,nonatomic) int isModify;//標識是修改還是新增,通過選擇cell進行導航則認為是修改,否則視為新增 @property (assign,nonatomic) UITableViewCell *selectedCell;//當前選中的單元格 @end @implementation KCContactTableViewController #pragma mark - 控制器視圖 - (void)viewDidLoad { [super viewDidLoad]; //請求訪問通訊錄並初始化數據 [self requestAddressBook]; } //由於在整個視圖控制器周期內addressBook都駐留在內存中,所有當控制器視圖銷毀時銷毀該對象 -(void)dealloc{ if (self.addressBook!=NULL) { CFRelease(self.addressBook); } } #pragma mark - UI事件 //點擊刪除按鈕 - (IBAction)trashClick:(UIBarButtonItem *)sender { self.tableView.editing=!self.tableView.editing; } #pragma mark - UITableView數據源方法 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.allPerson.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *identtityKey=@"myTableViewCellIdentityKey1"; UITableViewCell *cell=[tableView dequeueReusableCellWithIdentifier:identtityKey]; if(cell==nil){ cell=[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:identtityKey]; } //取得一條人員記錄 ABRecordRef recordRef=(__bridge ABRecordRef)self.allPerson[indexPath.row]; //取得記錄中得信息 NSString *firstName=(__bridge NSString *) ABRecordCopyValue(recordRef, kABPersonFirstNameProperty);//注意這裡進行了強轉,不用自己釋放資源 NSString *lastName=(__bridge NSString *)ABRecordCopyValue(recordRef, kABPersonLastNameProperty); ABMultiValueRef phoneNumbersRef= ABRecordCopyValue(recordRef, kABPersonPhoneProperty);//獲取手機號,注意手機號是ABMultiValueRef類,有可能有多條 // NSArray *phoneNumbers=(__bridge NSArray *)ABMultiValueCopyArrayOfAllValues(phoneNumbersRef);//取得CFArraryRef類型的手機記錄並轉化為NSArrary long count= ABMultiValueGetCount(phoneNumbersRef); // for(int i=0;i 0) { cell.detailTextLabel.text=(__bridge NSString *)(ABMultiValueCopyValueAtIndex(phoneNumbersRef, 0)); } if(ABPersonHasImageData(recordRef)){//如果有照片數據 NSData *imageData= (__bridge NSData *)(ABPersonCopyImageData(recordRef)); cell.imageView.image=[UIImage imageWithData:imageData]; }else{ cell.imageView.image=[UIImage imageNamed:@"avatar"];//沒有圖片使用默認頭像 } //使用cell的tag存儲記錄id cell.tag=ABRecordGetRecordID(recordRef); return cell; } - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { ABRecordRef recordRef=(__bridge ABRecordRef )self.allPerson[indexPath.row]; [self removePersonWithRecord:recordRef];//從通訊錄刪除 [self.allPerson removeObjectAtIndex:indexPath.row];//從數組移除 [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{ self.isModify=1; self.selectedCell=[tableView cellForRowAtIndexPath:indexPath]; [self performSegueWithIdentifier:@"AddPerson" sender:self]; } #pragma mark - Navigation - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { if([segue.identifier isEqualToString:@"AddPerson"]){ UINavigationController *navigationController=(UINavigationController *)segue.destinationViewController; //根據導航控制器取得添加/修改人員的控制器視圖 KCAddPersonViewController *addPersonController=(KCAddPersonViewController *)navigationController.topViewController; addPersonController.delegate=self; //如果是通過選擇cell進行的導航操作說明是修改,否則為添加 if (self.isModify) { UITableViewCell *cell=self.selectedCell; addPersonController.recordID=(ABRecordID)cell.tag;//設置 NSArray *array=[cell.textLabel.text componentsSeparatedByString:@" "]; if (array.count>0) { addPersonController.firstNameText=[array firstObject]; } if (array.count>1) { addPersonController.lastNameText=[array lastObject]; } addPersonController.workPhoneText=cell.detailTextLabel.text; } } } #pragma mark - KCContact代理方法 -(void)editPersonWithFirstName:(NSString *)firstName lastName:(NSString *)lastName workNumber:(NSString *)workNumber{ if (self.isModify) { UITableViewCell *cell=self.selectedCell; NSIndexPath *indexPath= [self.tableView indexPathForCell:cell]; [self modifyPersonWithRecordID:(ABRecordID)cell.tag firstName:firstName lastName:lastName workNumber:workNumber]; [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationRight]; }else{ [self addPersonWithFirstName:firstName lastName:lastName workNumber:workNumber];//通訊簿中添加信息 [self initAllPerson];//重新初始化數據 [self.tableView reloadData]; } self.isModify=0; } -(void)cancelEdit{ self.isModify=0; } #pragma mark - 私有方法 /** * 請求訪問通訊錄 */ -(void)requestAddressBook{ //創建通訊錄對象 self.addressBook=ABAddressBookCreateWithOptions(NULL, NULL); //請求訪問用戶通訊錄,注意無論成功與否block都會調用 ABAddressBookRequestAccessWithCompletion(self.addressBook, ^(bool granted, CFErrorRef error) { if (!granted) { NSLog(@"未獲得通訊錄訪問權限!"); } [self initAllPerson]; }); } /** * 取得所有通訊錄記錄 */ -(void)initAllPerson{ //取得通訊錄訪問授權 ABAuthorizationStatus authorization= ABAddressBookGetAuthorizationStatus(); //如果未獲得授權 if (authorization!=kABAuthorizationStatusAuthorized) { NSLog(@"尚未獲得通訊錄訪問授權!"); return ; } //取得通訊錄中所有人員記錄 CFArrayRef allPeople= ABAddressBookCopyArrayOfAllPeople(self.addressBook); self.allPerson=(__bridge NSMutableArray *)allPeople; //釋放資源 CFRelease(allPeople); } /** * 刪除指定的記錄 * * @param recordRef 要刪除的記錄 */ -(void)removePersonWithRecord:(ABRecordRef)recordRef{ ABAddressBookRemoveRecord(self.addressBook, recordRef, NULL);//刪除 ABAddressBookSave(self.addressBook, NULL);//刪除之後提交更改 } /** * 根據姓名刪除記錄 */ -(void)removePersonWithName:(NSString *)personName{ CFStringRef personNameRef=(__bridge CFStringRef)(personName); CFArrayRef recordsRef= ABAddressBookCopyPeopleWithName(self.addressBook, personNameRef);//根據人員姓名查找 CFIndex count= CFArrayGetCount(recordsRef);//取得記錄數 for (CFIndex i=0; i 新增或修改控制器視圖,用於顯示一個聯系人的信息或者新增一個聯系人:
KCAddPersonViewController.h
// // KCAddPersonViewController.h // AddressBook // // kABPersonFirstNameProperty // Copyright (c) 2014年 cmjstudio. All rights reserved. // #import@protocol KCContactDelegate; @interface KCAddPersonViewController : UIViewController @property (assign,nonatomic) int recordID;//通訊錄記錄id,如果ID不為0則代表修改否則認為是新增 @property (strong,nonatomic) NSString *firstNameText; @property (strong,nonatomic) NSString *lastNameText; @property (strong,nonatomic) NSString *workPhoneText; @property (strong,nonatomic) id delegate; @end KCAddPersonViewController.m
// // KCAddPersonViewController.m // AddressBook // // kABPersonFirstNameProperty // Copyright (c) 2014年 cmjstudio. All rights reserved. // #import "KCAddPersonViewController.h" #import "KCContactTableViewController.h" @interface KCAddPersonViewController () @property (weak, nonatomic) IBOutlet UITextField *firstName; @property (weak, nonatomic) IBOutlet UITextField *lastName; @property (weak, nonatomic) IBOutlet UITextField *workPhone; @end @implementation KCAddPersonViewController - (void)viewDidLoad { [super viewDidLoad]; [self setupUI]; } #pragma mark - UI事件 - (IBAction)cancelClick:(UIBarButtonItem *)sender { [self.delegate cancelEdit]; [self dismissViewControllerAnimated:YES completion:nil]; } - (IBAction)doneClick:(UIBarButtonItem *)sender { //調用代理方法 [self.delegate editPersonWithFirstName:self.firstName.text lastName:self.lastName.text workNumber:self.workPhone.text]; [self dismissViewControllerAnimated:YES completion:nil]; } #pragma mark - 私有方法 -(void)setupUI{ if (self.recordID) {//如果ID不為0則認為是修改,此時需要初始化界面 self.firstName.text=self.firstNameText; self.lastName.text=self.lastNameText; self.workPhone.text=self.workPhoneText; } } @end運行效果:
備注:
1.上文中所指的以Ref結尾的對象事實上是該對象的指針(或引用),在C語言的框架中多數類型會以Ref結尾,這個類型本身就是一個指針,定義時不需要加“*”。
2.通常方法中包含copy、create、new、retain等關鍵字的方法創建的變量使用之後需要調用對應的release方法釋放。例如:使用ABPersonCreate();創建完ABRecordRef變量後使用CFRelease()方法釋放。
3.在與很多C語言框架交互時可以都存在Obj-C和C語言類型之間的轉化(特別是Obj-C和Core Foundation框架中的一些轉化),此時可能會用到橋接,只要在強轉之後前面加上”__bridge”即可,經過橋接轉化後的類型不需要再去手動維護內存,也就不需要使用對應的release方法釋放內存。
4.AddressBook框架中很多類型的創建、屬性設置等都是以這個類型名開發頭的方法來創建的,事實上如果大家熟悉了其他框架會發現也都是類似的,這是Apple開發中約定俗成的命名規則(特別是C語言框架)。例如:要給ABRecordRef類型的變量設置屬性則可以通過ABRecordSetValue()方法完成。
AddressBookUI
使用AddressBook.framework來操作通訊錄特點就是可以對通訊錄有更加精確的控制,但是缺點就是面對大量C語言API稍嫌麻煩,於是Apple官方提供了另一套框架供開發者使用,那就是AddressBookUI.framework。例如前面查看、新增、修改人員的界面這個框架就提供了現成的控制器視圖供開發者使用。下面是這個框架中提供的控制器視圖:
- ABPersonViewController:用於查看聯系人信息(可設置編輯)。需要設置displayedPerson屬性來設置要顯示或編輯的聯系人。
- ABNewPersonViewController:用於新增聯系人信息。
- ABUnknownPersonViewController:用於顯示一個未知聯系人(尚未保存的聯系人)信息。需要設置displayedPerson屬性來設置要顯示的未知聯系人。
以上三個控制器視圖均繼承於UIViewController,在使用過程中必須使用一個UINavigationController進行包裝,否則只能看到視圖內容無法進行操作(例如對於ABNewPersonViewController如果不使用UINavigationController進行包裝則沒有新增和取消按鈕),同時注意包裝後的控制器視圖不需要處理具體新增、修改邏輯(增加和修改的處理邏輯對應的控制器視圖內部已經完成),但是必須處理控制器的關閉操作(調用dismissViewControllerAnimated::方法),並且可以通過代理方法獲得新增、修改的聯系人。下面看一下三個控制器視圖的代理方法:
1.ABPersonViewController的displayViewDelegate代理方法:
-(BOOL)personViewController:(ABPersonViewController *)personViewController shouldPerformDefaultActionForPerson:(ABRecordRef)person property:(ABPropertyID)property identifier:(ABMultiValueIdentifier)identifier:此方法會在選擇了一個聯系人屬性後觸發,四個參數分別代表:使用的控制器視圖、所查看的聯系人、所選則的聯系人屬性、該屬性是否是多值屬性。
2.ABNewPersonViewController的newPersonViewDelegate代理方法:
-(void)newPersonViewController:(ABNewPersonViewController *)newPersonView didCompleteWithNewPerson:(ABRecordRef)person:點擊取消或完成後觸發,如果參數中的person為NULL說明點擊了取消,否則說明點擊了完成。無論是取消還是完成操作,此方法調用時保存操作已經進行完畢,不需要在此方法中自己保存聯系人信息。
3.ABUnkownPersonViewcontroller的unkownPersonViewDelegate代理方法:
-(void)unknownPersonViewController:(ABUnknownPersonViewController *)unknownCardViewController didResolveToPerson:(ABRecordRef)person:保存此聯系人時調用,調用後將此聯系人返回。
-(BOOL)unknownPersonViewController:(ABUnknownPersonViewController *)personViewController shouldPerformDefaultActionForPerson:(ABRecordRef)person property:(ABPropertyID)property identifier:(ABMultiValueIdentifier)identifier:選擇一個位置聯系人屬性之後執行,返回值代表是否執行默認的選擇操作(例如如果是手機號,默認操作會撥打此電話)
除了上面三類控制器視圖在AddressBookUI中還提供了另外一個控制器視圖ABPeoplePickerNavigationController,它與之前介紹的UIImagePickerController、MPMediaPickerController類似,只是他是用來選擇一個聯系人的。這個控制器視圖本身繼承於UINavigationController,視圖自身的“組”、“取消”按鈕操作不需要開發者來完成(例如開發者不用在點擊取消是關閉當前控制器視圖,它自身已經實現了關閉方法),當然這裡主要說一下這個控制器視圖的peoplePickerDelegate代理方法:
-(void)peoplePickerNavigationController:(ABPeoplePickerNavigationController *)peoplePicker didSelectPerson:(ABRecordRef)person:選擇一個聯系人後執行。此代理方法實現後代理方法“-(void)peoplePickerNavigationController:(ABPeoplePickerNavigationController *)peoplePicker didSelectPerson:(ABRecordRef)person property:(ABPropertyID)property identifier:(ABMultiValueIdentifier)identifier”不會再執行。並且一旦實現了這個代理方法用戶只能選擇到聯系人視圖,無法查看具體聯系人的信息。
-(void)peoplePickerNavigationControllerDidCancel:(ABPeoplePickerNavigationController *)peoplePicker:用戶點擊取消後執行。
-(void)peoplePickerNavigationController:(ABPeoplePickerNavigationController *)peoplePicker didSelectPerson:(ABRecordRef)person property:(ABPropertyID)property identifier:(ABMultiValueIdentifier)identifier:選擇聯系人具體的屬性後執行,注意如果要執行此方法則不能實現-(void)peoplePickerNavigationController:(ABPeoplePickerNavigationController *)peoplePicker didSelectPerson:(ABRecordRef)person代理方法,此時如果點擊一個具體聯系人會導航到聯系人詳細信息界面,用戶點擊具體的屬性後觸發此方法。
下面就看一下上面四個控制器視圖的使用方法,在下面的程序中定義了四個按鈕,點擊不同的按鈕調用不同的控制器視圖用於演示:
// // ViewController.m // AddressBookUI // // Created by Kenshin Cui on 14/04/05. // Copyright (c) 2014年 cmjstudio. All rights reserved. // #import "ViewController.h" #import @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; } #pragma mark - UI事件 //添加聯系人 - (IBAction)addPersonClick:(UIButton *)sender { ABNewPersonViewController *newPersonController=[[ABNewPersonViewController alloc]init]; //設置代理 newPersonController.newPersonViewDelegate=self; //注意ABNewPersonViewController必須包裝一層UINavigationController才能使用,否則不會出現取消和完成按鈕,無法進行保存等操作 UINavigationController *navigationController=[[UINavigationController alloc]initWithRootViewController:newPersonController]; [self presentViewController:navigationController animated:YES completion:nil]; } // - (IBAction)unknownPersonClick:(UIButton *)sender { ABUnknownPersonViewController *unknownPersonController=[[ABUnknownPersonViewController alloc]init]; //設置未知人員 ABRecordRef recordRef=ABPersonCreate(); ABRecordSetValue(recordRef, kABPersonFirstNameProperty, @"Kenshin", NULL); ABRecordSetValue(recordRef, kABPersonLastNameProperty, @"Cui", NULL); ABMultiValueRef multiValueRef=ABMultiValueCreateMutable(kABStringPropertyType); ABMultiValueAddValueAndLabel(multiValueRef, @"18500138888", kABHomeLabel, NULL); ABRecordSetValue(recordRef, kABPersonPhoneProperty, multiValueRef, NULL); unknownPersonController.displayedPerson=recordRef; //設置代理 unknownPersonController.unknownPersonViewDelegate=self; //設置其他屬性 unknownPersonController.allowsActions=YES;//顯示標准操作按鈕 unknownPersonController.allowsAddingToAddressBook=YES;//是否允許將聯系人添加到地址簿 CFRelease(multiValueRef); CFRelease(recordRef); //使用導航控制器包裝 UINavigationController *navigationController=[[UINavigationController alloc]initWithRootViewController:unknownPersonController]; [self presentViewController:navigationController animated:YES completion:nil]; } - (IBAction)showPersonClick:(UIButton *)sender { ABPersonViewController *personController=[[ABPersonViewController alloc]init]; //設置聯系人 ABAddressBookRef addressBook=ABAddressBookCreateWithOptions(NULL, NULL); ABRecordRef recordRef= ABAddressBookGetPersonWithRecordID(addressBook, 1);//取得id為1的聯系人記錄 personController.displayedPerson=recordRef; //設置代理 personController.personViewDelegate=self; //設置其他屬性 personController.allowsActions=YES;//是否顯示發送信息、共享聯系人等按鈕 personController.allowsEditing=YES;//允許編輯 // personController.displayedProperties=@[@(kABPersonFirstNameProperty),@(kABPersonLastNameProperty)];//顯示的聯系人屬性信息,默認顯示所有信息 //使用導航控制器包裝 UINavigationController *navigationController=[[UINavigationController alloc]initWithRootViewController:personController]; [self presentViewController:navigationController animated:YES completion:nil]; } - (IBAction)selectPersonClick:(UIButton *)sender { ABPeoplePickerNavigationController *peoplePickerController=[[ABPeoplePickerNavigationController alloc]init]; //設置代理 peoplePickerController.peoplePickerDelegate=self; [self presentViewController:peoplePickerController animated:YES completion:nil]; } #pragma mark - ABNewPersonViewController代理方法 //完成新增(點擊取消和完成按鈕時調用),注意這裡不用做實際的通訊錄增加工作,此代理方法調用時已經完成新增,當保存成功的時候參數中得person會返回保存的記錄,如果點擊取消person為NULL -(void)newPersonViewController:(ABNewPersonViewController *)newPersonView didCompleteWithNewPerson:(ABRecordRef)person{ //如果有聯系人信息 if (person) { NSLog(@"%@ 信息保存成功.",(__bridge NSString *)(ABRecordCopyCompositeName(person))); }else{ NSLog(@"點擊了取消."); } //關閉模態視圖窗口 [self dismissViewControllerAnimated:YES completion:nil]; } #pragma mark - ABUnknownPersonViewController代理方法 //保存未知聯系人時觸發 -(void)unknownPersonViewController:(ABUnknownPersonViewController *)unknownCardViewController didResolveToPerson:(ABRecordRef)person{ if (person) { NSLog(@"%@ 信息保存成功!",(__bridge NSString *)(ABRecordCopyCompositeName(person))); } [self dismissViewControllerAnimated:YES completion:nil]; } //選擇一個人員屬性後觸發,返回值YES表示觸發默認行為操作,否則執行代理中自定義的操作 -(BOOL)unknownPersonViewController:(ABUnknownPersonViewController *)personViewController shouldPerformDefaultActionForPerson:(ABRecordRef)person property:(ABPropertyID)property identifier:(ABMultiValueIdentifier)identifier{ if (person) { NSLog(@"選擇了屬性:%i,值:%@.",property,(__bridge NSString *)ABRecordCopyValue(person, property)); } return NO; } #pragma mark - ABPersonViewController代理方法 //選擇一個人員屬性後觸發,返回值YES表示觸發默認行為操作,否則執行代理中自定義的操作 -(BOOL)personViewController:(ABPersonViewController *)personViewController shouldPerformDefaultActionForPerson:(ABRecordRef)person property:(ABPropertyID)property identifier:(ABMultiValueIdentifier)identifier{ if (person) { NSLog(@"選擇了屬性:%i,值:%@.",property,(__bridge NSString *)ABRecordCopyValue(person, property)); } return NO; } #pragma mark - ABPeoplePickerNavigationController代理方法 //選擇一個聯系人後,注意這個代理方法實現後屬性選擇的方法將不會再調用 -(void)peoplePickerNavigationController:(ABPeoplePickerNavigationController *)peoplePicker didSelectPerson:(ABRecordRef)person{ if (person) { NSLog(@"選擇了%@.",(__bridge NSString *)(ABRecordCopyCompositeName(person))); } } //選擇屬性之後,注意如果上面的代理方法實現後此方法不會被調用 //-(void)peoplePickerNavigationController:(ABPeoplePickerNavigationController *)peoplePicker didSelectPerson:(ABRecordRef)person property:(ABPropertyID)property identifier:(ABMultiValueIdentifier)identifier{ // if (person && property) { // NSLog(@"選擇了屬性:%i,值:%@.",property,(__bridge NSString *)ABRecordCopyValue(person, property)); // } //} //點擊取消按鈕 -(void)peoplePickerNavigationControllerDidCancel:(ABPeoplePickerNavigationController *)peoplePicker{ NSLog(@"取消選擇."); } @end運行效果:
注意:
為了讓大家可以更加清楚的看到幾個控制器視圖的使用,這裡並沒有結合前面的UITableViewController來使用,事實上大家結合前面UITableViewController可以做一個完善的通訊錄應用。
藍牙
隨著藍牙低功耗技術BLE(Bluetooth Low Energy)的發展,藍牙技術正在一步步成熟,如今的大部分移動設備都配備有藍牙4.0,相比之前的藍牙技術耗電量大大降低。從iOS的發展史也不難看出蘋果目前對藍牙技術也是越來越關注,例如蘋果於2013年9月發布的iOS7就配備了iBeacon技術,這項技術完全基於藍牙傳輸。但是眾所周知蘋果的設備對於權限要求也是比較高的,因此在iOS中並不能像Android一樣隨意使用藍牙進行文件傳輸(除非你已經越獄)。在iOS中進行藍牙傳輸應用開發常用的框架有如下幾種:
GameKit.framework:iOS7之前的藍牙通訊框架,從iOS7開始過期,但是目前多數應用還是基於此框架。
MultipeerConnectivity.framework:iOS7開始引入的新的藍牙通訊開發框架,用於取代GameKit。
CoreBluetooth.framework:功能強大的藍牙開發框架,要求設備必須支持藍牙4.0。
前兩個框架使用起來比較簡單,但是缺點也比較明顯:僅僅支持iOS設備,傳輸內容僅限於沙盒或者照片庫中用戶選擇的文件,並且第一個框架只能在同一個應用之間進行傳輸(一個iOS設備安裝應用A,另一個iOS設備上安裝應用B是無法傳輸的)。當然CoreBluetooth就擺脫了這些束縛,它不再局限於iOS設備之間進行傳輸,你可以通過iOS設備向Android、Windows Phone以及其他安裝有藍牙4.0芯片的智能設備傳輸,因此也是目前智能家居、無線支付等熱門智能設備所推崇的技術。
GameKit
其實從名稱來看這個框架並不是專門為了支持藍牙傳輸而設計的,它是為游戲設計的。而很多游戲中會用到基於藍牙的點對點信息傳輸,因此這個框架中集成了藍牙傳輸模塊。前面也說了這個框架本身有很多限制,但是在iOS7之前的很多藍牙傳輸都是基於此框架的,所以有必要對它進行了解。GameKit中的藍牙使用設計很簡單,並沒有給開發者留有太多的復雜接口,而多數連接細節開發者是不需要關注的。GameKit中提供了兩個關鍵類來操作藍牙連接:
GKPeerPickerController:藍牙查找、連接用的視圖控制器,通常情況下應用程序A打開後會調用此控制器的show方法來展示一個藍牙查找的視圖,一旦發現了另一個同樣在查找藍牙連接的客戶客戶端B就會出現在視圖列表中,此時如果用戶點擊連接B,B客戶端就會詢問用戶是否允許A連接B,如果允許後A和B之間建立一個藍牙連接。
GKSession:連接會話,主要用於發送和接受傳輸數據。一旦A和B建立連接GKPeerPickerController的代理方法會將A、B兩者建立的會話(GKSession)對象傳遞給開發人員,開發人員拿到此對象可以發送和接收數據。
其實理解了上面兩個類之後,使用起來就比較簡單了,下面就以一個圖片發送程序來演示GameKit中藍牙的使用。此程序一個客戶端運行在模擬器上作為客戶端A,另一個運行在iPhone真機上作為客戶端B(注意A、B必須運行同一個程序,GameKit藍牙開發是不支持兩個不同的應用傳輸數據的)。兩個程序運行之後均調用GKPeerPickerController來發現周圍藍牙設備,一旦A發現了B之後就開始連接B,然後iOS會詢問用戶是否接受連接,一旦接受之後就會調用GKPeerPickerController的-(void)peerPickerController:(GKPeerPickerController *)picker didConnectPeer:(NSString *)peerID toSession:(GKSession *)session代理方法,在此方法中可以獲得連接的設備id(peerID)和連接會話(session);此時可以設置會話的數據接收句柄(相當於一個代理)並保存會話以便發送數據時使用;一旦一端(假設是A)調用會話的sendDataToAllPeers: withDataMode: error:方法發送數據,此時另一端(假設是B)就會調用句柄的- (void) receiveData:(NSData *)data fromPeer:(NSString *)peer inSession: (GKSession *)session context:(void *)context方法,在此方法可以獲得發送數據並處理。下面是程序代碼:
// // ViewController.m // GameKit // // Created by Kenshin Cui on 14/04/05. // Copyright (c) 2014年 cmjstudio. All rights reserved. // #import "ViewController.h" #import@interface ViewController () @property (weak, nonatomic) IBOutlet UIImageView *imageView;//照片顯示視圖 @property (strong,nonatomic) GKSession *session;//藍牙連接會話 @end @implementation ViewController #pragma mark - 控制器視圖方法 - (void)viewDidLoad { [super viewDidLoad]; GKPeerPickerController *pearPickerController=[[GKPeerPickerController alloc]init]; pearPickerController.delegate=self; [pearPickerController show]; } #pragma mark - UI事件 - (IBAction)selectClick:(UIBarButtonItem *)sender { UIImagePickerController *imagePickerController=[[UIImagePickerController alloc]init]; imagePickerController.delegate=self; [self presentViewController:imagePickerController animated:YES completion:nil]; } - (IBAction)sendClick:(UIBarButtonItem *)sender { NSData *data=UIImagePNGRepresentation(self.imageView.image); NSError *error=nil; [self.session sendDataToAllPeers:data withDataMode:GKSendDataReliable error:&error]; if (error) { NSLog(@"發送圖片過程中發生錯誤,錯誤信息:%@",error.localizedDescription); } } #pragma mark - GKPeerPickerController代理方法 /** * 連接到某個設備 * * @param picker 藍牙點對點連接控制器 * @param peerID 連接設備藍牙傳輸ID * @param session 連接會話 */ -(void)peerPickerController:(GKPeerPickerController *)picker didConnectPeer:(NSString *)peerID toSession:(GKSession *)session{ self.session=session; NSLog(@"已連接客戶端設備:%@.",peerID); //設置數據接收處理句柄,相當於代理,一旦數據接收完成調用它的-receiveData:fromPeer:inSession:context:方法處理數據 [self.session setDataReceiveHandler:self withContext:nil]; [picker dismiss];//一旦連接成功關閉窗口 } #pragma mark - 藍牙數據接收方法 - (void) receiveData:(NSData *)data fromPeer:(NSString *)peer inSession: (GKSession *)session context:(void *)context{ UIImage *image=[UIImage imageWithData:data]; self.imageView.image=image; UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil); NSLog(@"數據發送成功!"); } #pragma mark - UIImagePickerController代理方法 -(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info{ self.imageView.image=[info objectForKey:UIImagePickerControllerOriginalImage]; [self dismissViewControllerAnimated:YES completion:nil]; } -(void)imagePickerControllerDidCancel:(UIImagePickerController *)picker{ [self dismissViewControllerAnimated:YES completion:nil]; } @end 運行效果(左側是真機,右側是模擬器,程序演示了兩個客戶端互發圖片的場景:首先是模擬器發送圖片給真機,然後真機發送圖片給模擬器):
MultipeerConnectivity
前面已經說了GameKit相關的藍牙操作類從iOS7已經全部過期,蘋果官方推薦使用MultipeerConnectivity代替。但是應該了解,MultipeerConnectivity.framework並不僅僅支持藍牙連接,准確的說它是一種支持Wi-Fi網絡、P2P Wi-Fi已經藍牙個人局域網的通信框架,它屏蔽了具體的連接技術,讓開發人員有統一的接口編程方法。通過MultipeerConnectivity連接的節點之間可以安全的傳遞信息、流或者其他文件資源而不必通過網絡服務。此外使用MultipeerConnectivity進行近場通信也不再局限於同一個應用之間傳輸,而是可以在不同的應用之間進行數據傳輸(當然如果有必要的話你仍然可以選擇在一個應用程序之間傳輸)。
要了解MultipeerConnectivity的使用必須要清楚一個概念:廣播(Advertisting)和發現(Disconvering),這很類似於一種Client-Server模式。假設有兩台設備A、B,B作為廣播去發送自身服務,A作為發現的客戶端。一旦A發現了B就試圖建立連接,經過B同意二者建立連接就可以相互發送數據。在使用GameKit框架時,A和B既作為廣播又作為發現,當然這種情況在MultipeerConnectivity中也很常見。
A.廣播
無論是作為服務器端去廣播還是作為客戶端去發現廣播服務,那麼兩個(或更多)不同的設備之間必須要有區分,通常情況下使用MCPeerID對象來區分一台設備,在這個設備中可以指定顯示給對方查看的名稱(display name)。另外不管是哪一方,還必須建立一個會話MCSession用於發送和接受數據。通常情況下會在會話的-(void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state代理方法中跟蹤會話狀態(已連接、正在連接、未連接);在會話的-(void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID代理方法中接收數據;同時還會調用會話的-(void)sendData: toPeers:withMode: error:方法去發送數據。
廣播作為一個服務器去發布自身服務,供周邊設備發現連接。在MultipeerConnectivity中使用MCAdvertiserAssistant來表示一個廣播,通常創建廣播時指定一個會話MCSession對象將廣播服務和會話關聯起來。一旦調用廣播的start方法周邊的設備就可以發現該廣播並可以連接到此服務。在MCSession的代理方法中可以隨時更新連接狀態,一旦建立了連接之後就可以通過MCSession的connectedPeers獲得已經連接的設備。
B.發現
前面已經說過作為發現的客戶端同樣需要一個MCPeerID來標志一個客戶端,同時會擁有一個MCSession來監聽連接狀態並發送、接受數據。除此之外,要發現廣播服務,客戶端就必須要隨時查找服務來連接,在MultipeerConnectivity中提供了一個控制器MCBrowserViewController來展示可連接和已連接的設備(這類似於GameKit中的GKPeerPickerController),當然如果想要自己定制一個界面來展示設備連接的情況你可以選擇自己開發一套UI界面。一旦通過MCBroserViewController選擇一個節點去連接,那麼作為廣播的節點就會收到通知,詢問用戶是否允許連接。由於初始化MCBrowserViewController的過程已經指定了會話MCSession,所以連接過程中會隨時更新會話狀態,一旦建立了連接,就可以通過會話的connected屬性獲得已連接設備並且可以使用會話發送、接受數據。
下面用兩個不同的應用程序來演示使用MultipeerConnectivity的使用過程,其中一個應用運行在模擬器中作為廣播節點,另一個運行在iPhone真機上作為發現節點,並且實現兩個節點的圖片互傳。
首先看一下作為廣播節點的程序:
界面:
點擊“開始廣播”來發布服務,一旦有節點連接此服務就可以使用“選擇照片”來從照片庫中選取一張圖片並發送到所有已連接節點。
程序:
// // ViewController.m // MultipeerConnectivity_Advertiser // // Created by Kenshin Cui on 14/04/05. // Copyright (c) 2015年 cmjstudio. All rights reserved. // #import "ViewController.h" #import@interface ViewController () @property (strong,nonatomic) MCSession *session; @property (strong,nonatomic) MCAdvertiserAssistant *advertiserAssistant; @property (strong,nonatomic) UIImagePickerController *imagePickerController; @property (weak, nonatomic) IBOutlet UIImageView *photo; @end @implementation ViewController #pragma mark - 控制器視圖事件 - (void)viewDidLoad { [super viewDidLoad]; //創建節點,displayName是用於提供給周邊設備查看和區分此服務的 MCPeerID *peerID=[[MCPeerID alloc]initWithDisplayName:@"KenshinCui_Advertiser"]; _session=[[MCSession alloc]initWithPeer:peerID]; _session.delegate=self; //創建廣播 _advertiserAssistant=[[MCAdvertiserAssistant alloc]initWithServiceType:@"cmj-stream" discoveryInfo:nil session:_session]; _advertiserAssistant.delegate=self; } #pragma mark - UI事件 - (IBAction)advertiserClick:(UIBarButtonItem *)sender { //開始廣播 [self.advertiserAssistant start]; } - (IBAction)selectClick:(UIBarButtonItem *)sender { _imagePickerController=[[UIImagePickerController alloc]init]; _imagePickerController.delegate=self; [self presentViewController:_imagePickerController animated:YES completion:nil]; } #pragma mark - MCSession代理方法 -(void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state{ NSLog(@"didChangeState"); switch (state) { case MCSessionStateConnected: NSLog(@"連接成功."); break; case MCSessionStateConnecting: NSLog(@"正在連接..."); break; default: NSLog(@"連接失敗."); break; } } //接收數據 -(void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID{ NSLog(@"開始接收數據..."); UIImage *image=[UIImage imageWithData:data]; [self.photo setImage:image]; //保存到相冊 UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil); } #pragma mark - MCAdvertiserAssistant代理方法 #pragma mark - UIImagePickerController代理方法 -(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info{ UIImage *image=[info objectForKey:UIImagePickerControllerOriginalImage]; [self.photo setImage:image]; //發送數據給所有已連接設備 NSError *error=nil; [self.session sendData:UIImagePNGRepresentation(image) toPeers:[self.session connectedPeers] withMode:MCSessionSendDataUnreliable error:&error]; NSLog(@"開始發送數據..."); if (error) { NSLog(@"發送數據過程中發生錯誤,錯誤信息:%@",error.localizedDescription); } [self.imagePickerController dismissViewControllerAnimated:YES completion:nil]; } -(void)imagePickerControllerDidCancel:(UIImagePickerController *)picker{ [self.imagePickerController dismissViewControllerAnimated:YES completion:nil]; } @end 再看一下作為發現節點的程序:
界面:
點擊“查找設備”浏覽可用服務,點擊服務建立連接;一旦建立了連接之後就可以點擊“選擇照片”會從照片庫中選擇一張圖片並發送給已連接的節點。
程序:
// // ViewController.m // MultipeerConnectivity // // Created by Kenshin Cui on 14/04/05. // Copyright (c) 2015年 cmjstudio. All rights reserved. // #import "ViewController.h" #import@interface ViewController () @property (strong,nonatomic) MCSession *session; @property (strong,nonatomic) MCBrowserViewController *browserController; @property (strong,nonatomic) UIImagePickerController *imagePickerController; @property (weak, nonatomic) IBOutlet UIImageView *photo; @end @implementation ViewController #pragma mark - 控制器視圖事件 - (void)viewDidLoad { [super viewDidLoad]; //創建節點 MCPeerID *peerID=[[MCPeerID alloc]initWithDisplayName:@"KenshinCui"]; //創建會話 _session=[[MCSession alloc]initWithPeer:peerID]; _session.delegate=self; } #pragma mark- UI事件 - (IBAction)browserClick:(UIBarButtonItem *)sender { _browserController=[[MCBrowserViewController alloc]initWithServiceType:@"cmj-stream" session:self.session]; _browserController.delegate=self; [self presentViewController:_browserController animated:YES completion:nil]; } - (IBAction)selectClick:(UIBarButtonItem *)sender { _imagePickerController=[[UIImagePickerController alloc]init]; _imagePickerController.delegate=self; [self presentViewController:_imagePickerController animated:YES completion:nil]; } #pragma mark - MCBrowserViewController代理方法 -(void)browserViewControllerDidFinish:(MCBrowserViewController *)browserViewController{ NSLog(@"已選擇"); [self.browserController dismissViewControllerAnimated:YES completion:nil]; } -(void)browserViewControllerWasCancelled:(MCBrowserViewController *)browserViewController{ NSLog(@"取消浏覽."); [self.browserController dismissViewControllerAnimated:YES completion:nil]; } #pragma mark - MCSession代理方法 -(void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state{ NSLog(@"didChangeState"); switch (state) { case MCSessionStateConnected: NSLog(@"連接成功."); [self.browserController dismissViewControllerAnimated:YES completion:nil]; break; case MCSessionStateConnecting: NSLog(@"正在連接..."); break; default: NSLog(@"連接失敗."); break; } } //接收數據 -(void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID{ NSLog(@"開始接收數據..."); UIImage *image=[UIImage imageWithData:data]; [self.photo setImage:image]; //保存到相冊 UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil); } #pragma mark - UIImagePickerController代理方法 -(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info{ UIImage *image=[info objectForKey:UIImagePickerControllerOriginalImage]; [self.photo setImage:image]; //發送數據給所有已連接設備 NSError *error=nil; [self.session sendData:UIImagePNGRepresentation(image) toPeers:[self.session connectedPeers] withMode:MCSessionSendDataUnreliable error:&error]; NSLog(@"開始發送數據..."); if (error) { NSLog(@"發送數據過程中發生錯誤,錯誤信息:%@",error.localizedDescription); } [self.imagePickerController dismissViewControllerAnimated:YES completion:nil]; } -(void)imagePickerControllerDidCancel:(UIImagePickerController *)picker{ [self.imagePickerController dismissViewControllerAnimated:YES completion:nil]; } @end 在兩個程序中無論是MCBrowserViewController還是MCAdvertiserAssistant在初始化的時候都指定了一個服務類型“cmj-photo”,這是唯一標識一個服務類型的標記,可以按照官方的要求命名,應該盡可能表達服務的作用。需要特別指出的是,如果廣播命名為“cmj-photo”那麼發現節點只有在MCBrowserViewController中指定為“cmj-photo”才能發現此服務。
運行效果:
CoreBluetooth
無論是GameKit還是MultipeerConnectivity,都只能在iOS設備之間進行數據傳輸,這就大大降低了藍牙的使用范圍,於是從iOS6開始蘋果推出了CoreBluetooth.framework,這個框架最大的特點就是完全基於BLE4.0標准並且支持非iOS設備。當前BLE應用相當廣泛,不再僅僅是兩個設備之間的數據傳輸,它還有很多其他應用市場,例如室內定位、無線支付、智能家居等等,這也使得CoreBluetooth成為當前最熱門的藍牙技術。
CoreBluetooth設計同樣也是類似於客戶端-服務器端的設計,作為服務器端的設備稱為外圍設備(Peripheral),作為客戶端的設備叫做中央設備(Central),CoreBlueTooth整個框架就是基於這兩個概念來設計的。
外圍設備和中央設備在CoreBluetooth中使用CBPeripheralManager和CBCentralManager表示。
CBPeripheralManager:外圍設備通常用於發布服務、生成數據、保存數據。外圍設備發布並廣播服務,告訴周圍的中央設備它的可用服務和特征。
CBCentralManager:中央設備使用外圍設備的數據。中央設備掃描到外圍設備後會就會試圖建立連接,一旦連接成功就可以使用這些服務和特征。
外圍設備和中央設備之間交互的橋梁是服務(CBService)和特征(CBCharacteristic),二者都有一個唯一的標識UUID(CBUUID類型)來唯一確定一個服務或者特征,每個服務可以擁有多個特征,下面是他們之間的關系:
一台iOS設備(注意iPhone4以下設備不支持BLE,另外iOS7.0、8.0模擬器也無法模擬BLE)既可以作為外圍設備又可以作為中央設備,但是不能同時即是外圍設備又是中央設備,同時注意建立連接的過程不需要用戶手動選擇允許,這一點和前面兩個框架是不同的,這主要是因為BLE應用場景不再局限於兩台設備之間資源共享了。
A.外圍設備
創建一個外圍設備通常分為以下幾個步驟:
- 創建外圍設備CBPeripheralManager對象並指定代理。
- 創建特征CBCharacteristic、服務CBSerivce並添加到外圍設備
- 外圍設備開始廣播服務(startAdvertisting:)。
- 和中央設備CBCentral進行交互。
下面是簡單的程序示例,程序有兩個按鈕“啟動”和“更新”,點擊啟動按鈕則創建外圍設備、添加服務和特征並開始廣播,一旦發現有中央設備連接並訂閱了此服務的特征則通過更新按鈕更新特征數據,此時已訂閱的中央設備就會收到更新數據。
界面設計:
程序設計:
// // ViewController.m // PeripheralApp // // Created by Kenshin Cui on 14/04/05. // Copyright (c) 2014年 cmjstudio. All rights reserved. // 外圍設備(周邊設備) #import "ViewController.h" #import#define kPeripheralName @"Kenshin Cui's Device" //外圍設備名稱 #define kServiceUUID @"C4FB2349-72FE-4CA2-94D6-1F3CB16331EE" //服務的UUID #define kCharacteristicUUID @"6A3E4B28-522D-4B3B-82A9-D5E2004534FC" //特征的UUID @interface ViewController () @property (strong,nonatomic) CBPeripheralManager *peripheralManager;//外圍設備管理器 @property (strong,nonatomic) NSMutableArray *centralM;//訂閱此外圍設備特征的中心設備 @property (strong,nonatomic) CBMutableCharacteristic *characteristicM;//特征 @property (weak, nonatomic) IBOutlet UITextView *log; //日志記錄 @end @implementation ViewController #pragma mark - 視圖控制器方法 - (void)viewDidLoad { [super viewDidLoad]; } #pragma mark - UI事件 //創建外圍設備 - (IBAction)startClick:(UIBarButtonItem *)sender { _peripheralManager=[[CBPeripheralManager alloc]initWithDelegate:self queue:nil]; } //更新數據 - (IBAction)transferClick:(UIBarButtonItem *)sender { [self updateCharacteristicValue]; } #pragma mark - CBPeripheralManager代理方法 //外圍設備狀態發生變化後調用 -(void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral{ switch (peripheral.state) { case CBPeripheralManagerStatePoweredOn: NSLog(@"BLE已打開."); [self writeToLog:@"BLE已打開."]; //添加服務 [self setupService]; break; default: NSLog(@"此設備不支持BLE或未打開藍牙功能,無法作為外圍設備."); [self writeToLog:@"此設備不支持BLE或未打開藍牙功能,無法作為外圍設備."]; break; } } //外圍設備添加服務後調用 -(void)peripheralManager:(CBPeripheralManager *)peripheral didAddService:(CBService *)service error:(NSError *)error{ if (error) { NSLog(@"向外圍設備添加服務失敗,錯誤詳情:%@",error.localizedDescription); [self writeToLog:[NSString stringWithFormat:@"向外圍設備添加服務失敗,錯誤詳情:%@",error.localizedDescription]]; return; } //添加服務後開始廣播 NSDictionary *dic=@{CBAdvertisementDataLocalNameKey:kPeripheralName};//廣播設置 [self.peripheralManager startAdvertising:dic];//開始廣播 NSLog(@"向外圍設備添加了服務並開始廣播..."); [self writeToLog:@"向外圍設備添加了服務並開始廣播..."]; } -(void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral error:(NSError *)error{ if (error) { NSLog(@"啟動廣播過程中發生錯誤,錯誤信息:%@",error.localizedDescription); [self writeToLog:[NSString stringWithFormat:@"啟動廣播過程中發生錯誤,錯誤信息:%@",error.localizedDescription]]; return; } NSLog(@"啟動廣播..."); [self writeToLog:@"啟動廣播..."]; } //訂閱特征 -(void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central didSubscribeToCharacteristic:(CBCharacteristic *)characteristic{ NSLog(@"中心設備:%@ 已訂閱特征:%@.",central,characteristic); [self writeToLog:[NSString stringWithFormat:@"中心設備:%@ 已訂閱特征:%@.",central.identifier.UUIDString,characteristic.UUID]]; //發現中心設備並存儲 if (![self.centralM containsObject:central]) { [self.centralM addObject:central]; } /*中心設備訂閱成功後外圍設備可以更新特征值發送到中心設備,一旦更新特征值將會觸發中心設備的代理方法: -(void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error */ // [self updateCharacteristicValue]; } //取消訂閱特征 -(void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central didUnsubscribeFromCharacteristic:(CBCharacteristic *)characteristic{ NSLog(@"didUnsubscribeFromCharacteristic"); } -(void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveWriteRequests:(CBATTRequest *)request{ NSLog(@"didReceiveWriteRequests"); } -(void)peripheralManager:(CBPeripheralManager *)peripheral willRestoreState:(NSDictionary *)dict{ NSLog(@"willRestoreState"); } #pragma mark -屬性 -(NSMutableArray *)centralM{ if (!_centralM) { _centralM=[NSMutableArray array]; } return _centralM; } #pragma mark - 私有方法 //創建特征、服務並添加服務到外圍設備 -(void)setupService{ /*1.創建特征*/ //創建特征的UUID對象 CBUUID *characteristicUUID=[CBUUID UUIDWithString:kCharacteristicUUID]; //特征值 // NSString *valueStr=kPeripheralName; // NSData *value=[valueStr dataUsingEncoding:NSUTF8StringEncoding]; //創建特征 /** 參數 * uuid:特征標識 * properties:特征的屬性,例如:可通知、可寫、可讀等 * value:特征值 * permissions:特征的權限 */ CBMutableCharacteristic *characteristicM=[[CBMutableCharacteristic alloc]initWithType:characteristicUUID properties:CBCharacteristicPropertyNotify value:nil permissions:CBAttributePermissionsReadable]; self.characteristicM=characteristicM; // CBMutableCharacteristic *characteristicM=[[CBMutableCharacteristic alloc]initWithType:characteristicUUID properties:CBCharacteristicPropertyRead value:nil permissions:CBAttributePermissionsReadable]; // characteristicM.value=value; /*創建服務並且設置特征*/ //創建服務UUID對象 CBUUID *serviceUUID=[CBUUID UUIDWithString:kServiceUUID]; //創建服務 CBMutableService *serviceM=[[CBMutableService alloc]initWithType:serviceUUID primary:YES]; //設置服務的特征 [serviceM setCharacteristics:@[characteristicM]]; /*將服務添加到外圍設備*/ [self.peripheralManager addService:serviceM]; } //更新特征值 -(void)updateCharacteristicValue{ //特征值 NSString *valueStr=[NSString stringWithFormat:@"%@ --%@",kPeripheralName,[NSDate date]]; NSData *value=[valueStr dataUsingEncoding:NSUTF8StringEncoding]; //更新特征值 [self.peripheralManager updateValue:value forCharacteristic:self.characteristicM onSubscribedCentrals:nil]; [self writeToLog:[NSString stringWithFormat:@"更新特征值:%@",valueStr]]; } /** * 記錄日志 * * @param info 日志信息 */ -(void)writeToLog:(NSString *)info{ self.log.text=[NSString stringWithFormat:@"%@\r\n%@",self.log.text,info]; } @end 上面程序運行的流程如下(圖中藍色代表外圍設備操作,綠色部分表示中央設備操作):
B.中央設備
中央設備的創建一般可以分為如下幾個步驟:
- 創建中央設備管理對象CBCentralManager並指定代理。
- 掃描外圍設備,一般發現可用外圍設備則連接並保存外圍設備。
- 查找外圍設備服務和特征,查找到可用特征則讀取特征數據。
下面是一個簡單的中央服務器端實現,點擊“啟動”按鈕則開始掃描周圍的外圍設備,一旦發現了可用的外圍設備則建立連接並設置外圍設備的代理,之後開始查找其服務和特征。一旦外圍設備的特征值做了更新,則可以在代理方法中讀取更新後的特征值。
界面設計:
程序設計:
// // ViewController.m // CentralApp // // Created by Kenshin Cui on 14/04/05. // Copyright (c) 2014年 cmjstudio. All rights reserved. // 中心設備 #import "ViewController.h" #import#define kServiceUUID @"C4FB2349-72FE-4CA2-94D6-1F3CB16331EE" //服務的UUID #define kCharacteristicUUID @"6A3E4B28-522D-4B3B-82A9-D5E2004534FC" //特征的UUID @interface ViewController () @property (strong,nonatomic) CBCentralManager *centralManager;//中心設備管理器 @property (strong,nonatomic) NSMutableArray *peripherals;//連接的外圍設備 @property (weak, nonatomic) IBOutlet UITextView *log;//日志記錄 @end @implementation ViewController #pragma mark - 控制器視圖事件 - (void)viewDidLoad { [super viewDidLoad]; } #pragma mark - UI事件 - (IBAction)startClick:(UIBarButtonItem *)sender { //創建中心設備管理器並設置當前控制器視圖為代理 _centralManager=[[CBCentralManager alloc]initWithDelegate:self queue:nil]; } #pragma mark - CBCentralManager代理方法 //中心服務器狀態更新後 -(void)centralManagerDidUpdateState:(CBCentralManager *)central{ switch (central.state) { case CBPeripheralManagerStatePoweredOn: NSLog(@"BLE已打開."); [self writeToLog:@"BLE已打開."]; //掃描外圍設備 // [central scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:kServiceUUID]] options:@{CBCentralManagerScanOptionAllowDuplicatesKey:@YES}]; [central scanForPeripheralsWithServices:nil options:@{CBCentralManagerScanOptionAllowDuplicatesKey:@YES}]; break; default: NSLog(@"此設備不支持BLE或未打開藍牙功能,無法作為外圍設備."); [self writeToLog:@"此設備不支持BLE或未打開藍牙功能,無法作為外圍設備."]; break; } } /** * 發現外圍設備 * * @param central 中心設備 * @param peripheral 外圍設備 * @param advertisementData 特征數據 * @param RSSI 信號質量(信號強度) */ -(void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI{ NSLog(@"發現外圍設備..."); [self writeToLog:@"發現外圍設備..."]; //停止掃描 [self.centralManager stopScan]; //連接外圍設備 if (peripheral) { //添加保存外圍設備,注意如果這裡不保存外圍設備(或者說peripheral沒有一個強引用,無法到達連接成功(或失敗)的代理方法,因為在此方法調用完就會被銷毀 if(![self.peripherals containsObject:peripheral]){ [self.peripherals addObject:peripheral]; } NSLog(@"開始連接外圍設備..."); [self writeToLog:@"開始連接外圍設備..."]; [self.centralManager connectPeripheral:peripheral options:nil]; } } //連接到外圍設備 -(void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral{ NSLog(@"連接外圍設備成功!"); [self writeToLog:@"連接外圍設備成功!"]; //設置外圍設備的代理為當前視圖控制器 peripheral.delegate=self; //外圍設備開始尋找服務 [peripheral discoverServices:@[[CBUUID UUIDWithString:kServiceUUID]]]; } //連接外圍設備失敗 -(void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error{ NSLog(@"連接外圍設備失敗!"); [self writeToLog:@"連接外圍設備失敗!"]; } #pragma mark - CBPeripheral 代理方法 //外圍設備尋找到服務後 -(void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error{ NSLog(@"已發現可用服務..."); [self writeToLog:@"已發現可用服務..."]; if(error){ NSLog(@"外圍設備尋找服務過程中發生錯誤,錯誤信息:%@",error.localizedDescription); [self writeToLog:[NSString stringWithFormat:@"外圍設備尋找服務過程中發生錯誤,錯誤信息:%@",error.localizedDescription]]; } //遍歷查找到的服務 CBUUID *serviceUUID=[CBUUID UUIDWithString:kServiceUUID]; CBUUID *characteristicUUID=[CBUUID UUIDWithString:kCharacteristicUUID]; for (CBService *service in peripheral.services) { if([service.UUID isEqual:serviceUUID]){ //外圍設備查找指定服務中的特征 [peripheral discoverCharacteristics:@[characteristicUUID] forService:service]; } } } //外圍設備尋找到特征後 -(void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error{ NSLog(@"已發現可用特征..."); [self writeToLog:@"已發現可用特征..."]; if (error) { NSLog(@"外圍設備尋找特征過程中發生錯誤,錯誤信息:%@",error.localizedDescription); [self writeToLog:[NSString stringWithFormat:@"外圍設備尋找特征過程中發生錯誤,錯誤信息:%@",error.localizedDescription]]; } //遍歷服務中的特征 CBUUID *serviceUUID=[CBUUID UUIDWithString:kServiceUUID]; CBUUID *characteristicUUID=[CBUUID UUIDWithString:kCharacteristicUUID]; if ([service.UUID isEqual:serviceUUID]) { for (CBCharacteristic *characteristic in service.characteristics) { if ([characteristic.UUID isEqual:characteristicUUID]) { //情景一:通知 /*找到特征後設置外圍設備為已通知狀態(訂閱特征): *1.調用此方法會觸發代理方法:-(void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error *2.調用此方法會觸發外圍設備的訂閱代理方法 */ [peripheral setNotifyValue:YES forCharacteristic:characteristic]; //情景二:讀取 // [peripheral readValueForCharacteristic:characteristic]; // if(characteristic.value){ // NSString *value=[[NSString alloc]initWithData:characteristic.value encoding:NSUTF8StringEncoding]; // NSLog(@"讀取到特征值:%@",value); // } } } } } //特征值被更新後 -(void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error{ NSLog(@"收到特征更新通知..."); [self writeToLog:@"收到特征更新通知..."]; if (error) { NSLog(@"更新通知狀態時發生錯誤,錯誤信息:%@",error.localizedDescription); } //給特征值設置新的值 CBUUID *characteristicUUID=[CBUUID UUIDWithString:kCharacteristicUUID]; if ([characteristic.UUID isEqual:characteristicUUID]) { if (characteristic.isNotifying) { if (characteristic.properties==CBCharacteristicPropertyNotify) { NSLog(@"已訂閱特征通知."); [self writeToLog:@"已訂閱特征通知."]; return; }else if (characteristic.properties ==CBCharacteristicPropertyRead) { //從外圍設備讀取新值,調用此方法會觸發代理方法:-(void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error [peripheral readValueForCharacteristic:characteristic]; } }else{ NSLog(@"停止已停止."); [self writeToLog:@"停止已停止."]; //取消連接 [self.centralManager cancelPeripheralConnection:peripheral]; } } } //更新特征值後(調用readValueForCharacteristic:方法或者外圍設備在訂閱後更新特征值都會調用此代理方法) -(void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error{ if (error) { NSLog(@"更新特征值時發生錯誤,錯誤信息:%@",error.localizedDescription); [self writeToLog:[NSString stringWithFormat:@"更新特征值時發生錯誤,錯誤信息:%@",error.localizedDescription]]; return; } if (characteristic.value) { NSString *value=[[NSString alloc]initWithData:characteristic.value encoding:NSUTF8StringEncoding]; NSLog(@"讀取到特征值:%@",value); [self writeToLog:[NSString stringWithFormat:@"讀取到特征值:%@",value]]; }else{ NSLog(@"未發現特征值."); [self writeToLog:@"未發現特征值."]; } } #pragma mark - 屬性 -(NSMutableArray *)peripherals{ if(!_peripherals){ _peripherals=[NSMutableArray array]; } return _peripherals; } #pragma mark - 私有方法 /** * 記錄日志 * * @param info 日志信息 */ -(void)writeToLog:(NSString *)info{ self.log.text=[NSString stringWithFormat:@"%@\r\n%@",self.log.text,info]; } @end 上面程序運行的流程圖如下:
有了上面兩個程序就可以分別運行在兩台支持BLE的iOS設備上,當兩個應用建立連接後,一旦外圍設備更新特征之後,中央設備就可以立即獲取到更新後的值。需要強調的是使用CoreBluetooth開發的應用不僅僅可以和其他iOS設備進行藍牙通信,還可以同其他第三方遵循BLE規范的設備進行藍牙通訊,這裡就不再贅述。
注意:本節部分圖片來自於互聯網,版權歸原作者所有。
社交
Social
現在很多應用都內置“社交分享”功能,可以將看到的新聞、博客、廣告等內容分享到微博、微信、QQ、空間等,其實從iOS6.0開始蘋果官方就內置了Social.framework專門來實現社交分享功能,利用這個框架開發者只需要幾句代碼就可以實現內容分享。下面就以一個分享到新浪微博的功能為例來演示Social框架的應用,整個過程分為:創建內容編輯控制器,設置分享內容(文本內容、圖片、超鏈接等),設置發送(或取消)後的回調事件,展示控制器。
程序代碼:
// // ViewController.m // Social // // Created by Kenshin Cui on 14/04/05. // Copyright (c) 2015年 cmjstudio. All rights reserved. // #import "ViewController.h" #import@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,而且現在百度也出了社會化分享組件。今天無法對所有組件都進行一一介紹,這裡就以友盟社交化組件為例簡單做一下介紹:
分享時調用presentSnsIconSheetView: appKey: shareText: shareImage: shareToSnsNames: delegate:方法或者presentSnsController: appKey: shareText: shareImage: shareToSnsNames: delegate:方法顯示分享列表(注意這個過程中要使用某些服務需要到對應的平台去申請並對應擴展框架進行設置,否則分享列表中不會顯示對應的分享按鈕)。
- 注冊友盟賬號並新建應用獲得AppKey。
- 下載友盟SDK並將下載的文件放到項目中(注意下載的過程中可以選擇所需要的分享服務)。
- 在應用程序中設置友盟的AppKey。
下面是一個簡單的示例:
// // ViewController.m // Social_UM // // Created by Kenshin Cui on 14/04/05. // Copyright (c) 2015年 cmjstudio. All rights reserved. // #import "ViewController.h" #import "UMSocial.h" #import "UMSocialWechatHandler.h" @interface ViewController ()@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 運行效果:
注意:在第一次使用某個分享服務是需要輸入相應的賬號獲得授權才能分享。
GameCenter
Game Center是由蘋果發布的在線多人游戲社交網絡,通過它游戲玩家可以邀請好友進行多人游戲,它也會記錄玩家的成績並在排行榜中展示,同時玩家每經過一定的階段會獲得不同的成就。這裡就簡單介紹一下如何在自己的應用中集成Game Center服務來讓用戶獲得積分、成就以及查看游戲排行和已獲得成就。
由於Game Center是蘋果推出的一項重要服務,蘋果官方對於它的控制相當嚴格,因此使用Game Center之前必須要做許多准備工作。通常需要經過以下幾個步驟(下面的准備工作主要是針對真機的,模擬器省略Provisioning Profile配置過程):
- 在蘋果開發者中心創建支持Game Center服務的App ID並指定具體的Bundle ID,假設是“com.cmjstudio.kctest”(注意這個Bundle ID就是日後要開發的游戲的Bundle ID)。
- 基於“com.cmjstudio.kctest”創建開發者配置文件(或描述文件)並導入對應的設備(創建過程中選擇支持Game Center服務的App ID,這樣iOS設備在運行指定Boundle ID應用程序就知道此應用支持Game Center服務)。
- 在iTunes Connect中創建一個應用(假設叫“KCTest”,這是一款足球競技游戲)並指定“套裝ID”為之前創建的“com.cmjstudio.kctest”,讓應用和這個App關聯(注意這個應用不需要提交)。
- 在iTunes Connect的“用戶和職能”中創建沙盒測試用戶(由於在測試階段應用還沒有正式提交到App Store,所以只有沙盒用戶可以登錄Game Center)。
- 在iTunes Connect中配置此應用Game Center(這裡配置了游戲在游戲中心的顯示名稱為“CMJ”),在其中添加排行榜和成就(假設添加一個排行榜ID“Goals”表示進球個數;兩個成就ID分別為“AdidasGoldBall”、“AdidasGoldBoot”代表金球獎和金靴獎成就,點數分別為80、100)。
在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來展示排行榜、成就、玩家信息,這是系統自帶的一個游戲中心視圖方便和後面我們自己獲得的信息對比。
程序如下
// // KCMainTableViewController.m // kctest // // Created by Kenshin Cui on 14/4/5. // Copyright (c) 2015年 cmjstudio. All rights reserved. // 靜態表格 #import "KCMainTableViewController.h" #import#import "KCLeaderboardTableViewController.h" #import "KCAchievementTableViewController.h" @interface KCMainTableViewController () @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來和排行榜關聯起來的)。
程序如下
// // KCLeaderboardTableViewController.m // kctest // // Created by Kenshin Cui on 14/4/5. // Copyright (c) 2015年 cmjstudio. All rights reserved. // #import "KCLeaderboardTableViewController.h" #import//排行榜標識,就是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服務器。
程序如下
// // KCAchievementTableViewController.m // kctest // // Created by Kenshin Cui on 14/4/5. // Copyright (c) 2015年 cmjstudio. All rights reserved. // #import "KCAchievementTableViewController.h" #import//成就標識,就是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 ;//成就標題 //如果已經獲得成就則加載進度,否則為0 double 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 運行效果:
注意首次使用游戲時由於沒有對Game Center授權,會提示用戶登錄Game Center。
內購
大家都知道做iOS開發本身的收入有三種來源:出售應用、內購和廣告。國內用戶通常很少直接購買應用,因此對於開發者而言(特別是個人開發者),內購和廣告收入就成了主要的收入來源。內購營銷模式,通常軟件本身是不收費的,但是要獲得某些特權就必須購買一些道具,而內購的過程是由蘋果官方統一來管理的,所以和Game Center一樣,在開發內購程序之前要做一些准備工作(下面的准備工作主要是針對真機的,模擬器省略Provisioning Profile配置過程):
- 前四步和Game Center基本完全一致,只是在選擇服務時不是選擇Game Center而是要選擇內購服務(In-App Purchase)。
- 到iTuens Connect中設置“App 內購買項目”,這裡仍然以上面的“KCTest”項目為例,假設這個足球競技游戲中有三種道具,分別為“強力手套”(增強防御)、“金球”(增加金球率)和“能量瓶”(提供足夠體力),前兩者是非消耗品只用一次性購買,後者是消耗品用完一次必須再次購買。
- 到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方法反饋恢復的交易(也就是已購買的非消耗品交易,注意這個過程中如果沒有非消耗品可恢復,是不會調用此方法的)。
下面通過一個示例程序演示內購和恢復的整個過程,程序界面大致如下:
主界面中展示了所有可購買產品和售價,以及購買情況。
選擇一個產品點”購買“可以購買此商品,購買完成後刷新購買狀態(如果是非消耗品則顯示已購買,如果是消耗品則顯示購買個數)。
程序卸載後重新安裝可以點擊”恢復購買“來恢復已購買的非消耗品。
程序代碼:
// // 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 運行效果(這是程序在卸載後重新安裝的運行效果,卸載前已經購買”強力手套“,因此程序運行後點擊了”恢復購買“):
擴展--廣告
上面也提到做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 運行效果:
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)是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屬性存儲數據:
// // KCDocument.m // kctest // // Created by Kenshin Cui on 14/4/5. // Copyright (c) 2015年 cmjstudio. All rights reserved. // #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完成文檔添加並導航到文檔詳情界面編輯文檔內容。
// // KCMainTableViewController.m // kctest // // Created by Kenshin Cui on 14/4/5. // Copyright (c) 2015年 cmjstudio. All rights reserved. // #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{ //取得保存URL fileName=[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當新增一個筆記或選擇一個已存在的筆記後可以查看、保存筆記內容。
// // ViewController.m // kctest // // Created by Kenshin Cui on 14/4/5. // Copyright (c) 2015年 cmjstudio. All rights reserved. // #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的首選項來存儲的。
// // KCSettingTableViewController.m // kctest // // Created by Kenshin Cui on 14/4/5. // Copyright (c) 2015年 cmjstudio. All rights reserved. // #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運行效果:
注意:所有的存儲到iCloud的文檔、首選項都是首先存儲到本地,然後通過daemon進程同步到iCloud的,保存、讀取的文件都是本地同步副本並不一定是真實的iCloud存儲文件。
Passbook
Passbook是蘋果推出的一個管理登機牌、會員卡、電影票、優惠券等信息的工具。Passbook就像一個卡包,用於存放你的購物卡、積分卡、電影票、禮品卡等,而這些票據就是一個“Pass”。和物理票據不同的是你可以動態更新Pass的信息,提醒用戶優惠券即將過期;甚至如果你的Pass中包含地理位置信息的話當你到達某個商店還可以動態提示用戶最近商店有何種優惠活動;當用戶將一張團購券添加到Passbook之後,用戶到了商店之後Passbook可以自動彈出團購券,店員掃描之後進行消費、積分等等都是Passbook的應用場景。Passbook可以管理多類票據,蘋果將其劃分為五類:
- 登機牌(Boarding pass)
- 優惠券(Coupon)
- 活動票據、入場券(Event ticket)
- 購物卡、積分卡(Store Cards)
- 普通票據(自定義票據)(Generic pass)
蘋果的劃分一方面出於不同票據功能及展示信息不同,另一方面也是為了統一票據的設計,下面是蘋果官方關於五種票據的布局設計布局:
既然一個票據就是一個Pass,那麼什麼是Pass呢?在iOS中一個Pass其實就是一個.pkpass文件,事實上它是一個Zip壓縮包,只是這個壓縮包要按照一定的目錄結構來設計,下面是一個Pass包的目錄結構(注意不同的票據類型會適當刪減):
Pass Package
├── icon.png
├── logo.png
├── thumbnail.png
├── background.png
├── strip.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開發同樣需要一些准備工作:
在蘋果開發者中心新建Pass Type ID(例如這裡新建一個“pass.com.cmjstudio.mypassbook”),並且基於這個Pass Type ID創建一個Passbook證書(在mac上找到鑰匙串,選擇”從證書頒發機構請求證書“,生成一個證書請求文件;將此文件上傳到對應的Pass Type ID下生成證書文件)如下圖:下載證書後,將此證書導入Mac中(此處配置的Pass Type ID對應pass.json中的”passTypeIdentitifier“,此證書用於生成簽名文件signature。)。
在Xcode中-Targets-Capabilities啟用Pasbook服務,這裡需要注意的是”Allow all team pass types“選項,如果勾選了這一項,那麼pass.json中的passTypeIdentifier和teamIdentifier就可以是任何團隊創建的任何Pass項目了,這裡使用前面創建的項目,所以選擇”Allow subset of pass types“。
有了上面的准備工作,下面看一下如何制作一個Pass:
- 根據所選擇的Passbook類型准備圖片素材,由於這裡以一個Store Card舉例,所以需要准備icon、logo和strip三類圖片。
- 配置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)" }- 根據pass所需文件創建manifest.json文件,可以通過”openssl sha1 [文件路徑]“分別計算出所有文件的哈希值:
{ "pass.json":"3292f96c4676aefe7122abb47f86be0d95a6faaf", "[email protected]":"83438c13dfd7c4a5819a12f6df6dc74b71060289", "icon.png":"83438c13dfd7c4a5819a12f6df6dc74b71060289", "[email protected]":"83438c13dfd7c4a5819a12f6df6dc74b71060289", "logo.png":"83438c13dfd7c4a5819a12f6df6dc74b71060289", "[email protected]":"885ff9639c90147a239a7a77e7adc870d5e047e2", "strip.png":"885ff9639c90147a239a7a77e7adc870d5e047e2" }- 接下來下來准備生成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:456789d.從.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、 將icon.png、[email protected]、logo.png、[email protected]、strip.png、[email protected]、pass.json、manifest.json、signature壓縮成pass包(這裡命名為”mypassbook.pkpass“)。
zip -r mypassbook.pkpass manifest.json pass.json signature logo.png [email protected] icon.png [email protected] strip.png [email protected]到這裡一個pass制作完成了,此處可以在mac中打開預覽:
到這裡一個Pass就只做完成了,下面就看一下在iOS中如何添加這個Pass到Passbook,這裡直接將上面制作完成的Pass放到Bundle中完成添加。當然這些都是一步步手動完成的,前面也說了實際開發中這個Pass是服務器端來動態生成的,在添加時會從服務器端下載,這個過程在示例中就不再演示。iOS中提供了PassKit.framework框架來進行Passbook開發,下面的代碼演示了添加Pass到Passbook應用的過程:
// // ViewController.m // Passbook // // Created by Kenshin Cui on 14/4/5. // Copyright (c) 2015年 cmjstudio. All rights reserved. // #import "ViewController.h" #import@interface ViewController () @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應用並展示添加的Pass NSLog(@"%@",self.pass.passURL); [[UIApplication sharedApplication] openURL:self.pass.passURL]; } @end 運行效果: