iOS開發系列--通信錄、藍牙、內購、GameCenter、iCloud、Passbook系統服務開發彙總

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

  1. 調用系統應用
  2. 使用系統服務
    1. 短信與郵件
    2. 通信錄
    3. 藍牙
    4. 社交
    5. Game Center
    6. 應用內購買
    7. iCloud
    8. Passbook
  3. 目 錄

系統應用

在開發某些應用時可能但願可以調用iOS系統內置的電話、短信、郵件、瀏覽器應用,此時你能夠直接使用UIApplication的OpenURL:方法指定特定的協議來打開不一樣的系統應用。經常使用的協議以下:ios

打電話:tel:或者tel://、telprompt:或telprompt://(撥打電話前有提示)git

發短信:sms:或者sms://算法

發送郵件:mailto:或者mailto://數據庫

啓動瀏覽器:http:或者http://編程

下面以一個簡單的demo演示如何調用上面幾種系統應用:json

//
//  ViewController.m
//  iOSSystemApplication
//
//  Created by Kenshin Cui on 14/04/05.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

}

#pragma mark - UI事件
//打電話
- (IBAction)callClicK:(UIButton *)sender {
    NSString *phoneNumber=@"18500138888";
//    NSString *url=[NSString stringWithFormat:@"tel://%@",phoneNumber];//這種方式會直接撥打電話
    NSString *url=[NSString stringWithFormat:@"telprompt://%@",phoneNumber];//這種方式會提示用戶確認是否撥打電話
    [self openUrl:url];
}

//發送短信
- (IBAction)sendMessageClick:(UIButton *)sender {
    NSString *phoneNumber=@"18500138888";
    NSString *url=[NSString stringWithFormat:@"sms://%@",phoneNumber];
    [self openUrl:url];
}

//發送郵件
- (IBAction)sendEmailClick:(UIButton *)sender {
    NSString *mailAddress=@"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];
}

@end

不難發現當openURL:方法只要指定一個URL Schame而且已經安裝了對應的應用程序就能夠打開此應用。固然,若是是本身開發的應用也能夠調用openURL方法來打開。假設你如今開發了一個應用A,若是用戶機器上已經安裝了此應用,而且在應用B中但願可以直接打開A。那麼首先須要確保應用A已經配置了Url Types,具體方法就是在plist文件中添加URL types節點並配置URL Schemas做爲具體協議,配置URL identifier做爲這個URL的惟一標識,以下圖:數組

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使用步驟:

  1. 建立MFMessageComposeViewController對象。
  2. 設置收件人recipients、信息正文body,若是運行商支持主題和附件的話能夠設置主題subject、附件attachments(能夠經過canSendSubject、canSendAttachments方法判斷是否支持)
  3. 設置代理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 <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

這裏須要強調一下:

  • MFMessageComposeViewController的代理不是經過delegate屬性指定的而是經過messageComposeDelegate指定的。
  • 能夠經過幾種方式來指定發送的附件,在這個過程當中請務必指定文件的後綴,不然在發送後沒法正確識別文件類別(例如若是發送的是一張jpg圖片,在發送後沒法正確查看圖片)。
  • 不管發送成功與否代理方法-(void)messageComposeViewController:(MFMessageComposeViewController *)controller didFinishWithResult:(MessageComposeResult)result都會執行,經過代理參數中的result來得到發送狀態。

其實只要熟悉了MFMessageComposeViewController以後,那麼用於發送郵件的MFMailComposeViewController用法和步驟徹底一致,只是功能不一樣。下面看一下MFMailComposeViewController的使用:

//
//  KCSendEmailViewController.m
//  iOSSystemApplication
//
//  Created by Kenshin Cui on 14/04/05.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//

#import "KCSendEmailViewController.h"
#import <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

運行效果:

MFMailComposeViewController_Layout     MFMailComposeViewController

通信錄

 

AddressBook

iOS中帶有一個Contacts應用程序來管理聯繫人,可是有些時候咱們但願本身的應用可以訪問或者修改這些信息,這個時候就要用到AddressBook.framework框架。iOS中的通信錄是存儲在數據庫中的,因爲iOS的權限設計,開發人員是不容許直接訪問通信錄數據庫的,必須依靠AddressBook提供的標準API來實現通信錄操做。經過AddressBook.framework開發者能夠從底層去操做AddressBook.framework的全部信息,可是須要注意的是這個框架是基於C語言編寫的,沒法使用ARC來管理內存,開發者須要本身管理內存。下面大體介紹一下通信錄操做中經常使用的類型:

  • ABAddressBookRef:表明通信錄對象,經過該對象開發人員不用過多的關注通信錄的存儲方式,能夠直接以透明的方式去訪問、保存(在使用AddressBook.framework操做聯繫人時,全部的增長、刪除、修改後都必須執行保存操做,相似於Core Data)等。
  • ABRecordRef:表明一個通用的記錄對象,能夠是一條聯繫人信息,也能夠是一個羣組,能夠經過ABRecordGetRecordType()函數得到具體類型。若是做爲聯繫人(事實上也常用它做爲聯繫人),那麼這個記錄記錄了一個完整的聯繫人信息(姓名、性別、電話、郵件等),每條記錄都有一個惟一的ID標示這條記錄(能夠經過ABRecordGetRecordID()函數得到)。
  • ABPersonRef:表明聯繫人信息,不多直接使用,實際開發過程當中一般會使用類型爲「kABPersonType」的ABRecordRef來表示聯繫人(因而可知ABPersonRef實際上是一種類型爲「kABPersonType」的ABRecordRef)
  • 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

通信錄的訪問步驟通常以下:

  1. 調用ABAddressBookCreateWithOptions()方法建立通信錄對象ABAddressBookRef。
  2. 調用ABAddressBookRequestAccessWithCompletion()方法得到用戶受權訪問通信錄。
  3. 調用ABAddressBookCopyArrayOfAllPeople()、ABAddressBookCopyPeopleWithName()方法查詢聯繫人信息。
  4. 讀取聯繫人後若是要顯示聯繫人信息則能夠調用ABRecord相關方法讀取相應的數據;若是要進行修改聯繫人信息,則可使用對應的方法修改ABRecord信息,而後調用ABAddressBookSave()方法提交修改;若是要刪除聯繫人,則能夠調用ABAddressBookRemoveRecord()方法刪除,而後調用ABAddressBookSave()提交修改操做。
  5. 也就是說若是要修改或者刪除都須要首先查詢對應的聯繫人,而後修改或刪除後提交更改。若是用戶要增長一個聯繫人則不用進行查詢,直接調用ABPersonCreate()方法建立一個ABRecord而後設置具體的屬性,調用ABAddressBookAddRecord方法添加便可。

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

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

KCContactViewController.h

//
//  KCTableViewController.h
//  AddressBook
//
//  Created by Kenshin Cui on 14/04/05.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//

#import <UIKit/UIKit.h>

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

@interface KCContactTableViewController : UITableViewController

@end

KCContactViewController.m

//
//  KCTableViewController.m
//  AddressBook
//
//  Created by Kenshin Cui on 14/04/05.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//

#import "KCContactTableViewController.h"
#import <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

新增或修改控制器視圖,用於顯示一個聯繫人的信息或者新增一個聯繫人:

KCAddPersonViewController.h

//
//  KCAddPersonViewController.h
//  AddressBook
//
//  kABPersonFirstNameProperty
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//

#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

KCAddPersonViewController.m

//
//  KCAddPersonViewController.m
//  AddressBook
//
//  kABPersonFirstNameProperty
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//

#import "KCAddPersonViewController.h"
#import "KCContactTableViewController.h"

@interface KCAddPersonViewController ()

@property (weak, nonatomic) IBOutlet UITextField *firstName;
@property (weak, nonatomic) IBOutlet UITextField *lastName;
@property (weak, nonatomic) IBOutlet UITextField *workPhone;
@end

@implementation KCAddPersonViewController

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

#pragma mark - UI事件
- (IBAction)cancelClick:(UIBarButtonItem *)sender {
    [self.delegate cancelEdit];
    [self dismissViewControllerAnimated:YES completion:nil];
}

- (IBAction)doneClick:(UIBarButtonItem *)sender {
    //調用代理方法
    [self.delegate editPersonWithFirstName:self.firstName.text lastName:self.lastName.text workNumber:self.workPhone.text];
    
    [self dismissViewControllerAnimated:YES completion:nil];
}

#pragma mark - 私有方法
-(void)setupUI{
    if (self.recordID) {//若是ID不爲0則認爲是修改,此時須要初始化界面
        self.firstName.text=self.firstNameText;
        self.lastName.text=self.lastNameText;
        self.workPhone.text=self.workPhoneText;
    }
}
@end

運行效果:

AddressBook

備註:

1.上文中所指的以Ref結尾的對象事實上是該對象的指針(或引用),在C語言的框架中多數類型會以Ref結尾,這個類型自己就是一個指針,定義時不須要加「*」。

2.一般方法中包含copy、create、new、retain等關鍵字的方法建立的變量使用以後須要調用對應的release方法釋放。例如:使用ABPersonCreate();建立完ABRecordRef變量後使用CFRelease()方法釋放。

3.在與不少C語言框架交互時能夠都存在Obj-C和C語言類型之間的轉化(特別是Obj-C和Core Foundation框架中的一些轉化),此時可能會用到橋接,只要在強轉以後前面加上」__bridge」便可,通過橋接轉化後的類型不須要再去手動維護內存,也就不須要使用對應的release方法釋放內存。

4.AddressBook框架中不少類型的建立、屬性設置等都是以這個類型名開發頭的方法來建立的,事實上若是你們熟悉了其餘框架會發現也都是相似的,這是Apple開發中約定俗成的命名規則(特別是C語言框架)。例如:要給ABRecordRef類型的變量設置屬性則能夠經過ABRecordSetValue()方法完成。

AddressBookUI

使用AddressBook.framework來操做通信錄特色就是能夠對通信錄有更加精確的控制,可是缺點就是面對大量C語言API稍嫌麻煩,因而Apple官方提供了另外一套框架供開發者使用,那就是AddressBookUI.framework。例如前面查看、新增、修改人員的界面這個框架就提供了現成的控制器視圖供開發者使用。下面是這個框架中提供的控制器視圖:

  • ABPersonViewController:用於查看聯繫人信息(可設置編輯)。須要設置displayedPerson屬性來設置要顯示或編輯的聯繫人。
  • ABNewPersonViewController:用於新增聯繫人信息。
  • ABUnknownPersonViewController:用於顯示一個未知聯繫人(還沒有保存的聯繫人)信息。須要設置displayedPerson屬性來設置要顯示的未知聯繫人。

以上三個控制器視圖均繼承於UIViewController,在使用過程當中必須使用一個UINavigationController進行包裝,不然只能看到視圖內容沒法進行操做(例如對於ABNewPersonViewController若是不使用UINavigationController進行包裝則沒有新增和取消按鈕),同時注意包裝後的控制器視圖不須要處理具體新增、修改邏輯(增長和修改的處理邏輯對應的控制器視圖內部已經完成),可是必須處理控制器的關閉操做(調用dismissViewControllerAnimated::方法),而且能夠經過代理方法得到新增、修改的聯繫人。下面看一下三個控制器視圖的代理方法:

1.ABPersonViewController的displayViewDelegate代理方法:

-(BOOL)personViewController:(ABPersonViewController *)personViewController shouldPerformDefaultActionForPerson:(ABRecordRef)person property:(ABPropertyID)property identifier:(ABMultiValueIdentifier)identifier:此方法會在選擇了一個聯繫人屬性後觸發,四個參數分別表明:使用的控制器視圖、所查看的聯繫人、所選則的聯繫人屬性、該屬性是不是多值屬性。

2.ABNewPersonViewController的newPersonViewDelegate代理方法:

-(void)newPersonViewController:(ABNewPersonViewController *)newPersonView didCompleteWithNewPerson:(ABRecordRef)person:點擊取消或完成後觸發,若是參數中的person爲NULL說明點擊了取消,不然說明點擊了完成。不管是取消仍是完成操做,此方法調用時保存操做已經進行完畢,不須要在此方法中本身保存聯繫人信息。

3.ABUnkownPersonViewcontroller的unkownPersonViewDelegate代理方法:

-(void)unknownPersonViewController:(ABUnknownPersonViewController *)unknownCardViewController didResolveToPerson:(ABRecordRef)person:保存此聯繫人時調用,調用後將此聯繫人返回。

-(BOOL)unknownPersonViewController:(ABUnknownPersonViewController *)personViewController shouldPerformDefaultActionForPerson:(ABRecordRef)person property:(ABPropertyID)property identifier:(ABMultiValueIdentifier)identifier:選擇一個位置聯繫人屬性以後執行,返回值表明是否執行默認的選擇操做(例如若是是手機號,默認操做會撥打此電話)

除了上面三類控制器視圖在AddressBookUI中還提供了另一個控制器視圖ABPeoplePickerNavigationController,它與以前介紹的UIImagePickerController、MPMediaPickerController相似,只是他是用來選擇一個聯繫人的。這個控制器視圖自己繼承於UINavigationController,視圖自身的「組」、「取消」按鈕操做不須要開發者來完成(例如開發者不用在點擊取消是關閉當前控制器視圖,它自身已經實現了關閉方法),固然這裏主要說一下這個控制器視圖的peoplePickerDelegate代理方法:

-(void)peoplePickerNavigationController:(ABPeoplePickerNavigationController *)peoplePicker didSelectPerson:(ABRecordRef)person:選擇一個聯繫人後執行。此代理方法實現後代理方法「-(void)peoplePickerNavigationController:(ABPeoplePickerNavigationController *)peoplePicker didSelectPerson:(ABRecordRef)person property:(ABPropertyID)property identifier:(ABMultiValueIdentifier)identifier」不會再執行。而且一旦實現了這個代理方法用戶只能選擇到聯繫人視圖,沒法查看具體聯繫人的信息。

-(void)peoplePickerNavigationControllerDidCancel:(ABPeoplePickerNavigationController *)peoplePicker:用戶點擊取消後執行。

-(void)peoplePickerNavigationController:(ABPeoplePickerNavigationController *)peoplePicker didSelectPerson:(ABRecordRef)person property:(ABPropertyID)property identifier:(ABMultiValueIdentifier)identifier:選擇聯繫人具體的屬性後執行,注意若是要執行此方法則不能實現-(void)peoplePickerNavigationController:(ABPeoplePickerNavigationController *)peoplePicker didSelectPerson:(ABRecordRef)person代理方法,此時若是點擊一個具體聯繫人會導航到聯繫人詳細信息界面,用戶點擊具體的屬性後觸發此方法。

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

//
//  ViewController.m
//  AddressBookUI
//
//  Created by Kenshin Cui on 14/04/05.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//

#import "ViewController.h"
#import <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

運行效果:

ABPersonViewController     ABNewPersonViewController

ABUnkownPersonViewController     ABPeoplePickerNaviagationController

注意:

爲了讓你們能夠更加清楚的看到幾個控制器視圖的使用,這裏並無結合前面的UITableViewController來使用,事實上你們結合前面UITableViewController能夠作一個完善的通信錄應用。

藍牙

隨着藍牙低功耗技術BLE(Bluetooth Low Energy)的發展,藍牙技術正在一步步成熟,現在的大部分移動設備都配備有藍牙4.0,相比以前的藍牙技術耗電量大大下降。從iOS的發展史也不難看出蘋果目前對藍牙技術也是愈來愈關注,例如蘋果於2013年9月發佈的iOS7就配備了iBeacon技術,這項技術徹底基於藍牙傳輸。可是衆所周知蘋果的設備對於權限要求也是比較高的,所以在iOS中並不能像Android同樣隨意使用藍牙進行文件傳輸(除非你已經越獄)。在iOS中進行藍牙傳輸應用開發經常使用的框架有以下幾種:

GameKit.framework:iOS7以前的藍牙通信框架,從iOS7開始過時,可是目前多數應用仍是基於此框架。

MultipeerConnectivity.framework:iOS7開始引入的新的藍牙通信開發框架,用於取代GameKit。

CoreBluetooth.framework:功能強大的藍牙開發框架,要求設備必須支持藍牙4.0。

前兩個框架使用起來比較簡單,可是缺點也比較明顯:僅僅支持iOS設備,傳輸內容僅限於沙盒或者照片庫中用戶選擇的文件,而且第一個框架只能在同一個應用之間進行傳輸(一個iOS設備安裝應用A,另外一個iOS設備上安裝應用B是沒法傳輸的)。固然CoreBluetooth就擺脫了這些束縛,它再也不侷限於iOS設備之間進行傳輸,你能夠經過iOS設備向Android、Windows Phone以及其餘安裝有藍牙4.0芯片的智能設備傳輸,所以也是目前智能家居、無線支付等熱門智能設備所推崇的技術。

GameKit

其實從名稱來看這個框架並非專門爲了支持藍牙傳輸而設計的,它是爲遊戲設計的。而不少遊戲中會用到基於藍牙的點對點信息傳輸,所以這個框架中集成了藍牙傳輸模塊。前面也說了這個框架自己有不少限制,可是在iOS7以前的不少藍牙傳輸都是基於此框架的,因此有必要對它進行了解。GameKit中的藍牙使用設計很簡單,並無給開發者留有太多的複雜接口,而多數鏈接細節開發者是不須要關注的。GameKit中提供了兩個關鍵類來操做藍牙鏈接:

GKPeerPickerController:藍牙查找、鏈接用的視圖控制器,一般狀況下應用程序A打開後會調用此控制器的show方法來展現一個藍牙查找的視圖,一旦發現了另外一個一樣在查找藍牙鏈接的客戶客戶端B就會出如今視圖列表中,此時若是用戶點擊鏈接B,B客戶端就會詢問用戶是否容許A鏈接B,若是容許後A和B之間創建一個藍牙鏈接。

GKSession:鏈接會話,主要用於發送和接受傳輸數據。一旦A和B創建鏈接GKPeerPickerController的代理方法會將A、B二者創建的會話(GKSession)對象傳遞給開發人員,開發人員拿到此對象能夠發送和接收數據。

其實理解了上面兩個類以後,使用起來就比較簡單了,下面就以一個圖片發送程序來演示GameKit中藍牙的使用。此程序一個客戶端運行在模擬器上做爲客戶端A,另外一個運行在iPhone真機上做爲客戶端B(注意A、B必須運行同一個程序,GameKit藍牙開發是不支持兩個不一樣的應用傳輸數據的)。兩個程序運行以後均調用GKPeerPickerController來發現周圍藍牙設備,一旦A發現了B以後就開始鏈接B,而後iOS會詢問用戶是否接受鏈接,一旦接受以後就會調用GKPeerPickerController的-(void)peerPickerController:(GKPeerPickerController *)picker didConnectPeer:(NSString *)peerID toSession:(GKSession *)session代理方法,在此方法中能夠得到鏈接的設備id(peerID)和鏈接會話(session);此時能夠設置會話的數據接收句柄(至關於一個代理)並保存會話以便發送數據時使用;一旦一端(假設是A)調用會話的sendDataToAllPeers: withDataMode: error:方法發送數據,此時另外一端(假設是B)就會調用句柄的- (void) receiveData:(NSData *)data fromPeer:(NSString *)peer inSession: (GKSession *)session context:(void *)context方法,在此方法能夠得到發送數據並處理。下面是程序代碼:

//
//  ViewController.m
//  GameKit
//
//  Created by Kenshin Cui on 14/04/05.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//

#import "ViewController.h"
#import <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

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

MultipeerConnectivity

前面已經說了GameKit相關的藍牙操做類從iOS7已經所有過時,蘋果官方推薦使用MultipeerConnectivity代替。可是應該瞭解,MultipeerConnectivity.framework並不只僅支持藍牙鏈接,準確的說它是一種支持Wi-Fi網絡、P2P Wi-Fi已經藍牙我的局域網的通訊框架,它屏蔽了具體的鏈接技術,讓開發人員有統一的接口編程方法。經過MultipeerConnectivity鏈接的節點之間能夠安全的傳遞信息、流或者其餘文件資源而沒必要經過網絡服務。此外使用MultipeerConnectivity進行近場通訊也再也不侷限於同一個應用之間傳輸,而是能夠在不一樣的應用之間進行數據傳輸(固然若是有必要的話你仍然能夠選擇在一個應用程序之間傳輸)。

要了解MultipeerConnectivity的使用必需要清楚一個概念:廣播(Advertisting)和發現(Disconvering),這很相似於一種Client-Server模式。假設有兩臺設備A、B,B做爲廣播去發送自身服務,A做爲發現的客戶端。一旦A發現了B就試圖創建鏈接,通過B贊成兩者創建鏈接就能夠相互發送數據。在使用GameKit框架時,A和B既做爲廣播又做爲發現,固然這種狀況在MultipeerConnectivity中也很常見。

A.廣播

不管是做爲服務器端去廣播仍是做爲客戶端去發現廣播服務,那麼兩個(或更多)不一樣的設備之間必需要有區分,一般狀況下使用MCPeerID對象來區分一臺設備,在這個設備中能夠指定顯示給對方查看的名稱(display name)。另外無論是哪一方,還必須創建一個會話MCSession用於發送和接受數據。一般狀況下會在會話的-(void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state代理方法中跟蹤會話狀態(已鏈接、正在鏈接、未鏈接);在會話的-(void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID代理方法中接收數據;同時還會調用會話的-(void)sendData: toPeers:withMode: error:方法去發送數據。

廣播做爲一個服務器去發佈自身服務,供周邊設備發現鏈接。在MultipeerConnectivity中使用MCAdvertiserAssistant來表示一個廣播,一般建立廣播時指定一個會話MCSession對象將廣播服務和會話關聯起來。一旦調用廣播的start方法周邊的設備就能夠發現該廣播並能夠鏈接到此服務。在MCSession的代理方法中能夠隨時更新鏈接狀態,一旦創建了鏈接以後就能夠經過MCSession的connectedPeers得到已經鏈接的設備。

B.發現

前面已經說過做爲發現的客戶端一樣須要一個MCPeerID來標誌一個客戶端,同時會擁有一個MCSession來監聽鏈接狀態併發送、接受數據。除此以外,要發現廣播服務,客戶端就必需要隨時查找服務來鏈接,在MultipeerConnectivity中提供了一個控制器MCBrowserViewController來展現可鏈接和已鏈接的設備(這相似於GameKit中的GKPeerPickerController),固然若是想要本身定製一個界面來展現設備鏈接的狀況你能夠選擇本身開發一套UI界面。一旦經過MCBroserViewController選擇一個節點去鏈接,那麼做爲廣播的節點就會收到通知,詢問用戶是否容許鏈接。因爲初始化MCBrowserViewController的過程已經指定了會話MCSession,因此鏈接過程當中會隨時更新會話狀態,一旦創建了鏈接,就能夠經過會話的connected屬性得到已鏈接設備而且可使用會話發送、接受數據。

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

首先看一下做爲廣播節點的程序:

界面:

MultipeerConnectivity_Advertiser

點擊「開始廣播」來發布服務,一旦有節點鏈接此服務就可使用「選擇照片」來從照片庫中選取一張圖片併發送到全部已鏈接節點。

程序:

//
//  ViewController.m
//  MultipeerConnectivity_Advertiser
//
//  Created by Kenshin Cui on 14/04/05.
//  Copyright (c) 2015年 cmjstudio. All rights reserved.
//

#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

再看一下做爲發現節點的程序:

界面:

MultipeerConnectivity_Discover

點擊「查找設備」瀏覽可用服務,點擊服務創建鏈接;一旦創建了鏈接以後就能夠點擊「選擇照片」會從照片庫中選擇一張圖片併發送給已鏈接的節點。

程序:

//
//  ViewController.m
//  MultipeerConnectivity
//
//  Created by Kenshin Cui on 14/04/05.
//  Copyright (c) 2015年 cmjstudio. All rights reserved.
//

#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

在兩個程序中不管是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整個框架就是基於這兩個概念來設計的。

Central_Peripheral

外圍設備和中央設備在CoreBluetooth中使用CBPeripheralManager和CBCentralManager表示。

CBPeripheralManager:外圍設備一般用於發佈服務、生成數據、保存數據。外圍設備發佈並廣播服務,告訴周圍的中央設備它的可用服務和特徵。

CBCentralManager:中央設備使用外圍設備的數據。中央設備掃描到外圍設備後會就會試圖創建鏈接,一旦鏈接成功就可使用這些服務和特徵。

外圍設備和中央設備之間交互的橋樑是服務(CBService)和特徵(CBCharacteristic),兩者都有一個惟一的標識UUID(CBUUID類型)來惟一肯定一個服務或者特徵,每一個服務能夠擁有多個特徵,下面是他們之間的關係:

Peripheral_Central_Service_Characteristic

一臺iOS設備(注意iPhone4如下設備不支持BLE,另外iOS7.0、8.0模擬器也沒法模擬BLE)既能夠做爲外圍設備又能夠做爲中央設備,可是不能同時便是外圍設備又是中央設備,同時注意創建鏈接的過程不須要用戶手動選擇容許,這一點和前面兩個框架是不一樣的,這主要是由於BLE應用場景再也不侷限於兩臺設備之間資源共享了。

A.外圍設備

建立一個外圍設備一般分爲如下幾個步驟:

  1. 建立外圍設備CBPeripheralManager對象並指定代理。
  2. 建立特徵CBCharacteristic、服務CBSerivce並添加到外圍設備
  3. 外圍設備開始廣播服務(startAdvertisting:)。
  4. 和中央設備CBCentral進行交互。

下面是簡單的程序示例,程序有兩個按鈕「啓動」和「更新」,點擊啓動按鈕則建立外圍設備、添加服務和特徵並開始廣播,一旦發現有中央設備鏈接並訂閱了此服務的特徵則經過更新按鈕更新特徵數據,此時已訂閱的中央設備就會收到更新數據。

界面設計:

PeripheralPeer

程序設計:

//
//  ViewController.m
//  PeripheralApp
//
//  Created by Kenshin Cui on 14/04/05.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//  外圍設備(周邊設備)

#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

上面程序運行的流程以下(圖中藍色表明外圍設備操做,綠色部分表示中央設備操做):

PeripheralFlow

B.中央設備

中央設備的建立通常能夠分爲以下幾個步驟:

  1. 建立中央設備管理對象CBCentralManager並指定代理。
  2. 掃描外圍設備,通常發現可用外圍設備則鏈接並保存外圍設備。
  3. 查找外圍設備服務和特徵,查找到可用特徵則讀取特徵數據。

下面是一個簡單的中央服務器端實現,點擊「啓動」按鈕則開始掃描周圍的外圍設備,一旦發現了可用的外圍設備則創建鏈接並設置外圍設備的代理,以後開始查找其服務和特徵。一旦外圍設備的特徵值作了更新,則能夠在代理方法中讀取更新後的特徵值。

界面設計:

CentralPeer

程序設計:

//
//  ViewController.m
//  CentralApp
//
//  Created by Kenshin Cui on 14/04/05.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//  中心設備

#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

上面程序運行的流程圖以下:

CentralFlow

有了上面兩個程序就能夠分別運行在兩臺支持BLE的iOS設備上,當兩個應用創建鏈接後,一旦外圍設備更新特徵以後,中央設備就能夠當即獲取到更新後的值。須要強調的是使用CoreBluetooth開發的應用不只僅能夠和其餘iOS設備進行藍牙通訊,還能夠同其餘第三方遵循BLE規範的設備進行藍牙通信,這裏就再也不贅述。

注意:本節部分圖片來自於互聯網,版權歸原做者全部。

社交

Social

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

程序代碼:

//
//  ViewController.m
//  Social
//
//  Created by Kenshin Cui on 14/04/05.
//  Copyright (c) 2015年 cmjstudio. All rights reserved.
//

#import "ViewController.h"
#import <Social/Social.h>

@interface ViewController ()

@end

@implementation ViewController
#pragma mark - 控制器視圖事件
- (void)viewDidLoad {
    [super viewDidLoad];
    
}

#pragma mark - UI事件
- (IBAction)shareClick:(UIBarButtonItem *)sender {
    [self shareToSina];
}


#pragma mark - 私有方法
-(void)shareToSina{
    //檢查新浪微博服務是否可用
    if(![SLComposeViewController isAvailableForServiceType:SLServiceTypeSinaWeibo]){
        NSLog(@"新浪微博服務不可用.");
        return;
    }
    //初始化內容編寫控制器,注意這裏指定分享類型爲新浪微博
    SLComposeViewController *composeController=[SLComposeViewController composeViewControllerForServiceType:SLServiceTypeSinaWeibo];
    //設置默認信息
    [composeController setInitialText:@"Kenshin Cui's Blog..."];
    //添加圖片
    [composeController addImage:[UIImage imageNamed:@"stevenChow"]];
    //添加鏈接
    [composeController addURL:[NSURL URLWithString:@"http://www.cnblogs.com/kenshincui"]];
    //設置發送完成後的回調事件
    __block SLComposeViewController *composeControllerForBlock=composeController;
    composeController.completionHandler=^(SLComposeViewControllerResult result){
        if (result==SLComposeViewControllerResultDone) {
            NSLog(@"開始發送...");
        }
        [composeControllerForBlock dismissViewControllerAnimated:YES completion:nil];
    };
    //顯示編輯視圖
    [self presentViewController:composeController animated:YES completion:nil];
}

@end

運行效果:

Social

發送成功以後:

Social_SinaWeibo

在這個過程當中開發人員不須要知道新浪微博的更多分享細節,Social框架中已經統一了分享的接口,你能夠經過ServiceType設置是分享到Facebook、Twitter、新浪微博、騰訊微博,而不關心具體的細節實現。那麼當運行上面的示例時它是怎麼知道用哪一個帳戶來發送微博呢?其實在iOS的設置中有專門設置Facebook、Twitter、微博的地方:

Social_SinaSetting

必須首先在這裏設置微博帳戶才能完成上面的發送,否則Social框架也不可能知道具體使用哪一個帳戶來發送。

第三方框架

固然,經過上面的設置界面應該能夠看到,蘋果官方默認支持的分享並不太多,特別是對於國內的應用只支持新浪微博和騰訊微博(事實上從iOS7蘋果才考慮支持騰訊微博),那麼若是要分享到微信、人人、開心等等國內較爲知名的社交網絡怎麼辦呢?目前最好的選擇就是使用第三方框架,由於若是要本身實現各個應用的接口仍是比較複雜的。當前使用較多的就是友盟社會化組件ShareSDK,並且如今百度也出了社會化分享組件。今天沒法對全部組件都進行一一介紹,這裏就以友盟社交化組件爲例簡單作一下介紹:

  1. 註冊友盟帳號並新建應用得到AppKey。
  2. 下載友盟SDK並將下載的文件放到項目中(注意下載的過程當中能夠選擇所須要的分享服務)。
  3. 在應用程序中設置友盟的AppKey。
  4. 分享時調用presentSnsIconSheetView: appKey: shareText: shareImage: shareToSnsNames: delegate:方法或者presentSnsController: appKey: shareText: shareImage: shareToSnsNames: delegate:方法顯示分享列表(注意這個過程當中要使用某些服務須要到對應的平臺去申請並對應擴展框架進行設置,不然分享列表中不會顯示對應的分享按鈕)。

下面是一個簡單的示例:

//
//  ViewController.m
//  Social_UM
//
//  Created by Kenshin Cui on 14/04/05.
//  Copyright (c) 2015年 cmjstudio. All rights reserved.
//

#import "ViewController.h"
#import "UMSocial.h"
#import "UMSocialWechatHandler.h"

@interface ViewController ()<UMSocialUIDelegate>

@end

@implementation ViewController
#pragma mark - 控制器視圖事件
- (void)viewDidLoad {
    [super viewDidLoad];
    
}

#pragma mark - UI事件
- (IBAction)shareClick:(UIBarButtonItem *)sender {
    //設置微信AppId、appSecret,分享url
//    [UMSocialWechatHandler setWXAppId:@"wx30dbea5d5a258ed3" appSecret:@"cd36a9829e4b49a0dcac7b4162da5a5" url:@"http://www.cmj.com/social-UM"];
    //微信好友、微信朋友圈、微信收藏、QQ空間、QQ好友、來往好友等都必須通過各自的平臺集成不然不會出如今分享列表,例如上面是設置微信的AppId和appSecret
    [UMSocialSnsService presentSnsIconSheetView:self appKey:@"54aa0a0afd98c5209f000efa" shareText:@"Kenshin Cui's Blog..." shareImage:[UIImage imageNamed:@"stevenChow"] shareToSnsNames:@[UMShareToSina,UMShareToTencent,UMShareToRenren,UMShareToDouban] delegate:self];

}

#pragma mark - UMSocialSnsService代理
//分享完成
-(void)didFinishGetUMSocialDataInViewController:(UMSocialResponseEntity *)response{
    //分享成功
    if(response.responseCode==UMSResponseCodeSuccess){
        NSLog(@"分享成功");
    }
}
@end

運行效果:

Social_UM

注意:在第一次使用某個分享服務是須要輸入相應的帳號得到受權才能分享。

GameCenter

Game Center是由蘋果發佈的在線多人遊戲社交網絡,經過它遊戲玩家能夠邀請好友進行多人遊戲,它也會記錄玩家的成績並在排行榜中展現,同時玩家每通過必定的階段會得到不一樣的成就。這裏就簡單介紹一下如何在本身的應用中集成Game Center服務來讓用戶得到積分、成就以及查看遊戲排行和已得到成就。

因爲Game Center是蘋果推出的一項重要服務,蘋果官方對於它的控制至關嚴格,所以使用Game Center以前必需要作許多準備工做。一般須要通過如下幾個步驟(下面的準備工做主要是針對真機的,模擬器省略Provisioning Profile配置過程):

  1. 在蘋果開發者中心建立支持Game Center服務的App ID並指定具體的Bundle ID,假設是「com.cmjstudio.kctest」(注意這個Bundle ID就是往後要開發的遊戲的Bundle ID)。 GameCenter_AppID
  2. 基於「com.cmjstudio.kctest」建立開發者配置文件(或描述文件)並導入對應的設備(建立過程當中選擇支持Game Center服務的App ID,這樣iOS設備在運行指定Boundle ID應用程序就知道此應用支持Game Center服務)。 GameCenter_AppProfiler
  3. 在iTunes Connect中建立一個應用(假設叫「KCTest」,這是一款足球競技遊戲)並指定「套裝ID」爲以前建立的「com.cmjstudio.kctest」,讓應用和這個App關聯(注意這個應用不須要提交)。
  4. 在iTunes Connect的「用戶和職能」中建立沙盒測試用戶(因爲在測試階段應用尚未正式提交到App Store,因此只有沙盒用戶能夠登陸Game Center)。
  5. 在iTunes Connect中配置此應用Game Center(這裏配置了遊戲在遊戲中心的顯示名稱爲「CMJ」),在其中添加排行榜和成就(假設添加一個排行榜ID「Goals」表示進球個數;兩個成就ID分別爲「AdidasGoldBall」、「AdidasGoldBoot」表明金球獎和金靴獎成就,點數分別爲80、100)。GameCenter_AppConfig
  6. 在iOS「設置」中找到Game Center容許沙盒,不然真機沒法調試(若是是模擬器不須要此項設置)。 GameCenter_DeviceConfig

有了以上準備就能夠在應用程序中增長積分、添加成就了,固然在實際開發過程積分和成就都是基於玩家所經過的關卡來完成的,爲了簡化這個過程這裏就直接經過幾個按鈕手動觸發這些事件。Game Center開發須要使用GameKit框架,首先熟悉一下經常使用的幾個類:

GKLocalPlayer:表示本地玩家,在GameKit中還有一個GKPlayer表示聯機玩家,爲了保證非聯網用戶也能夠正常使用遊戲功能,通常使用GKLocalPlayer。

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

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

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

GKAchievementDescription:成就描述信息,包含成就的標題、得到前描述、得到後描述、是否可重複得到成就等信息。

GKGameCenterViewController:排行榜、成就查看視圖控制器。若是應用自己不須要本身開發排行榜、成就查看試圖能夠直接調用此控制器。

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

GameCenter_Layout

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

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

調用GKAchievement的loadAchievementsWithCompletionHandler:方法加載加載成就,注意這個方法只能得到完成度不爲0的成就,若是完成度爲0是得到不到的;而後調用GKAchievementDesciption的loadAchievementDescriptionsWithCompletionHandler:方法加載了全部成就描述,這裏加載的是全部成就描述(無論完成度是否爲0);緊接着調用了每一個成就描述的loadImageWithCompletionHandler:方法加載成就圖片。

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

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

程序以下

//
//  KCMainTableViewController.m
//  kctest
//
//  Created by Kenshin Cui on 14/4/5.
//  Copyright (c) 2015年 cmjstudio. All rights reserved.
//  靜態表格

#import "KCMainTableViewController.h"
#import <GameKit/GameKit.h>
#import "KCLeaderboardTableViewController.h"
#import "KCAchievementTableViewController.h"

@interface KCMainTableViewController ()<GKGameCenterControllerDelegate>

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

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

@end

@implementation KCMainTableViewController

#pragma mark - 控制器視圖事件
- (void)viewDidLoad {
    [super viewDidLoad];

    [self authorize];
}

#pragma mark - UI事件
- (IBAction)viewGameCenterClick:(UIBarButtonItem *)sender {
    [self viewGameCenter];
}

#pragma mark - GKGameCenterViewController代理方法
//點擊完成
-(void)gameCenterViewControllerDidFinish:(GKGameCenterViewController *)gameCenterViewController{
    NSLog(@"完成.");
    [gameCenterViewController dismissViewControllerAnimated:YES completion:nil];
}

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

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

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

2.而後看一下排行榜控制器視圖KCLeaderboardTableViewController:

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

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

程序以下

//
//  KCLeaderboardTableViewController.m
//  kctest
//
//  Created by Kenshin Cui on 14/4/5.
//  Copyright (c) 2015年 cmjstudio. All rights reserved.
//

#import "KCLeaderboardTableViewController.h"
#import <GameKit/GameKit.h>
//排行榜標識,就是iTunes Connect中配置的排行榜ID
#define kLeaderboardIdentifier1 @"Goals"

@interface KCLeaderboardTableViewController ()
@end

@implementation KCLeaderboardTableViewController

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

#pragma mark - 屬性

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

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

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

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

程序以下

//
//  KCAchievementTableViewController.m
//  kctest
//
//  Created by Kenshin Cui on 14/4/5.
//  Copyright (c) 2015年 cmjstudio. All rights reserved.
//

#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

運行效果:

GameCenter_Effect

注意首次使用遊戲時因爲沒有對Game Center受權,會提示用戶登陸Game Center。

內購

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

  1. 前四步和Game Center基本徹底一致,只是在選擇服務時不是選擇Game Center而是要選擇內購服務(In-App Purchase)。
  2. 到iTuens Connect中設置「App 內購買項目」,這裏仍然以上面的「KCTest」項目爲例,假設這個足球競技遊戲中有三種道具,分別爲「強力手套」(加強防護)、「金球」(增長金球率)和「能量瓶」(提供足夠體力),前二者是非消耗品只用一次性購買,後者是消耗品用完一次必須再次購買。In-App_Purchase_Config
  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方法反饋恢復的交易(也就是已購買的非消耗品交易,注意這個過程當中若是沒有非消耗品可恢復,是不會調用此方法的)。

下面經過一個示例程序演示內購和恢復的整個過程,程序界面大體以下:

主界面中展現了全部可購買產品和售價,以及購買狀況。

選擇一個產品點」購買「能夠購買此商品,購買完成後刷新購買狀態(若是是非消耗品則顯示已購買,若是是消耗品則顯示購買個數)。

程序卸載後從新安裝能夠點擊」恢復購買「來恢復已購買的非消耗品。

In-App_Purchase_Layout

程序代碼:

//
//  KCMainTableViewController.m
//  kctest
//
//  Created by Kenshin Cui on 14/4/5.
//  Copyright (c) 2015年 cmjstudio. All rights reserved.
//

#import "KCMainTableViewController.h"
#import <StoreKit/StoreKit.h>
#define kAppStoreVerifyURL @"https://buy.itunes.apple.com/verifyReceipt" //實際購買驗證URL
#define kSandboxVerifyURL @"https://sandbox.itunes.apple.com/verifyReceipt" //開發階段沙盒驗證URL


//定義能夠購買的產品ID,必須和iTunes Connect中設置的一致
#define kProductID1 @"ProtectiveGloves" //強力手套,非消耗品
#define kProductID2 @"GoldenGlobe" //金球,非消耗品
#define kProductID3 @"EnergyBottle" //能量瓶,消耗品


@interface KCMainTableViewController ()<SKProductsRequestDelegate,SKPaymentTransactionObserver>

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

@implementation KCMainTableViewController
#pragma mark - 控制器視圖方法
- (void)viewDidLoad {
    [super viewDidLoad];

    [self loadProducts];
    [self addTransactionObjserver];
}

#pragma mark - UI事件
//購買產品
- (IBAction)purchaseClick:(UIBarButtonItem *)sender {
    NSString *productIdentifier=self.products.allKeys[self.selectedRow];
    SKProduct *product=self.products[productIdentifier];
    if (product) {
        [self purchaseProduct:product];
    }else{
        NSLog(@"沒有可用商品.");
    }
    
}
//恢復購買
- (IBAction)restorePurchaseClick:(UIBarButtonItem *)sender {
    [self restoreProduct];
}

#pragma mark - UITableView數據源方法

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

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

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *identtityKey=@"myTableViewCellIdentityKey1";
    UITableViewCell *cell=[self.tableView dequeueReusableCellWithIdentifier:identtityKey];
    if(cell==nil){
        cell=[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:identtityKey];
    }
    cell.accessoryType=UITableViewCellAccessoryNone;
    NSString *key=self.products.allKeys[indexPath.row];
    SKProduct *product=self.products[key];
    NSString *purchaseString;
    NSUserDefaults *defaults=[NSUserDefaults standardUserDefaults];
    if ([product.productIdentifier isEqualToString:kProductID3]) {
        purchaseString=[NSString stringWithFormat:@"已購買%i個",[defaults integerForKey:product.productIdentifier]];
    }else{
        if([defaults boolForKey:product.productIdentifier]){
            purchaseString=@"已購買";
        }else{
            purchaseString=@"還沒有購買";
        }
    }
    cell.textLabel.text=[NSString stringWithFormat:@"%@(%@)",product.localizedTitle,purchaseString] ;
    cell.detailTextLabel.text=[NSString stringWithFormat:@"%@",product.price];
    return cell;
}
#pragma mark - UITableView代理方法
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{

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

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

#pragma mark - SKPaymentQueue監聽方法
/**
 *  交易狀態更新後執行
 *
 *  @param queue        支付隊列
 *  @param transactions 交易數組,裏面存儲了本次請求的全部交易對象
 */
-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions{
    [transactions enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        SKPaymentTransaction *paymentTransaction=obj;
        if (paymentTransaction.transactionState==SKPaymentTransactionStatePurchased){//已購買成功
            NSLog(@"交易\"%@\"成功.",paymentTransaction.payment.productIdentifier);
            //購買成功後進行驗證
            [self verifyPurchaseWithPaymentTransaction];
            //結束支付交易
            [queue finishTransaction:paymentTransaction];
        }else if(paymentTransaction.transactionState==SKPaymentTransactionStateRestored){//恢復成功,對於非消耗品才能恢復,若是恢復成功則transaction中記錄的恢復的產品交易
            NSLog(@"恢復交易\"%@\"成功.",paymentTransaction.payment.productIdentifier);
            [queue finishTransaction:paymentTransaction];//結束支付交易
            
            //恢復後從新寫入偏好配置,從新加載UITableView
            [[NSUserDefaults standardUserDefaults]setBool:YES forKey:paymentTransaction.payment.productIdentifier];
            [self.tableView reloadData];
        }else if(paymentTransaction.transactionState==SKPaymentTransactionStateFailed){
            if (paymentTransaction.error.code==SKErrorPaymentCancelled) {//若是用戶點擊取消
                NSLog(@"取消購買.");
            }
            NSLog(@"ErrorCode:%i",paymentTransaction.error.code);
        }
        
    }];
}
//恢復購買完成
-(void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue{
    NSLog(@"恢復完成.");
}

#pragma mark - 私有方法
/**
 *  添加支付觀察者監控,一旦支付後則會回調觀察者的狀態更新方法:
 -(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
 */
-(void)addTransactionObjserver{
    //設置支付觀察者(相似於代理),經過觀察者來監控購買狀況
    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
/**
 *  加載全部產品,注意產品必定是從服務器端請求得到,由於有些產品可能開發人員知道其存在性,可是不通過審覈是無效的;
 */
-(void)loadProducts{
    //定義要獲取的產品標識集合
    NSSet *sets=[NSSet setWithObjects:kProductID1,kProductID2,kProductID3, nil];
    //定義請求用於獲取產品
    SKProductsRequest *productRequest=[[SKProductsRequest alloc]initWithProductIdentifiers:sets];
    //設置代理,用於獲取產品加載狀態
    productRequest.delegate=self;
    //開始請求
    [productRequest start];
}
/**
 *  購買產品
 *
 *  @param product 產品對象
 */
-(void)purchaseProduct:(SKProduct *)product{
    //若是是非消耗品,購買過則提示用戶
    NSUserDefaults *defaults=[NSUserDefaults standardUserDefaults];
    if ([product.productIdentifier isEqualToString:kProductID3]) {
        NSLog(@"當前已經購買\"%@\" %i 個.",kProductID3,[defaults integerForKey:product.productIdentifier]);
    }else if([defaults boolForKey:product.productIdentifier]){
        NSLog(@"\"%@\"已經購買過,無需購買!",product.productIdentifier);
        return;
    }
    
    //建立產品支付對象
    SKPayment *payment=[SKPayment paymentWithProduct:product];
    //支付隊列,將支付對象加入支付隊列就造成一次購買請求
    if (![SKPaymentQueue canMakePayments]) {
        NSLog(@"設備不支持購買.");
        return;
    }
    SKPaymentQueue *paymentQueue=[SKPaymentQueue defaultQueue];
    //添加都支付隊列,開始請求支付
//    [self addTransactionObjserver];
    [paymentQueue addPayment:payment];
}

/**
 *  恢復購買,對於非消耗品若是應用從新安裝或者機器重置後能夠恢復購買
 *  注意恢復時只能一次性恢復全部非消耗品
 */
-(void)restoreProduct{
    SKPaymentQueue *paymentQueue=[SKPaymentQueue defaultQueue];
    //設置支付觀察者(相似於代理),經過觀察者來監控購買狀況
//    [paymentQueue addTransactionObserver:self];
    //恢復全部非消耗品
    [paymentQueue restoreCompletedTransactions];
}

/**
 *  驗證購買,避免越獄軟件模擬蘋果請求達到非法購買問題
 *
 */
-(void)verifyPurchaseWithPaymentTransaction{
    //從沙盒中獲取交易憑證而且拼接成請求體數據
    NSURL *receiptUrl=[[NSBundle mainBundle] appStoreReceiptURL];
    NSData *receiptData=[NSData dataWithContentsOfURL:receiptUrl];
    
    NSString *receiptString=[receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];//轉化爲base64字符串
    
    NSString *bodyString = [NSString stringWithFormat:@"{\"receipt-data\" : \"%@\"}", receiptString];//拼接請求數據
    NSData *bodyData = [bodyString dataUsingEncoding:NSUTF8StringEncoding];

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

運行效果(這是程序在卸載後從新安裝的運行效果,卸載前已經購買」強力手套「,所以程序運行後點擊了」恢復購買「):

In_AppPurchase_Effect

擴展--廣告

上面也提到作iOS開發另外一收益來源就是廣告,在iOS上有不少廣告服務能夠集成,使用比較多的就是蘋果的iAd、谷歌的Admob,下面簡單演示一下如何使用iAd來集成廣告。使用iAd集成廣告的過程比較簡單,首先引入iAd.framework框架,而後建立ADBannerView來展現廣告,一般會設置ADBannerView的代理方法來監聽廣告點擊並在廣告加載失敗時隱藏廣告展現控件。下面的代碼簡單的演示了這個過程:

//
//  ViewController.m
//  kctest
//
//  Created by Kenshin Cui on 14/4/5.
//  Copyright (c) 2015年 cmjstudio. All rights reserved.
//

#import "ViewController.h"
#import <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

運行效果:

iAd

iCloud

iCloud是蘋果提供的雲端服務,用戶能夠將通信錄、備忘錄、郵件、照片、音樂、視頻等備份到雲服務器並在各個蘋果設備間直接進行共享而無需關心數據同步問題,甚至即便你的設備丟失後在一臺新的設備上也能夠經過Apple ID登陸同步。固然這些內容都是iOS內置的功能,那麼對於開放者如何利用iCloud呢?蘋果已經將雲端存儲功能開放給開發者,利用iCloud開發者能夠存儲兩類數據:用戶文檔和應用數據、應用配置項。前者主要用於一些用戶文檔、文件的存儲,後者更相似於平常開放中的偏好設置,只是這些配置信息會同步到雲端。

要進行iCloud開發一樣須要一些準備工做(下面的準備工做主要是針對真機的,模擬器省略Provisioning Profile配置過程):

一、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」:

iCloud_CapabilitiesConfig

在項目中會自動生成一個」kctest.entitlements」配置文件,這個文檔配置了文檔存儲容器標識、鍵值對存儲容器標識等信息。

iCloud_EntiementsFile

4.不管是真機仍是模擬器都必須在iOS「設置」中找到iCloud設置登陸帳戶,注意這個帳戶沒必要是沙盒測試用戶。

A.首先看一下如何進行文檔存儲。文檔存儲主要是使用UIDocument類來完成,這個類提供了新建、修改(其實在API中是覆蓋操做)、查詢文檔、打開文檔、刪除文檔的功能。

UIDocument對文檔的新增、修改、刪除、讀取所有基於一個雲端URL來完成(事實上在開發過程當中新增、修改只是一步簡單的保存操做),對於開發者而言沒有本地和雲端之分,這樣大大簡化了開發過程。這個URL能夠經過NSFileManager的URLForUbiquityContainerIdentifier:方法獲取,identifier是雲端存儲容器的惟一標識,若是傳入nil則表明第一個容器(事實上這個容器能夠經過前面生成的「kctest.entiements」中的Ubiquity Container Identifiers來獲取。如上圖能夠看到這是一個數組,能夠配置多個容器,例如咱們的第一個容器標識是「iCloud.$(CFBundleIdentifier)」,其中$(CFBundleIdentifier)是Bundle ID,那麼根據應用的Bundle ID就能夠得知第一個容器的標識是「iCloud.com.cmjstudio.kctest」。)。下面是經常使用的文檔操做方法:

-(void)saveToURL:forSaveOperation:completionHandler::將指定URL的文檔保存到iCloud(能夠是新增或者覆蓋,經過saveOperation參數設定)。

-(void)openWithCompletionHandler::打開當前文檔。

注意:刪除一個iCloud文檔是使用NSFileManager的removeItemAtURL:error:方法來完成的。

因爲實際開發過程當中數據的存儲和讀取狀況是複雜的,所以UIDocument在設計時並無提供統一的存儲方式來保存數據,而是但願開發者本身繼承UIDocument類並重寫-(id)contentsForType:(NSString *)typeName error:(NSError *__autoreleasing *)outError-(BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError *__autoreleasing *)outError方法來根據不一樣的文檔類型本身來操做數據(contents)。這兩個方法分別在保存文檔(-(void)saveToURL:forSaveOperation:completionHandler:)和打開文檔(-(void)openWithCompletionHandler:)時調用。一般在子類中會定義一個屬性A來存儲文檔數據,當保存文檔時,會經過-(id)contentsForType:(NSString *)typeName error:(NSError *__autoreleasing *)outError方法將A轉化爲NSData或者NSFileWrapper(UIDocument保存數據的本質就是保存轉化獲得的NSData或者NSFileWrapper);當打開文檔時,會經過-(BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError *__autoreleasing *)outError方法將雲端下載的NSData或者NSFileWrapper數據轉化爲A對應類型的數據。爲了方便演示下面簡單定義一個繼承自UIDocument的KCDocument類,在其中定義一個data屬性存儲數據:

//
//  KCDocument.m
//  kctest
//
//  Created by Kenshin Cui on 14/4/5.
//  Copyright (c) 2015年 cmjstudio. All rights reserved.
//

#import "KCDocument.h"

@interface KCDocument()

@end

@implementation KCDocument

#pragma mark - 重寫父類方法
/**
 *  保存時調用
 *
 *  @param typeName <#typeName description#>
 *  @param outError <#outError description#>
 *
 *  @return <#return value description#>
 */
-(id)contentsForType:(NSString *)typeName error:(NSError *__autoreleasing *)outError{
    if (self.data) {
        return [self.data copy];
    }
    return [NSData data];
}

/**
 *  讀取數據時調用
 *
 *  @param contents <#contents description#>
 *  @param typeName <#typeName description#>
 *  @param outError <#outError description#>
 *
 *  @return <#return value description#>
 */
-(BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError *__autoreleasing *)outError{
    self.data=[contents copy];
    return true;
}
@end

若是要加載iCloud中的文檔列表就須要使用另外一個類NSMetadataQuery,一般考慮到網絡的緣由並不會一次性加載全部數據,而利用NSMetadataQuery並指定searchScopes爲NSMetadataQueryUbiquitousDocumentScope來限制查找iCloud文檔數據。使用NSMetadataQuery還能夠經過謂詞限制搜索關鍵字等信息,並在搜索完成以後經過通知的形式通知客戶端搜索的狀況。

你們都知道微軟的OneNote雲筆記本軟件,經過它能夠實現多種不一樣設置間的筆記同步,這裏就簡單實現一個基於iCloud服務的筆記軟件。在下面的程序中實現筆記的新增、修改、保存、讀取等操做。程序界面大體以下,點擊界面右上方增長按鈕增長一個筆記,點擊某個筆記能夠查看並編輯。

iCloud_layout

在主視圖控制器首先查詢全部iCloud保存的文檔並在查詢通知中遍歷查詢結果保存文檔名稱和建立日期到UITableView展現;其次當用戶點擊了增長按鈕會調用KCDocument完成文檔添加並導航到文檔詳情界面編輯文檔內容。

//
//  KCMainTableViewController.m
//  kctest
//
//  Created by Kenshin Cui on 14/4/5.
//  Copyright (c) 2015年 cmjstudio. All rights reserved.
//

#import "KCMainTableViewController.h"
#import "KCDocument.h"
#import "KCDetailViewController.h"
#define kContainerIdentifier @"iCloud.com.cmjstudio.kctest" //容器id,能夠從生產的entitiements文件中查看Ubiquity Container Identifiers(注意其中的$(CFBundleIdentifier)替換爲BundleID)


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

@end

@implementation KCMainTableViewController
#pragma mark - 控制器視圖方法
- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self loadDocuments];
}

#pragma mark - UI事件
//新建文檔
- (IBAction)addDocumentClick:(UIBarButtonItem *)sender {
    UIAlertController *promptController=[UIAlertController alertControllerWithTitle:@"KCTest" message:@"請輸入筆記名稱" preferredStyle:UIAlertControllerStyleAlert];
    [promptController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
        textField.placeholder=@"筆記名稱";
    }];
    UIAlertAction *okAction=[UIAlertAction actionWithTitle:@"肯定" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
        UITextField *textField= promptController.textFields[0];
        [self addDocument:textField.text];
    }];
    [promptController addAction:okAction];
    UIAlertAction *cancelAction=[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
        
    }];
    [promptController addAction:cancelAction];
    [self presentViewController:promptController animated:YES completion:nil];

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

#pragma mark - 屬性
-(NSMetadataQuery *)dataQuery{
    if (!_dataQuery) {
        //建立一個iCloud查詢對象
        _dataQuery=[[NSMetadataQuery alloc]init];
        _dataQuery.searchScopes=@[NSMetadataQueryUbiquitousDocumentsScope];
        //注意查詢狀態是經過通知的形式告訴監聽對象的
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(metadataQueryFinish:) name:NSMetadataQueryDidFinishGatheringNotification object:_dataQuery];//數據獲取完成通知
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(metadataQueryFinish:) name:NSMetadataQueryDidUpdateNotification object:_dataQuery];//查詢更新通知
    }
    return _dataQuery;
}
#pragma mark - 私有方法
/**
 *  取得雲端存儲文件的地址
 *
 *  @param fileName 文件名,若是文件名爲nil則從新建立一個url
 *
 *  @return 文件地址
 */
-(NSURL *)getUbiquityFileURL:(NSString *)fileName{
    //取得雲端URL基地址(參數中傳入nil則會默認獲取第一個容器)
    NSURL *url= [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:kContainerIdentifier];
    //取得Documents目錄
    url=[url URLByAppendingPathComponent:@"Documents"];
    //取得最終地址
    url=[url URLByAppendingPathComponent:fileName];
    return url;
}

/**
 *  添加文檔到iCloud
 *
 *  @param fileName 文件名稱(不包括後綴)
 */
-(void)addDocument:(NSString *)fileName{
    //取得保存URL
    fileName=[NSString stringWithFormat:@"%@.txt",fileName];
    NSURL *url=[self getUbiquityFileURL:fileName];
    
    /**
     建立雲端文檔操做對象
     */
    KCDocument *document= [[KCDocument alloc]initWithFileURL:url];
    [document saveToURL:url forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
        if (success) {
            NSLog(@"保存成功.");
            [self loadDocuments];
            [self.tableView reloadData];
            self.document=document;
            [self performSegueWithIdentifier:@"noteDetail" sender:self];
        }else{
            NSLog(@"保存失敗.");
        }
        
    }];
}

/**
 *  加載文檔列表
 */
-(void)loadDocuments{
    [self.dataQuery startQuery];
}
/**
 *  獲取數據完成後的通知執行方法
 *
 *  @param notification 通知對象
 */
-(void)metadataQueryFinish:(NSNotification *)notification{
    NSLog(@"數據獲取成功!");
    NSArray *items=self.dataQuery.results;//查詢結果集
    self.files=[NSMutableDictionary dictionary];
    //變量結果集,存儲文件名稱、建立日期
    [items enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        NSMetadataItem *item=obj;
        NSString *fileName=[item valueForAttribute:NSMetadataItemFSNameKey];
        NSDate *date=[item valueForAttribute:NSMetadataItemFSContentChangeDateKey];
        NSDateFormatter *dateformate=[[NSDateFormatter alloc]init];
        dateformate.dateFormat=@"YY-MM-dd HH:mm";
        NSString *dateString= [dateformate stringFromDate:date];
        [self.files setObject:dateString forKey:fileName];
    }];
    [self.tableView reloadData];
}

-(void)removeDocument:(NSString *)fileName{
    NSURL *url=[self getUbiquityFileURL:fileName];
    NSError *error=nil;
    //刪除文件
    [[NSFileManager defaultManager] removeItemAtURL:url error:&error];
    if (error) {
        NSLog(@"刪除文檔過程當中發生錯誤,錯誤信息:%@",error.localizedDescription);
    }
    [self.files removeObjectForKey:fileName];//從集合中刪除
}


#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

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

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *identtityKey=@"myTableViewCellIdentityKey1";
    UITableViewCell *cell=[self.tableView dequeueReusableCellWithIdentifier:identtityKey];
    if(cell==nil){
        cell=[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:identtityKey];
        cell.accessoryType=UITableViewCellAccessoryDisclosureIndicator;
    }
    NSArray *fileNames=self.files.allKeys;
    NSString *fileName=fileNames[indexPath.row];
    cell.textLabel.text=fileName;
    cell.detailTextLabel.text=[self.files valueForKey:fileName];
    return cell;
}

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
    if (editingStyle == UITableViewCellEditingStyleDelete) {
        UITableViewCell *cell=[self.tableView cellForRowAtIndexPath:indexPath];
        [self removeDocument:cell.textLabel.text];
        [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
    } else if (editingStyle == UITableViewCellEditingStyleInsert) {
        // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
    }   
}

#pragma mark - UITableView 代理方法
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    UITableViewCell *cell=[self.tableView cellForRowAtIndexPath:indexPath];
    NSURL *url=[self getUbiquityFileURL:cell.textLabel.text];
    self.document=[[KCDocument alloc]initWithFileURL:url];
    [self performSegueWithIdentifier:@"noteDetail" sender:self];
}

@end

當新增一個筆記或選擇一個已存在的筆記後能夠查看、保存筆記內容。

//
//  ViewController.m
//  kctest
//
//  Created by Kenshin Cui on 14/4/5.
//  Copyright (c) 2015年 cmjstudio. All rights reserved.
//

#import "KCDetailViewController.h"
#import "KCDocument.h"
#define kSettingAutoSave @"com.cmjstudio.kctest.settings.autosave"

@interface KCDetailViewController ()
@property (weak, nonatomic) IBOutlet UITextView *textView;

@end

@implementation KCDetailViewController
#pragma mark - 控制器視圖方法
- (void)viewDidLoad {
    [super viewDidLoad];
    [self setupUI];
}

-(void)viewWillDisappear:(BOOL)animated{
    [super viewWillDisappear:animated];
    //根據首選項來肯定離開當前控制器視圖是否自動保存
    BOOL autoSave=[[NSUbiquitousKeyValueStore defaultStore] boolForKey:kSettingAutoSave];
    if (autoSave) {
        [self saveDocument];
    }
}

#pragma mark - 私有方法
-(void)setupUI{
    UIBarButtonItem *rightButtonItem=[[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemSave target:self action:@selector(saveDocument)];
    self.navigationItem.rightBarButtonItem=rightButtonItem;
    
    if (self.document) {
        //打開文檔,讀取文檔
        [self.document openWithCompletionHandler:^(BOOL success) {
            if(success){
                NSLog(@"讀取數據成功.");
                NSString *dataText=[[NSString alloc]initWithData:self.document.data encoding:NSUTF8StringEncoding];
                self.textView.text=dataText;
            }else{
                NSLog(@"讀取數據失敗.");
            }
        }];
    }
}
/**
 *  保存文檔
 */
-(void)saveDocument{
    if (self.document) {
        NSString *dataText=self.textView.text;
        NSData *data=[dataText dataUsingEncoding:NSUTF8StringEncoding];
        self.document.data=data;
        [self.document saveToURL:self.document.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:^(BOOL success) {
            NSLog(@"保存成功!");
        }];
    }
}

@end

到目前爲止都是關於如何使用iCloud來保存文檔的內容,上面也提到過還可使用iCloud來保存首選項,這在不少狀況下一般頗有用,特別是對於開發了iPhone版又開發了iPad版的應用,若是用戶在一臺設備上進行了首選項配置以後到另外一臺設備上也能使用是多麼優秀的體驗啊。相比文檔存儲,首選項存儲要簡單的多,在上面「kctest.entitlements」中能夠看到首選項配置並不是像文檔同樣能夠包含多個容器,這裏只有一個Key-Value Store,一般使用NSUbiquitousKeyValueStore的defaultStore來獲取,它的使用方法和NSUserDefaults幾乎徹底同樣,當鍵值對存儲發生變化後能夠經過NSUbiquitousKeyValueStoreDidChangeExternallyNotification等得到對應的通知。在上面的筆記應用中有一個」設置「按鈕用於設置退出筆記詳情視圖後是否自動保存,這個選項就是經過iCloud的首選項來存儲的。

//
//  KCSettingTableViewController.m
//  kctest
//
//  Created by Kenshin Cui on 14/4/5.
//  Copyright (c) 2015年 cmjstudio. All rights reserved.
//

#import "KCSettingTableViewController.h"
#define kSettingAutoSave @"com.cmjstudio.kctest.settings.autosave"

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

@end

@implementation KCSettingTableViewController

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

#pragma mark - UI事件
- (IBAction)autoSaveClick:(UISwitch *)sender {
    [self setSetting:sender.on];
}

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

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

運行效果:

iCloud_Effect

注意:全部的存儲到iCloud的文檔、首選項都是首先存儲到本地,而後經過daemon進程同步到iCloud的,保存、讀取的文件都是本地同步副本並不必定是真實的iCloud存儲文件。

Passbook

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

  1. 登機牌(Boarding pass)
  2. 優惠券(Coupon)
  3. 活動票據、入場券(Event ticket)
  4. 購物卡、積分卡(Store Cards)
  5. 普通票據(自定義票據)(Generic pass)

蘋果的劃分一方面出於不一樣票據功能及展現信息不一樣,另外一方面也是爲了統一票據的設計,下面是蘋果官方關於五種票據的佈局設計佈局:

Passbook_LayoutCompare

既然一個票據就是一個Pass,那麼什麼是Pass呢?在iOS中一個Pass其實就是一個.pkpass文件,事實上它是一個Zip壓縮包,只是這個壓縮包要按照必定的目錄結構來設計,下面是一個Pass包的目錄結構(注意不一樣的票據類型會適當刪減):

Pass Package

├── icon.png

├── icon@2x.png

├── logo.png

├── logo@2x.png

├── thumbnail.png

├── thumbnail@2x.png

├── background.png

├── background@2x.png

├── strip.png

├── strip@2x.png

├── manifest.json

├── fr.lproj

│ └── pass.strings

├── de.lproj

│ └── pass.strings

├── pass.json

└── signature

也就是說在Passbook應用中顯示的內容其實就是一個按照上面文件列表來組織的一個壓縮包。在.pkpass文件中除了圖標icon、縮略圖thumbnail和logo外最重要的就是pass.json、manifest.json和signature。

1.pass.json

這個文件描述了Pass的佈局、顏色設置、文本描述信息等,也就是說具體Pass包如何展現其實就是經過這個JSON文件來配置的,關於pass.json的具體配置項在此再也不一一介紹,你們能夠查看蘋果官方幫助文檔「Pass Design and Creation」。這裏主要說一下其中關鍵的幾個配置項:

passTypeIdentifier:pass惟一標識,這個值相似於App ID,須要從開發者中心建立,而且這個標識必須以「pass」開頭(例以下面的示例中取名爲「pass.com.cmjstudio.mypassbook」)。

teamIdentifier:團隊標識,申請蘋果開發者帳號時會分配一個惟一的團隊標識(能夠在蘋果開發者中心--查看帳戶信息中查看」Team ID「)。

barcode:二維碼信息配置,主要指定二維碼內容、類型、編碼格式。

locations:地理位置信息,能夠配置相關位置的文本信息。

2.manifest.json

manifest.json從名稱能夠看出這個文件主要用來描述當前Pass包中的文件目錄組織結構。這個文件記錄了除「manifest.json」、「signature」外的文件和對應的sha1哈希值(注意:哈希值能夠經過」openssl sha1 [ 文件路徑]「命令得到)。

3.signature

signature是一個簽名文件。雖然manifest.json存儲了哈希值,可是你們都知道hash算法是公開的,如何保證一個pass包是合法的,未經修改的呢?那就是使用一個簽名文件來驗證。

瞭解了以上內容後基本上對於如何定義一個pass包有了簡單的概念。有了pass包以後對於添加pass到passbook應用是比較簡單的。但事實上一般你們看到的passbook應用中添加的pass包並非手動組織的,而是經過程序來完成pass包製做的。舉例來講:若是你在美團上購買一張電影票以後,會告訴你一個優惠碼,這個優惠碼會顯示到pass中。因爲這個優惠碼是動態生成的,因此直接手動製做出一個pass包是不現實的。一般狀況下pass包的生成都是經過後臺服務器動態生成,而後返回給iOS客戶端來讀取和添加的,手動製做pass包的狀況是比較少的,除非你的票據信息是一成不變的。固然爲了演示Passbook應用,這裏仍是會以手動方式演示一個pass包的生成過程,瞭解了這個過程以後相信在服務器端經過一些後臺程序生成一個pass包也不在話下(下面的生成過程都可經過服務器端編程來實現)。

同其餘Apple服務開發相似,作Passbook開發一樣須要一些準備工做:

  1. 在蘋果開發者中心新建Pass Type ID(例如這裏新建一個「pass.com.cmjstudio.mypassbook」),而且基於這個Pass Type ID建立一個Passbook證書(在mac上找到鑰匙串,選擇」從證書頒發機構請求證書「,生成一個證書請求文件;將此文件上傳到對應的Pass Type ID下生成證書文件)以下圖:Passbook_PassTypeIDAndCertification

    下載證書後,將此證書導入Mac中(此處配置的Pass Type ID對應pass.json中的」passTypeIdentitifier「,此證書用於生成簽名文件signature。)。

  2. 在Xcode中-Targets-Capabilities啓用Pasbook服務,這裏須要注意的是」Allow all team pass types「選項,若是勾選了這一項,那麼pass.json中的passTypeIdentifier和teamIdentifier就能夠是任何團隊建立的任何Pass項目了,這裏使用前面建立的項目,因此選擇」Allow subset of pass types「。Passbook_EnablePassbook

有了上面的準備工做,下面看一下如何製做一個Pass:

  1. 根據所選擇的Passbook類型準備圖片素材,因爲這裏以一個Store Card舉例,因此須要準備icon、logo和strip三類圖片。
  2. 配置pass.json,這裏仍是強調一下passTypeIdentifier和teamIdentifier,前者就是上面在開發者中心建立的Pass Type ID(」pass.com.cmjstudio.mypassbook「),後者是對應的團隊標識,其餘信息根據實際狀況配置。
    {
        "formatVersion":1,
        "passTypeIdentifier":"pass.com.cmjstudio.mypassbook",
        "serialNumber":"54afe978584e3",
        "teamIdentifier":"JB74M3J7RY",
        "authenticationToken":"bc83dde3304d766d5b1aea631827f84c",
        "barcode":{"message":"userName KenshinCui","altText":"會員詳情見背面","format":"PKBarcodeFormatQR","messageEncoding":"iso-8859-1"},
        "locations":[
                     {"longitude":-122.3748889,"latitude":37.6189722},{"longitude":-122.03118,"latitude":37.33182}],
        "organizationName":"CMJ Coffee",
        "logoText":"CMJ Coffee",
        "description":"",
        "foregroundColor":"rgb(2,2,4)",
        "backgroundColor":"rgb(244,244,254)",
        "storeCard":{
            "headerFields":[{"key":"date","label":"餘額","value":"¥8888.50"}],
            "secondaryFields":[{"key":"more","label":"VIP會員","value":"Kenshin Cui"}],
            "backFields":[
                          {"key":"records","label":"消費記錄(最近10次)","value":" 9/23    ¥107.00     無糖冰美式\n 9/21    ¥58.00      黑魔卡\n 8/25    ¥44.00      魔卡\n 8/23    ¥107.00     無糖冰美式\n 8/18    ¥107.00     無糖冰美式\n 7/29    ¥58.00      黑魔卡\n 7/26    ¥44.00      魔卡\n 7/13    ¥58.00      黑魔卡\n 7/11    ¥44.00      魔卡\n 6/20    ¥44.00      魔卡\n"},
                          {"key":"phone","label":"聯繫方式","value":"4008-888-88"},
                          {"key":"terms","label":"會員規則","value":"(1)本電子票涉及多個環節,均爲人工操做,用戶下單後,1-2個工做日內下發,電子票並不必定能當即收到,建議千品用戶提早1天購買,如急需使用,請謹慎下單; \n(2)此劵爲電子劵,屬特殊產品,一經購買不支持退款(敬請諒解); \n(3)特別注意:下單時請將您須要接收電子票的手機號碼,填入收件人信息,如號碼填寫錯誤,損失自負;購買成功後,商家於週一至週五天天中午11點和下午17點發2維碼/短信到您手機(週六至週日當天晚上發1次),請用戶提早購買,憑此信息前往影院前臺兌換便可; \n(4)訂購成功後,(您在購買下單後的當天,給您發送電子券,系統會自動識別;若是您的手機能接收二維碼,那收到的就是彩信,不能接收二維碼的話,系統將會自動轉成短信發送給您),短信爲16位數,如:1028**********; 每一個手機號碼只可購買6張,如需購買6張以上的請在訂單附言填寫不一樣的手機號碼,並註明張數(例如團購10張,1350755****號碼4張,1860755****號碼6張);\n(5)電子票有效期至2016年2月30日,不與其餘優惠券同時使用"},
                          {"key":"support","label":"技術支持","value":"http://www.cmjstudio.com\n\n                                            \n                                            \n                                          "}]
        },
        "labelColor":"rgb(87,88,93)"
    }
  3. 根據pass所需文件建立manifest.json文件,能夠經過」openssl sha1 [文件路徑]「分別計算出全部文件的哈希值:
    {
        "pass.json":"3292f96c4676aefe7122abb47f86be0d95a6faaf",
        "icon@2x.png":"83438c13dfd7c4a5819a12f6df6dc74b71060289",
        "icon.png":"83438c13dfd7c4a5819a12f6df6dc74b71060289",
        "logo@2x.png":"83438c13dfd7c4a5819a12f6df6dc74b71060289",
        "logo.png":"83438c13dfd7c4a5819a12f6df6dc74b71060289",
        "strip@2x.png":"885ff9639c90147a239a7a77e7adc870d5e047e2",
        "strip.png":"885ff9639c90147a239a7a77e7adc870d5e047e2"
    }
  4. 接下來下來準備生成signature文件:
    a.經過前面導入的Pass Type證書(Pass Type ID:pass.com.cmjstudio.mypassbook)導出我的信息交換(.p12)文件並指定密碼(假設密碼爲456789),保存成」mypassbook.p12「(注意是導出證書而不是導出證書下的專用祕鑰)。
    b.在鑰匙串中找到」Apple Worldwide Developer Relations Certification Authority「證書導出加強保密郵件(.pem),保存成」AWDRCA.pem「。
    c.將.p12證書轉化爲.pem證書mypassbook.pem(須要輸入導出時設置的密碼456789),輸入以下命令:
    openssl pkcs12 -in mypassbook.p12 -clcerts -nokeys -out mypassbook.pem -passin pass:456789
    d.從.p12導出祕鑰文件mypassbookkey.pem(這裏設置密碼爲123456):
    openssl pkcs12 -in mypassbook.p12 -nocerts -out mypassbookkey.pem -passin pass:456789 -passout pass:123456

    e.根據AWDRCA.pem、mypassbook.pem、mypassbookkey.pem、manifest.json生成signature文件(按照提示輸入mypassbookkey.pem導出時設置的密碼123456):
    openssl smime -binary -sign -certfile AWDRCA.pem -signer mypassbook.pem -inkey mypassbookkey.pem -in manifest.json -out signature -outform DER
  5. 將icon.png、icon@2x.png、logo.png、logo@2x.png、strip.png、strip@2x.png 、pass.json、manifest.json、signature壓縮成pass包(這裏命名爲」mypassbook.pkpass「)。
    zip -r mypassbook.pkpass manifest.json pass.json signature logo.png logo@2x.png icon.png icon@2x.png strip.png strip@2x.png

到這裏一個pass製做完成了,此處能夠在mac中打開預覽:

Passbook_Mypassbook

到這裏一個Pass就只作完成了,下面就看一下在iOS中如何添加這個Pass到Passbook,這裏直接將上面製做完成的Pass放到Bundle中完成添加。固然這些都是一步步手動完成的,前面也說了實際開發中這個Pass是服務器端來動態生成的,在添加時會從服務器端下載,這個過程在示例中就再也不演示。iOS中提供了PassKit.framework框架來進行Passbook開發,下面的代碼演示了添加Pass到Passbook應用的過程:

//
//  ViewController.m
//  Passbook
//
//  Created by Kenshin Cui on 14/4/5.
//  Copyright (c) 2015年 cmjstudio. All rights reserved.
//

#import "ViewController.h"
#import <PassKit/PassKit.h>

@interface ViewController ()<PKAddPassesViewControllerDelegate>
@property (strong,nonatomic) PKPass *pass;//票據
@property (strong,nonatomic) PKAddPassesViewController *addPassesController;//票據添加控制器
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
}
#pragma mark - UI事件
- (IBAction)addPassClick:(UIBarButtonItem *)sender {
    //確保pass合法,不然沒法添加
    [self addPass];
}

#pragma mark - 屬性
/**
 *  建立Pass對象
 *
 *  @return Pass對象
 */
-(PKPass *)pass{
    if (!_pass) {
        NSString *passPath=[[NSBundle mainBundle] pathForResource:@"mypassbook.pkpass" ofType:nil];
        NSData *passData=[NSData dataWithContentsOfFile:passPath];
        NSError *error=nil;
        _pass=[[PKPass alloc]initWithData:passData error:&error];
        if (error) {
            NSLog(@"建立Pass過程當中發生錯誤,錯誤信息:%@",error.localizedDescription);
            return nil;
        }
    }
    return _pass;
}

/**
 *  建立添加Pass的控制器
 *
 *  @return <#return value description#>
 */
-(PKAddPassesViewController *)addPassesController{
    if (!_addPassesController) {
        _addPassesController=[[PKAddPassesViewController alloc]initWithPass:self.pass];
        _addPassesController.delegate=self;//設置代理
    }
    return _addPassesController;
}

#pragma mark - 私有方法
-(void)addPass{
    if (![PKAddPassesViewController canAddPasses]) {
        NSLog(@"沒法添加Pass.");
        return;
    }

    [self presentViewController:self.addPassesController animated:YES completion:nil];
}

#pragma mark - PKAddPassesViewController代理方法
-(void)addPassesViewControllerDidFinish:(PKAddPassesViewController *)controller{
    NSLog(@"添加成功.");
    [self.addPassesController dismissViewControllerAnimated:YES completion:nil];
    //添加成功後轉到Passbook應用並展現添加的Pass
    NSLog(@"%@",self.pass.passURL);
    [[UIApplication sharedApplication] openURL:self.pass.passURL];
}
@end

運行效果:

Passbook_Effect

注意:若是你們對於Pass不是太熟悉,剛開始可使用一些製做工具來完成Pass製做,例如:PassSource、國內的PassQuan等都支持在線製做Pass包。

相關文章
相關標籤/搜索