這節課主要講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