創(chuàng)建自定義文檔對象
基于文檔的應(yīng)用程序必須具有代表和管理文檔數(shù)據(jù)的UIDocument子類的實例。本章討論了覆蓋大多數(shù)應(yīng)用程序所需的方法,并提供了覆蓋其他方法的建議。對于核心重寫點,loadFromContents:ofType:error:和contentsForType:error:methods-examples被給定為NSData和NSFileWrapper作為從文件讀取和寫入文檔數(shù)據(jù)的類型。將文檔數(shù)據(jù)存儲在文件包中進一步說明了如何對文檔數(shù)據(jù)使用文件包裝對象。
除了本章討論的UIDocument以外,您可以覆蓋UIDocument的方法,以便為特定目的讀取和寫入文檔數(shù)據(jù),例如逐步寫入和讀取文檔數(shù)據(jù)。然而,這些更高級的覆蓋具有更復(fù)雜的要求,如果可能的話應(yīng)該避免。有關(guān)這些覆蓋的討論,請參閱UIDocument類參考。
聲明文檔類接口
在Xcode中,將新的Objective-C源文件和頭文件添加到您的項目中,并將其正確命名(建議:將“文檔”作為名稱)。在頭文件中,將超類更改為UIDocument,并添加屬性以保存文檔數(shù)據(jù)。在清單3-1中,文檔數(shù)據(jù)是純文本,因此NSString屬性是保存它所需要的。 (文本將被轉(zhuǎn)換為寫入文檔文件的NSData對象。)
清單3-1文檔子類聲明(NSData)
@interface MyDocument : UIDocument {
}
@property(nonatomic, strong) NSString *documentText;
@end
清單3-2說明了另一個使用NSFileWrapper對象作為數(shù)據(jù)表示類型的應(yīng)用程序的聲明集。 (本章中的代碼示例在兩個應(yīng)用程序之間交替)。不僅有一個屬性來保存文件包裝對象,還有一些屬性可以保存表示文件包的文本和圖像組件。
清單3-2文檔子類聲明(NSFileWrapper)
@interface ImageNotesDocument : UIDocument
@property (nonatomic, strong) NSString* text;
@property (nonatomic, strong) UIImage* image;
@property (nonatomic, strong) NSFileWrapper *fileWrapper;
@property (nonatomic, weak) id <ImageNotesDocumentDelegate> delegate;
@end
@protocol ImageNotesDocumentDelegate <NSObject>
-(void)noteDocumentContentsUpdated:(ImageNotesDocument*)noteDocument;
@end
此代碼顯示代理的其他聲明及其采用的協(xié)議。文檔對象的視圖控制器使自己成為文檔對象的代理(并采用協(xié)議),以便它可以通過文檔文件的修改(通過noteDocumentContentsUpdated:messages)進行通知。清單3-4顯示了發(fā)送noteDocumentContentsUpdated:消息的時間和方式。
加載文檔數(shù)據(jù)
當(dāng)應(yīng)用程序打開文檔(按照用戶的請求)時,UIDocument將讀取文檔文件的內(nèi)容,并調(diào)用loadFromContents:ofType:error:method,傳遞封裝文檔數(shù)據(jù)的對象。該對象可以是NSData對象或NSFileWrapper對象。在覆蓋該方法時,從傳入對象的內(nèi)容中初始化文檔的內(nèi)部數(shù)據(jù)結(jié)構(gòu)(即其模型對象)。
清單3-3中的示例從傳入的NSData對象創(chuàng)建一個字符串,并將其分配給documentText屬性。它還通過調(diào)用協(xié)議方法通知其代表(在這種情況下,文檔的視圖控制器)更新的文檔內(nèi)容。這個委托消息背后的動機是loadFromContents:ofType:error:method不僅是打開文檔的結(jié)果,也是因為iCloud更新和恢復(fù)操作(revertToContentsOfURL:completionHandler :)的結(jié)果。
清單3-3加載文檔的數(shù)據(jù)(NSData)
- (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError **)outError {
if ([contents length] > 0) {
self.documentText = [[NSString alloc] initWithData:(NSData *)contents encoding:NSUTF8StringEncoding];
} else {
self.documentText = @"";
}
if ([_delegate respondsToSelector:@selector(noteDocumentContentsUpdated:)]) {
[_delegate noteDocumentContentsUpdated:self];
}
return YES;
}
如果您有多個文檔類型,請檢查typeName參數(shù); 不同的文檔類型可能會影響代碼如何處理文檔數(shù)據(jù)對象。 如果您的代碼遇到阻止其加載文檔數(shù)據(jù)的錯誤,請返回NO; 可選地,您可以通過引用返回描述錯誤的NSError對象。
清單3-4中的示例以NSFileWrapper對象的形式處理文檔數(shù)據(jù)。 它只是將此對象分配給其屬性。
清單3-4加載文檔的數(shù)據(jù)(NSFileWrapper)
-(BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError **)outError {
self.fileWrapper = (NSFileWrapper *)contents;
if ([_delegate respondsToSelector:@selector(noteDocumentContentsUpdated:)]) {
[_delegate noteDocumentContentsUpdated:self];
}
return YES;
}
在此代碼中,方法實現(xiàn)不會提取文件包裝器的文本和圖像組件,并將其分配給其屬性。 這在文本和圖像屬性的getter方法中做得很懶。
提供文件數(shù)據(jù)快照
當(dāng)文檔關(guān)閉或自動保存文檔時,UIDocument會將文檔對象發(fā)送一個contentsForType:error:message。 您必須重寫此方法以將文檔數(shù)據(jù)的快照返回到UIDocument,然后將其寫入文檔文件。 清單3-5給出了以NSData對象的形式返回文檔數(shù)據(jù)快照的示例。
清單3-5返回文檔數(shù)據(jù)的快照(NSData)
- (id)contentsForType:(NSString *)typeName error:(NSError **)outError {
if (!self.documentText) {
self.documentText = @"";
}
NSData *docData = [self.documentText dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:NO];
return docData;
}
如果documentText屬性尚未分配任何字符串值,則在使用它創(chuàng)建NSData對象之前,將為其分配一個空字符串。
清單3-6顯示了返回NSFileWrapper對象的相同方法的實現(xiàn)。 基本上,如果頂級(目錄)文件包裝對象不存在,則代碼創(chuàng)建它; 并且如果兩個包含(常規(guī)文件)文件包裝對象不存在,則代碼將從文本和圖像屬性的值創(chuàng)建它們。 然后,它將頂級文件包裝器返回到UIDocument,該文件在文件系統(tǒng)中創(chuàng)建一個文件包。 請參閱將文檔數(shù)據(jù)存儲在文件包中,以獲取有關(guān)文件包和文檔的更詳細說明。
清單3-6返回文檔數(shù)據(jù)的快照(NSFileWrapper)
-(id)contentsForType:(NSString *)typeName error:(NSError **)outError {
if (self.fileWrapper == nil) {
self.fileWrapper = [[NSFileWrapper alloc] initDirectoryWithFileWrappers:nil];
}
NSDictionary *fileWrappers = [self.fileWrapper fileWrappers];
if (([fileWrappers objectForKey:TextFileName] == nil) && (self.text != nil)) {
NSData *textData = [self.text dataUsingEncoding:TextFileEncoding];
NSFileWrapper *textFileWrapper = [[NSFileWrapper alloc] initRegularFileWithContents:textData];
[textFileWrapper setPreferredFilename:TextFileName];
[self.fileWrapper addFileWrapper:textFileWrapper];
}
if (([fileWrappers objectForKey:ImageFileName] == nil) && (self.image != nil)) {
@autoreleasepool {
NSData *imageData = UIImagePNGRepresentation(self.image);
NSFileWrapper *imageFileWrapper = [[NSFileWrapper alloc] initRegularFileWithContents:imageData];
[imageFileWrapper setPreferredFilename:ImageFileName];
[self.fileWrapper addFileWrapper:imageFileWrapper];
}
}
return self.fileWrapper;
}
將文檔數(shù)據(jù)存儲在文件包中
一個文件包的內(nèi)部結(jié)構(gòu)體現(xiàn)在NSFileWrapper類的方法中。 文件包裝器是文件系統(tǒng)節(jié)點的運行時表示,它是目錄,常規(guī)文件或符號鏈接。 如圖3-1所示,文件包是文件系統(tǒng)節(jié)點,通常是目錄及其內(nèi)容,操作系統(tǒng)將其視為單個不透明實體。 它在概念上與捆綁類似。
圖3-1文件包的結(jié)構(gòu)

您可以通過創(chuàng)建一個頂級目錄文件包裝器來編程文件包,然后向該容器添加常規(guī)文件和子目錄,每個文件和子目錄由其他NSFileWrapper對象表示。 頂層目錄中的文件包裝應(yīng)該具有與它們相關(guān)聯(lián)的首選名稱。
考慮到這個簡要概述,請再次查看清單3-6中的contentsForType:error:方法中的以下代碼行。 在此方法中創(chuàng)建的文件包具有兩個組件,一個文本文件和一個圖像文件。 (圖片文件包裝器的創(chuàng)建不會顯示在代碼段中。)
if (self.fileWrapper == nil) {
self.fileWrapper = [[NSFileWrapper alloc] initDirectoryWithFileWrappers:nil];
}
NSDictionary *fileWrappers = [self.fileWrapper fileWrappers];
if (([fileWrappers objectForKey:TextFileName] == nil) && (self.text != nil)) {
NSData *textData = [self.text dataUsingEncoding:TextFileEncoding];
NSFileWrapper *textFileWrapper = [[NSFileWrapper alloc] initRegularFileWithContents:textData];
[textFileWrapper setPreferredFilename:TextFileName];
[self.fileWrapper addFileWrapper:textFileWrapper];
}
代碼創(chuàng)建一個頂級目錄(如果不存在)。如果文本文件不存在文件包裝器,它將從text屬性的字符串內(nèi)容中創(chuàng)建一個。它給這個文件包裝器一個首選文件名,然后將其添加到頂級目錄文件包裝器。
有關(guān)NSFileWrapper的更多信息,請參閱NSFileWrapper類參考;另請參閱導(dǎo)出文檔UTI的文檔文件包所需的Info.plist屬性。
其他方法可以覆蓋你可能做的
許多基于文檔的應(yīng)用程序可能想要做的其他一些UIDocument覆蓋:
disableEditing ... enableEditing-UIDocument在不安全的情況下調(diào)用第一種方法,以便用戶更改文檔內(nèi)容,例如當(dāng)iCloud有更新或還原操作正在進行時。您可以在此期間實施此方法以防止編輯。再次編輯變得安全時,UIDocument調(diào)用第二種方法。
注意:作為這些覆蓋的替代方法,您可以觀察文檔狀態(tài)更改時發(fā)布的通知,如果新文檔狀態(tài)為UIDocumentStateEditingDisabled,則會在文檔狀態(tài)再次更改之前防止編輯。有關(guān)此主題的更多信息,請參閱監(jiān)控文檔狀態(tài)更改和處理錯誤。
savingFileType - 默認情況下,此方法返回fileType屬性的值。如果由于任何原因而將當(dāng)前文檔保存在不同的文件類型下,可以覆蓋此方法以返回替換文件類型UTI。一個例子(來自Mac OS X)是將圖像添加到RTF文件時,應(yīng)將其保存為RTFD文件包。
管理文件的生命周期
一個文件經(jīng)歷了一個典型的生命周期。基于文檔的應(yīng)用程序負責(zé)管理其在該周期中的進展。從以下列表可以看出,大多數(shù)這些生命周期事件是由用戶發(fā)起的:
- 用戶首先創(chuàng)建一個文檔。
- 用戶打開現(xiàn)有文檔,應(yīng)用程序?qū)⑵滹@示在文檔的視圖或視圖中。
- 用戶編輯文檔。
- 用戶可能會要求將文檔放在iCloud存儲中,或者可以請求從iCloud存儲中刪除文檔。
- 在編輯,保存或其他操作期間,可能會發(fā)生錯誤或沖突;應(yīng)用程序應(yīng)該了解這些錯誤和沖突,并嘗試處理它們或通知用戶。
- 用戶關(guān)閉選定的文檔。
- 用戶刪除現(xiàn)有文檔。
以下部分將討論基于文檔的應(yīng)用程序必須為這些生命周期操作完成的過程。
設(shè)置文檔文件的首選存儲位置
應(yīng)用程序的所有文檔都存儲在本地沙箱或iCloud容器目錄中。用戶不能選擇單獨的文檔存儲在iCloud中。
應(yīng)用程序首次在設(shè)備上啟動應(yīng)用程序時,應(yīng)執(zhí)行以下操作:
- 如果iCloud未配置,請告知用戶,如果要在其中保存文件,則需要配置iCloud。
- 如果iCloud已配置但未啟用應(yīng)用程序,請詢問用戶是否要啟用iCloud,換句話說,詢問他們是否希望將所有文檔保存到iCloud。將響應(yīng)存儲為用戶偏好。
基于此首選項,應(yīng)用程序?qū)⑽臋n文件寫入本地應(yīng)用程序沙箱或iCloud容器目錄。 (有關(guān)詳細信息,請參閱將文檔移入和移出iCloud Storage。)應(yīng)用程序應(yīng)在“設(shè)置”應(yīng)用程序中公開交換機,以使用戶能夠在本地存儲和iCloud存儲之間移動文檔。
創(chuàng)建新文檔
文檔對象(即,您的自定義UIDocument子類的實例)必須具有將文檔文件定位到本地應(yīng)用程序沙箱或iCloud容器目錄中的文件URL,取決于用戶的偏好。另外,一個新的文件可以給一個名字。以下討論與文件URL,文檔名稱和創(chuàng)建新文檔相關(guān)的準則和過程。
文件文件名與文件名稱
UIDocument類假定文檔的文件名與文檔名稱(也稱為顯示名稱)之間的對應(yīng)關(guān)系。默認情況下,UIDocument將文件名存儲為localizedName屬性的值。但是,當(dāng)應(yīng)用程序創(chuàng)建新文檔時,應(yīng)用程序不應(yīng)要求用戶提供文檔名稱或顯示名稱。
對于您的應(yīng)用程序,您應(yīng)該制定一些自動生成新文檔的文件名的約定。一些建議是:
- 為每個文檔生成UUID(通用唯一標(biāo)識符),可選地使用應(yīng)用程序特定的前綴。
- 為每個文檔生成時間戳(日期和時間),可選地使用應(yīng)用程序特定的前綴。
使用順序編號系統(tǒng),例如:“注1”,“注2”等。
對于文檔(顯示)名稱,如果這是有意義的(例如使用“Notes 1”),則可能最初使用文檔文件名?;蛘?,如果文檔包含文本,并且用戶在文檔中輸入一些文本,則可以使用第一行(或第一行的某些部分)作為顯示名稱。您的應(yīng)用程序可以在用戶創(chuàng)建文檔之后給用戶一些自定義文檔名稱的方法。
編寫文件URL并保存文檔文件
您無法創(chuàng)建沒有有效的文件URL的文檔對象。文件URL有三個部分:文檔目錄在用戶首選文檔位置的路徑,文檔文件名和文檔文件的擴展名。您可以通過文檔文件名與文檔名稱中的方法獲取表示本地應(yīng)用程序沙箱中的Documents目錄的路徑的URL。
清單4-1獲取本地沙箱中應(yīng)用程序的Documents目錄的URL
-(NSURL*)localDocumentsDirectoryURL {
static NSURL *localDocumentsDirectoryURL = nil;
if (localDocumentsDirectoryURL == nil) {
NSString *documentsDirectoryPath = [NSSearchPathForDirectoriesInDomains( NSDocumentDirectory,
NSUserDomainMask, YES ) objectAtIndex:0];
localDocumentsDirectoryURL = [NSURL fileURLWithPath:documentsDirectoryPath];
}
return localDocumentsDirectoryURL;
}
文件擴展名必須是您為文檔類型指定的擴展名(請參閱創(chuàng)建和配置項目)。 您可以聲明一個全局字符串來表示擴展名。 例如:
static NSString *FileExtension = @"imageNotes”;
文檔URL的最后一部分是文件名組件。當(dāng)文檔文件名與文檔名稱解釋時,應(yīng)用程序最初應(yīng)根據(jù)對應(yīng)用程序有意義的約定生成文檔文件名。該生成的文件名可以用作文檔名稱,也可以將第一行(或其一部分)用作文檔名稱。該應(yīng)用程序可以給用戶在創(chuàng)建文檔對象后自定義文檔名稱的選項。
連接基本URL,文檔文件名和文件擴展名后,您可以分配一個自定義UIDocument子類的實例,并使用initWithFileURL:方法初始化它,傳遞構(gòu)造的文件URL。創(chuàng)建新文檔的最后一步是將其保存到首選文檔存儲位置(即使此時沒有內(nèi)容)。如通過設(shè)置文檔文件的首選存儲位置所示,您可以通過調(diào)用文檔對象上的saveToURL:forSaveOperation:completionHandler:method來執(zhí)行此操作。
清單4-2將新文檔保存到文件系統(tǒng)
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (_createFile) {
[self.document saveToURL:self.document.fileURL
forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
if (success)
_textView.text = self.document.text;
}];
_createFile = NO;
}
// .....
}
方法調(diào)用的save-operation參數(shù)應(yīng)為UIDocumentSaveForCreating。調(diào)用的最終參數(shù)是完成處理程序:在保存操作結(jié)束后調(diào)用的塊。該塊的參數(shù)告訴您操作是否成功。如果確實成功,則此代碼將文檔文本分配給顯示文檔內(nèi)容的文本視圖的text屬性。
注意:如果要將新文檔保存到應(yīng)用程序的iCloud容器目錄中,建議首先將其保存在本地,然后調(diào)用NSFileManager方法setUbiquitous:itemAtURL:destinationURL:error:將文檔文件移動到iCloud存儲。 (可以在saveToURL的完成處理程序中進行此調(diào)用:forSaveOperation:completionHandler:method。)有關(guān)更多信息,請參閱將文檔移動到iCloud Storage和iCloud Storage。
打開和關(guān)閉文檔
打開文件可能乍看起來似乎是一個相當(dāng)簡單的過程。您的應(yīng)用程序掃描其文檔目錄中的文件的文件的文件擴展名,并將這些文檔提供給用戶進行選擇。然而,當(dāng)iCloud存儲被考慮在內(nèi)時,事情會變得更加復(fù)雜。應(yīng)用程序的文檔可能位于應(yīng)用程序沙箱的Documents目錄中,也可能位于iCloud容器目錄的Documents目錄中。
發(fā)現(xiàn)申請文件
要獲取iCloud存儲中應(yīng)用程序文檔的列表,請運行元數(shù)據(jù)查詢。查詢是NSMetadataQuery類的一個實例。創(chuàng)建一個NSMetadataQuery對象后,給它一個范圍和一個謂詞。對于iCloud存儲,范圍應(yīng)為NSMetadataQueryUbiquitousDocumentsScope。謂詞是一個NSPredicate對象,在這種情況下,限制文件擴展名的搜索。在開始運行查詢之前,請注冊以觀察NSMetadataQueryDidFinishGatheringNotification和NSMetadataQueryDidUpdateNotification通知。接受傳遞這些通知的方法處理查詢的結(jié)果。
清單4-3說明了如何設(shè)置和運行元數(shù)據(jù)查詢以獲取iCloud移動容器中的應(yīng)用程序文檔列表。該方法首先測試用戶對文檔(documentsInCloud屬性)的首選存儲位置。如果該位置是移動容器,則它運行元數(shù)據(jù)查詢。如果位置是應(yīng)用程序沙箱,它會遍歷應(yīng)用程序的Documents目錄的內(nèi)容,以獲取所有本地文檔文件的名稱和位置。
清單4-3獲取存儲在本地和iCloud存儲中的文檔的位置
-(void)viewDidLoad {
[super viewDidLoad];
// set up Add and Edit navigation items here....
if (self.documentsInCloud) {
_query = [[NSMetadataQuery alloc] init];
[_query setSearchScopes:[NSArray arrayWithObjects:NSMetadataQueryUbiquitousDocumentsScope, nil]];
[_query setPredicate:[NSPredicate predicateWithFormat:@"%K LIKE '*.txt'", NSMetadataItemFSNameKey]];
NSNotificationCenter* notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self selector:@selector(fileListReceived)
name:NSMetadataQueryDidFinishGatheringNotification object:nil];
[notificationCenter addObserver:self selector:@selector(fileListReceived)
name:NSMetadataQueryDidUpdateNotification object:nil];
[_query startQuery];
} else {
NSArray* localDocuments = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:
[self.documentsDir path] error:nil];
for (NSString* document in localDocuments) {
[_fileList addObject:[[[FileRepresentation alloc] initWithFileName:[document lastPathComponent]
url:[NSURL fileURLWithPath:[[self.documentsDir path]
stringByAppendingPathComponent:document]]] autorelease]];
}
}
}
在這個例子中,謂詞格式是@“%K LIKE'* .txt'”,這意味著返回所有文件名(NSMetadataItemFSNameKey鍵),擴展名為txt,這是該應(yīng)用程序文檔文件的文件擴展名。
初始查詢結(jié)束后,如果有后續(xù)更新,則再次調(diào)用清單4-3(fileListReceived)中指定的通知方法。 清單4-4顯示了該方法的實現(xiàn)。 如果查詢更新在用戶進行選擇后到達,則代碼還會跟蹤當(dāng)前選擇。
清單4-4收集有關(guān)iCloud存儲中的文檔的信息
-(void)fileListReceived {
NSString* selectedFileName=nil;
NSInteger newSelectionRow = [self.tableView indexPathForSelectedRow].row;
if (newSelectionRow != NSNotFound) {
selectedFileName = [[_fileList objectAtIndex:newSelectionRow] fileName];
}
[_fileList removeAllObjects];
NSArray* queryResults = [_query results];
for (NSMetadataItem* result in queryResults) {
NSString* fileName = [result valueForAttribute:NSMetadataItemFSNameKey];
if (selectedFileName && [selectedFileName isEqualToString:fileName]) {
newSelectionRow = [_fileList count];
}
[_fileList addObject:[[[FileRepresentation alloc] initWithFileName:fileName
url:[result valueForAttribute:NSMetadataItemURLKey]] autorelease]];
}
[self.tableView reloadData];
if (newSelectionRow != NSNotFound) {
NSIndexPath* selectionPath = [NSIndexPath indexPathForRow:newSelectionRow inSection:0];
[self.tableView selectRowAtIndexPath:selectionPath animated:NO scrollPosition:UITableViewScrollPositionNone];
}
}
示例應(yīng)用程序現(xiàn)在具有一個定制模型對象的數(shù)組(_fileList),該對象封裝了每個應(yīng)用程序文檔的名稱和文件URL。 (FileRepresentation是這些對象的自定義類。)根視圖控制器使用文檔名稱填充一個簡單表視圖
注意:您應(yīng)該將元數(shù)據(jù)查詢僅在您的應(yīng)用程序處于前臺時運行。當(dāng)應(yīng)用程序移動到后臺時,您應(yīng)該停止查詢。
從iCloud下載文件文件
運行元數(shù)據(jù)查詢以了解應(yīng)用程序的iCloud文檔時,查詢結(jié)果將是文檔文件的占位符項(NSMetadataItem對象)。這些項目包含有關(guān)文件的元數(shù)據(jù),例如其URL及其修改日期。文檔文件不在iCloud容器目錄中。
直到發(fā)生以下情況之一才會下載文檔的實際數(shù)據(jù):
- 您的應(yīng)用程序嘗試打開或訪問該文件,例如通過調(diào)用openWithCompletionHandler:。
- 您的應(yīng)用程序調(diào)用NSFileManager方法startDownloadingUbiquitousItemAtURL:錯誤:明確下載數(shù)據(jù)。
因為從iCloud下載大型文件可能會導(dǎo)致顯示文檔數(shù)據(jù)的可察覺的延遲,您應(yīng)該向用戶指出下載已經(jīng)開始(例如,顯示“加載”或“更新”),并且該文件當(dāng)前不是無障礙。下載完成后,請刪除該指示。
注意:與文檔文件的URL相關(guān)聯(lián)的NSURLUbiquitousItemIsDownloadedKey會告訴您文檔當(dāng)前的下載狀態(tài)。您可以使用NSURL類的getResourceValue:forKey:error:method來檢索此鍵的值。其他鍵也可以告訴你有關(guān)文件的上傳和下載狀態(tài)的相關(guān)信息。有關(guān)更多信息,請參閱NSURL類參考。
打開文件
基于示例文檔的應(yīng)用程序在表視圖中列出已知文檔。當(dāng)用戶點擊列出的文檔打開它時,UITableView調(diào)用其委托的tableView:didSelectRowAtIndexPath:方法。清單4-5所示的這種方法的實現(xiàn)是導(dǎo)航模式的典型:根視圖控制器按順序分配下一個視圖控制器 - 在這種情況下,視圖控制器顯示文檔數(shù)據(jù),并使用基本數(shù)據(jù) - 在這種情況下,文件的文件URL。根據(jù)設(shè)備成語是iPad還是iPhone(或iPhone touch),根視圖控制器將視圖控制器添加到拆分視圖或?qū)⑵渫扑偷綄?dǎo)航控制器的堆棧上。
清單4-5響應(yīng)打開文檔的請求
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[self selectFileAtIndexPath:indexPath create:NO];
}
-(void)selectFileAtIndexPath:(NSIndexPath*)indexPath create:(BOOL)create
{
NSArray* fileList = indexPath.section == 0 ? _localFileList : _ubiquitousFileList;
DetailViewController* detailViewController = [[DetailViewController alloc]
initWithFileURL:[[fileList objectAtIndex:indexPath.row] url] createNewFile:create];
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
self.splitViewController.viewControllers =
[NSArray arrayWithObjects:self.navigationController, detailViewController, nil];
}
else {
[self.navigationController pushViewController:detailViewController animated:YES];
}
[detailViewController release];
}
在其初始化方法(未顯示)中,文檔的視圖控制器(示例中的DetailViewController)分配UIDocument子類的實例,并通過調(diào)用initWithFileURL:方法來初始化它,傳遞文件URL。 它將新創(chuàng)建的文檔對象分配給文檔屬性。
打開文檔的最后一步是調(diào)用UIDocument對象上的openWithCompletionHandler:方法; 我們的示例應(yīng)用程序中的文檔視圖控制器在viewWillAppear中調(diào)用此方法,如清單4-6所示。 代碼檢查文檔狀態(tài)以驗證文檔在嘗試打開文檔之前是否已關(guān)閉 - 無需打開已打開的文檔。
清單4-6打開文檔
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (_createFile) {
[self.document saveToURL:self.document.fileURL forSaveOperation:UIDocumentSaveForCreating
completionHandler:^(BOOL success) {
_textView.text = self.document.text;
}];
_createFile = NO;
}
else {
if (self.document.documentState & UIDocumentStateClosed) {
[self.document openWithCompletionHandler:nil];
}
}
}
當(dāng)openWithCompletionHandler:被調(diào)用時,UIDocument從文檔文件讀取數(shù)據(jù),文檔對象本身從數(shù)據(jù)中創(chuàng)建其模型對象。在此操作順序結(jié)束時,執(zhí)行openWithCompletionHandler:方法的完成處理程序。雖然示例中的視圖控制器不實現(xiàn)完成塊,但是完成處理程序有時用于將文檔數(shù)據(jù)分配給文檔的視圖或視圖進行顯示。 (要回顧什么DetailViewController而不是更新文檔視圖,請參閱清單3-4及隨附的文本。)
關(guān)閉文件
要關(guān)閉文檔,請向文檔對象發(fā)送一個closeWithCompletionHandler:方法。如果需要,該方法保存文檔數(shù)據(jù),然后在其唯一參數(shù)中執(zhí)行完成處理程序。
關(guān)閉文檔的好時機是當(dāng)文檔的視圖控制器被關(guān)閉時,例如當(dāng)用戶點擊后退按鈕時。在視圖控制器的視圖消失之前,將調(diào)用viewWillDisappear:方法。您的視圖控制器子類可以覆蓋此方法,以便調(diào)用closeWithCompletionHandler:在文檔對象上,如清單4-7所示。
清單4-7關(guān)閉文檔
-(void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
[self.document closeWithCompletionHandler:nil];
}
將文檔移動到iCloud Storage和
如設(shè)置文檔文件的首選存儲位置所述,應(yīng)用程序應(yīng)該向其用戶提供在本地文件系統(tǒng)(應(yīng)用程序沙箱)或iCloud(容器目錄)中存儲所有文檔的選項。它將此選項作為用戶首選項存儲,并在保存和打開文檔時引用此首選項。當(dāng)用戶更改首選項時,應(yīng)用程序應(yīng)將應(yīng)用程序沙箱中的所有文檔文件移動到iCloud,或者根據(jù)更改的性質(zhì)將所有文件移動到另一個方向。
獲取iCloud容器目錄的位置
將文檔文件從本地存儲移動到iCloud容器目錄的Documents子目錄時,其文件名不變。文件URL路徑中唯一不同的部分是文檔導(dǎo)出的部分。要獲得該路徑的一部分,您需要調(diào)用NSFileManager的URLForUbiquityContainerIdentifier:方法。大多數(shù)情況下,您將無法通過此方法獲取應(yīng)用程序的默認容器目錄。如果您的應(yīng)用程序支持多個容器,則可以通過傳遞具有相應(yīng)iCloud容器標(biāo)識符的字符串來顯式地請求容器,該容器標(biāo)識符是您的團隊ID和應(yīng)用程序包ID的連接,以期間隔開。這些容器標(biāo)識符字符串與您在Xcode中的應(yīng)用程序目標(biāo)“摘要”視圖的“標(biāo)識符”字段中指定的相同。為每個應(yīng)用程序的容器標(biāo)識符聲明一個字符串常量是個好主意,如下例所示:
static NSString *UbiquityContainerIdentifier = @"A93A5CM278.com.acme.document.billabong”;
清單4-8中的兩個方法可以獲得iCloud容器標(biāo)識符,并附加“/ Documents”。
清單4-8獲取iCloud容器目錄URL
-(NSURL*)ubiquitousContainerURL {
return [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
}
-(NSURL*)ubiquitousDocumentsDirectoryURL {
return [[self ubiquitousContainerURL] URLByAppendingPathComponent:@"Documents"];
}
注意:有關(guān)獲取應(yīng)用程序沙箱的基本URL的示例,請參閱編寫文件URL并保存文檔文件。
將文檔移動到iCloud存儲
以編程方式,您可以通過調(diào)用NSFileManager方法setUbiquitous:itemAtURL:destinationURL:error :.將文檔放在iCloud存儲中。此方法需要應(yīng)用程序沙箱(源URL)中文檔文件的文件URL和應(yīng)用程序的iCloud容器目錄中文檔文件的目標(biāo)文件URL。第一個參數(shù)取一個布爾值,應(yīng)為YES。
重要:您不應(yīng)該調(diào)用setUbiquitous:itemAtURL:destinationURL:error:從應(yīng)用程序的主線程,特別是如果文檔未關(guān)閉。因為該方法對指定的文件執(zhí)行協(xié)調(diào)的寫入操作,所以從主線程調(diào)用此方法可能會導(dǎo)致任何文件主持人監(jiān)視文件的死鎖。 (此外,在主線程上執(zhí)行的此方法可能需要不間斷的時間來完成)。而是調(diào)用在主線程隊列以外的調(diào)度隊列中運行的塊中的方法。調(diào)用完成后,您可以隨時發(fā)送主線程來更新應(yīng)用程序的其余數(shù)據(jù)結(jié)構(gòu)。
清單4-9中的方法說明了如何將文檔文件從應(yīng)用程序沙箱移動到iCloud存儲。在示例應(yīng)用程序中,當(dāng)用戶的首選存儲位置(iCloud或本地)更改時,將為應(yīng)用程序沙箱中的每個文檔文件調(diào)用此方法。這個方法大概有三個部分:
- 撰寫源URL和目標(biāo)URL。
- 在二級調(diào)度隊列中:調(diào)用setUbiquitous:itemAtURL:destinationURL:error:method并緩存結(jié)果,一個布爾值(成功),指示文檔文件是否成功移動到iCloud容器目錄。
- 在主調(diào)度隊列中:如果調(diào)用成功,請更新文檔的模型對象及其對象的呈現(xiàn);如果調(diào)用不成功,請記錄錯誤(否則處理它)。
清單4-9將文檔文件從本地存儲移動到iCloud存儲
- (void)moveFileToiCloud:(FileRepresentation *)fileToMove {
NSURL *sourceURL = fileToMove.url;
NSString *destinationFileName = fileToMove.fileName;
NSURL *destinationURL = [self.documentsDir URLByAppendingPathComponent:destinationFileName];
dispatch_queue_t q_default;
q_default = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(q_default, ^(void) {
NSFileManager *fileManager = [[[NSFileManager alloc] init] autorelease];
NSError *error = nil;
BOOL success = [fileManager setUbiquitous:YES itemAtURL:sourceURL
destinationURL:destinationURL error:&error];
dispatch_queue_t q_main = dispatch_get_main_queue();
dispatch_async(q_main, ^(void) {
if (success) {
FileRepresentation *fileRepresentation = [[FileRepresentation alloc]
initWithFileName:fileToMove.fileName url:destinationURL];
[_fileList removeObject:fileToMove];
[_fileList addObject:fileRepresentation];
NSLog(@"moved file to cloud: %@", fileRepresentation);
}
if (!success) {
NSLog(@"Couldn't move file to iCloud: %@", fileToMove);
}
});
});
}
從iCloud存儲中刪除文檔
要將文檔文件從iCloud容器目錄移動到應(yīng)用程序沙箱的Documents目錄,請按照將文檔移動到iCloud Storage中所述的相同過程,但切換源URL(現(xiàn)在是iCloud容器目錄中的文檔文件)和 目標(biāo)網(wǎng)址(現(xiàn)在應(yīng)用程序沙箱中的文檔文件)。 另外,setUbiquitous:itemAtURL:destinationURL:error:method的第一個參數(shù)現(xiàn)在應(yīng)該是NO。 清單4-10顯示了實現(xiàn)此過程的方法; 它被稱為iCloud容器目錄中的每個文件,將其移動到應(yīng)用程序沙箱。
清單4-10將文檔文件從iCloud存儲移動到本地存儲
- (void)moveFileToLocal:(FileRepresentation *)fileToMove {
NSURL *sourceURL = fileToMove.url;
NSString *destinationFileName = fileToMove.fileName;
NSURL *destinationURL = [self.documentsDir URLByAppendingPathComponent:destinationFileName];
dispatch_queue_t q_default;
q_default = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(q_default, ^(void) {
NSFileManager *fileManager = [[[NSFileManager alloc] init] autorelease];
NSError *error = nil;
BOOL success = [fileManager setUbiquitous:NO itemAtURL:sourceURL destinationURL:destinationURL
error:&error];
dispatch_queue_t q_main = dispatch_get_main_queue();
dispatch_async(q_main, ^(void) {
if (success) {
FileRepresentation *fileRepresentation = [[FileRepresentation alloc]
initWithFileName:fileToMove.fileName url:destinationURL];
[_fileList removeObject:fileToMove];
[_fileList addObject:fileRepresentation];
NSLog(@"moved file to local storage: %@", fileRepresentation);
}
if (!success) {
NSLog(@"Couldn't move file to local storage: %@", fileToMove);
}
});
});
}
監(jiān)控文檔狀態(tài)更改和處理錯誤
文檔可以在其運行時間內(nèi)通過不同的狀態(tài)。狀態(tài)可以告訴您文檔是否遇到錯誤,版本沖突或其他不正常的情況。 UIDocument聲明常量(類型為UIDocumentState)來表示文檔狀態(tài),并且當(dāng)更改是文檔的狀態(tài)發(fā)生時,使用這些常量之一設(shè)置documentState屬性。表4-1列出了狀態(tài)常數(shù)。
表4-1 UIDocumentState常量
| 文件狀態(tài)常數(shù) | 這是什么意思 |
|---|---|
| UIDocumentStateNormal | 該文件是開放的,并且沒有發(fā)生任何沖突或其他問題。 |
| UIDocumentStateClosed | 該文件已關(guān)閉。如果UIDocument無法打開文檔,則文檔處于此狀態(tài),在這種情況下,文檔屬性可能無效。 |
| UIDocumentStateInConflict | 文檔的版本有沖突。 |
| UIDocumentStateSavingError | 一個錯誤會阻止UIDocument保存文檔。 |
| UIDocumentStateEditingDisabled | 目前還不允許用戶編輯文檔。 |
當(dāng)文檔狀態(tài)發(fā)生變化時,UIDocument還會發(fā)布類型為UIDocumentStateChangedNotification的通知。您的應(yīng)用程序應(yīng)遵守此通知并作出適當(dāng)響應(yīng)。文檔視圖控制器的初始化方法是添加觀察者的好地方,如清單4-11所示。這種情況下的觀察者是視圖控制器。
清單4-11添加UIDocumentStateChangedNotification通知的觀察者
-(id)initWithFileURL:(NSURL*)url createNewFile:(BOOL)createNewFile {
NSString* nibName = [[UIDevice currentDevice] userInterfaceIdiom] ==
UIUserInterfaceIdiomPad ? @"DetailViewController_iPad" : @"DetailViewController_iPhone";
self = [super initWithNibName:nibName bundle:nil];
if (self) {
_document = [[ImageNotesDocument alloc] initWithFileURL:url];
// other code here....
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(documentStateChanged)
name:UIDocumentStateChangedNotification object:_document];
}
return self;
}
確保在類的dealloc方法中從通知中心刪除觀察者。
當(dāng)文檔的狀態(tài)發(fā)生變化時,UIDocument會發(fā)布UIDocumentStateChangedNotification通知,通知中心通過調(diào)用通知方法(在示例中為documentStateChanged)來發(fā)送。 在清單4-12中,觀察視圖控制器從documentState屬性獲取當(dāng)前狀態(tài)并對其進行評估。 如果狀態(tài)是UIDocumentStateEditingDisabled,它會隱藏鍵盤。 如果文檔的不同版本(UIDocumentStateInConflict)之間存在沖突,它將在文檔視圖的工具欄中顯示“顯示沖突”按鈕。 (有關(guān)處理文檔版本沖突的詳細信息,請參閱解決文檔版本沖突。)
清單4-12評估當(dāng)前文檔狀態(tài)
-(void)documentStateChanged {
UIDocumentState state = _document.documentState;
[_statusView setDocumentState:state];
if (state & UIDocumentStateEditingDisabled) {
[_textView resignFirstResponder];
}
if (state & UIDocumentStateInConflict) {
[self showConflictButton];
}
else {
[self hideConflictButton];
[self dismissModalViewControllerAnimated:YES];
}
}
通知處理方法還調(diào)用由私有視圖類實現(xiàn)的setDocumentState:方法。 如清單4-13所示,該方法會根據(jù)文檔狀態(tài)更改文檔視圖工具欄中的其他項目。
清單4-13更新文檔的用戶界面以反映其狀態(tài)
-(void)setDocumentState:(UIDocumentState)documentState {
if (documentState & UIDocumentStateSavingError) {
self.unsavedLabel.hidden = NO;
self.circleView.image = [UIImage imageNamed:@"Red"];
}
else {
self.unsavedLabel.hidden = YES;
if (documentState & UIDocumentStateInConflict) {
self.circleView.image = [UIImage imageNamed:@"Yellow"];
}
else {
self.circleView.image = [UIImage imageNamed:@"Green"];
}
}
}
如果無法保存文檔(UIDocumentStateSavingError),則視圖控制器將狀態(tài)指示器更改為紅色,并在其旁顯示未保存。如果有沖突的文檔版本,它會使?fàn)顟B(tài)指示燈變黃(這是前面提到的“顯示沖突”按鈕)。否則,狀態(tài)指示燈為綠色。
刪除文檔
就像您想允許用戶創(chuàng)建文檔一樣,您也希望讓他們刪除所選文檔。刪除文檔需要做三件事:
- 從存儲(從本地沙箱或iCloud容器目錄)中刪除文檔文件。
- 刪除用于表示內(nèi)存中的文檔數(shù)據(jù)的模型對象。
- 刪除文檔視圖中顯示的文檔數(shù)據(jù)。
當(dāng)您從存儲中刪除文檔時,您的代碼應(yīng)該近似UIDocument用于讀寫操作。它應(yīng)該在后臺隊列上異步執(zhí)行刪除,并且應(yīng)該使用文件協(xié)調(diào)。清單4-14說明了這個過程。它在后臺隊列上分派一個任務(wù),創(chuàng)建一個NSFileCoordinator對象,并調(diào)用coordinateWritingItemAtURL:options:error:byAccessor:方法。此方法的byAccessor塊調(diào)用NSFileManager方法來刪除文件removeItemAtURL:error :.
清單4-14刪除所選文檔
-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath {
NSMutableArray* fileList = nil;
if (indexPath.section == 0) {
fileList = self.localFileList;
}
else {
fileList = self.ubiquitousFileList;
}
NSURL* fileURL = [[fileList objectAtIndex:indexPath.row] url];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
NSFileCoordinator* fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
[fileCoordinator coordinateWritingItemAtURL:fileURL options:NSFileCoordinatorWritingForDeleting
error:nil byAccessor:^(NSURL* writingURL) {
NSFileManager* fileManager = [[NSFileManager alloc] init];
[fileManager removeItemAtURL:writingURL error:nil];
}];
});
[fileList removeObjectAtIndex:indexPath.row];
[tableView deleteRowsAtIndexPaths:[[NSArray alloc] initWithObjects:&indexPath count:1]
withRowAnimation:UITableViewRowAnimationLeft];
}
在此示例中,當(dāng)表視圖處于編輯模式時,當(dāng)用戶點擊“刪除”按鈕時,用戶觸發(fā)該方法的調(diào)用。
更改跟蹤和撤消操作
UIDocument類的無保存模型功能確保文檔數(shù)據(jù)以頻繁的間隔自動保存,減輕用戶需要顯式保存其文檔。 UIDocument實現(xiàn)了無節(jié)制模型的許多行為,但是基于文檔的應(yīng)用程序必須發(fā)揮自己的作用,才能使功能發(fā)揮作用。
UIKit如何自動保存文檔數(shù)據(jù)
用于文檔的UIKit框架實現(xiàn)的無節(jié)制模型有兩個主要部分:用于將文檔標(biāo)記為需要保存的機制以及框架檢查該標(biāo)志時的可變周期。定期地,UIKit調(diào)用UIDocument對象的hasUnsavedChanges方法并計算返回的值。如果值為YES,則將文檔數(shù)據(jù)保存到文檔文件。 hasUnsavedChanges值檢查之間的時間間隔根據(jù)幾個因素而變化,包括用戶輸入的速率。
基于文檔的應(yīng)用程序通過實現(xiàn)撤消和重做或跟蹤對文檔的更改來間接設(shè)置hasUnsavedChanges返回的值。更改跟蹤需要應(yīng)用程序調(diào)用updateChangeCount:方法,傳入UIDocumentChangeDone(UIDocumentChangeKind類型的常量)。當(dāng)應(yīng)用程序注冊一個撤消操作,然后發(fā)送撤消或重做消息到文檔的撤銷管理器時,UIDocument代表它調(diào)用updateChangeCount:。
因為給用戶撤消和重做更改的功能可以是一個區(qū)別的功能,所以建議大多數(shù)應(yīng)用程序使用該方法。
執(zhí)行撤消和重做
您可以通過遵循撤消體系結(jié)構(gòu)中的過程和建議,在應(yīng)用程序中執(zhí)行撤消和重做操作。請注意,UIDocument定義了一個undoManager屬性。您可以通過訪問此屬性獲取默認的NSUndoManager對象,也可以為其分配自己的NSUndoManager對象。 undo管理器必須通過屬性與UIDocument對象相關(guān)聯(lián),以便啟用更改跟蹤,從而自動保存文檔數(shù)據(jù)。
清單5-1說明了文本字段的撤消和重做的實現(xiàn)。
清單5-1為文本字段實現(xiàn)撤消和重做
- (void)textFieldDidEndEditing:(UITextField *)textField {
self.undoButton.enabled = YES;
self.redoButton.enabled = YES;
if (textField.tag == 1) {
[self setLocationText:textField.text];
}
// code for other text fields here....
}
- (void)setLocationText:(NSString *)newText {
NSString *currentText = _document.location;
if (newText != currentText) {
[_document.undoManager registerUndoWithTarget:self
selector:@selector(setLocationText:)
object:currentText];
_document.location = newText;
self.locationField.text = newText;
}
}
- (IBAction)handleUndo:(id)sender {
[_document.undoManager undo];
if (![_document.undoManager canUndo]) self.undoButton.enabled = NO;
}
- (IBAction)handleRedo:(id)sender {
[_document.undoManager redo];
if (![_document.undoManager canRedo]) self.redoButton.enabled = NO;
}
實施變更跟蹤
要實施更改跟蹤,而不是執(zhí)行撤消/重做,請在代碼中的適當(dāng)點調(diào)用UIDocument對象上的updateChangeCount:方法。 就像您注冊撤消動作一樣,通常情況下,您可以使用用戶剛剛輸入的數(shù)據(jù)更新文檔的模型對象。 傳入的參數(shù)應(yīng)該是一個UIDocumentChangeDone常量。
清單5-2顯示了如何在文本視圖中進行更改時調(diào)用的UITextViewDelegate方法中調(diào)用updateChangeCount:。
清單5-2更新文檔的更改計數(shù)
-(void)textViewDidChange:(UITextView *)textView {
_document.documentText = textView.text;
[_document updateChangeCount:UIDocumentChangeDone];
}
解決文件版本沖突
在iCloud世界中,當(dāng)用戶在多個設(shè)備或桌面系統(tǒng)上安裝了基于文檔的應(yīng)用程序時,同一文檔的不同版本之間可能會有沖突?;叵胍幌聭?yīng)用程序會更新本地容器目錄中的文檔文件,然后這些更改通常會立即發(fā)送到iCloud。但是如果這種傳播不是即時的呢?例如,您可以使用Mac OS X版本的應(yīng)用程序編輯文檔,但是您也使用iPad版本的應(yīng)用程序編輯了相同的文檔,而您在設(shè)備處于“飛行模式”時也是如此。當(dāng)您關(guān)閉飛行模式時,文檔的本地更改將傳輸?shù)絠Cloud。 iCloud注意到?jīng)_突并通知應(yīng)用程序。
學(xué)習(xí)文檔版本沖突
隨著監(jiān)控文檔狀態(tài)更改和處理錯誤的描述,您的應(yīng)用程序通過觀察UIDocumentStateChangedNotification通知來了解文檔版本沖突。如果documentState屬性更改為UIDocumentStateInConflict,則存在相同文檔的多個版本。該應(yīng)用程序負責(zé)在有或沒有用戶幫助的情況下盡快解決這些沖突。
您通過NSFileVersion類的兩個類方法了解文檔的沖突版本。 currentVersionOfItemAtURL:方法返回一個表示當(dāng)前文件的NSFileVersion對象;當(dāng)前文件由iCloud在某些基礎(chǔ)上選為當(dāng)前的“沖突獲勝者”,并且在所有設(shè)備上都相同。通過調(diào)用unresolvedConflictVersionsOfItemAtURL:方法,可以得到一個NSFileVersion對象的數(shù)組;這些對象稱為沖突版本,每個對象表示位于指定URL的文件的未解決的版本沖突。 NSFileVersion對象可以為您提供有助于解決沖突的信息,例如修改日期,本地化文檔名稱和保存計算機的本地化名稱。
解決文件版本沖突的策略
您的應(yīng)用程序可以按照解決文檔版本沖突的三種策略之一:
- 合并來自沖突版本的更改。
- 根據(jù)一些相關(guān)因素選擇其中一個文檔版本,例如最新修改日期的版本。
- 允許用戶查看文檔的沖突版本,并選擇要使用的版本。
哪種策略最適合使用取決于您的文檔數(shù)據(jù)。如果您可以合并不同文檔版本的內(nèi)容,而不引入矛盾元素,那么請遵循該策略?;蛘呷绻膽?yīng)用程序沒有遭受任何數(shù)據(jù)丟失,請選擇具有最新修改日期的文檔版本。
一般來說,您應(yīng)該嘗試解決沖突而不涉及用戶,但對于某些可能無法實現(xiàn)的應(yīng)用程序。如果應(yīng)用程序采用以用戶為中心的方法,則應(yīng)謹慎地通知用戶版本沖突,并公開啟動解決過程的按鈕或其他控件。示例:讓用戶選擇版本檢查允許用戶選擇要使用的文檔版本的應(yīng)用程序的代碼。
如何解決iOS文檔版本沖突?
當(dāng)您的應(yīng)用程序或其用戶通過選擇文檔版本來解決文檔版本沖突時,應(yīng)用程序應(yīng)完成以下步驟:
如果所選版本是沖突版本,請將當(dāng)前文檔文件替換為沖突版本文檔文件。
為此,請在表示版本的NSFileVersion對象上調(diào)用replaceItemAtURL:options:error:方法,傳遞文檔的當(dāng)前文件URL。如果所選版本是沖突版本,請還原文檔,以便在文檔文件中顯示新數(shù)據(jù)
為此,請調(diào)用UIDocument方法revertToContentsOfURL:completionHandler:在文檔對象上傳入文檔的當(dāng)前文件URL。將所有沖突版本與文檔的文件URL相關(guān)聯(lián)。
為此,請調(diào)用NSFileVersion類方法removeOtherVersionsOfItemAtURL:error :,傳遞文檔的文件URL。將每個沖突版本標(biāo)記為已解決,以便iOS不再作為沖突版本提高它。
為此,將表示沖突版本的每個NSFileVersion對象的已解析屬性設(shè)置為YES。這一步應(yīng)該始終如一地完成。刪除文檔的已解析版本。
對于您不再需要的任何版本,請調(diào)用NSFileVersion的removeAndReturnError:方法來回收該文件的存儲。刪除文檔修訂版本將保留在服務(wù)器上。
一個例子:讓用戶選擇版本
我們的基于文檔的應(yīng)用程序是一個簡單的文本編輯器。這樣的應(yīng)用程序難以在文檔的沖突版本中定位和合并文本差異,即使如此,生成的文檔也可能不是用戶想要的。應(yīng)用程序可以選擇最近修改日期的文檔版本,但是再次無法確定用戶想要的版本。在這種情況下,一個很好的解決沖突的策略是讓最熟悉文檔內(nèi)容的用戶選擇自己想要的版本。
您可能會從監(jiān)控文檔狀態(tài)更改和處理錯誤中記住清單6-1中所示的代碼。此代碼顯示文檔視圖控制器的方法,處理UIDocument在文檔狀態(tài)發(fā)生更改時發(fā)布的UIDocumentStateChangedNotification通知。如果新文檔狀態(tài)為UIDocumentStateInConflict,則視圖控制器在自定義狀態(tài)視圖中顯示“解決沖突”按鈕。 (它還將狀態(tài)指示燈的顏色設(shè)置為紅色。)
清單6-1檢測文檔版本中的沖突
-(void)documentStateChanged {
UIDocumentState state = _document.documentState;
[_statusView setDocumentState:state];
if (state & UIDocumentStateEditingDisabled) {
[_textView resignFirstResponder];
}
if (state & UIDocumentStateInConflict) {
[self showConflictButton]; // <------ Shows "Resolve Conflicts" button
}
else {
[self hideConflictButton];
[self dismissModalViewControllerAnimated:YES];
}
}
當(dāng)用戶點擊按鈕時,UIKit將調(diào)用清單6-2中的方法。 此方法以模式顯示自定義沖突解決程序視圖控制器的視圖。
清單6-2顯示用于解決文檔版本沖突的用戶界面
-(void)conflictButtonPushed
{
ConflictResolverViewController* conflictResolver = [[ConflictResolverViewController alloc]
initWithURL:_document.fileURL delegate:self];
[self presentViewController:conflictResolver animated:YES completion:nil];
[conflictResolver release];
}
當(dāng)用戶點擊按鈕時,UIKit將調(diào)用清單6-2中的方法。 此方法以模式顯示自定義沖突解決程序視圖控制器的視圖。
清單6-2顯示用于解決文檔版本沖突的用戶界面…
-(void)conflictResolver:(ConflictResolverViewController *)conflictResolver
didResolveWithFileVersion:(NSFileVersion *)fileVersion {
[self dismissViewControllerAnimated:YES completion:nil];
[fileVersion replaceItemAtURL:_document.fileURL options:0 error:nil];
[NSFileVersion removeOtherVersionsOfItemAtURL:_document.fileURL error:nil];
[_document revertToContentsOfURL:_document.fileURL completionHandler:nil];
NSArray* conflictVersions = [NSFileVersion unresolvedConflictVersionsOfItemAtURL:_document.fileURL];
for (NSFileVersion* fileVersion in conflictVersions) {
fileVersion.resolved = YES;
}
}
-(void)conflictResolverDidResolveWithCurrentVersion:(ConflictResolverViewController*)conflictResolver {
[self dismissViewControllerAnimated:YES completion:nil];
[NSFileVersion removeOtherVersionsOfItemAtURL:_document.fileURL error:nil];
NSArray* conflictVersions = [NSFileVersion unresolvedConflictVersionsOfItemAtURL:_document.fileURL];
for (NSFileVersion* fileVersion in conflictVersions) {
fileVersion.resolved = YES;
}
}
這些方法說明了如何解決iOS文檔版本沖突的步驟。如果選擇的文檔是沖突版本,則代理將在傳入的NSFileVersion對象中調(diào)用replaceItemAtURL:options:error:將iCloud容器目錄中的文檔文件替換為所選文檔。然后委托枚舉包含表示文檔的所有沖突版本的NSFileVersion對象的數(shù)組,并將每個對象的已解析屬性設(shè)置為YES。然后,它要求NSFileVersion刪除與文檔的文件URL相關(guān)聯(lián)的文檔的所有其他沖突版本,并調(diào)用revertToContentsOfURL:completionHandler:將顯示的文檔還原到文檔文件的新內(nèi)容。
選擇當(dāng)前文檔文件時調(diào)用的第二個委托方法要簡單得多。它將表示沖突版本的所有NSFileVersion對象的已解析屬性設(shè)置為YES,并刪除與文檔文件URL相關(guān)聯(lián)的所有沖突版本。