这节课主要讲iCloud以及demo。
iCloud
什么是iCloud呢?基本上,对用户来说iCloud只是个网络共享目录的URL,它的意图主要是让用户把他们的文档、数据、备份、app文件放到网上去,然后在他们任意的其他设备上,都可以访问该数据。这是它最主要的用途。
为了app能访问云,它需要获得正确的权限,在xcode中只要点击一个按钮就能获得权限。
只要在project target里单击黄色箭头指向的按钮,它会自动填写授权信息。
授权有两部分内容:一个是创建iCloud需要提供的档案资料;另一个是,它可以被授权写入数据到你多个app共享的云,也就是你用的云不一定是绑定到一个app的。
这个URL是什么?怎么得到这个URL?从NSFileManager获得:
[[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
如果有多个app写入到同一个地方,那么必须指定container identifier,而且必须和之前的Entitlements匹配。此调用将会返回一个URL,就是可以在云端创建文件的URL。通常情况下你创建的是一个文档,那么应该放到这个URL的Documents目录下,所以URL要追加Documents路径。
大多数的NSFileManager的事情都要另开线程去做。用NSFileManager时还要协调好,因为两个设备可能会访问同一个文件,甚至是同时访问。
NSFilePresenter是一个protocol,和UIDocument一样是抽象的,作用就是为用户显示文档。当文件有变化或有人要写入或读取时,NSFilePresenter需要被通知。
UIDocument是NSFilePresenter的子类,它会自动协调文件的变更。
UIManagedDocument有个内建机制,使得一开始只上传一种基本的SQL数据库,之后所有上传的都是其变化,就是所做的更改记录,然后任何时间任何其他设备都从这个基本数据库获得变化记录后再作用到本地数据库。如何做到这一点呢?UIManagedDocument有个很重要的字典property叫做persistentStoreOptions,需要设置它的NSPersistentStoreUbiquitousContentNameKey,该key的值只是个string,它是文件的名称。NSPersistentStoreUbiquitousContentURLKey是可选的,这是所有变化日志在云端的存放URL,要放到document同级的位置。
从iCloud打开一个UIManagedDocument,它被包装成文件了,文件包装基本上是个目录,而不是一个文件。变化日志就保存在文件包装内部的DocumentMetadata.plist,这是个字典,所以可以从该plist中获得NSPersistentStoreUbiquitousContentNameKey。要从document URL里获取DocumentMetadata.plist。这样做了之后就可以用UIManagedDocument的saveToURL和openWithCompletionHandler了。
需要云中的内容时,要通过特定的查询语句来查询,然后如果该文件改变了,你会得到通知。可以用NSMetadataQuery来创建查询,这里要指定两个东西:一是在iCloud中的搜索范围,有两种范围:一是documentsScope,就是只查documents目录;另一个是datascope,除了documents之外的目录。然后需要指定predicate,用的仍然是NSPredicate,但和Core Data有点不同,要查询目标中某个特定的属性,这种类似于在一个文件系统中搜索。
NSMetadataQuery *query = [[NSMetadataQuery alloc] init]; query.searchScopes = [NSArray arrayWithObjects:scope1, scope2, nil]; NSMetadataQueryUbiquitousDocumentsScope is all files in the Documents directory. NSMetadataQueryUbiquitousDataScope is all files NOT in the Documents directory. query.predicate = [NSPredicate predicateWithFormat:@“%K like ‘*’”, NSMetadataItemFSNameKey]; // all
下面是个注册接收消息的例子:
NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center addObserver:self selector:@selector(processQueryResults:) name:NSMetadataQueryDidFinishGatheringNotification // first results object:query]; [center addObserver:self selector:@selector(processQueryResults:) name:NSMetadataQueryDidUpdateNotification // subsequent updates object:query];
NSMetadataQueryDidFinishGatheringNotification在第一次从云端接收数据时会发过来,NSMetadataQueryDidUpdateNotification是云中的内容改变了之后发的。任何时候添加自己为observer,都要在dealloc里remove掉。
- (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; }
这是processQueryResults方法:
- (void)processQueryResults:(NSNotification *)notification{ [query disableUpdates]; int resultCount = [query resultCount]; for (int i = 0; i < resultCount; i++) { NSMetadataItem *item = [query resultAtIndex:i]; NSURL *url = [item valueForAttribute:NSMetadataItemURLKey]; // do something with the urls here // but remember that these are URLs of the files inside the file wrapper! } [query enableUpdates]; }
调用disableUpdates是不希望在处理过程中间得到另一个更新,NSMetadataItem包含查询的各种信息,NSMetadataItemURLKey是查询文件在iCloud的地址。有一件事要小心,这些是文件的URL,没有得到目录的URL,所以你要做的是用一些代码去剥离出文件名再添加到文档目录的末尾。这就是如何列举云端文件,总是要小心会不会有新状况。
如果用的是UIManagedDocument,它会自动协调。如果用NSFileManager删除一个iCloud的文件,就需要协调了。任何需要协调的访问都必须在主线程之外完成。这是文件协调的样子:
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; NSError *coordinationError; [coordinator coordinateReadingItemAtURL:(NSURL *)url options:(NSFileCoordinatorReadingOptions)options error:(NSError **)error byAccessor:^(NSURL *urlToUse) { // do your NSFileManager stuff here using urlToUse }];
filePresenter表示,如果是presenter,得要做协调,但你不用参与到协调中,因为你是提出要求的人。block里的url需要用来读或写的,该url是一个协调的url,它代表了你传进去的url。可以把协调想象为检查所有其他的file presenter,确保一切ok,如果是删除的话,也要锁定文件,强迫它们关闭。都设置好后,它会给你一个删除文件的url,然后就可以删除它了,所有其他还是老的url的presenter可能都会被关闭。
有两个documentState因为延迟和共享访问,发生的概率大了很多,一是EditingDisabled,另一个是InConflict。SavingError状态发生可能需要尝试重新保存。
在冲突的情况下怎么做呢?iCloud会保存所有的版本,它会把你的文档状态设为InConflict,你必须要解决这一冲突。如何解决冲突呢?它会有API能查看所有的版本,让你来决定是合并两个版本的变化,还是只要最新版本。一旦解决了所有的冲突,应该删除所有不打算使用的版本,只保留当前版本。
EditingDisabled状态得更要重视,这个过程是暂时的,基本上如果编辑被禁用时就不要去保存。这些状态是一直随着时间和设备的使用在变化的,所以需要观察documentState,它们变化后会发通知。
有个界面让用户选择是否要把文档保存到iCloud很重要。NSFileManager有个方法可以让你在云端和本地来回移动文件,这总是在主线程之外执行,主要的原因是file presenter需要参与进来,而它们可能是在主线程,不想因为这个也在主线程而进入死锁。这边有一个例子:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSURL *localURL = ...; // url of document to transfer to iCloud NSString *name = [localURL lastPathComponent]; NSURL *iCloudURL = [iCloudDocumentsURL URLByAppendingPathComponent:name]; NSError *error = nil; [[[NSFileManager alloc] init] setUbiquitous:YES itemAtURL:localURL destinationURL:iCloudURL error:&error]; // move the document });
这是一种根本性的协调,是在移动整个文档,这里是将一个东西从本地沙箱中移动到云端,localURL是本地沙盒的document目录下的某个文件,调用setUbiquitous:YES就是同步到云,itemAtURL是文件目前的位置,destinationURL是在云端的位置。也可以反过来,调用setUbiquitous:NO,itemAtURL将是在云端的位置,destinationURL是文件目前的位置。
Demo
这个demo主要演示从云端获取文件,每个文件将是一个photographer的fetch,将有多个文档就可以进行编辑了,当改变文件清单或改变提取结果后,如何自动更新到其他设备上。
DocumentViewController.h文件代码:
#import <UIKit/UIKit.h> // implement this protocol if you want DocumentViewController to be able to segue to you @protocol DocumentViewControllerSegue <NSObject> @property (nonatomic, strong) UIManagedDocument *document; @end @interface DocumentViewController : UITableViewController @end
DocumentViewController.m文件代码:
#import "DocumentViewController.h" #import <CoreData/CoreData.h> #import "AskerViewController.h" // 1. Add a UITableViewController of this class to the storyboard as the rootViewController of the UINavigationController // Step 2 in cellForRowAtIndexPath: // 4. Add Model "documents" which is an NSArray of NSURL // 7. Add property for an iCloud metadata query called iCloudQuery @interface DocumentViewController() <AskerViewControllerDelegate> @property (nonatomic, strong) NSArray *documents; // of NSURL @property (nonatomic, strong) NSMetadataQuery *iCloudQuery; @end @implementation DocumentViewController @synthesize documents = _documents; @synthesize iCloudQuery = _iCloudQuery; // 5. Implement documents setter to sort the array of urls (and only reload if actual changes) // Step 6 below in UITableViewDataSource section - (void)setDocuments:(NSArray *)documents { documents = [documents sortedArrayUsingComparator:^NSComparisonResult(NSURL *url1, NSURL *url2) { return [[url1 lastPathComponent] caseInsensitiveCompare:[url2 lastPathComponent]]; }]; if (![_documents isEqualToArray:documents]) { _documents = documents; [self.tableView reloadData]; } } #pragma mark - iCloud Query // 8. Implement getter of iCloudQuery to lazily instantiate it (set it to find all Documents files in cloud) // Step 9 in viewWillAppear: // 10. Add ourself as observer for both initial iCloudQuery results and any updates that happen later // Step 11 at the very bottom of this file, then step 12 in viewWillAppear: again. // 36. Observe changes to the ubiquious key-value store - (NSMetadataQuery *)iCloudQuery { if (!_iCloudQuery) { _iCloudQuery = [[NSMetadataQuery alloc] init]; _iCloudQuery.searchScopes = [NSArray arrayWithObject:NSMetadataQueryUbiquitousDocumentsScope]; _iCloudQuery.predicate = [NSPredicate predicateWithFormat:@"%K like '*'", NSMetadataItemFSNameKey]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(processCloudQueryResults:) name:NSMetadataQueryDidFinishGatheringNotification object:_iCloudQuery]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(processCloudQueryResults:) name:NSMetadataQueryDidUpdateNotification object:_iCloudQuery]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(ubiquitousKeyValueStoreChanged:) name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification object:[NSUbiquitousKeyValueStore defaultStore]]; } return _iCloudQuery; } // 37. Reload the table whenever the ubiquitous key-value store changes // (Don't miss step 38!) - (void)ubiquitousKeyValueStoreChanged:(NSNotification *)notification { [self.tableView reloadData]; } // 15. Add a methods to get the URL for the entire cloud and for the Documents directory in the cloud // 16. Click Add Entitlements in the Summary tab of the Targets section of the Project to enable iCloud for this app // (The application is now capable of displaying a list of documents in the cloud.) // Step 17 is to segue (see Segue section below). - (NSURL *)iCloudURL { return [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil]; } - (NSURL *)iCloudDocumentsURL { return [[self iCloudURL] URLByAppendingPathComponent:@"Documents"]; } // 14. Extract the file package that the passed url is contained in and return it - (NSURL *)filePackageURLForCloudURL:(NSURL *)url { if ([[url path] hasPrefix:[[self iCloudDocumentsURL] path]]) { NSArray *iCloudDocumentsURLComponents = [[self iCloudDocumentsURL] pathComponents]; NSArray *urlComponents = [url pathComponents]; if ([iCloudDocumentsURLComponents count] < [urlComponents count]) { urlComponents = [urlComponents subarrayWithRange:NSMakeRange(0, [iCloudDocumentsURLComponents count]+1)]; url = [NSURL fileURLWithPathComponents:urlComponents]; } } return url; } // 13. Handle changes to the iCloudQuery's results by iterating through and adding file packages to our Model - (void)processCloudQueryResults:(NSNotification *)notification { [self.iCloudQuery disableUpdates]; NSMutableArray *documents = [NSMutableArray array]; int resultCount = [self.iCloudQuery resultCount]; for (int i = 0; i < resultCount; i++) { NSMetadataItem *item = [self.iCloudQuery resultAtIndex:i]; NSURL *url = [item valueForAttribute:NSMetadataItemURLKey]; // this will be a file, not a directory url = [self filePackageURLForCloudURL:url]; if (url && ![documents containsObject:url]) [documents addObject:url]; // in case a file package contains multiple files, don't add twice } self.documents = documents; [self.iCloudQuery enableUpdates]; } #pragma mark - View Controller Lifecycle // 9. Start up the iCloudQuery in viewWillAppear: if not already started // 12. Turn iCloudQuery updates on and off as we appear/disappear from the screen // 38. Since changes that WE make to the ubiquitous key-value store don't generate an NSNotification, // we are responsible for updating our UI when we change it. // We'll be cheap here and just reload ourselves each time we appear! // Probably would be a lot better to have our own internal NSNotification or some such. - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [self.tableView reloadData]; // step 38: ugh! if (![self.iCloudQuery isStarted]) [self.iCloudQuery startQuery]; [self.iCloudQuery enableUpdates]; } - (void)viewWillDisappear:(BOOL)animated { [self.iCloudQuery disableUpdates]; [super viewWillDisappear:animated]; } #pragma mark - Autorotation // 3. Autorotation YES in all orientations // Back to the top for step 4. - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { return YES; } #pragma mark - UITableViewDataSource // 6. Implement UITableViewDataSource number of rows in section and cellForRowAtIndexPath: using Model // Back to top for step 7. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [self.documents count]; } // 2. Set the reuse identifier of the prototype to be "Document Cell" and set in cellForRowAtIndexPath: // 33. Set the subtitle of the cell to whatever string is in the ubiquitious key-value store under the document name - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Document Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; } // Configure the cell... NSURL *url = [self.documents objectAtIndex:indexPath.row]; cell.textLabel.text = [url lastPathComponent]; cell.detailTextLabel.text = [[NSUbiquitousKeyValueStore defaultStore] objectForKey:[url lastPathComponent]]; return cell; } // Convenience method for logging errors returned through NSError - (void)logError:(NSError *)error inMethod:(SEL)method { NSString *errorDescription = error.localizedDescription; if (!errorDescription) errorDescription = @"???"; NSString *errorFailureReason = error.localizedFailureReason; if (!errorFailureReason) errorFailureReason = @"???"; if (error) NSLog(@"[%@ %@] %@ (%@)", NSStringFromClass([self class]), NSStringFromSelector(method), errorDescription, errorFailureReason); } // 25. Remove the url from the cloud in a coordinated manner (and in a separate thread) // (At this point, the application is capable of both adding and deleting documents from the cloud.) // The next step is to be able to edit the documents themselves in PhotographersTableViewController (step 26). // 34. Remove any ubiquitous key-value store entry for this document too (since we're deleting it) // Next step is to actually set the key-value store entry for a document. Back in PhotographersTVC (step 35). - (void)removeCloudURL:(NSURL *)url { [[NSUbiquitousKeyValueStore defaultStore] removeObjectForKey:[url lastPathComponent]]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; NSError *coordinationError; [coordinator coordinateWritingItemAtURL:url options:NSFileCoordinatorWritingForDeleting error:&coordinationError byAccessor:^(NSURL *newURL) { NSError *removeError; [[NSFileManager defaultManager] removeItemAtURL:newURL error:&removeError]; [self logError:removeError inMethod:_cmd]; // _cmd means "this method" (it's a SEL) // should also remove log files in CoreData directory in the cloud! // i.e., delete the files in [self iCloudCoreDataLogFilesURL]/[url lastPathComponent] }]; [self logError:coordinationError inMethod:_cmd]; }); } // 24. Make documents deletable by removing them from the Model, and from the table, and from the cloud. // (Note that we access _documents directly here! Ack! That's bad form! We should probably find a better way.) - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { NSURL *url = [self.documents objectAtIndex:indexPath.row]; NSMutableArray *documents = [self.documents mutableCopy]; [documents removeObject:url]; _documents = documents; // Argh! [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; [self removeCloudURL:url]; } } #pragma mark - Segue - (NSURL *)iCloudCoreDataLogFilesURL { return [[self iCloudURL] URLByAppendingPathComponent:@"CoreData"]; } // 19. Set persistentStoreOptions in the document before segueing // (Both the automatic schema-migration options and the "logging-based Core Data" options are set.) // (The application is now capable of showing the contents of documents in the cloud.) // See step 20 in PhotographersTableViewController (adding a spinner to better see what's happening). - (void)setPersistentStoreOptionsInDocument:(UIManagedDocument *)document { NSMutableDictionary *options = [NSMutableDictionary dictionary]; [options setObject:[NSNumber numberWithBool:YES] forKey:NSMigratePersistentStoresAutomaticallyOption]; [options setObject:[NSNumber numberWithBool:YES] forKey:NSInferMappingModelAutomaticallyOption]; [options setObject:[document.fileURL lastPathComponent] forKey:NSPersistentStoreUbiquitousContentNameKey]; [options setObject:[self iCloudCoreDataLogFilesURL] forKey:NSPersistentStoreUbiquitousContentURLKey]; document.persistentStoreOptions = options; } // 17. In the storyboard, create a Push segue called "Show Document" from this VC to our old Photomania VC chain // 18. Prepare for segue by getting the URL at the segued-from row, creating a document, and setting it in destination // (Note the generic mechanism we use to get the segued-from indexPath.) // (Note how we use a protocol (DocumentViewControllerSegue) to generically segue to any destination.) // 21. Add a + button to the storyboard which segues Modally to an AskerViewController (get this from KitchenSink) // (Note, you will have to add the questionLabel and answerTextFields to the AskerViewController scene.) // 22. Modify prepare for segue to set ourself as the delegate and set the question of the AskerViewController. - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { if ([segue.identifier isEqualToString:@"New Document"]) { AskerViewController *asker = (AskerViewController *)segue.destinationViewController; asker.delegate = self; asker.question = @"New document name:"; } else { NSIndexPath *indexPath = nil; if ([sender isKindOfClass:[NSIndexPath class]]) { indexPath = (NSIndexPath *)sender; } else if ([sender isKindOfClass:[UITableViewCell class]]) { indexPath = [self.tableView indexPathForCell:sender]; } else if (!sender || (sender == self) || (sender == self.tableView)) { indexPath = [self.tableView indexPathForSelectedRow]; } if (indexPath && [segue.identifier isEqualToString:@"Show Document"]) { if ([segue.destinationViewController conformsToProtocol:@protocol(DocumentViewControllerSegue)]) { NSURL *url = [self.documents objectAtIndex:indexPath.row]; [segue.destinationViewController setTitle:[url lastPathComponent]]; UIManagedDocument *document = [[UIManagedDocument alloc] initWithFileURL:url]; [self setPersistentStoreOptionsInDocument:document]; // make cloud Core Data documents efficient! [segue.destinationViewController setDocument:document]; } } } } // 23. Implement AVC delegate to create an NSURL in the cloud with the chosen name, add it to our Model, // then segue to create it (and we must dismiss the AVC too) // (It is now possible to create documents in the cloud using the application!) - (void)askerViewController:(AskerViewController *)sender didAskQuestion:(NSString *)question andGotAnswer:(NSString *)answer { NSURL *url = [[self iCloudDocumentsURL] URLByAppendingPathComponent:answer]; NSMutableArray *documents = [self.documents mutableCopy]; [documents addObject:url]; self.documents = documents; int row = [self.documents indexOfObject:url]; NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0]; [self performSegueWithIdentifier:@"Show Document" sender:indexPath]; [self dismissModalViewControllerAnimated:YES]; } #pragma mark - Dealloc // 11. Remove ourself as an observer (of anything) when we leave the heap - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } @end