iOS開發之--通訊錄、藍牙、內購、GameCenter、iCloud、Passbook功能開發匯總

XavierIbsch 8年前發布 | 66K 次閱讀 iOS開發 移動開發 IOS

前言

iOS開發過程中有時候難免會使用iOS內置的一些應用軟件和服務,例如QQ通訊錄、微信電話本會使用iOS的通訊錄,一些第三方軟件會在應用內發送短信等。今天將和大家一起學習如何使用系統應用、使用系統服務:

1. 系統應用

在開發某些應用時可能希望能夠調用iOS系統內置的電話、短信、郵件、瀏覽器應用,此時你可以直接使用UIApplication的OpenURL:方法指定特定的協議來打開不同的系統應用。常用的協議如下:
打電話:tel:或者tel://、telprompt:或telprompt://(撥打電話前有提示)
發短信:sms:或者sms://
發送郵件:mailto:或者mailto://
啟動瀏覽器:http:或者http://
下面以一個簡單的demo演示如何調用上面幾種系統應用:

#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=@"kenshin@hotmail.com"; NSString *url=[NSString stringWithFormat:@"mailto://%@",mailAddress]; [self openUrl:url]; }

//瀏覽網頁

  • (IBAction)browserClick:(UIButton )sender { NSString 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</code></pre>

不難發現當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代理方法中接收參數并解析。</code></pre>
    -(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;//是否打開
    }</code></pre> 
    

    2. 系統服務

    2.1短信與郵件

    調用系統內置的應用來發送短信、郵件相當簡單,但是這么操作也存在著一些弊端:當你點擊了發送短信(或郵件)操作之后直接啟動了系統的短信(或郵件)應用程序,我們的應用其實此時已經處于一種掛起狀態,發送完(短信或郵件)之后無法自動回到應用界面。如果想要在應用程序內部完成這些操作則可以利用iOS中的MessageUI.framework,它提供了關于短信和郵件的UI接口供開發者在應用程序內部調用。從框架名稱不難看出這是一套UI接口,提供有現成的短信和郵件的編輯界面,開發人員只需要通過編程的方式給短信和郵件控制器設置對應的參數即可。

    在MessageUI.framework中主要有兩個控制器類分別用于發送短信(MFMessageComposeViewController)和郵件(MFMailComposeViewController),它們均繼承于UINavigationController。由于兩個類使用方法十分類似,這里主要介紹一下MFMessageComposeViewController使用步驟:

    創建MFMessageComposeViewController對象。
    設置收件人recipients、信息正文body,如果運行商支持主題和附件的話可以設置主題subject、附件attachments(可以通過canSendSubject、canSendAttachments方法判斷是否支持)
    設置代理messageComposeDelegate(注意這里不是delegate屬性,因為delegate屬性已經留給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 <MessageUI/MessageUI.h>

@interface KCSendMessageViewController ()<MFMessageComposeViewControllerDelegate>

@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</code></pre>

這里需要強調一下:

.1) MFMessageComposeViewController的代理不是通過delegate屬性指定的而是通過messageComposeDelegate指定的。

.2) 可以通過幾種方式來指定發送的附件,在這個過程中請務必指定文件的后綴,否則在發送后無法正確識別文件類別(例如如果發送的是一張jpg圖片,在發送后無法正確查看圖片)。

.3) 無論發送成功與否代理方法-(void)messageComposeViewController:(MFMessageComposeViewController *)controller didFinishWithResult:(MessageComposeResult)result都會執行,通過代理參數中的result來獲得發送狀態。

其實只要熟悉了MFMessageComposeViewController之后,那么用于發送郵件的MFMailComposeViewController用法和步驟完全一致,只是功能不同。下面看一下MFMailComposeViewController的使用:

#import "KCSendEmailViewController.h"

import <MessageUI/MessageUI.h>

@interface KCSendEmailViewController ()<MFMailComposeViewControllerDelegate> @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</code></pre>

運行效果:
這里寫圖片描述

這里寫圖片描述

2.2. 通訊錄

iOS中帶有一個Contacts應用程序來管理聯系人,但是有些時候我們希望自己的應用能夠訪問或者修改這些信息,這個時候就要用到AddressBook.framework框架。iOS中的通訊錄是存儲在數據庫中的,由于iOS的權限設計,開發人員是不允許直接訪問通訊錄數據庫的,必須依靠AddressBook提供的標準API來實現通訊錄操作。通過AddressBook.framework開發者可以從底層去操作AddressBook.framework的所有信息,但是需要注意的是這個框架是基于C語言編寫的,無法使用ARC來管理內存,開發者需要自己管理內存。下面大致介紹一下通訊錄操作中常用的類型:

.1) ABAddressBookRef:代表通訊錄對象,通過該對象開發人員不用過多的關注通訊錄的存儲方式,可以直接以透明的方式去訪問、保存(在使用AddressBook.framework操作聯系人時,所有的增加、刪除、修改后都必須執行保存操作,類似于Core Data)等。

.2) ABRecordRef:代表一個通用的記錄對象,可以是一條聯系人信息,也可以是一個群組,可以通過ABRecordGetRecordType()函數獲得具體類型。如果作為聯系人(事實上也經常使用它作為聯系人),那么這個記錄記錄了一個完整的聯系人信息(姓名、性別、電話、郵件等),每條記錄都有一個唯一的ID標示這條記錄(可以通過ABRecordGetRecordID()函數獲得)。

.3) ABPersonRef:代表聯系人信息,很少直接使用,實際開發過程中通常會使用類型為“kABPersonType”的ABRecordRef來表示聯系人(由此可見ABPersonRef其實是一種類型為“kABPersonType”的ABRecordRef)

.4)ABGroupRef:代表群組,與ABPersonRef類似,很少直接使用ABGroupRef,而是使用類型為“kABGroupType”的ABRecordRef來表示群組,一個群組可以包含多個聯系人,一個聯系人也同樣可以多個群組。
由于通訊錄操作的關鍵是對ABRecordRef的操作,首先看一下常用的操作通訊錄記錄的方法:

ABPersonCreate():創建一個類型為“kABPersonType”的ABRecordRef。 ABRecordCopyValue():取得指定屬性的值。

ABRecordCopyCompositeName():取得聯系人(或群組)的復合信息(對于聯系人則包括:姓、名、公司等信息,對于群組則返回組名稱)。

ABRecordSetValue():設置ABRecordRef的屬性值。注意在設置ABRecordRef的值時又分為單值屬性和多值屬性:單值屬性設置只要通過ABRecordSetValue()方法指定屬性名和值即可;多值屬性則要先通過創建一個ABMutableMultiValueRef類型的變量,然后通過ABMultiValueAddValueAndLabel()方法依次添加屬性值,最后通過ABRecordSetValue()方法將ABMutableMultiValueRef類型的變量設置為記錄值。

ABRecordRemoveValue():刪除指定的屬性值。

注意:

由于聯系人訪問時(讀取、設置、刪除時)牽扯到大量聯系人屬性,可以到ABPerson.h中查詢或者直接到幫助文檔“Personal Information Properties”</code></pre>

通訊錄的訪問步驟一般如下:

  1. 調用ABAddressBookCreateWithOptions()方法創建通訊錄對象ABAddressBookRef。
  2. 調用ABAddressBookRequestAccessWithCompletion()方法獲得用戶授權訪問通訊錄。
  3. 調用ABAddressBookCopyArrayOfAllPeople()、ABAddressBookCopyPeopleWithName()方法查詢聯系人信息。
  4. 調用ABAddressBookCopyArrayOfAllPeople()、ABAddressBookCopyPeopleWithName()方法查詢聯系人信息。
  5. 也就是說如果要修改或者刪除都需要首先查詢對應的聯系人,然后修改或刪除后提交更改。如果用戶要增加一個聯系人則不用進行查詢,直接調用ABPersonCreate()方法創建一個ABRecord然后設置具體的屬性,調用ABAddressBookAddRecord方法添加即可

下面就通過一個示例演示一下如何通過ABAddressBook.framework訪問通訊錄,這個例子中通過一個UITableViewController模擬一下通訊錄的查看、刪除、添加操作。

主控制器視圖,用于顯示聯系人,修改刪除聯系人:

KCContactViewController.h

#import <UIKit/UIKit.h>

/* 定義一個協議作為代理 / @protocol KCContactDelegate //新增或修改聯系人 -(void)editPersonWithFirstName:(NSString )firstName lastName:(NSString )lastName workNumber:(NSString )workNumber; //取消修改或新增 -(void)cancelEdit; @end

@interface KCContactTableViewController : UITableViewController

@end</code></pre>

KCContactViewController.m

#import "KCContactTableViewController.h"

import <AddressBook/AddressBook.h>

import "KCAddPersonViewController.h"

@interface KCContactTableViewController ()<KCContactDelegate>

@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<count;++i){ // NSString phoneLabel= (__bridge NSString )(ABMultiValueCopyLabelAtIndex(phoneNumbersRef, i)); // NSString phoneNumber=(__bridge NSString )(ABMultiValueCopyValueAtIndex(phoneNumbersRef, i)); // NSLog(@"%@:%@",phoneLabel,phoneNumber); // }

    cell.textLabel.text=[NSString stringWithFormat:@"%@ %@",firstName,lastName]; if (count>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<count; ++i) { ABRecordRef recordRef=CFArrayGetValueAtIndex(recordsRef, i);//取得指定的記錄 ABAddressBookRemoveRecord(self.addressBook, recordRef, NULL);//刪除 } ABAddressBookSave(self.addressBook, NULL);//刪除之后提交更改 CFRelease(recordsRef); }

/* 添加一條記錄 @param firstName 名 @param lastName 姓 @param iPhoneName iPhone手機號 / -(void)addPersonWithFirstName:(NSString )firstName lastName:(NSString )lastName workNumber:(NSString )workNumber{ //創建一條記錄 ABRecordRef recordRef= ABPersonCreate(); ABRecordSetValue(recordRef, kABPersonFirstNameProperty, (bridge CFTypeRef)(firstName), NULL);//添加名 ABRecordSetValue(recordRef, kABPersonLastNameProperty, (bridge CFTypeRef)(lastName), NULL);//添加姓

ABMutableMultiValueRef multiValueRef =ABMultiValueCreateMutable(kABStringPropertyType);//添加設置多值屬性
ABMultiValueAddValueAndLabel(multiValueRef, (__bridge CFStringRef)(workNumber), kABWorkLabel, NULL);//添加工作電話
ABRecordSetValue(recordRef, kABPersonPhoneProperty, multiValueRef, NULL);

//添加記錄
ABAddressBookAddRecord(self.addressBook, recordRef, NULL);

//保存通訊錄,提交更改
ABAddressBookSave(self.addressBook, NULL);
//釋放資源
CFRelease(recordRef);
CFRelease(multiValueRef);

}

/* 根據RecordID修改聯系人信息 @param recordID 記錄唯一ID @param firstName 姓 @param lastName 名 @param homeNumber 工作電話 / -(void)modifyPersonWithRecordID:(ABRecordID)recordID firstName:(NSString )firstName lastName:(NSString )lastName workNumber:(NSString *)workNumber{ ABRecordRef recordRef=ABAddressBookGetPersonWithRecordID(self.addressBook,recordID); ABRecordSetValue(recordRef, kABPersonFirstNameProperty, (bridge CFTypeRef)(firstName), NULL);//添加名 ABRecordSetValue(recordRef, kABPersonLastNameProperty, (bridge CFTypeRef)(lastName), NULL);//添加姓

ABMutableMultiValueRef multiValueRef =ABMultiValueCreateMutable(kABStringPropertyType);
ABMultiValueAddValueAndLabel(multiValueRef, (__bridge CFStringRef)(workNumber), kABWorkLabel, NULL);
ABRecordSetValue(recordRef, kABPersonPhoneProperty, multiValueRef, NULL);
//保存記錄,提交更改
ABAddressBookSave(self.addressBook, NULL);
//釋放資源
CFRelease(multiValueRef);

} @end</code></pre>

新增或修改控制器視圖,用于顯示一個聯系人的信息或者新增一個聯系人:
KCAddPersonViewController.h

#import <UIKit/UIKit.h>
@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<KCContactDelegate> delegate;

@end</code></pre>

KCAddPersonViewController.m

#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</code></pre>

運行效果:
這里寫圖片描述

備注:

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()方法完成。</code></pre>

AddressBookUI

使用AddressBook.framework來操作通訊錄特點就是可以對通訊錄有更加精確的控制,但是缺點就是面對大量C語言API稍嫌麻煩,于是Apple官方提供了另一套框架供開發者使用,那就是AddressBookUI.framework。例如前面查看、新增、修改人員的界面這個框架就提供了現成的控制器視圖供開發者使用。下面是這個框架中提供的控制器視圖:
1. ABPersonViewController:用于查看聯系人信息(可設置編輯)。需要設置displayedPerson屬性來設置要顯示或編輯的聯系人。
2. ABNewPersonViewController:用于新增聯系人信息。
3. 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代理方法:</code></pre>

-(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代理方法,此時如果點擊一個具體聯系人會導航到聯系人詳細信息界面,用戶點擊具體的屬性后觸發此方法。

下面就看一下上面四個控制器視圖的使用方法,在下面的程序中定義了四個按鈕,點擊不同的按鈕調用不同的控制器視圖用于演示:

#import "ViewController.h"

import <AddressBookUI/AddressBookUI.h>

@interface ViewController ()<ABNewPersonViewControllerDelegate,ABUnknownPersonViewControllerDelegate,ABPeoplePickerNavigationControllerDelegate,ABPersonViewControllerDelegate>

@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</code></pre>

這里寫圖片描述

這里寫圖片描述

這里寫圖片描述

這里寫圖片描述

注意:

為了讓大家可以更加清楚的看到幾個控制器視圖的使用,這里并沒有結合前面的UITableViewController來使用,事實上大家結合前面UITableViewController可以做一個完善的通訊錄應用。</code></pre>

2.3. 藍牙

隨著藍牙低功耗技術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方法,在此方法可以獲得發送數據并處理。下面是程序代碼:

#import "ViewController.h"

import <GameKit/GameKit.h>

@interface ViewController ()<GKPeerPickerControllerDelegate,UIImagePickerControllerDelegate,UINavigationBarDelegate>

@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</code></pre>

運行效果(左側是真機,右側是模擬器,程序演示了兩個客戶端互發圖片的場景:首先是模擬器發送圖片給真機,然后真機發送圖片給模擬器):
這里寫圖片描述
這里寫圖片描述

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:方法去發送數據。</p>

廣播作為一個服務器去發布自身服務,供周邊設備發現連接。在MultipeerConnectivity中使用MCAdvertiserAssistant來表示一個廣播,通常創建廣播時指定一個會話MCSession對象將廣播服務和會話關聯起來。一旦調用廣播的start方法周邊的設備就可以發現該廣播并可以連接到此服務。在MCSession的代理方法中可以隨時更新連接狀態,一旦建立了連接之后就可以通過MCSession的connectedPeers獲得已經連接的設備。

B.發現

前面已經說過作為發現的客戶端同樣需要一個MCPeerID來標志一個客戶端,同時會擁有一個MCSession來監聽連接狀態并發送、接受數據。除此之外,要發現廣播服務,客戶端就必須要隨時查找服務來連接,在MultipeerConnectivity中提供了一個控制器MCBrowserViewController來展示可連接和已連接的設備(這類似于GameKit中的GKPeerPickerController),當然如果想要自己定制一個界面來展示設備連接的情況你可以選擇自己開發一套UI界面。一旦通過MCBroserViewController選擇一個節點去連接,那么作為廣播的節點就會收到通知,詢問用戶是否允許連接。由于初始化MCBrowserViewController的過程已經指定了會話MCSession,所以連接過程中會隨時更新會話狀態,一旦建立了連接,就可以通過會話的connected屬性獲得已連接設備并且可以使用會話發送、接受數據。

下面用兩個不同的應用程序來演示使用MultipeerConnectivity的使用過程,其中一個應用運行在模擬器中作為廣播節點,另一個運行在iPhone真機上作為發現節點,并且實現兩個節點的圖片互傳。

首先看一下作為廣播節點的程序:

界面:
這里寫圖片描述

點擊“開始廣播”來發布服務,一旦有節點連接此服務就可以使用“選擇照片”來從照片庫中選取一張圖片并發送到所有已連接節點。

程序

#import "ViewController.h"

import <MultipeerConnectivity/MultipeerConnectivity.h>

@interface ViewController ()<MCSessionDelegate,MCAdvertiserAssistantDelegate, UIImagePickerControllerDelegate,UINavigationControllerDelegate> @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</code></pre>

再看一下作為發現節點的程序:

界面:
這里寫圖片描述
點擊“查找設備”瀏覽可用服務,點擊服務建立連接;一旦建立了連接之后就可以點擊“選擇照片”會從照片庫中選擇一張圖片并發送給已連接的節點。

程序:

#import "ViewController.h"

import <MultipeerConnectivity/MultipeerConnectivity.h>

@interface ViewController ()<MCSessionDelegate,MCBrowserViewControllerDelegate,UIImagePickerControllerDelegate,UINavigationControllerDelegate> @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</code></pre>

在兩個程序中無論是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.外圍設備

創建一個外圍設備通常分為以下幾個步驟:
1. 創建外圍設備CBPeripheralManager對象并指定代理。
2. 創建特征CBCharacteristic、服務CBSerivce并添加到外圍設備
3. 外圍設備開始廣播服務(startAdvertisting:)。
4. 和中央設備CBCentral進行交互。
下面是簡單的程序示例,程序有兩個按鈕“啟動”和“更新”,點擊啟動按鈕則創建外圍設備、添加服務和特征并開始廣播,一旦發現有中央設備連接并訂閱了此服務的特征則通過更新按鈕更新特征數據,此時已訂閱的中央設備就會收到更新數據。

界面設計:
這里寫圖片描述
程序設計:

#import "ViewController.h"

import <CoreBluetooth/CoreBluetooth.h>

define kPeripheralName @"Kenshin Cui's Device" //外圍設備名稱

define kServiceUUID @"C4FB2349-72FE-4CA2-94D6-1F3CB16331EE" //服務的UUID

define kCharacteristicUUID @"6A3E4B28-522D-4B3B-82A9-D5E2004534FC" //特征的UUID

@interface ViewController ()<CBPeripheralManagerDelegate>

@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</code></pre>

上面程序運行的流程如下(圖中藍色代表外圍設備操作,綠色部分表示中央設備操作):
這里寫圖片描述

B.中央設備

中央設備的創建一般可以分為如下幾個步驟:
1. 創建中央設備管理對象CBCentralManager并指定代理。
2. 掃描外圍設備,一般發現可用外圍設備則連接并保存外圍設備。
3. 查找外圍設備服務和特征,查找到可用特征則讀取特征數據。

下面是一個簡單的中央服務器端實現,點擊“啟動”按鈕則開始掃描周圍的外圍設備,一旦發現了可用的外圍設備則建立連接并設置外圍設備的代理,之后開始查找其服務和特征。一旦外圍設備的特征值做了更新,則可以在代理方法中讀取更新后的特征值。

界面設計:
這里寫圖片描述

程序設計:

#import "ViewController.h"

import <CoreBluetooth/CoreBluetooth.h>

define kServiceUUID @"C4FB2349-72FE-4CA2-94D6-1F3CB16331EE" //服務的UUID

define kCharacteristicUUID @"6A3E4B28-522D-4B3B-82A9-D5E2004534FC" //特征的UUID

@interface ViewController ()<CBCentralManagerDelegate,CBPeripheralDelegate>

@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</code></pre>

上面程序運行的流程圖如下:
這里寫圖片描述
iOS開發之--通訊錄、藍牙、內購、GameCenter、iCloud、Passbook功能開發匯總

有了上面兩個程序就可以分別運行在兩臺支持BLE的iOS設備上,當兩個應用建立連接后,一旦外圍設備更新特征之后,中央設備就可以立即獲取到更新后的值。需要強調的是使用CoreBluetooth開發的應用不僅僅可以和其他iOS設備進行藍牙通信,還可以同其他第三方遵循BLE規范的設備進行藍牙通訊,這里就不再贅述。

2.4. 社交

Social

現在很多應用都內置“社交分享”功能,可以將看到的新聞、博客、廣告等內容分享到微博、微信、QQ、空間等,其實從iOS6.0開始蘋果官方就內置了Social.framework專門來實現社交分享功能,利用這個框架開發者只需要幾句代碼就可以實現內容分享。下面就以一個分享到新浪微博的功能為例來演示Social框架的應用,整個過程分為:創建內容編輯控制器,設置分享內容(文本內容、圖片、超鏈接等),設置發送(或取消)后的回調事件,展示控制器。

程序代碼:

#import "ViewController.h"

import <Social/Social.h>

@interface ViewController ()

@end

@implementation ViewController

pragma mark - 控制器視圖事件

  • (void)viewDidLoad { [super viewDidLoad];

}

pragma mark - UI事件

  • (IBAction)shareClick:(UIBarButtonItem *)sender { [self shareToSina]; }

pragma mark - 私有方法

-(void)shareToSina{ //檢查新浪微博服務是否可用 if(![SLComposeViewController isAvailableForServiceType:SLServiceTypeSinaWeibo]){ NSLog(@"新浪微博服務不可用."); return; } //初始化內容編寫控制器,注意這里指定分享類型為新浪微博 SLComposeViewController composeController=[SLComposeViewController composeViewControllerForServiceType:SLServiceTypeSinaWeibo]; //設置默認信息 [composeController setInitialText:@"Kenshin Cui's Blog..."]; //添加圖片 [composeController addImage:[UIImage imageNamed:@"stevenChow"]]; //添加連接 [composeController addURL:[NSURL URLWithString:@"composeControllerForBlock=composeController; composeController.completionHandler=^(SLComposeViewControllerResult result){ if (result==SLComposeViewControllerResultDone) { NSLog(@"開始發送..."); } [composeControllerForBlock dismissViewControllerAnimated:YES completion:nil]; }; //顯示編輯視圖 [self presentViewController:composeController animated:YES completion:nil]; }

@end</code></pre>

運行效果:
這里寫圖片描述
發送成功之后:
這里寫圖片描述
在這個過程中開發人員不需要知道新浪微博的更多分享細節,Social框架中已經統一了分享的接口,你可以通過ServiceType設置是分享到非死book、推ter、新浪微博、騰訊微博,而不關心具體的細節實現。那么當運行上面的示例時它是怎么知道用哪個賬戶來發送微博呢?其實在iOS的設置中有專門設置非死book、推ter、微博的地方:

這里寫圖片描述
必須首先在這里設置微博賬戶才能完成上面的發送,不然Social框架也不可能知道具體使用哪個賬戶來發送。

第三方框架

當然,通過上面的設置界面應該可以看到,蘋果官方默認支持的分享并不太多,特別是對于國內的應用只支持新浪微博和騰訊微博(事實上從iOS7蘋果才考慮支持騰訊微博),那么如果要分享到微信、人人、開心等等國內較為知名的社交網絡怎么辦呢?目前最好的選擇就是使用第三方框架,因為如果要自己實現各個應用的接口還是比較復雜的。當前使用較多的就是友盟社會化組件、ShareSDK,而且現在百度也出了社會化分享組件。今天無法對所有組件都進行一一介紹,這里就以友盟社交化組件為例簡單做一下介紹:
1. 注冊友盟賬號并新建應用獲得AppKey。
2. 下載友盟SDK并將下載的文件放到項目中(注意下載的過程中可以選擇所需要的分享服務)
3. 在應用程序中設置友盟的AppKey。
4. 分享時調用presentSnsIconSheetView: appKey: shareText: shareImage: shareToSnsNames: delegate:方法或者presentSnsController: appKey: shareText: shareImage: shareToSnsNames: delegate:方法顯示分享列表(注意這個過程中要使用某些服務需要到對應的平臺去申請并對應擴展框架進行設置,否則分享列表中不會顯示對應的分享按鈕)。

下面是一個簡單的示例:

#import "ViewController.h"

import "UMSocial.h"

import "UMSocialWechatHandler.h"

@interface ViewController ()<UMSocialUIDelegate>

@end

@implementation ViewController

pragma mark - 控制器視圖事件

  • (void)viewDidLoad { [super viewDidLoad];

}

pragma mark - UI事件

}

pragma mark - UMSocialSnsService代理

//分享完成 -(void)didFinishGetUMSocialDataInViewController:(UMSocialResponseEntity *)response{ //分享成功 if(response.responseCode==UMSResponseCodeSuccess){ NSLog(@"分享成功"); } } @end</code></pre>

運行效果:
這里寫圖片描述

2.5. Game Center

Game Center是由蘋果發布的在線多人游戲社交網絡,通過它游戲玩家可以邀請好友進行多人游戲,它也會記錄玩家的成績并在排行榜中展示,同時玩家每經過一定的階段會獲得不同的成就。這里就簡單介紹一下如何在自己的應用中集成Game Center服務來讓用戶獲得積分、成就以及查看游戲排行和已獲得成就。

由于Game Center是蘋果推出的一項重要服務,蘋果官方對于它的控制相當嚴格,因此使用Game Center之前必須要做許多準備工作。通常需要經過以下幾個步驟(下面的準備工作主要是針對真機的,模擬器省略Provisioning Profile配置過程):
1. 在蘋果開發者中心創建支持Game Center服務的App ID并指定具體的Bundle ID,假設是“com.cmjstudio.kctest”(注意這個Bundle ID就是日后要開發的游戲的Bundle ID)。
這里寫圖片描述

  1. 基于“com.cmjstudio.kctest”創建開發者配置文件(或描述文件)并導入對應的設備(創建過程中選擇支持Game Center服務的App ID,這樣iOS設備在運行指定Boundle ID應用程序就知道此應用支持Game Center服務)。
    這里寫圖片描述
  2. 在iTunes Connect中創建一個應用(假設叫“KCTest”,這是一款足球競技游戲)并指定“套裝ID”為之前創建的“com.cmjstudio.kctest”,讓應用和這個App關聯(注意這個應用不需要提交)。
  3. 在iTunes Connect的“用戶和職能”中創建沙盒測試用戶(由于在測試階段應用還沒有正式提交到App Store,所以只有沙盒用戶可以登錄Game Center)。
    這里寫圖片描述
  4. 在iOS“設置”中找到Game Center允許沙盒,否則真機無法調試
    這里寫圖片描述
    有了以上準備就可以在應用程序中增加積分、添加成就了,當然在實際開發過程積分和成就都是基于玩家所通過的關卡來完成的,為了簡化這個過程這里就直接通過幾個按鈕手動觸發這些事件。Game Center開發需要使用GameKit框架,首先熟悉一下常用的幾個類:

GKLocalPlayer:表示本地玩家,在GameKit中還有一個GKPlayer表示聯機玩家,為了保證非聯網用戶也可以正常使用游戲功能,一般使用GKLocalPlayer。

GKScore:管理游戲積分,例如設置積分、排名等。

GKLeaderboard:表示游戲排行榜,主用用于管理玩家排名,例如加載排行榜、設置默認排行榜、加載排行榜圖片等。

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

GKAchievementDescription:成就描述信息,包含成就的標題、獲得前描述、獲得后描述、是否可重復獲得成就等信息。

GKGameCenterViewController:排行榜、成就查看視圖控制器。如果應用本身不需要自己開發排行榜、成就查看試圖可以直接調用此控制器。

下面就以一個簡單的示例來完成排行榜、成就設置和查看,在這個演示程序中通過兩種方式來查看排行和成就:一種是直接使用框架自帶的GKGameCenterViewContrller調用系統視圖查看,另一種是通過API自己讀取排行榜、成就信息并顯示。此外在應用中有兩個添加按鈕分別用于設置得分和成就。應用大致布局如下(圖片較大可點擊查看大圖):
這里寫圖片描述

1.首先看一下主視圖控制器KCMainTableViewController:

主視圖控制器調用GKLeaderboard的loadLeaderboardsWithCompletionHandler:方法加載了所有排行榜,這個過程需要注意每個排行榜(GKLeaderboard)中的scores屬性是沒有值的,如果要讓每個排行榜的scores屬性有值必須調用一次排行榜的loadScoresWithCompletionHandler:方法。

調用GKAchievement的loadAchievementsWithCompletionHandler:方法加載加載成就,注意這個方法只能獲得完成度不為0的成就,如果完成度為0是獲得不到的;然后調用GKAchievementDesciption的loadAchievementDescriptionsWithCompletionHandler:方法加載了所有成就描述,這里加載的是所有成就描述(不管完成度是否為0);緊接著調用了每個成就描述的loadImageWithCompletionHandler:方法加載成就圖片。

將獲得的排行榜、成就、成就描述、成就圖片信息保存,并在導航到詳情視圖時傳遞給排行榜視圖控制器和成就視圖控制器以便在子控制器視圖中展示。

在主視圖控制器左上方添加查看游戲中心控制按鈕,點擊按鈕調用GKGameCenterViewController來展示排行榜、成就、玩家信息,這是系統自帶的一個游戲中心視圖方便和后面我們自己獲得的信息對比。

程序如下

#import "KCMainTableViewController.h"

import <GameKit/GameKit.h>

import "KCLeaderboardTableViewController.h"

import "KCAchievementTableViewController.h"

@interface KCMainTableViewController ()<GKGameCenterControllerDelegate>

@property (strong,nonatomic) NSArray leaderboards;//排行榜對象數組 @property (strong,nonatomic) NSArray achievements;//成就 @property (strong,nonatomic) NSArray achievementDescriptions;//成就描述 @property (strong,nonatomic) NSMutableDictionary achievementImages;//成就圖片

@property (weak, nonatomic) IBOutlet UILabel leaderboardLabel; //排行個數 @property (weak, nonatomic) IBOutlet UILabel achievementLable; //成就個數

@end

@implementation KCMainTableViewController

pragma mark - 控制器視圖事件

  • (void)viewDidLoad { [super viewDidLoad];

    [self authorize]; }

pragma mark - UI事件

  • (IBAction)viewGameCenterClick:(UIBarButtonItem *)sender { [self viewGameCenter]; }

pragma mark - GKGameCenterViewController代理方法

//點擊完成 -(void)gameCenterViewControllerDidFinish:(GKGameCenterViewController *)gameCenterViewController{ NSLog(@"完成."); [gameCenterViewController dismissViewControllerAnimated:YES completion:nil]; }

pragma mark -導航

-(void)prepareForSegue:(UIStoryboardSegue )segue sender:(id)sender{ //如果是導航到排行榜,則將當前排行榜傳遞到排行榜視圖 if ([segue.identifier isEqualToString:@"leaderboard"]) { UINavigationController navigationController=segue.destinationViewController; KCLeaderboardTableViewController leaderboardController=[navigationController.childViewControllers firstObject]; leaderboardController.leaderboards=self.leaderboards; }else if ([segue.identifier isEqualToString:@"achievement"]) { UINavigationController navigationController=segue.destinationViewController; KCAchievementTableViewController *achievementController=[navigationController.childViewControllers firstObject]; achievementController.achievements=self.achievements; achievementController.achievementDescriptions=self.achievementDescriptions; achievementController.achievementImages=self.achievementImages; } }

pragma mark - 私有方法

//檢查是否經過認證,如果沒經過認證則彈出Game Center登錄界面 -(void)authorize{ //創建一個本地用戶 GKLocalPlayer localPlayer= [GKLocalPlayer localPlayer]; //檢查用于授權,如果沒有登錄則讓用戶登錄到GameCenter(注意此事件設置之后或點擊登錄界面的取消按鈕都會被調用) [localPlayer setAuthenticateHandler:^(UIViewController controller, NSError error) { if ([[GKLocalPlayer localPlayer] isAuthenticated]) { NSLog(@"已授權."); [self setupUI]; }else{ //注意:在設置中找到Game Center,設置其允許沙盒,否則controller為nil [self presentViewController:controller animated:YES completion:nil]; } }]; } //UI布局 -(void)setupUI{ //更新排行榜個數 [GKLeaderboard loadLeaderboardsWithCompletionHandler:^(NSArray leaderboards, NSError error) { if (error) { NSLog(@"加載排行榜過程中發生錯誤,錯誤信息:%@",error.localizedDescription); } self.leaderboards=leaderboards; self.leaderboardLabel.text=[NSString stringWithFormat:@"%i",leaderboards.count]; //獲取得分,注意只有調用了loadScoresWithCompletionHandler:方法之后leaderboards中的排行榜中的scores屬性才有值,否則為nil [leaderboards enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL stop) { GKLeaderboard leaderboard=obj; [leaderboard loadScoresWithCompletionHandler:^(NSArray scores, NSError error) { }]; }]; }]; //更新獲得成就個數,注意這個個數不一定等于iTunes Connect中的總成就個數,此方法只能獲取到成就完成進度不為0的成就 [GKAchievement loadAchievementsWithCompletionHandler:^(NSArray achievements, NSError error) { if (error) { NSLog(@"加載成就過程中發生錯誤,錯誤信息:%@",error.localizedDescription); } self.achievements=achievements; self.achievementLable.text=[NSString stringWithFormat:@"%i",achievements.count]; //加載成就描述(注意,即使沒有獲得此成就也能獲取到) [GKAchievementDescription loadAchievementDescriptionsWithCompletionHandler:^(NSArray descriptions, NSError error) { if (error) { NSLog(@"加載成就描述信息過程中發生錯誤,錯誤信息:%@",error.localizedDescription); return ; } self.achievementDescriptions=descriptions; //加載成就圖片 _achievementImages=[NSMutableDictionary dictionary]; [descriptions enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL stop) { GKAchievementDescription description=(GKAchievementDescription )obj; [description loadImageWithCompletionHandler:^(UIImage image, NSError error) { [_achievementImages setObject:image forKey:description.identifier]; }]; }]; }]; }]; }

//查看Game Center -(void)viewGameCenter{ if (![GKLocalPlayer localPlayer].isAuthenticated) { NSLog(@"未獲得用戶授權."); return; } //Game Center視圖控制器 GKGameCenterViewController *gameCenterController=[[GKGameCenterViewController alloc]init]; //設置代理 gameCenterController.gameCenterDelegate=self; //顯示 [self presentViewController:gameCenterController animated:YES completion:nil]; } @end</code></pre>

2.然后看一下排行榜控制器視圖KCLeaderboardTableViewController:

在排行榜控制器視圖中定義一個leaderboards屬性用于接收主視圖控制器傳遞的排行榜信息并且通過一個UITableView展示排行榜名稱、得分等。

在排行榜控制器視圖中通過GKScore的reportScores: withCompletionHandler:設置排行榜得分,注意每個GKScore對象必須設置value屬性來表示得分(GKScore是通過identifier來和排行榜關聯起來的)。
程序如下

#import "KCLeaderboardTableViewController.h"

import <GameKit/GameKit.h>

//排行榜標識,就是iTunes Connect中配置的排行榜ID

define kLeaderboardIdentifier1 @"Goals"

@interface KCLeaderboardTableViewController () @end

@implementation KCLeaderboardTableViewController

  • (void)viewDidLoad { [super viewDidLoad]; } #pragma mark - UI事件 //添加得分(這里指的是進球數)
  • (IBAction)addScoreClick:(UIBarButtonItem )sender { [self addScoreWithIdentifier:kLeaderboardIdentifier1 value:100]; } #pragma mark - UITableView數據源方法 -(NSInteger)tableView:(UITableView )tableView sectionForSectionIndexTitle:(NSString )title atIndex:(NSInteger)index{ return 1; } -(NSInteger)tableView:(UITableView )tableView numberOfRowsInSection:(NSInteger)section{ return self.leaderboards.count; } -(UITableViewCell )tableView:(UITableView )tableView cellForRowAtIndexPath:(NSIndexPath )indexPath{ static NSString identtityKey=@"myTableViewCellIdentityKey1"; UITableViewCell *cell=[self.tableView dequeueReusableCellWithIdentifier:identtityKey]; if(cell==nil){
      cell=[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:identtityKey];
    
    } GKLeaderboard leaderboard=self.leaderboards[indexPath.row]; GKScore score=[leaderboard.scores firstObject]; NSLog(@"scores:%@",leaderboard.scores); cell.textLabel.text=leaderboard.title;//排行榜標題 cell.detailTextLabel.text=[NSString stringWithFormat:@"%lld",score.value]; //排行榜得分 return cell; }

pragma mark - 屬性

pragma mark - 私有方法

/* 設置得分 @param identifier 排行榜標識 @param value 得分 / -(void)addScoreWithIdentifier:(NSString )identifier value:(int64_t)value{ if (![GKLocalPlayer localPlayer].isAuthenticated) { NSLog(@"未獲得用戶授權."); return; } //創建積分對象 GKScore score=[[GKScore alloc]initWithLeaderboardIdentifier:identifier]; //設置得分 score.value=value; //提交積分到Game Center服務器端,注意保存是異步的,并且支持離線提交 [GKScore reportScores:@[score] withCompletionHandler:^(NSError *error) { if(error){ NSLog(@"保存積分過程中發生錯誤,錯誤信息:%@",error.localizedDescription); return ; } NSLog(@"添加積分成功."); }]; } @end</code></pre>

3.最后就是成就視圖控制器KCAchievementTableViewController:

在成就視圖控制器定義achievements、achievementDescriptions、achievementImages三個屬性分別表示成就、成就描述、成就圖片,這三個屬性均從主視圖控制器中傳遞進來,然后使用UITableView展示成就、成就圖片、成就進度。

創建GKAchievemnt對象(通過identifier屬性來表示具體的成就)并指定完成度,通過調用GKAchievement的reportAchievements: withCompletionHandler:方法提交完成度到Game Center服務器。
程序如下

#import "KCAchievementTableViewController.h"

import <GameKit/GameKit.h>

//成就標識,就是iTunes Connect中配置的成就ID

define kAchievementIdentifier1 @"AdidasGoldenBall"

define kAchievementIdentifier2 @"AdidasGoldBoot"

@interface KCAchievementTableViewController ()

@end

@implementation KCAchievementTableViewController

pragma mark - 控制器視圖方法

  • (void)viewDidLoad { [super viewDidLoad];

}

pragma mark - UI事件

//添加成就

  • (IBAction)addAchievementClick:(UIBarButtonItem *)sender { [self addAchievementWithIdentifier:kAchievementIdentifier1]; }

pragma mark - UITableView數據源方法

-(NSInteger)tableView:(UITableView )tableView sectionForSectionIndexTitle:(NSString )title atIndex:(NSInteger)index{ return 1; } -(NSInteger)tableView:(UITableView )tableView numberOfRowsInSection:(NSInteger)section{ return self.achievementDescriptions.count; } -(UITableViewCell )tableView:(UITableView )tableView cellForRowAtIndexPath:(NSIndexPath )indexPath{ static NSString identtityKey=@"myTableViewCellIdentityKey1"; UITableViewCell cell=[self.tableView dequeueReusableCellWithIdentifier:identtityKey]; if(cell==nil){ cell=[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:identtityKey]; } GKAchievementDescription desciption=[self.achievementDescriptions objectAtIndex:indexPath.row]; cell.textLabel.text=desciption.title ;//成就標題 //如果已經獲得成就則加載進度,否則為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</code></pre>

運行效果:
這里寫圖片描述

2.6. 應用內購買

大家都知道做iOS開發本身的收入有三種來源:出售應用、內購和廣告。國內用戶通常很少直接購買應用,因此對于開發者而言(特別是個人開發者),內購和廣告收入就成了主要的收入來源。內購營銷模式,通常軟件本身是不收費的,但是要獲得某些特權就必須購買一些道具,而內購的過程是由蘋果官方統一來管理的,所以和Game Center一樣,在開發內購程序之前要做一些準備工作(下面的準備工作主要是針對真機的,模擬器省略Provisioning Profile配置過程):

  1. 前四步和Game Center基本完全一致,只是在選擇服務時不是選擇Game Center而是要選擇內購服務(In-App Purchase)。
  2. 到iTuens Connect中設置“App 內購買項目”,這里仍然以上面的“KCTest”項目為例,假設這個足球競技游戲中有三種道具,分別為“強力手套”(增強防御)、“金球”(增加金球率)和“能量瓶”(提供足夠體力),前兩者是非消耗品只用一次性購買,后者是消耗品用完一次必須再次購買。
    這里寫圖片描述
  3. 到iTunes Connect中找到“協議、稅務和銀行業務”增加“iOS Paid Applications”協議,并完成所有配置后等待審核通過(注意這一步如果不設置在應用程序中無法獲得可購買產品)。
  4. 在iOS“設置”中找到”iTunes Store與App Store“,在這里可以選擇使用沙盒用戶登錄或者處于注銷狀態,但是一定注意不能使用真實用戶登錄,否則下面的購買測試不會成功,因為到目前為止我們的應用并沒有真正通過蘋果官方審核只能用沙盒測試用戶(如果是模擬器不需要此項設置)。

  5. 有了上面的設置之后保證應用程序Bundle ID和iTunes Connect中的Bundle ID(或者說App ID中配置的Bundle ID)一致即可準備開發。

開發內購應用時需要使用StoreKit.framework,下面是這個框架中常用的幾個類:

SKProduct:可購買的產品(例如上面設置的能量瓶、強力手套等),其productIdentifier屬性對應iTunes Connect中配置的“產品ID“,但是此類不建議直接初始化使用,而是要通過SKProductRequest來加載可用產品(避免出現購買到無效的產品)。

SKProductRequest:產品請求類,主要用于加載產品列表(包括可用產品和不可用產品),通常加載完之后會通過其-(void)productsRequest:(SKProductsRequest )request didReceiveResponse:(SKProductsResponse )response代理方法獲得響應,拿到響應中的可用產品。

SKPayment:產品購買支付類,保存了產品ID、購買數量等信息(注意與其對應的有一個SKMutablePayment對象,此對象可以修改產品數量等信息)。

SKPaymentQueue:產品購買支付隊列,一旦將一個SKPayment添加到此隊列就會向蘋果服務器發送請求完成此次交易。注意交易的狀態反饋不是通過代理完成的,而是通過一個交易監聽者(類似于代理,可以通過隊列的addTransactionObserver來設置)。

SKPaymentTransaction:一次產品購買交易,通常交易完成后支付隊列會調用交易監聽者的-(void)paymentQueue:(SKPaymentQueue )queue updatedTransactions:(NSArray )transaction方法反饋交易情況,并在此方法中將交易對象返回。

SKStoreProductViewController:應用程序商店產品展示視圖控制器,用于在應用程序內部展示此應用在應用商店的情況。(例如可以使用它讓用戶在應用內完成評價,注意由于本次演示的示例程序沒有正式提交到應用商店,所以在此暫不演示此控制器視圖的使用)。

了解了以上幾個常用的開發API之后,下面看一下應用內購買的流程:
1. 通過SKProductRequest獲得可購買產品SKProduct數組(SKProductRequest會根據程序的Bundle ID去對應的內購配置中獲取指定ID的產品對象),這個過程中需要知道產品標識(必須和iTuens Connect中的對應起來),可以存儲到沙盒中也可以存儲到數據庫中(下面的Demo中定義成了宏定義)。
2. 請求完成后可以在SKProductRequest的-(void)productsRequest:(SKProductsRequest )request didReceiveResponse:(SKProductsResponse )response代理方法中獲得SKProductResponse對象,這個對象中保存了products屬性表示可用產品對象數組。
3. 給SKPaymentQueue設置一個監聽者來獲得交易的狀態(它類似于一個代理),監聽者通過-(void)paymentQueue:(SKPaymentQueue )queue updatedTransactions:(NSArray )transaction方法反饋交易的變化狀態(通常在此方法中可以根據交易成功、恢復成功等狀態來做一些處理)。
4. 一旦用戶決定購買某個產品(SKProduct),就可以根據SKProduct來創建一個對應的支付對象SKPayment,只要將這個對象加入到SKPaymentQueue中就會觸發購買行為(將訂單提交到蘋果服務器),一旦一個交易發生變化就會觸發SKPaymentQueue監聽者來反饋交易情況。
5. 交易提交給蘋果服務器之后如果不出意外的話通常就會彈出一個確認購買的對話框,引導用戶完成交易,最終完成交易后(通常是完成交易,用戶點擊”好“)會調用交易監聽者-(void)paymentQueue:(SKPaymentQueue )queue updatedTransactions:(NSArray )transaction方法將此次交易的所有交易對象SKPaymentTransaction數組返回,可以通過交易狀態判斷交易情況。
6. 通常一次交易完成后需要對本次交易進行驗證,避免越獄機器模擬蘋果官方的反饋造成交易成功假象。蘋果官方提供了一個驗證的URL,只要將交易成功后的憑證(這個憑證從iOS7之后在交易成功會會存儲到沙盒中)傳遞給這個地址就會給出交易狀態和本次交易的詳細信息,通過這些信息(通常可以根據交易狀態、Bundler ID、ProductID等確認)可以標識出交易是否真正完成。
7. 對于非消耗品,用戶在完成購買后如果用戶使用其他機器登錄或者用戶卸載重新安裝應用后通常希望這些非消耗品能夠恢復(事實上如果不恢復用戶再次購買也不會成功)。調用SKPaymentQueue的restoreCompletedTransactions就可以完成恢復,恢復后會調用交易監聽者的paymentQueue:(SKPaymentQueue )queue updatedTransactions:(NSArray )transaction方法反饋恢復的交易(也就是已購買的非消耗品交易,注意這個過程中如果沒有非消耗品可恢復,是不會調用此方法的)。

下面通過一個示例程序演示內購和恢復的整個過程,程序界面大致如下:

主界面中展示了所有可購買產品和售價,以及購買情況。

選擇一個產品點”購買“可以購買此商品,購買完成后刷新購買狀態(如果是非消耗品則顯示已購買,如果是消耗品則顯示購買個數)。

程序卸載后重新安裝可以點擊”恢復購買“來恢復已購買的非消耗品。
這里寫圖片描述
程序代碼:

#import "KCMainTableViewController.h"

import <StoreKit/StoreKit.h>

define kAppStoreVerifyURL @"

define kSandboxVerifyURL @"

//定義可以購買的產品ID,必須和iTunes Connect中設置的一致

define kProductID1 @"ProtectiveGloves" //強力手套,非消耗品

define kProductID2 @"GoldenGlobe" //金球,非消耗品

define kProductID3 @"EnergyBottle" //能量瓶,消耗品

@interface KCMainTableViewController ()<SKProductsRequestDelegate,SKPaymentTransactionObserver>

@property (strong,nonatomic) NSMutableDictionary *products;//有效的產品 @property (assign,nonatomic) int selectedRow;//選中行 @end

@implementation KCMainTableViewController

pragma mark - 控制器視圖方法

  • (void)viewDidLoad { [super viewDidLoad];

    [self loadProducts]; [self addTransactionObjserver]; }

pragma mark - UI事件

//購買產品

  • (IBAction)purchaseClick:(UIBarButtonItem )sender { NSString productIdentifier=self.products.allKeys[self.selectedRow]; SKProduct *product=self.products[productIdentifier]; if (product) {
      [self purchaseProduct:product];
    
    }else{
      NSLog(@"沒有可用商品.");
    
    }

} //恢復購買

  • (IBAction)restorePurchaseClick:(UIBarButtonItem *)sender { [self restoreProduct]; }

pragma mark - UITableView數據源方法

  • (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; }

  • (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.products.count; }

  • (UITableViewCell )tableView:(UITableView )tableView cellForRowAtIndexPath:(NSIndexPath )indexPath { static NSString identtityKey=@"myTableViewCellIdentityKey1"; UITableViewCell *cell=[self.tableView dequeueReusableCellWithIdentifier:identtityKey]; if(cell==nil){

      cell=[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:identtityKey];
    

    } cell.accessoryType=UITableViewCellAccessoryNone; NSString key=self.products.allKeys[indexPath.row]; SKProduct product=self.products[key]; NSString purchaseString; NSUserDefaults defaults=[NSUserDefaults standardUserDefaults]; if ([product.productIdentifier isEqualToString:kProductID3]) {

      purchaseString=[NSString stringWithFormat:@"已購買%i個",[defaults integerForKey:product.productIdentifier]];
    

    }else{

      if([defaults boolForKey:product.productIdentifier]){
          purchaseString=@"已購買";
      }else{
          purchaseString=@"尚未購買";
      }
    

    } cell.textLabel.text=[NSString stringWithFormat:@"%@(%@)",product.localizedTitle,purchaseString] ; cell.detailTextLabel.text=[NSString stringWithFormat:@"%@",product.price]; return cell; } #pragma mark - UITableView代理方法 -(void)tableView:(UITableView )tableView didSelectRowAtIndexPath:(NSIndexPath )indexPath{

    UITableViewCell currentSelected=[tableView cellForRowAtIndexPath:indexPath]; currentSelected.accessoryType=UITableViewCellAccessoryCheckmark; self.selectedRow=indexPath.row; } -(void)tableView:(UITableView )tableView didDeselectRowAtIndexPath:(NSIndexPath )indexPath{ UITableViewCell currentSelected=[tableView cellForRowAtIndexPath:indexPath]; currentSelected.accessoryType=UITableViewCellAccessoryNone; }

pragma mark - SKProductsRequestd代理方法

/* 產品請求完成后的響應方法 @param request 請求對象 @param response 響應對象,其中包含產品信息 / -(void)productsRequest:(SKProductsRequest )request didReceiveResponse:(SKProductsResponse )response{ //保存有效的產品 _products=[NSMutableDictionary dictionary]; [response.products enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL stop) { SKProduct product=obj; [_products setObject:product forKey:product.productIdentifier]; }]; //由于這個過程是異步的,加載成功后重新刷新表格 [self.tableView reloadData]; } -(void)requestDidFinish:(SKRequest )request{ NSLog(@"請求完成."); } -(void)request:(SKRequest )request didFailWithError:(NSError *)error{ if (error) { NSLog(@"請求過程中發生錯誤,錯誤信息:%@",error.localizedDescription); } }

pragma mark - SKPaymentQueue監聽方法

/* 交易狀態更新后執行 @param queue 支付隊列 @param transactions 交易數組,里面存儲了本次請求的所有交易對象 / -(void)paymentQueue:(SKPaymentQueue )queue updatedTransactions:(NSArray )transactions{ [transactions enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL stop) { SKPaymentTransaction paymentTransaction=obj; if (paymentTransaction.transactionState==SKPaymentTransactionStatePurchased){//已購買成功 NSLog(@"交易\"%@\"成功.",paymentTransaction.payment.productIdentifier); //購買成功后進行驗證 [self verifyPurchaseWithPaymentTransaction]; //結束支付交易 [queue finishTransaction:paymentTransaction]; }else if(paymentTransaction.transactionState==SKPaymentTransactionStateRestored){//恢復成功,對于非消耗品才能恢復,如果恢復成功則transaction中記錄的恢復的產品交易 NSLog(@"恢復交易\"%@\"成功.",paymentTransaction.payment.productIdentifier); [queue finishTransaction:paymentTransaction];//結束支付交易

        //恢復后重新寫入偏好配置,重新加載UITableView
        [[NSUserDefaults standardUserDefaults]setBool:YES forKey:paymentTransaction.payment.productIdentifier];
        [self.tableView reloadData];
    }else if(paymentTransaction.transactionState==SKPaymentTransactionStateFailed){
        if (paymentTransaction.error.code==SKErrorPaymentCancelled) {//如果用戶點擊取消
            NSLog(@"取消購買.");
        }
        NSLog(@"ErrorCode:%i",paymentTransaction.error.code);
    }

}];

} //恢復購買完成 -(void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue{ NSLog(@"恢復完成."); }

pragma mark - 私有方法

/ 添加支付觀察者監控,一旦支付后則會回調觀察者的狀態更新方法: -(void)paymentQueue:(SKPaymentQueue )queue updatedTransactions:(NSArray )transactions / -(void)addTransactionObjserver{ //設置支付觀察者(類似于代理),通過觀察者來監控購買情況 [[SKPaymentQueue defaultQueue] addTransactionObserver:self]; } / 加載所有產品,注意產品一定是從服務器端請求獲得,因為有些產品可能開發人員知道其存在性,但是不經過審核是無效的; / -(void)loadProducts{ //定義要獲取的產品標識集合 NSSet sets=[NSSet setWithObjects:kProductID1,kProductID2,kProductID3, nil]; //定義請求用于獲取產品 SKProductsRequest productRequest=[[SKProductsRequest alloc]initWithProductIdentifiers:sets]; //設置代理,用于獲取產品加載狀態 productRequest.delegate=self; //開始請求 [productRequest start]; } /* 購買產品 @param product 產品對象 / -(void)purchaseProduct:(SKProduct )product{ //如果是非消耗品,購買過則提示用戶 NSUserDefaults *defaults=[NSUserDefaults standardUserDefaults]; if ([product.productIdentifier isEqualToString:kProductID3]) { NSLog(@"當前已經購買\"%@\" %i 個.",kProductID3,[defaults integerForKey:product.productIdentifier]); }else if([defaults boolForKey:product.productIdentifier]){ NSLog(@"\"%@\"已經購買過,無需購買!",product.productIdentifier); return; }

//創建產品支付對象
SKPayment *payment=[SKPayment paymentWithProduct:product];
//支付隊列,將支付對象加入支付隊列就形成一次購買請求
if (![SKPaymentQueue canMakePayments]) {
    NSLog(@"設備不支持購買.");
    return;
}
SKPaymentQueue *paymentQueue=[SKPaymentQueue defaultQueue];
//添加都支付隊列,開始請求支付

// [self addTransactionObjserver]; [paymentQueue addPayment:payment]; }

/* 恢復購買,對于非消耗品如果應用重新安裝或者機器重置后可以恢復購買 注意恢復時只能一次性恢復所有非消耗品 / -(void)restoreProduct{ SKPaymentQueue *paymentQueue=[SKPaymentQueue defaultQueue]; //設置支付觀察者(類似于代理),通過觀察者來監控購買情況 // [paymentQueue addTransactionObserver:self]; //恢復所有非消耗品 [paymentQueue restoreCompletedTransactions]; }

/* 驗證購買,避免越獄軟件模擬蘋果請求達到非法購買問題 / -(void)verifyPurchaseWithPaymentTransaction{ //從沙盒中獲取交易憑證并且拼接成請求體數據 NSURL receiptUrl=[[NSBundle mainBundle] appStoreReceiptURL]; NSData receiptData=[NSData dataWithContentsOfURL:receiptUrl];

NSString *receiptString=[receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];//轉化為base64字符串

NSString *bodyString = [NSString stringWithFormat:@"{\"receipt-data\" : \"%@\"}", receiptString];//拼接請求數據
NSData *bodyData = [bodyString dataUsingEncoding:NSUTF8StringEncoding];

//創建請求到蘋果官方進行購買驗證
NSURL *url=[NSURL URLWithString:kSandboxVerifyURL];
NSMutableURLRequest *requestM=[NSMutableURLRequest requestWithURL:url];
requestM.HTTPBody=bodyData;
requestM.HTTPMethod=@"POST";
//創建連接并發送同步請求
NSError *error=nil;
NSData *responseData=[NSURLConnection sendSynchronousRequest:requestM returningResponse:nil error:&error];
if (error) {
    NSLog(@"驗證購買過程中發生錯誤,錯誤信息:%@",error.localizedDescription);
    return;
}
NSDictionary *dic=[NSJSONSerialization JSONObjectWithData:responseData options:NSJSONReadingAllowFragments error:nil];
NSLog(@"%@",dic);
if([dic[@"status"] intValue]==0){
    NSLog(@"購買成功!");
    NSDictionary *dicReceipt= dic[@"receipt"];
    NSDictionary *dicInApp=[dicReceipt[@"in_app"] firstObject];
    NSString *productIdentifier= dicInApp[@"product_id"];//讀取產品標識
    //如果是消耗品則記錄購買數量,非消耗品則記錄是否購買過
    NSUserDefaults *defaults=[NSUserDefaults standardUserDefaults];
    if ([productIdentifier isEqualToString:kProductID3]) {
        int purchasedCount=[defaults integerForKey:productIdentifier];//已購買數量
        [[NSUserDefaults standardUserDefaults] setInteger:(purchasedCount+1) forKey:productIdentifier];
    }else{
        [defaults setBool:YES forKey:productIdentifier];
    }
    [self.tableView reloadData];
    //在此處對購買記錄進行存儲,可以存儲到開發商的服務器端
}else{
    NSLog(@"購買失敗,未通過驗證!");
}

} @end</code></pre>

運行效果(這是程序在卸載后重新安裝的運行效果,卸載前已經購買”強力手套“,因此程序運行后點擊了”恢復購買“):
這里寫圖片描述

擴展–廣告

上面也提到做iOS開發另一收益來源就是廣告,在iOS上有很多廣告服務可以集成,使用比較多的就是蘋果的iAd、谷歌的Admob,下面簡單演示一下如何使用iAd來集成廣告。使用iAd集成廣告的過程比較簡單,首先引入iAd.framework框架,然后創建ADBannerView來展示廣告,通常會設置ADBannerView的代理方法來監聽廣告點擊并在廣告加載失敗時隱藏廣告展示控件。下面的代碼簡單的演示了這個過程:

#import "ViewController.h"

import <iAd/iAd.h>

@interface ViewController ()<ADBannerViewDelegate> @property (weak, nonatomic) IBOutlet ADBannerView *advertiseBanner;//廣告展示視圖

@end

@implementation ViewController

  • (void)viewDidLoad { [super viewDidLoad];

    //設置代理 self.advertiseBanner.delegate=self; }

pragma mark - ADBannerView代理方法

//廣告加載完成 -(void)bannerViewDidLoadAd:(ADBannerView )banner{ NSLog(@"廣告加載完成."); } //點擊Banner后離開之前,返回NO則不會展開全屏廣告 -(BOOL)bannerViewActionShouldBegin:(ADBannerView )banner willLeaveApplication:(BOOL)willLeave{ NSLog(@"點擊Banner后離開之前."); return YES; } //點擊banner后全屏顯示,關閉后調用 -(void)bannerViewActionDidFinish:(ADBannerView )banner{ NSLog(@"廣告已關閉."); } //獲取廣告失敗 -(void)bannerView:(ADBannerView )banner didFailToReceiveAdWithError:(NSError *)error{ NSLog(@"加載廣告失敗."); self.advertiseBanner.hidden=YES; }

@end</code></pre>

運行效果:
這里寫圖片描述

2.7. 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屬性存儲數據:

#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</code></pre>

如果要加載iCloud中的文檔列表就需要使用另一個類NSMetadataQuery,通常考慮到網絡的原因并不會一次性加載所有數據,而利用NSMetadataQuery并指定searchScopes為NSMetadataQueryUbiquitousDocumentScope來限制查找iCloud文檔數據。使用NSMetadataQuery還可以通過謂詞限制搜索關鍵字等信息,并在搜索完成之后通過通知的形式通知客戶端搜索的情況。

大家都知道微軟的OneNote云筆記本軟件,通過它可以實現多種不同設置間的筆記同步,這里就簡單實現一個基于iCloud服務的筆記軟件。在下面的程序中實現筆記的新增、修改、保存、讀取等操作。程序界面大致如下,點擊界面右上方增加按鈕增加一個筆記,點擊某個筆記可以查看并編輯。
這里寫圖片描述
在主視圖控制器首先查詢所有iCloud保存的文檔并在查詢通知中遍歷查詢結果保存文檔名稱和創建日期到UITableView展示;其次當用戶點擊了增加按鈕會調用KCDocument完成文檔添加并導航到文檔詳情界面編輯文檔內容。

#import "KCMainTableViewController.h"

import "KCDocument.h"

import "KCDetailViewController.h"

define kContainerIdentifier @"iCloud.com.cmjstudio.kctest" //容器id,可以從生產的entitiements文件中查看Ubiquity Container Identifiers(注意其中的$(CFBundleIdentifier)替換為BundleID)

@interface KCMainTableViewController () @property (strong,nonatomic) KCDocument document;//當前選中的管理對象 @property (strong,nonatomic) NSMutableDictionary files; //現有文件名、創建日期集合 @property (strong,nonatomic) NSMetadataQuery *dataQuery;//數據查詢對象,用于查詢iCloud文檔

@end

@implementation KCMainTableViewController

pragma mark - 控制器視圖方法

  • (void)viewDidLoad { [super viewDidLoad];

    [self loadDocuments]; }

pragma mark - UI事件

//新建文檔

  • (IBAction)addDocumentClick:(UIBarButtonItem )sender { UIAlertController promptController=[UIAlertController alertControllerWithTitle:@"KCTest" message:@"請輸入筆記名稱" preferredStyle:UIAlertControllerStyleAlert]; [promptController addTextFieldWithConfigurationHandler:^(UITextField *textField) {

      textField.placeholder=@"筆記名稱";
    

    }]; UIAlertAction okAction=[UIAlertAction actionWithTitle:@"確定" style:UIAlertActionStyleDefault handler:^(UIAlertAction action) {

      UITextField *textField= promptController.textFields[0];
      [self addDocument:textField.text];
    

    }]; [promptController addAction:okAction]; UIAlertAction cancelAction=[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction action) {

    }]; [promptController addAction:cancelAction]; [self presentViewController:promptController animated:YES completion:nil];

}

pragma mark - 導航

  • (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { if ([segue.identifier isEqualToString:@"noteDetail"]) {
      KCDetailViewController *detailController= segue.destinationViewController;
      detailController.document=self.document;
    
    } }

pragma mark - 屬性

-(NSMetadataQuery *)dataQuery{ if (!_dataQuery) { //創建一個iCloud查詢對象 _dataQuery=[[NSMetadataQuery alloc]init]; _dataQuery.searchScopes=@[NSMetadataQueryUbiquitousDocumentsScope]; //注意查詢狀態是通過通知的形式告訴監聽對象的 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(metadataQueryFinish:) name:NSMetadataQueryDidFinishGatheringNotification object:_dataQuery];//數據獲取完成通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(metadataQueryFinish:) name:NSMetadataQueryDidUpdateNotification object:_dataQuery];//查詢更新通知 } return _dataQuery; }

pragma mark - 私有方法

/* 取得云端存儲文件的地址 @param fileName 文件名,如果文件名為nil則重新創建一個url @return 文件地址 / -(NSURL )getUbiquityFileURL:(NSString )fileName{ //取得云端URL基地址(參數中傳入nil則會默認獲取第一個容器) NSURL url= [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:kContainerIdentifier]; //取得Documents目錄 url=[url URLByAppendingPathComponent:@"Documents"]; //取得最終地址 url=[url URLByAppendingPathComponent:fileName]; return url; }

/* 添加文檔到iCloud @param fileName 文件名稱(不包括后綴) / -(void)addDocument:(NSString )fileName{ //取得保存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</code></pre>

當新增一個筆記或選擇一個已存在的筆記后可以查看、保存筆記內容。

#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</code></pre>

到目前為止都是關于如何使用iCloud來保存文檔的內容,上面也提到過還可以使用iCloud來保存首選項,這在很多情況下通常很有用,特別是對于開發了iPhone版又開發了iPad版的應用,如果用戶在一臺設備上進行了首選項配置之后到另一臺設備上也能使用是多么優秀的體驗啊。相比文檔存儲,首選項存儲要簡單的多,在上面“kctest.entitlements”中可以看到首選項配置并非像文檔一樣可以包含多個容器,這里只有一個Key-Value Store,通常使用NSUbiquitousKeyValueStore的defaultStore來獲取,它的使用方法和NSUserDefaults幾乎完全一樣,當鍵值對存儲發生變化后可以通過NSUbiquitousKeyValueStoreDidChangeExternallyNotification等獲得對應的通知。在上面的筆記應用中有一個”設置“按鈕用于設置退出筆記詳情視圖后是否自動保存,這個選項就是通過iCloud的首選項來存儲的。

#import "KCSettingTableViewController.h"

define kSettingAutoSave @"com.cmjstudio.kctest.settings.autosave"

@interface KCSettingTableViewController () @property (weak, nonatomic) IBOutlet UISwitch *autoSaveSetting;

@end

@implementation KCSettingTableViewController

  • (void)viewDidLoad { [super viewDidLoad]; [self setupUI]; }

pragma mark - UI事件

  • (IBAction)autoSaveClick:(UISwitch *)sender { [self setSetting:sender.on]; }

pragma mark - 私有方法

-(void)setupUI{ //設置iCloud中的首選項值 NSUbiquitousKeyValueStore defaults=[NSUbiquitousKeyValueStore defaultStore]; self.autoSaveSetting.on= [defaults boolForKey:kSettingAutoSave]; //添加存儲變化通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( keyValueStoreChange:) name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification object:defaults]; } /** key-value store發生變化或存儲空間不足 @param notification 通知對象 / -(void)keyValueStoreChange:(NSNotification )notification{ NSLog(@"Key-value store change..."); }

/* 設置首選項 @param value 是否自動保存 / -(void)setSetting:(BOOL)value{ //iCloud首選項設置 NSUbiquitousKeyValueStore defaults=[NSUbiquitousKeyValueStore defaultStore]; [defaults setBool:value forKey:kSettingAutoSave]; [defaults synchronize];//同步 } @end</code></pre>

運行效果:
這里寫圖片描述

2.8. Passbook

Passbook是蘋果推出的一個管理登機牌、會員卡、電影票、優惠券等信息的工具。Passbook就像一個卡包,用于存放你的購物卡、積分卡、電影票、禮品卡等,而這些票據就是一個“Pass”。和物理票據不同的是你可以動態更新Pass的信息,提醒用戶優惠券即將過期;甚至如果你的Pass中包含地理位置信息的話當你到達某個商店還可以動態提示用戶最近商店有何種優惠活動;當用戶將一張團購券添加到Passbook之后,用戶到了商店之后Passbook可以自動彈出團購券,店員掃描之后進行消費、積分等等都是Passbook的應用場景。Passbook可以管理多類票據,蘋果將其劃分為五類:

  1. 登機牌(Boarding pass)
  2. 優惠券(Coupon)
  3. 活動票據、入場券(Event ticket)
  4. 購物卡、積分卡(Store Cards)
  5. 普通票據(自定義票據)(Generic pass)
    蘋果的劃分一方面出于不同票據功能及展示信息不同,另一方面也是為了統一票據的設計,下面是蘋果官方關于五種票據的布局設計布局:
    這里寫圖片描述
    iOS開發之--通訊錄、藍牙、內購、GameCenter、iCloud、Passbook功能開發匯總

iOS開發之--通訊錄、藍牙、內購、GameCenter、iCloud、Passbook功能開發匯總

iOS開發之--通訊錄、藍牙、內購、GameCenter、iCloud、Passbook功能開發匯總
iOS開發之--通訊錄、藍牙、內購、GameCenter、iCloud、Passbook功能開發匯總

–系統應用與系統服務

iOS開發過程中有時候難免會使用iOS內置的一些應用軟件和服務,例如QQ通訊錄、微信電話本會使用iOS的通訊錄,一些第三方軟件會在應用內發送短信等。今天將和大家一起學習如何使用系統應用、使用系統服務:

調用系統應用
使用系統服務
短信與郵件
通訊錄
藍牙
社交
Game Center
應用內購買
iCloud
Passbook


系統應用
在開發某些應用時可能希望能夠調用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”

<p>@interface ViewController ()</p> <p>@end</p> <p>@implementation ViewController</p>
  • (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=@”kenshin@hotmail.com”;
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];
}

<p>@end
不難發現當openURL:方法只要指定一個URL Schame并且已經安裝了對應的應用程序就可以打開此應用。當然,如果是自己開發的應用也可以調用openURL方法來打開。假設你現在開發了一個應用A,如果用戶機器上已經安裝了此應用,并且在應用B中希望能夠直接打開A。那么首先需要確保應用A已經配置了Url Types,具體方法就是在plist文件中添加URL types節點并配置URL Schemas作為具體協議,配置URL identifier作為這個URL的唯一標識,如下圖:</p>

iOSApplication_URLTypes

然后就可以調用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對象。
設置收件人recipients、信息正文body,如果運行商支持主題和附件的話可以設置主題subject、附件attachments(可以通過canSendSubject、canSendAttachments方法判斷是否支持)
設置代理messageComposeDelegate(注意這里不是delegate屬性,因為delegate屬性已經留給UINavigationController,MFMessageComposeViewController沒有覆蓋此屬性而是重新定義了一個代理),實現代理方法獲得發送狀態。
下面自定義一個發送短信的界面演示MFMessageComposeViewController的使用:

MFMessageComposeViewController_Layout

用戶通過在此界面輸入短信信息點擊“發送信息”調用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

pragma mark - 控制器視圖方法

  • (void)viewDidLoad {
    [super viewDidLoad];

}

pragma mark - UI事件

 本文由用戶 XavierIbsch 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!