iCloud是苹果提供的云端服务,用户可以将通讯录、备忘录、邮件、照片、音乐、视频等备份到云服务器并在各个苹果设备间直接进行共享而无需关心数据同步问题,甚至即使你的设备丢失后在一台新的设备上也可以通过Apple ID登录同步。当然这些内容都是iOS内置的功能,那么对于开放者如何利用iCloud呢?苹果已经将云端存储功能开放给开发者,利用iCloud开发者可以存储两类数据:用户文档和应用数据、应用配置项。前者主要用于一些用户文档、文件的存储,后者更类似于日常开放中的偏好设置,只是这些配置信息会同步到云端。
要进行iCloud开发同样需要一些准备工作(下面的准备工作主要是针对真机的,模拟器省略Provisioning Profile配置过程):
1、2步骤仍然是创建App ID启用iCloud服务、生成对应的配置(Provisioning Profile),这个过程中Bundle ID可以使用通配符(Data Protection、iCloud、Inter-App Audio、Passbook服务在创建App ID时其中的Bundle ID是可以使用通配ID的)。
3.在Xcode中创建项目(假设项目名称为“kctest”)并在项目的Capabilities中找到iCloud并打开。这里需要注意的就是由于在此应用中要演示文档存储和首选项存储,因此在Service中勾选“Key-value storae”和“iCloud Documents”:
在项目中会自动生成一个”kctest.entitlements”配置文件,这个文档配置了文档存储容器标识、键值对存储容器标识等信息。
4.无论是真机还是模拟器都必须在iOS“设置”中找到iCloud设置登录账户,注意这个账户不必是沙盒测试用户。
A.首先看一下如何进行文档存储。文档存储主要是使用UIDocument类来完成,这个类提供了新建、修改(其实在API中是覆盖操作)、查询文档、打开文档、删除文档的功能。
UIDocument对文档的新增、修改、删除、读取全部基于一个云端URL来完成(事实上在开发过程中新增、修改只是一步简单的保存操作),对于开发者而言没有本地和云端之分,这样大大简化了开发过程。这个URL可以通过NSFileManager的URLForUbiquityContainerIdentifier:方法获取,identifier是云端存储容器的唯一标识,如果传入nil则代表第一个容器(事实上这个容器可以通过前面生成的“kctest.entiements”中的Ubiquity Container Identifiers来获取。如上图可以看到这是一个数组,可以配置多个容器,例如我们的第一个容器标识是“iCloud.$(CFBundleIdentifier)”,其中$(CFBundleIdentifier)是Bundle ID,那么根据应用的Bundle ID就可以得知第一个容器的标识是“iCloud.com.cmjstudio.kctest”。)。下面是常用的文档操作方法:
-(void)saveToURL:forSaveOperation:completionHandler::将指定URL的文档保存到iCloud(可以是新增或者覆盖,通过saveOperation参数设定)。
-(void)openWithCompletionHandler::打开当前文档。
注意:删除一个iCloud文档是使用NSFileManager的removeItemAtURL:error:方法来完成的。
由于实际开发过程中数据的存储和读取情况是复杂的,因此UIDocument在设计时并没有提供统一的存储方式来保存数据,而是希望开发者自己继承UIDocument类并重写-(id)contentsForType:(NSString *)typeName error:(NSError *__autoreleasing *)outError和-(BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError *__autoreleasing *)outError方法来根据不同的文档类型自己来操作数据(contents)。这两个方法分别在保存文档(-(void)saveToURL:forSaveOperation:completionHandler:)和打开文档(-(void)openWithCompletionHandler:)时调用。通常在子类中会定义一个属性A来存储文档数据,当保存文档时,会通过-(id)contentsForType:(NSString *)typeName error:(NSError *__autoreleasing *)outError方法将A转化为NSData或者NSFileWrapper(UIDocument保存数据的本质就是保存转化得到的NSData或者NSFileWrapper);当打开文档时,会通过-(BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError *__autoreleasing *)outError方法将云端下载的NSData或者NSFileWrapper数据转化为A对应类型的数据。为了方便演示下面简单定义一个继承自UIDocument的KCDocument类,在其中定义一个data属性存储数据:
// // KCDocument.m // kctest // // Created by Kenshin Cui on 14/4/5. // Copyright (c) 2015年 cmjstudio. All rights reserved. // #import "KCDocument.h" @interface KCDocument() @end @implementation KCDocument #pragma mark - 重写父类方法 /** * 保存时调用 * * @param typeName <#typeName description#> * @param outError <#outError description#> * * @return <#return value description#> */ -(id)contentsForType:(NSString *)typeName error:(NSError *__autoreleasing *)outError{ if (self.data) { return [self.data copy]; } return [NSData data]; } /** * 读取数据时调用 * * @param contents <#contents description#> * @param typeName <#typeName description#> * @param outError <#outError description#> * * @return <#return value description#> */ -(BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError *__autoreleasing *)outError{ self.data=[contents copy]; return true; } @end
如果要加载iCloud中的文档列表就需要使用另一个类NSMetadataQuery,通常考虑到网络的原因并不会一次性加载所有数据,而利用NSMetadataQuery并指定searchScopes为NSMetadataQueryUbiquitousDocumentScope来限制查找iCloud文档数据。使用NSMetadataQuery还可以通过谓词限制搜索关键字等信息,并在搜索完成之后通过通知的形式通知客户端搜索的情况。
大家都知道微软的OneNote云笔记本软件,通过它可以实现多种不同设置间的笔记同步,这里就简单实现一个基于iCloud服务的笔记软件。在下面的程序中实现笔记的新增、修改、保存、读取等操作。程序界面大致如下,点击界面右上方增加按钮增加一个笔记,点击某个笔记可以查看并编辑。
在主视图控制器首先查询所有iCloud保存的文档并在查询通知中遍历查询结果保存文档名称和创建日期到UITableView展示;其次当用户点击了增加按钮会调用KCDocument完成文档添加并导航到文档详情界面编辑文档内容。
// // KCMainTableViewController.m // kctest // // Created by Kenshin Cui on 14/4/5. // Copyright (c) 2015年 cmjstudio. All rights reserved. // #import "KCMainTableViewController.h" #import "KCDocument.h" #import "KCDetailViewController.h" #define kContainerIdentifier @"iCloud.com.cmjstudio.kctest" //容器id,可以从生产的entitiements文件中查看Ubiquity Container Identifiers(注意其中的$(CFBundleIdentifier)替换为BundleID) @interface KCMainTableViewController () @property (strong,nonatomic) KCDocument *document;//当前选中的管理对象 @property (strong,nonatomic) NSMutableDictionary *files; //现有文件名、创建日期集合 @property (strong,nonatomic) NSMetadataQuery *dataQuery;//数据查询对象,用于查询iCloud文档 @end @implementation KCMainTableViewController #pragma mark - 控制器视图方法 - (void)viewDidLoad { [super viewDidLoad]; [self loadDocuments]; } #pragma mark - UI事件 //新建文档 - (IBAction)addDocumentClick:(UIBarButtonItem *)sender { UIAlertController *promptController=[UIAlertController alertControllerWithTitle:@"KCTest" message:@"请输入笔记名称" preferredStyle:UIAlertControllerStyleAlert]; [promptController addTextFieldWithConfigurationHandler:^(UITextField *textField) { textField.placeholder=@"笔记名称"; }]; UIAlertAction *okAction=[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { UITextField *textField= promptController.textFields[0]; [self addDocument:textField.text]; }]; [promptController addAction:okAction]; UIAlertAction *cancelAction=[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { }]; [promptController addAction:cancelAction]; [self presentViewController:promptController animated:YES completion:nil]; } #pragma mark - 导航 - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { if ([segue.identifier isEqualToString:@"noteDetail"]) { KCDetailViewController *detailController= segue.destinationViewController; detailController.document=self.document; } } #pragma mark - 属性 -(NSMetadataQuery *)dataQuery{ if (!_dataQuery) { //创建一个iCloud查询对象 _dataQuery=[[NSMetadataQuery alloc]init]; _dataQuery.searchScopes=@[NSMetadataQueryUbiquitousDocumentsScope]; //注意查询状态是通过通知的形式告诉监听对象的 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(metadataQueryFinish:) name:NSMetadataQueryDidFinishGatheringNotification object:_dataQuery];//数据获取完成通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(metadataQueryFinish:) name:NSMetadataQueryDidUpdateNotification object:_dataQuery];//查询更新通知 } return _dataQuery; } #pragma mark - 私有方法 /** * 取得云端存储文件的地址 * * @param fileName 文件名,如果文件名为nil则重新创建一个url * * @return 文件地址 */ -(NSURL *)getUbiquityFileURL:(NSString *)fileName{ //取得云端URL基地址(参数中传入nil则会默认获取第一个容器) NSURL *url= [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:kContainerIdentifier]; //取得Documents目录 url=[url URLByAppendingPathComponent:@"Documents"]; //取得最终地址 url=[url URLByAppendingPathComponent:fileName]; return url; } /** * 添加文档到iCloud * * @param fileName 文件名称(不包括后缀) */ -(void)addDocument:(NSString *)fileName{ //取得保存URL fileName=[NSString stringWithFormat:@"%@.txt",fileName]; NSURL *url=[self getUbiquityFileURL:fileName]; /** 创建云端文档操作对象 */ KCDocument *document= [[KCDocument alloc]initWithFileURL:url]; [document saveToURL:url forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) { if (success) { NSLog(@"保存成功."); [self loadDocuments]; [self.tableView reloadData]; self.document=document; [self performSegueWithIdentifier:@"noteDetail" sender:self]; }else{ NSLog(@"保存失败."); } }]; } /** * 加载文档列表 */ -(void)loadDocuments{ [self.dataQuery startQuery]; } /** * 获取数据完成后的通知执行方法 * * @param notification 通知对象 */ -(void)metadataQueryFinish:(NSNotification *)notification{ NSLog(@"数据获取成功!"); NSArray *items=self.dataQuery.results;//查询结果集 self.files=[NSMutableDictionary dictionary]; //变量结果集,存储文件名称、创建日期 [items enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { NSMetadataItem *item=obj; NSString *fileName=[item valueForAttribute:NSMetadataItemFSNameKey]; NSDate *date=[item valueForAttribute:NSMetadataItemFSContentChangeDateKey]; NSDateFormatter *dateformate=[[NSDateFormatter alloc]init]; dateformate.dateFormat=@"YY-MM-dd HH:mm"; NSString *dateString= [dateformate stringFromDate:date]; [self.files setObject:dateString forKey:fileName]; }]; [self.tableView reloadData]; } -(void)removeDocument:(NSString *)fileName{ NSURL *url=[self getUbiquityFileURL:fileName]; NSError *error=nil; //删除文件 [[NSFileManager defaultManager] removeItemAtURL:url error:&error]; if (error) { NSLog(@"删除文档过程中发生错误,错误信息:%@",error.localizedDescription); } [self.files removeObjectForKey:fileName];//从集合中删除 } #pragma mark - Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.files.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *identtityKey=@"myTableViewCellIdentityKey1"; UITableViewCell *cell=[self.tableView dequeueReusableCellWithIdentifier:identtityKey]; if(cell==nil){ cell=[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:identtityKey]; cell.accessoryType=UITableViewCellAccessoryDisclosureIndicator; } NSArray *fileNames=self.files.allKeys; NSString *fileName=fileNames[indexPath.row]; cell.textLabel.text=fileName; cell.detailTextLabel.text=[self.files valueForKey:fileName]; return cell; } - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { UITableViewCell *cell=[self.tableView cellForRowAtIndexPath:indexPath]; [self removeDocument:cell.textLabel.text]; [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; } else if (editingStyle == UITableViewCellEditingStyleInsert) { // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view } } #pragma mark - UITableView 代理方法 -(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ UITableViewCell *cell=[self.tableView cellForRowAtIndexPath:indexPath]; NSURL *url=[self getUbiquityFileURL:cell.textLabel.text]; self.document=[[KCDocument alloc]initWithFileURL:url]; [self performSegueWithIdentifier:@"noteDetail" sender:self]; } @end
当新增一个笔记或选择一个已存在的笔记后可以查看、保存笔记内容。
// // ViewController.m // kctest // // Created by Kenshin Cui on 14/4/5. // Copyright (c) 2015年 cmjstudio. All rights reserved. // #import "KCDetailViewController.h" #import "KCDocument.h" #define kSettingAutoSave @"com.cmjstudio.kctest.settings.autosave" @interface KCDetailViewController () @property (weak, nonatomic) IBOutlet UITextView *textView; @end @implementation KCDetailViewController #pragma mark - 控制器视图方法 - (void)viewDidLoad { [super viewDidLoad]; [self setupUI]; } -(void)viewWillDisappear:(BOOL)animated{ [super viewWillDisappear:animated]; //根据首选项来确定离开当前控制器视图是否自动保存 BOOL autoSave=[[NSUbiquitousKeyValueStore defaultStore] boolForKey:kSettingAutoSave]; if (autoSave) { [self saveDocument]; } } #pragma mark - 私有方法 -(void)setupUI{ UIBarButtonItem *rightButtonItem=[[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemSave target:self action:@selector(saveDocument)]; self.navigationItem.rightBarButtonItem=rightButtonItem; if (self.document) { //打开文档,读取文档 [self.document openWithCompletionHandler:^(BOOL success) { if(success){ NSLog(@"读取数据成功."); NSString *dataText=[[NSString alloc]initWithData:self.document.data encoding:NSUTF8StringEncoding]; self.textView.text=dataText; }else{ NSLog(@"读取数据失败."); } }]; } } /** * 保存文档 */ -(void)saveDocument{ if (self.document) { NSString *dataText=self.textView.text; NSData *data=[dataText dataUsingEncoding:NSUTF8StringEncoding]; self.document.data=data; [self.document saveToURL:self.document.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:^(BOOL success) { NSLog(@"保存成功!"); }]; } } @end
到目前为止都是关于如何使用iCloud来保存文档的内容,上面也提到过还可以使用iCloud来保存首选项,这在很多情况下通常很有用,特别是对于开发了iPhone版又开发了iPad版的应用,如果用户在一台设备上进行了首选项配置之后到另一台设备上也能使用是多么优秀的体验啊。相比文档存储,首选项存储要简单的多,在上面“kctest.entitlements”中可以看到首选项配置并非像文档一样可以包含多个容器,这里只有一个Key-Value Store,通常使用NSUbiquitousKeyValueStore的defaultStore来获取,它的使用方法和NSUserDefaults几乎完全一样,当键值对存储发生变化后可以通过NSUbiquitousKeyValueStoreDidChangeExternallyNotification等获得对应的通知。在上面的笔记应用中有一个”设置“按钮用于设置退出笔记详情视图后是否自动保存,这个选项就是通过iCloud的首选项来存储的。
// // KCSettingTableViewController.m // kctest // // Created by Kenshin Cui on 14/4/5. // Copyright (c) 2015年 cmjstudio. All rights reserved. // #import "KCSettingTableViewController.h" #define kSettingAutoSave @"com.cmjstudio.kctest.settings.autosave" @interface KCSettingTableViewController () @property (weak, nonatomic) IBOutlet UISwitch *autoSaveSetting; @end @implementation KCSettingTableViewController - (void)viewDidLoad { [super viewDidLoad]; [self setupUI]; } #pragma mark - UI事件 - (IBAction)autoSaveClick:(UISwitch *)sender { [self setSetting:sender.on]; } #pragma mark - 私有方法 -(void)setupUI{ //设置iCloud中的首选项值 NSUbiquitousKeyValueStore *defaults=[NSUbiquitousKeyValueStore defaultStore]; self.autoSaveSetting.on= [defaults boolForKey:kSettingAutoSave]; //添加存储变化通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( keyValueStoreChange:) name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification object:defaults]; } /** * key-value store发生变化或存储空间不足 * * @param notification 通知对象 */ -(void)keyValueStoreChange:(NSNotification *)notification{ NSLog(@"Key-value store change..."); } /** * 设置首选项 * * @param value 是否自动保存 */ -(void)setSetting:(BOOL)value{ //iCloud首选项设置 NSUbiquitousKeyValueStore *defaults=[NSUbiquitousKeyValueStore defaultStore]; [defaults setBool:value forKey:kSettingAutoSave]; [defaults synchronize];//同步 } @end
运行效果: