iOS开发系列--通讯录、蓝牙、
iOS开发过程中有时候难免会使用iOS内置的一些应用软件和服务,例如QQ通讯录、微信电话本会使用iOS的通讯录,一些第三方软件会在应用内发送短信等。今天将和大家一起学习如何使用系统应用、使用系统服务:
系统应用
在开发某些应用时可能希望能够调用iOS系统内置的电话、短信、邮件、浏览器应用,此时你可以直接使用UIApplication的OpenURL:方法指定特定的协议来打开不同的系统应用。常用的协议如下:
打电话:tel:或者tel://、telprompt:或telprompt://(拨打电话前有提示)
发短信:sms:或者sms://
发送邮件:mailto:或者mailto://
启动浏览器:http:或者http://
下面以一个简单的demo演示如何调用上面几种系统应用:
// // ViewController.m // iOSSystemApplication // // Created by Kenshin Cui on 14/04/05. // Copyright (c) 2014年 cmjstudio. All rights reserved. // #import "ViewController.h" @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; } #pragma mark - UI事件 //打电话 - (IBAction)callClicK:(UIButton *)sender { NSString *phoneNumber=@"18500138888"; // NSString *url=[NSString stringWithFormat:@"tel://%@",phoneNumber];//这种方式会直接拨打电话 NSString *url=[NSString stringWithFormat:@"telprompt://%@",phoneNumber];//这种方式会提示用户确认是否拨打电话 [self openUrl:url]; } //发送短信 - (IBAction)sendMessageClick:(UIButton *)sender { NSString *phoneNumber=@"18500138888"; NSString *url=[NSString stringWithFormat:@"sms://%@",phoneNumber]; [self openUrl:url]; } //发送邮件 - (IBAction)sendEmailClick:(UIButton *)sender { NSString *mailAddress=@"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的唯一标识,如下图:
然后就可以调用openURL方法像打开系统应用一样打开第三方应用程序了:
//打开第三方应用 - (IBAction)thirdPartyApplicationClick:(UIButton *)sender { NSString *url=@"cmj://myparams"; [self openUrl:url]; }
就像调用系统应用一样,协议后面可以传递一些参数(例如上面传递的myparams),这样一来在应用中可以在AppDelegate的-(BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation代理方法中接收参数并解析。
-(BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation{ NSString *str=[NSString stringWithFormat:@"url:%@,source application:%@,params:%@",url,sourceApplication,[url host]]; NSLog(@"%@",str); return YES;//是否打开 }
系统服务
短信与邮件
调用系统内置的应用来发送短信、邮件相当简单,但是这么操作也存在着一些弊端:当你点击了发送短信(或邮件)操作之后直接启动了系统的短信(或邮件)应用程序,我们的应用其实此时已经处于一种挂起状态,发送完(短信或邮件)之后无法自动回到应用界面。如果想要在应用程序内部完成这些操作则可以利用iOS中的MessageUI.framework,它提供了关于短信和邮件的UI接口供开发者在应用程序内部调用。从框架名称不难看出这是一套UI接口,提供有现成的短信和邮件的编辑界面,开发人员只需要通过编程的方式给短信和邮件控制器设置对应的参数即可。
在MessageUI.framework中主要有两个控制器类分别用于发送短信(MFMessageComposeViewController)和邮件(MFMailComposeViewController),它们均继承于UINavigationController。由于两个类使用方法十分类似,这里主要介绍一下MFMessageComposeViewController使用步骤:
- 创建MFMessageComposeViewController对象。
- 设置收件人recipients、信息正文body,如果运行商支持主题和附件的话可以设置主题subject、附件attachments(可以通过canSendSubject、canSendAttachments方法判断是否支持)
- 设置代理messageComposeDelegate(注意这里不是delegate属性,因为delegate属性已经留给UINavigationController,MFMessageComposeViewController没有覆盖此属性而是重新定义了一个代理),实现代理方法获得发送状态。
下面自定义一个发送短信的界面演示MFMessageComposeViewController的使用:
用户通过在此界面输入短信信息点击“发送信息”调用MFMessageComposeViewController界面来展示或进一步编辑信息,点击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
运行效果:
通讯录
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”
通讯录的访问步骤一般如下:
- 调用ABAddressBookCreateWithOptions()方法创建通讯录对象ABAddressBookRef。
- 调用ABAddressBookRequestAccessWithCompletion()方法获得用户授权访问通讯录。
- 调用ABAddressBookCopyArrayOfAllPeople()、ABAddressBookCopyPeopleWithName()方法查询联系人信息。
- 读取联系人后如果要显示联系人信息则可以调用ABRecord相关方法读取相应的数据;如果要进行修改联系人信息,则可以使用对应的方法修改ABRecord信息,然后调用ABAddressBookSave()方法提交修改;如果要删除联系人,则可以调用ABAddressBookRemoveRecord()方法删除,然后调用ABAddressBookSave()提交修改操作。
- 也就是说如果要修改或者删除都需要首先查询对应的联系人,然后修改或删除后提交更改。如果用户要增加一个联系人则不用进行查询,直接调用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
运行效果:
备注:
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
运行效果:
注意:
为了让大家可以更加清楚的看到几个控制器视图的使用,这里并没有结合前面的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真机上作为发现节点,并且实现两个节点的图片互传。
首先看一下作为广播节点的程序:
界面:
点击“开始广播”来发布服务,一旦有节点连接此服务就可以使用“选择照片”来从照片库中选取一张图片并发送到所有已连接节点。
程序:
// // 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
再看一下作为发现节点的程序:
界面:
点击“查找设备”浏览可用服务,点击服务建立连接;一旦建立了连接之后就可以点击“选择照片”会从照片库中选择一张图片并发送给已连接的节点。
程序:
// // 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整个框架就是基于这两个概念来设计的。
外围设备和中央设备在CoreBluetooth中使用CBPeripheralManager和CBCentralManager表示。
CBPeripheralManager:外围设备通常用于发布服务、生成数据、保存数据。外围设备发布并广播服务,告诉周围的中央设备它的可用服务和特征。
CBCentralManager:中央设备使用外围设备的数据。中央设备扫描到外围设备后会就会试图建立连接,一旦连接成功就可以使用这些服务和特征。
外围设备和中央设备之间交互的桥梁是服务(CBService)和特征(CBCharacteristic),二者都有一个唯一的标识UUID(CBUUID类型)来唯一确定一个服务或者特征,每个服务可以拥有多个特征,下面是他们之间的关系:
一台iOS设备(注意iPhone4以下设备不支持BLE,另外iOS7.0、8.0模拟器也无法模拟BLE)既可以作为外围设备又可以作为中央设备,但是不能同时即是外围设备又是中央设备,同时注意建立连接的过程不需要用户手动选择允许,这一点和前面两个框架是不同的,这主要是因为BLE应用场景不再局限于两台设备之间资源共享了。
A.外围设备
创建一个外围设备通常分为以下几个步骤:
- 创建外围设备CBPeripheralManager对象并指定代理。
- 创建特征CBCharacteristic、服务CBSerivce并添加到外围设备
- 外围设备开始广播服务(startAdvertisting:)。
- 和中央设备CBCentral进行交互。
下面是简单的程序示例,程序有两个按钮“启动”和“更新”,点击启动按钮则创建外围设备、添加服务和特征并开始广播,一旦发现有中央设备连接并订阅了此服务的特征则通过更新按钮更新特征数据,此时已订阅的中央设备就会收到更新数据。
界面设计:
程序设计:
// // ViewController.m // PeripheralApp // // Created by Kenshin Cui on 14/04/05. // Copyright (c) 2014年 cmjstudio. All rights reserved. // 外围设备(周边设备) #import "ViewController.h" #import <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
上面程序运行的流程如下(图中蓝色代表外围设备操作,绿色部分表示中央设备操作):
B.中央设备
中央设备的创建一般可以分为如下几个步骤:
- 创建中央设备管理对象CBCentralManager并指定代理。
- 扫描外围设备,一般发现可用外围设备则连接并保存外围设备。
- 查找外围设备服务和特征,查找到可用特征则读取特征数据。
下面是一个简单的中央服务器端实现,点击“启动”按钮则开始扫描周围的外围设备,一旦发现了可用的外围设备则建立连接并设置外围设备的代理,之后开始查找其服务和特征。一旦外围设备的特征值做了更新,则可以在代理方法中读取更新后的特征值。
界面设计:
程序设计:
// // 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
上面程序运行的流程图如下:
有了上面两个程序就可以分别运行在两台支持BLE的iOS设备上,当两个应用建立连接后,一旦外围设备更新特征之后,中央设备就可以立即获取到更新后的值。需要强调的是使用CoreBluetooth开发的应用不仅仅可以和其他iOS设备进行蓝牙通信,还可以同其他第三方遵循BLE规范的设备进行蓝牙通讯,这里就不再赘述。