Document-Based App Programming Guide for iOS (二)

創(chuàng)建自定義文檔對(duì)象

基于文檔的應(yīng)用程序必須具有代表和管理文檔數(shù)據(jù)的UIDocument子類(lèi)的實(shí)例。本章討論了覆蓋大多數(shù)應(yīng)用程序所需的方法,并提供了覆蓋其他方法的建議。對(duì)于核心重寫(xiě)點(diǎn),loadFromContents:ofType:error:和contentsForType:error:methods-examples被給定為NSData和NSFileWrapper作為從文件讀取和寫(xiě)入文檔數(shù)據(jù)的類(lèi)型。將文檔數(shù)據(jù)存儲(chǔ)在文件包中進(jìn)一步說(shuō)明了如何對(duì)文檔數(shù)據(jù)使用文件包裝對(duì)象。

除了本章討論的UIDocument以外,您可以覆蓋UIDocument的方法,以便為特定目的讀取和寫(xiě)入文檔數(shù)據(jù),例如逐步寫(xiě)入和讀取文檔數(shù)據(jù)。然而,這些更高級(jí)的覆蓋具有更復(fù)雜的要求,如果可能的話應(yīng)該避免。有關(guān)這些覆蓋的討論,請(qǐng)參閱UIDocument類(lèi)參考。

聲明文檔類(lèi)接口

在Xcode中,將新的Objective-C源文件和頭文件添加到您的項(xiàng)目中,并將其正確命名(建議:將“文檔”作為名稱(chēng))。在頭文件中,將超類(lèi)更改為UIDocument,并添加屬性以保存文檔數(shù)據(jù)。在清單3-1中,文檔數(shù)據(jù)是純文本,因此NSString屬性是保存它所需要的。 (文本將被轉(zhuǎn)換為寫(xiě)入文檔文件的NSData對(duì)象。)

清單3-1文檔子類(lèi)聲明(NSData)

@interface MyDocument : UIDocument {
}
@property(nonatomic, strong) NSString *documentText;
@end

清單3-2說(shuō)明了另一個(gè)使用NSFileWrapper對(duì)象作為數(shù)據(jù)表示類(lèi)型的應(yīng)用程序的聲明集。 (本章中的代碼示例在兩個(gè)應(yīng)用程序之間交替)。不僅有一個(gè)屬性來(lái)保存文件包裝對(duì)象,還有一些屬性可以保存表示文件包的文本和圖像組件。

清單3-2文檔子類(lèi)聲明(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é)議。文檔對(duì)象的視圖控制器使自己成為文檔對(duì)象的代理(并采用協(xié)議),以便它可以通過(guò)文檔文件的修改(通過(guò)noteDocumentContentsUpdated:messages)進(jìn)行通知。清單3-4顯示了發(fā)送noteDocumentContentsUpdated:消息的時(shí)間和方式。

加載文檔數(shù)據(jù)

當(dāng)應(yīng)用程序打開(kāi)文檔(按照用戶的請(qǐng)求)時(shí),UIDocument將讀取文檔文件的內(nèi)容,并調(diào)用loadFromContents:ofType:error:method,傳遞封裝文檔數(shù)據(jù)的對(duì)象。該對(duì)象可以是NSData對(duì)象或NSFileWrapper對(duì)象。在覆蓋該方法時(shí),從傳入對(duì)象的內(nèi)容中初始化文檔的內(nèi)部數(shù)據(jù)結(jié)構(gòu)(即其模型對(duì)象)。

清單3-3中的示例從傳入的NSData對(duì)象創(chuàng)建一個(gè)字符串,并將其分配給documentText屬性。它還通過(guò)調(diào)用協(xié)議方法通知其代表(在這種情況下,文檔的視圖控制器)更新的文檔內(nèi)容。這個(gè)委托消息背后的動(dòng)機(jī)是loadFromContents:ofType:error:method不僅是打開(kāi)文檔的結(jié)果,也是因?yàn)閕Cloud更新和恢復(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;
}

如果您有多個(gè)文檔類(lèi)型,請(qǐng)檢查typeName參數(shù); 不同的文檔類(lèi)型可能會(huì)影響代碼如何處理文檔數(shù)據(jù)對(duì)象。 如果您的代碼遇到阻止其加載文檔數(shù)據(jù)的錯(cuò)誤,請(qǐng)返回NO; 可選地,您可以通過(guò)引用返回描述錯(cuò)誤的NSError對(duì)象。

清單3-4中的示例以NSFileWrapper對(duì)象的形式處理文檔數(shù)據(jù)。 它只是將此對(duì)象分配給其屬性。

清單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;
}

在此代碼中,方法實(shí)現(xiàn)不會(huì)提取文件包裝器的文本和圖像組件,并將其分配給其屬性。 這在文本和圖像屬性的getter方法中做得很懶。

提供文件數(shù)據(jù)快照

當(dāng)文檔關(guān)閉或自動(dòng)保存文檔時(shí),UIDocument會(huì)將文檔對(duì)象發(fā)送一個(gè)contentsForType:error:message。 您必須重寫(xiě)此方法以將文檔數(shù)據(jù)的快照返回到UIDocument,然后將其寫(xiě)入文檔文件。 清單3-5給出了以NSData對(duì)象的形式返回文檔數(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對(duì)象之前,將為其分配一個(gè)空字符串。

清單3-6顯示了返回NSFileWrapper對(duì)象的相同方法的實(shí)現(xiàn)。 基本上,如果頂級(jí)(目錄)文件包裝對(duì)象不存在,則代碼創(chuàng)建它; 并且如果兩個(gè)包含(常規(guī)文件)文件包裝對(duì)象不存在,則代碼將從文本和圖像屬性的值創(chuàng)建它們。 然后,它將頂級(jí)文件包裝器返回到UIDocument,該文件在文件系統(tǒng)中創(chuàng)建一個(gè)文件包。 請(qǐng)參閱將文檔數(shù)據(jù)存儲(chǔ)在文件包中,以獲取有關(guān)文件包和文檔的更詳細(xì)說(shuō)明。

清單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ù)存儲(chǔ)在文件包中

一個(gè)文件包的內(nèi)部結(jié)構(gòu)體現(xiàn)在NSFileWrapper類(lèi)的方法中。 文件包裝器是文件系統(tǒng)節(jié)點(diǎn)的運(yùn)行時(shí)表示,它是目錄,常規(guī)文件或符號(hào)鏈接。 如圖3-1所示,文件包是文件系統(tǒng)節(jié)點(diǎn),通常是目錄及其內(nèi)容,操作系統(tǒng)將其視為單個(gè)不透明實(shí)體。 它在概念上與捆綁類(lèi)似。

圖3-1文件包的結(jié)構(gòu)

document_file_package_2x.png

您可以通過(guò)創(chuàng)建一個(gè)頂級(jí)目錄文件包裝器來(lái)編程文件包,然后向該容器添加常規(guī)文件和子目錄,每個(gè)文件和子目錄由其他NSFileWrapper對(duì)象表示。 頂層目錄中的文件包裝應(yīng)該具有與它們相關(guān)聯(lián)的首選名稱(chēng)。

考慮到這個(gè)簡(jiǎn)要概述,請(qǐng)?jiān)俅尾榭辞鍐?-6中的contentsForType:error:方法中的以下代碼行。 在此方法中創(chuàng)建的文件包具有兩個(gè)組件,一個(gè)文本文件和一個(gè)圖像文件。 (圖片文件包裝器的創(chuàng)建不會(huì)顯示在代碼段中。)

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)建一個(gè)頂級(jí)目錄(如果不存在)。如果文本文件不存在文件包裝器,它將從text屬性的字符串內(nèi)容中創(chuàng)建一個(gè)。它給這個(gè)文件包裝器一個(gè)首選文件名,然后將其添加到頂級(jí)目錄文件包裝器。

有關(guān)NSFileWrapper的更多信息,請(qǐng)參閱NSFileWrapper類(lèi)參考;另請(qǐng)參閱導(dǎo)出文檔UTI的文檔文件包所需的Info.plist屬性。

其他方法可以覆蓋你可能做的

許多基于文檔的應(yīng)用程序可能想要做的其他一些UIDocument覆蓋:

disableEditing ... enableEditing-UIDocument在不安全的情況下調(diào)用第一種方法,以便用戶更改文檔內(nèi)容,例如當(dāng)iCloud有更新或還原操作正在進(jìn)行時(shí)。您可以在此期間實(shí)施此方法以防止編輯。再次編輯變得安全時(shí),UIDocument調(diào)用第二種方法。
注意:作為這些覆蓋的替代方法,您可以觀察文檔狀態(tài)更改時(shí)發(fā)布的通知,如果新文檔狀態(tài)為UIDocumentStateEditingDisabled,則會(huì)在文檔狀態(tài)再次更改之前防止編輯。有關(guān)此主題的更多信息,請(qǐng)參閱監(jiān)控文檔狀態(tài)更改和處理錯(cuò)誤。
savingFileType - 默認(rèn)情況下,此方法返回fileType屬性的值。如果由于任何原因而將當(dāng)前文檔保存在不同的文件類(lèi)型下,可以覆蓋此方法以返回替換文件類(lèi)型UTI。一個(gè)例子(來(lái)自Mac OS X)是將圖像添加到RTF文件時(shí),應(yīng)將其保存為RTFD文件包。

管理文件的生命周期

一個(gè)文件經(jīng)歷了一個(gè)典型的生命周期。基于文檔的應(yīng)用程序負(fù)責(zé)管理其在該周期中的進(jìn)展。從以下列表可以看出,大多數(shù)這些生命周期事件是由用戶發(fā)起的:

  • 用戶首先創(chuàng)建一個(gè)文檔。
  • 用戶打開(kāi)現(xiàn)有文檔,應(yīng)用程序?qū)⑵滹@示在文檔的視圖或視圖中。
  • 用戶編輯文檔。
  • 用戶可能會(huì)要求將文檔放在iCloud存儲(chǔ)中,或者可以請(qǐng)求從iCloud存儲(chǔ)中刪除文檔。
  • 在編輯,保存或其他操作期間,可能會(huì)發(fā)生錯(cuò)誤或沖突;應(yīng)用程序應(yīng)該了解這些錯(cuò)誤和沖突,并嘗試處理它們或通知用戶。
  • 用戶關(guān)閉選定的文檔。
  • 用戶刪除現(xiàn)有文檔。
    以下部分將討論基于文檔的應(yīng)用程序必須為這些生命周期操作完成的過(guò)程。

設(shè)置文檔文件的首選存儲(chǔ)位置

應(yīng)用程序的所有文檔都存儲(chǔ)在本地沙箱或iCloud容器目錄中。用戶不能選擇單獨(dú)的文檔存儲(chǔ)在iCloud中。

應(yīng)用程序首次在設(shè)備上啟動(dòng)應(yīng)用程序時(shí),應(yīng)執(zhí)行以下操作:

  • 如果iCloud未配置,請(qǐng)告知用戶,如果要在其中保存文件,則需要配置iCloud。
  • 如果iCloud已配置但未啟用應(yīng)用程序,請(qǐng)?jiān)儐?wèn)用戶是否要啟用iCloud,換句話說(shuō),詢問(wèn)他們是否希望將所有文檔保存到iCloud。將響應(yīng)存儲(chǔ)為用戶偏好。

基于此首選項(xiàng),應(yīng)用程序?qū)⑽臋n文件寫(xiě)入本地應(yīng)用程序沙箱或iCloud容器目錄。 (有關(guān)詳細(xì)信息,請(qǐng)參閱將文檔移入和移出iCloud Storage。)應(yīng)用程序應(yīng)在“設(shè)置”應(yīng)用程序中公開(kāi)交換機(jī),以使用戶能夠在本地存儲(chǔ)和iCloud存儲(chǔ)之間移動(dòng)文檔。

創(chuàng)建新文檔

文檔對(duì)象(即,您的自定義UIDocument子類(lèi)的實(shí)例)必須具有將文檔文件定位到本地應(yīng)用程序沙箱或iCloud容器目錄中的文件URL,取決于用戶的偏好。另外,一個(gè)新的文件可以給一個(gè)名字。以下討論與文件URL,文檔名稱(chēng)和創(chuàng)建新文檔相關(guān)的準(zhǔn)則和過(guò)程。

文件文件名與文件名稱(chēng)

UIDocument類(lèi)假定文檔的文件名與文檔名稱(chēng)(也稱(chēng)為顯示名稱(chēng))之間的對(duì)應(yīng)關(guān)系。默認(rèn)情況下,UIDocument將文件名存儲(chǔ)為localizedName屬性的值。但是,當(dāng)應(yīng)用程序創(chuàng)建新文檔時(shí),應(yīng)用程序不應(yīng)要求用戶提供文檔名稱(chēng)或顯示名稱(chēng)。

對(duì)于您的應(yīng)用程序,您應(yīng)該制定一些自動(dòng)生成新文檔的文件名的約定。一些建議是:

  • 為每個(gè)文檔生成UUID(通用唯一標(biāo)識(shí)符),可選地使用應(yīng)用程序特定的前綴。
  • 為每個(gè)文檔生成時(shí)間戳(日期和時(shí)間),可選地使用應(yīng)用程序特定的前綴。

使用順序編號(hào)系統(tǒng),例如:“注1”,“注2”等。
對(duì)于文檔(顯示)名稱(chēng),如果這是有意義的(例如使用“Notes 1”),則可能最初使用文檔文件名。或者,如果文檔包含文本,并且用戶在文檔中輸入一些文本,則可以使用第一行(或第一行的某些部分)作為顯示名稱(chēng)。您的應(yīng)用程序可以在用戶創(chuàng)建文檔之后給用戶一些自定義文檔名稱(chēng)的方法。

編寫(xiě)文件URL并保存文檔文件

您無(wú)法創(chuàng)建沒(méi)有有效的文件URL的文檔對(duì)象。文件URL有三個(gè)部分:文檔目錄在用戶首選文檔位置的路徑,文檔文件名和文檔文件的擴(kuò)展名。您可以通過(guò)文檔文件名與文檔名稱(chēng)中的方法獲取表示本地應(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;
}

文件擴(kuò)展名必須是您為文檔類(lèi)型指定的擴(kuò)展名(請(qǐng)參閱創(chuàng)建和配置項(xiàng)目)。 您可以聲明一個(gè)全局字符串來(lái)表示擴(kuò)展名。 例如:

static NSString *FileExtension = @"imageNotes”;

文檔URL的最后一部分是文件名組件。當(dāng)文檔文件名與文檔名稱(chēng)解釋時(shí),應(yīng)用程序最初應(yīng)根據(jù)對(duì)應(yīng)用程序有意義的約定生成文檔文件名。該生成的文件名可以用作文檔名稱(chēng),也可以將第一行(或其一部分)用作文檔名稱(chēng)。該應(yīng)用程序可以給用戶在創(chuàng)建文檔對(duì)象后自定義文檔名稱(chēng)的選項(xiàng)。

連接基本URL,文檔文件名和文件擴(kuò)展名后,您可以分配一個(gè)自定義UIDocument子類(lèi)的實(shí)例,并使用initWithFileURL:方法初始化它,傳遞構(gòu)造的文件URL。創(chuàng)建新文檔的最后一步是將其保存到首選文檔存儲(chǔ)位置(即使此時(shí)沒(méi)有內(nèi)容)。如通過(guò)設(shè)置文檔文件的首選存儲(chǔ)位置所示,您可以通過(guò)調(diào)用文檔對(duì)象上的saveToURL:forSaveOperation:completionHandler:method來(lái)執(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ù)告訴您操作是否成功。如果確實(shí)成功,則此代碼將文檔文本分配給顯示文檔內(nèi)容的文本視圖的text屬性。

注意:如果要將新文檔保存到應(yīng)用程序的iCloud容器目錄中,建議首先將其保存在本地,然后調(diào)用NSFileManager方法setUbiquitous:itemAtURL:destinationURL:error:將文檔文件移動(dòng)到iCloud存儲(chǔ)。 (可以在saveToURL的完成處理程序中進(jìn)行此調(diào)用:forSaveOperation:completionHandler:method。)有關(guān)更多信息,請(qǐng)參閱將文檔移動(dòng)到iCloud Storage和iCloud Storage。

打開(kāi)和關(guān)閉文檔

打開(kāi)文件可能乍看起來(lái)似乎是一個(gè)相當(dāng)簡(jiǎn)單的過(guò)程。您的應(yīng)用程序掃描其文檔目錄中的文件的文件的文件擴(kuò)展名,并將這些文檔提供給用戶進(jìn)行選擇。然而,當(dāng)iCloud存儲(chǔ)被考慮在內(nèi)時(shí),事情會(huì)變得更加復(fù)雜。應(yīng)用程序的文檔可能位于應(yīng)用程序沙箱的Documents目錄中,也可能位于iCloud容器目錄的Documents目錄中。

發(fā)現(xiàn)申請(qǐng)文件

要獲取iCloud存儲(chǔ)中應(yīng)用程序文檔的列表,請(qǐng)運(yùn)行元數(shù)據(jù)查詢。查詢是NSMetadataQuery類(lèi)的一個(gè)實(shí)例。創(chuàng)建一個(gè)NSMetadataQuery對(duì)象后,給它一個(gè)范圍和一個(gè)謂詞。對(duì)于iCloud存儲(chǔ),范圍應(yīng)為NSMetadataQueryUbiquitousDocumentsScope。謂詞是一個(gè)NSPredicate對(duì)象,在這種情況下,限制文件擴(kuò)展名的搜索。在開(kāi)始運(yùn)行查詢之前,請(qǐng)注冊(cè)以觀察NSMetadataQueryDidFinishGatheringNotification和NSMetadataQueryDidUpdateNotification通知。接受傳遞這些通知的方法處理查詢的結(jié)果。

清單4-3說(shuō)明了如何設(shè)置和運(yùn)行元數(shù)據(jù)查詢以獲取iCloud移動(dòng)容器中的應(yīng)用程序文檔列表。該方法首先測(cè)試用戶對(duì)文檔(documentsInCloud屬性)的首選存儲(chǔ)位置。如果該位置是移動(dòng)容器,則它運(yùn)行元數(shù)據(jù)查詢。如果位置是應(yīng)用程序沙箱,它會(huì)遍歷應(yīng)用程序的Documents目錄的內(nèi)容,以獲取所有本地文檔文件的名稱(chēng)和位置。

清單4-3獲取存儲(chǔ)在本地和iCloud存儲(chǔ)中的文檔的位置

-(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]];
        }
    }
}

在這個(gè)例子中,謂詞格式是@“%K LIKE'* .txt'”,這意味著返回所有文件名(NSMetadataItemFSNameKey鍵),擴(kuò)展名為txt,這是該應(yīng)用程序文檔文件的文件擴(kuò)展名。

初始查詢結(jié)束后,如果有后續(xù)更新,則再次調(diào)用清單4-3(fileListReceived)中指定的通知方法。 清單4-4顯示了該方法的實(shí)現(xiàn)。 如果查詢更新在用戶進(jìn)行選擇后到達(dá),則代碼還會(huì)跟蹤當(dāng)前選擇。

清單4-4收集有關(guān)iCloud存儲(chǔ)中的文檔的信息

-(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)在具有一個(gè)定制模型對(duì)象的數(shù)組(_fileList),該對(duì)象封裝了每個(gè)應(yīng)用程序文檔的名稱(chēng)和文件URL。 (FileRepresentation是這些對(duì)象的自定義類(lèi)。)根視圖控制器使用文檔名稱(chēng)填充一個(gè)簡(jiǎn)單表視圖

注意:您應(yīng)該將元數(shù)據(jù)查詢僅在您的應(yīng)用程序處于前臺(tái)時(shí)運(yùn)行。當(dāng)應(yīng)用程序移動(dòng)到后臺(tái)時(shí),您應(yīng)該停止查詢。

從iCloud下載文件文件

運(yùn)行元數(shù)據(jù)查詢以了解應(yīng)用程序的iCloud文檔時(shí),查詢結(jié)果將是文檔文件的占位符項(xiàng)(NSMetadataItem對(duì)象)。這些項(xiàng)目包含有關(guān)文件的元數(shù)據(jù),例如其URL及其修改日期。文檔文件不在iCloud容器目錄中。

直到發(fā)生以下情況之一才會(huì)下載文檔的實(shí)際數(shù)據(jù):

  • 您的應(yīng)用程序嘗試打開(kāi)或訪問(wèn)該文件,例如通過(guò)調(diào)用openWithCompletionHandler:。
  • 您的應(yīng)用程序調(diào)用NSFileManager方法startDownloadingUbiquitousItemAtURL:錯(cuò)誤:明確下載數(shù)據(jù)。
    因?yàn)閺膇Cloud下載大型文件可能會(huì)導(dǎo)致顯示文檔數(shù)據(jù)的可察覺(jué)的延遲,您應(yīng)該向用戶指出下載已經(jīng)開(kāi)始(例如,顯示“加載”或“更新”),并且該文件當(dāng)前不是無(wú)障礙。下載完成后,請(qǐng)刪除該指示。

注意:與文檔文件的URL相關(guān)聯(lián)的NSURLUbiquitousItemIsDownloadedKey會(huì)告訴您文檔當(dāng)前的下載狀態(tài)。您可以使用NSURL類(lèi)的getResourceValue:forKey:error:method來(lái)檢索此鍵的值。其他鍵也可以告訴你有關(guān)文件的上傳和下載狀態(tài)的相關(guān)信息。有關(guān)更多信息,請(qǐng)參閱NSURL類(lèi)參考。

打開(kāi)文件

基于示例文檔的應(yīng)用程序在表視圖中列出已知文檔。當(dāng)用戶點(diǎn)擊列出的文檔打開(kāi)它時(shí),UITableView調(diào)用其委托的tableView:didSelectRowAtIndexPath:方法。清單4-5所示的這種方法的實(shí)現(xiàn)是導(dǎo)航模式的典型:根視圖控制器按順序分配下一個(gè)視圖控制器 - 在這種情況下,視圖控制器顯示文檔數(shù)據(jù),并使用基本數(shù)據(jù) - 在這種情況下,文件的文件URL。根據(jù)設(shè)備成語(yǔ)是iPad還是iPhone(或iPhone touch),根視圖控制器將視圖控制器添加到拆分視圖或?qū)⑵渫扑偷綄?dǎo)航控制器的堆棧上。

清單4-5響應(yīng)打開(kāi)文檔的請(qǐ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子類(lèi)的實(shí)例,并通過(guò)調(diào)用initWithFileURL:方法來(lái)初始化它,傳遞文件URL。 它將新創(chuàng)建的文檔對(duì)象分配給文檔屬性。

打開(kāi)文檔的最后一步是調(diào)用UIDocument對(duì)象上的openWithCompletionHandler:方法; 我們的示例應(yīng)用程序中的文檔視圖控制器在viewWillAppear中調(diào)用此方法,如清單4-6所示。 代碼檢查文檔狀態(tài)以驗(yàn)證文檔在嘗試打開(kāi)文檔之前是否已關(guān)閉 - 無(wú)需打開(kāi)已打開(kāi)的文檔。

清單4-6打開(kāi)文檔

-(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)用時(shí),UIDocument從文檔文件讀取數(shù)據(jù),文檔對(duì)象本身從數(shù)據(jù)中創(chuàng)建其模型對(duì)象。在此操作順序結(jié)束時(shí),執(zhí)行openWithCompletionHandler:方法的完成處理程序。雖然示例中的視圖控制器不實(shí)現(xiàn)完成塊,但是完成處理程序有時(shí)用于將文檔數(shù)據(jù)分配給文檔的視圖或視圖進(jìn)行顯示。 (要回顧什么DetailViewController而不是更新文檔視圖,請(qǐng)參閱清單3-4及隨附的文本。)

關(guān)閉文件

要關(guān)閉文檔,請(qǐng)向文檔對(duì)象發(fā)送一個(gè)closeWithCompletionHandler:方法。如果需要,該方法保存文檔數(shù)據(jù),然后在其唯一參數(shù)中執(zhí)行完成處理程序。

關(guān)閉文檔的好時(shí)機(jī)是當(dāng)文檔的視圖控制器被關(guān)閉時(shí),例如當(dāng)用戶點(diǎn)擊后退按鈕時(shí)。在視圖控制器的視圖消失之前,將調(diào)用viewWillDisappear:方法。您的視圖控制器子類(lèi)可以覆蓋此方法,以便調(diào)用closeWithCompletionHandler:在文檔對(duì)象上,如清單4-7所示。

清單4-7關(guān)閉文檔

-(void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    [self.document closeWithCompletionHandler:nil];
}

將文檔移動(dòng)到iCloud Storage和

如設(shè)置文檔文件的首選存儲(chǔ)位置所述,應(yīng)用程序應(yīng)該向其用戶提供在本地文件系統(tǒng)(應(yīng)用程序沙箱)或iCloud(容器目錄)中存儲(chǔ)所有文檔的選項(xiàng)。它將此選項(xiàng)作為用戶首選項(xiàng)存儲(chǔ),并在保存和打開(kāi)文檔時(shí)引用此首選項(xiàng)。當(dāng)用戶更改首選項(xiàng)時(shí),應(yīng)用程序應(yīng)將應(yīng)用程序沙箱中的所有文檔文件移動(dòng)到iCloud,或者根據(jù)更改的性質(zhì)將所有文件移動(dòng)到另一個(gè)方向。

獲取iCloud容器目錄的位置

將文檔文件從本地存儲(chǔ)移動(dòng)到iCloud容器目錄的Documents子目錄時(shí),其文件名不變。文件URL路徑中唯一不同的部分是文檔導(dǎo)出的部分。要獲得該路徑的一部分,您需要調(diào)用NSFileManager的URLForUbiquityContainerIdentifier:方法。大多數(shù)情況下,您將無(wú)法通過(guò)此方法獲取應(yīng)用程序的默認(rèn)容器目錄。如果您的應(yīng)用程序支持多個(gè)容器,則可以通過(guò)傳遞具有相應(yīng)iCloud容器標(biāo)識(shí)符的字符串來(lái)顯式地請(qǐng)求容器,該容器標(biāo)識(shí)符是您的團(tuán)隊(duì)ID和應(yīng)用程序包ID的連接,以期間隔開(kāi)。這些容器標(biāo)識(shí)符字符串與您在Xcode中的應(yīng)用程序目標(biāo)“摘要”視圖的“標(biāo)識(shí)符”字段中指定的相同。為每個(gè)應(yīng)用程序的容器標(biāo)識(shí)符聲明一個(gè)字符串常量是個(gè)好主意,如下例所示:

static NSString *UbiquityContainerIdentifier = @"A93A5CM278.com.acme.document.billabong”;

清單4-8中的兩個(gè)方法可以獲得iCloud容器標(biāo)識(shí)符,并附加“/ Documents”。

清單4-8獲取iCloud容器目錄URL

-(NSURL*)ubiquitousContainerURL {

return [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];

}

-(NSURL*)ubiquitousDocumentsDirectoryURL {

return [[self ubiquitousContainerURL] URLByAppendingPathComponent:@"Documents"];

}

注意:有關(guān)獲取應(yīng)用程序沙箱的基本URL的示例,請(qǐng)參閱編寫(xiě)文件URL并保存文檔文件。

將文檔移動(dòng)到iCloud存儲(chǔ)

以編程方式,您可以通過(guò)調(diào)用NSFileManager方法setUbiquitous:itemAtURL:destinationURL:error :.將文檔放在iCloud存儲(chǔ)中。此方法需要應(yīng)用程序沙箱(源URL)中文檔文件的文件URL和應(yīng)用程序的iCloud容器目錄中文檔文件的目標(biāo)文件URL。第一個(gè)參數(shù)取一個(gè)布爾值,應(yīng)為YES。

重要:您不應(yīng)該調(diào)用setUbiquitous:itemAtURL:destinationURL:error:從應(yīng)用程序的主線程,特別是如果文檔未關(guān)閉。因?yàn)樵摲椒▽?duì)指定的文件執(zhí)行協(xié)調(diào)的寫(xiě)入操作,所以從主線程調(diào)用此方法可能會(huì)導(dǎo)致任何文件主持人監(jiān)視文件的死鎖。 (此外,在主線程上執(zhí)行的此方法可能需要不間斷的時(shí)間來(lái)完成)。而是調(diào)用在主線程隊(duì)列以外的調(diào)度隊(duì)列中運(yùn)行的塊中的方法。調(diào)用完成后,您可以隨時(shí)發(fā)送主線程來(lái)更新應(yīng)用程序的其余數(shù)據(jù)結(jié)構(gòu)。
清單4-9中的方法說(shuō)明了如何將文檔文件從應(yīng)用程序沙箱移動(dòng)到iCloud存儲(chǔ)。在示例應(yīng)用程序中,當(dāng)用戶的首選存儲(chǔ)位置(iCloud或本地)更改時(shí),將為應(yīng)用程序沙箱中的每個(gè)文檔文件調(diào)用此方法。這個(gè)方法大概有三個(gè)部分:

  • 撰寫(xiě)源URL和目標(biāo)URL。
  • 在二級(jí)調(diào)度隊(duì)列中:調(diào)用setUbiquitous:itemAtURL:destinationURL:error:method并緩存結(jié)果,一個(gè)布爾值(成功),指示文檔文件是否成功移動(dòng)到iCloud容器目錄。
  • 在主調(diào)度隊(duì)列中:如果調(diào)用成功,請(qǐng)更新文檔的模型對(duì)象及其對(duì)象的呈現(xiàn);如果調(diào)用不成功,請(qǐng)記錄錯(cuò)誤(否則處理它)。
    清單4-9將文檔文件從本地存儲(chǔ)移動(dòng)到iCloud存儲(chǔ)
- (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存儲(chǔ)中刪除文檔

要將文檔文件從iCloud容器目錄移動(dòng)到應(yīng)用程序沙箱的Documents目錄,請(qǐng)按照將文檔移動(dòng)到iCloud Storage中所述的相同過(guò)程,但切換源URL(現(xiàn)在是iCloud容器目錄中的文檔文件)和 目標(biāo)網(wǎng)址(現(xiàn)在應(yīng)用程序沙箱中的文檔文件)。 另外,setUbiquitous:itemAtURL:destinationURL:error:method的第一個(gè)參數(shù)現(xiàn)在應(yīng)該是NO。 清單4-10顯示了實(shí)現(xiàn)此過(guò)程的方法; 它被稱(chēng)為iCloud容器目錄中的每個(gè)文件,將其移動(dòng)到應(yīng)用程序沙箱。

清單4-10將文檔文件從iCloud存儲(chǔ)移動(dòng)到本地存儲(chǔ)

- (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)更改和處理錯(cuò)誤

文檔可以在其運(yùn)行時(shí)間內(nèi)通過(guò)不同的狀態(tài)。狀態(tài)可以告訴您文檔是否遇到錯(cuò)誤,版本沖突或其他不正常的情況。 UIDocument聲明常量(類(lèi)型為UIDocumentState)來(lái)表示文檔狀態(tài),并且當(dāng)更改是文檔的狀態(tài)發(fā)生時(shí),使用這些常量之一設(shè)置documentState屬性。表4-1列出了狀態(tài)常數(shù)。

表4-1 UIDocumentState常量

文件狀態(tài)常數(shù) 這是什么意思
UIDocumentStateNormal 該文件是開(kāi)放的,并且沒(méi)有發(fā)生任何沖突或其他問(wèn)題。
UIDocumentStateClosed 該文件已關(guān)閉。如果UIDocument無(wú)法打開(kāi)文檔,則文檔處于此狀態(tài),在這種情況下,文檔屬性可能無(wú)效。
UIDocumentStateInConflict 文檔的版本有沖突。
UIDocumentStateSavingError 一個(gè)錯(cuò)誤會(huì)阻止UIDocument保存文檔。
UIDocumentStateEditingDisabled 目前還不允許用戶編輯文檔。

當(dāng)文檔狀態(tài)發(fā)生變化時(shí),UIDocument還會(huì)發(fā)布類(lèi)型為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;

}

確保在類(lèi)的dealloc方法中從通知中心刪除觀察者。

當(dāng)文檔的狀態(tài)發(fā)生變化時(shí),UIDocument會(huì)發(fā)布UIDocumentStateChangedNotification通知,通知中心通過(guò)調(diào)用通知方法(在示例中為documentStateChanged)來(lái)發(fā)送。 在清單4-12中,觀察視圖控制器從documentState屬性獲取當(dāng)前狀態(tài)并對(duì)其進(jìn)行評(píng)估。 如果狀態(tài)是UIDocumentStateEditingDisabled,它會(huì)隱藏鍵盤(pán)。 如果文檔的不同版本(UIDocumentStateInConflict)之間存在沖突,它將在文檔視圖的工具欄中顯示“顯示沖突”按鈕。 (有關(guān)處理文檔版本沖突的詳細(xì)信息,請(qǐng)參閱解決文檔版本沖突。)

清單4-12評(píng)估當(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)用由私有視圖類(lèi)實(shí)現(xiàn)的setDocumentState:方法。 如清單4-13所示,該方法會(huì)根據(jù)文檔狀態(tài)更改文檔視圖工具欄中的其他項(xiàng)目。

清單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"];

}

}

}

如果無(wú)法保存文檔(UIDocumentStateSavingError),則視圖控制器將狀態(tài)指示器更改為紅色,并在其旁顯示未保存。如果有沖突的文檔版本,它會(huì)使?fàn)顟B(tài)指示燈變黃(這是前面提到的“顯示沖突”按鈕)。否則,狀態(tài)指示燈為綠色。

刪除文檔

就像您想允許用戶創(chuàng)建文檔一樣,您也希望讓他們刪除所選文檔。刪除文檔需要做三件事:

  • 從存儲(chǔ)(從本地沙箱或iCloud容器目錄)中刪除文檔文件。
  • 刪除用于表示內(nèi)存中的文檔數(shù)據(jù)的模型對(duì)象。
  • 刪除文檔視圖中顯示的文檔數(shù)據(jù)。
    當(dāng)您從存儲(chǔ)中刪除文檔時(shí),您的代碼應(yīng)該近似UIDocument用于讀寫(xiě)操作。它應(yīng)該在后臺(tái)隊(duì)列上異步執(zhí)行刪除,并且應(yīng)該使用文件協(xié)調(diào)。清單4-14說(shuō)明了這個(gè)過(guò)程。它在后臺(tái)隊(duì)列上分派一個(gè)任務(wù),創(chuàng)建一個(gè)NSFileCoordinator對(duì)象,并調(diào)用coordinateWritingItemAtURL:options:error:byAccessor:方法。此方法的byAccessor塊調(diào)用NSFileManager方法來(lái)刪除文件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)表視圖處于編輯模式時(shí),當(dāng)用戶點(diǎn)擊“刪除”按鈕時(shí),用戶觸發(fā)該方法的調(diào)用。

更改跟蹤和撤消操作

UIDocument類(lèi)的無(wú)保存模型功能確保文檔數(shù)據(jù)以頻繁的間隔自動(dòng)保存,減輕用戶需要顯式保存其文檔。 UIDocument實(shí)現(xiàn)了無(wú)節(jié)制模型的許多行為,但是基于文檔的應(yīng)用程序必須發(fā)揮自己的作用,才能使功能發(fā)揮作用。

UIKit如何自動(dòng)保存文檔數(shù)據(jù)

用于文檔的UIKit框架實(shí)現(xiàn)的無(wú)節(jié)制模型有兩個(gè)主要部分:用于將文檔標(biāo)記為需要保存的機(jī)制以及框架檢查該標(biāo)志時(shí)的可變周期。定期地,UIKit調(diào)用UIDocument對(duì)象的hasUnsavedChanges方法并計(jì)算返回的值。如果值為YES,則將文檔數(shù)據(jù)保存到文檔文件。 hasUnsavedChanges值檢查之間的時(shí)間間隔根據(jù)幾個(gè)因素而變化,包括用戶輸入的速率。

基于文檔的應(yīng)用程序通過(guò)實(shí)現(xiàn)撤消和重做或跟蹤對(duì)文檔的更改來(lái)間接設(shè)置hasUnsavedChanges返回的值。更改跟蹤需要應(yīng)用程序調(diào)用updateChangeCount:方法,傳入U(xiǎn)IDocumentChangeDone(UIDocumentChangeKind類(lèi)型的常量)。當(dāng)應(yīng)用程序注冊(cè)一個(gè)撤消操作,然后發(fā)送撤消或重做消息到文檔的撤銷(xiāo)管理器時(shí),UIDocument代表它調(diào)用updateChangeCount:。

因?yàn)榻o用戶撤消和重做更改的功能可以是一個(gè)區(qū)別的功能,所以建議大多數(shù)應(yīng)用程序使用該方法。

執(zhí)行撤消和重做

您可以通過(guò)遵循撤消體系結(jié)構(gòu)中的過(guò)程和建議,在應(yīng)用程序中執(zhí)行撤消和重做操作。請(qǐng)注意,UIDocument定義了一個(gè)undoManager屬性。您可以通過(guò)訪問(wèn)此屬性獲取默認(rèn)的NSUndoManager對(duì)象,也可以為其分配自己的NSUndoManager對(duì)象。 undo管理器必須通過(guò)屬性與UIDocument對(duì)象相關(guān)聯(lián),以便啟用更改跟蹤,從而自動(dòng)保存文檔數(shù)據(jù)。

清單5-1說(shuō)明了文本字段的撤消和重做的實(shí)現(xiàn)。

清單5-1為文本字段實(shí)現(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;

}

實(shí)施變更跟蹤

要實(shí)施更改跟蹤,而不是執(zhí)行撤消/重做,請(qǐng)?jiān)诖a中的適當(dāng)點(diǎn)調(diào)用UIDocument對(duì)象上的updateChangeCount:方法。 就像您注冊(cè)撤消動(dòng)作一樣,通常情況下,您可以使用用戶剛剛輸入的數(shù)據(jù)更新文檔的模型對(duì)象。 傳入的參數(shù)應(yīng)該是一個(gè)UIDocumentChangeDone常量。

清單5-2顯示了如何在文本視圖中進(jìn)行更改時(shí)調(diào)用的UITextViewDelegate方法中調(diào)用updateChangeCount:。

清單5-2更新文檔的更改計(jì)數(shù)

-(void)textViewDidChange:(UITextView *)textView {

_document.documentText = textView.text;

[_document updateChangeCount:UIDocumentChangeDone];

}

解決文件版本沖突

在iCloud世界中,當(dāng)用戶在多個(gè)設(shè)備或桌面系統(tǒng)上安裝了基于文檔的應(yīng)用程序時(shí),同一文檔的不同版本之間可能會(huì)有沖突。回想一下應(yīng)用程序會(huì)更新本地容器目錄中的文檔文件,然后這些更改通常會(huì)立即發(fā)送到iCloud。但是如果這種傳播不是即時(shí)的呢?例如,您可以使用Mac OS X版本的應(yīng)用程序編輯文檔,但是您也使用iPad版本的應(yīng)用程序編輯了相同的文檔,而您在設(shè)備處于“飛行模式”時(shí)也是如此。當(dāng)您關(guān)閉飛行模式時(shí),文檔的本地更改將傳輸?shù)絠Cloud。 iCloud注意到?jīng)_突并通知應(yīng)用程序。

學(xué)習(xí)文檔版本沖突

隨著監(jiān)控文檔狀態(tài)更改和處理錯(cuò)誤的描述,您的應(yīng)用程序通過(guò)觀察UIDocumentStateChangedNotification通知來(lái)了解文檔版本沖突。如果documentState屬性更改為UIDocumentStateInConflict,則存在相同文檔的多個(gè)版本。該應(yīng)用程序負(fù)責(zé)在有或沒(méi)有用戶幫助的情況下盡快解決這些沖突。

您通過(guò)NSFileVersion類(lèi)的兩個(gè)類(lèi)方法了解文檔的沖突版本。 currentVersionOfItemAtURL:方法返回一個(gè)表示當(dāng)前文件的NSFileVersion對(duì)象;當(dāng)前文件由iCloud在某些基礎(chǔ)上選為當(dāng)前的“沖突獲勝者”,并且在所有設(shè)備上都相同。通過(guò)調(diào)用unresolvedConflictVersionsOfItemAtURL:方法,可以得到一個(gè)NSFileVersion對(duì)象的數(shù)組;這些對(duì)象稱(chēng)為沖突版本,每個(gè)對(duì)象表示位于指定URL的文件的未解決的版本沖突。 NSFileVersion對(duì)象可以為您提供有助于解決沖突的信息,例如修改日期,本地化文檔名稱(chēng)和保存計(jì)算機(jī)的本地化名稱(chēng)。

解決文件版本沖突的策略

您的應(yīng)用程序可以按照解決文檔版本沖突的三種策略之一:

  • 合并來(lái)自沖突版本的更改。
  • 根據(jù)一些相關(guān)因素選擇其中一個(gè)文檔版本,例如最新修改日期的版本。
  • 允許用戶查看文檔的沖突版本,并選擇要使用的版本。

哪種策略最適合使用取決于您的文檔數(shù)據(jù)。如果您可以合并不同文檔版本的內(nèi)容,而不引入矛盾元素,那么請(qǐng)遵循該策略。或者如果您的應(yīng)用程序沒(méi)有遭受任何數(shù)據(jù)丟失,請(qǐng)選擇具有最新修改日期的文檔版本。

一般來(lái)說(shuō),您應(yīng)該嘗試解決沖突而不涉及用戶,但對(duì)于某些可能無(wú)法實(shí)現(xiàn)的應(yīng)用程序。如果應(yīng)用程序采用以用戶為中心的方法,則應(yīng)謹(jǐn)慎地通知用戶版本沖突,并公開(kāi)啟動(dòng)解決過(guò)程的按鈕或其他控件。示例:讓用戶選擇版本檢查允許用戶選擇要使用的文檔版本的應(yīng)用程序的代碼。

如何解決iOS文檔版本沖突?

當(dāng)您的應(yīng)用程序或其用戶通過(guò)選擇文檔版本來(lái)解決文檔版本沖突時(shí),應(yīng)用程序應(yīng)完成以下步驟:

  • 如果所選版本是沖突版本,請(qǐng)將當(dāng)前文檔文件替換為沖突版本文檔文件。
    為此,請(qǐng)?jiān)诒硎景姹镜腘SFileVersion對(duì)象上調(diào)用replaceItemAtURL:options:error:方法,傳遞文檔的當(dāng)前文件URL。

  • 如果所選版本是沖突版本,請(qǐng)還原文檔,以便在文檔文件中顯示新數(shù)據(jù)
    為此,請(qǐng)調(diào)用UIDocument方法revertToContentsOfURL:completionHandler:在文檔對(duì)象上傳入文檔的當(dāng)前文件URL。

  • 將所有沖突版本與文檔的文件URL相關(guān)聯(lián)。
    為此,請(qǐng)調(diào)用NSFileVersion類(lèi)方法removeOtherVersionsOfItemAtURL:error :,傳遞文檔的文件URL。

  • 將每個(gè)沖突版本標(biāo)記為已解決,以便iOS不再作為沖突版本提高它。
    為此,將表示沖突版本的每個(gè)NSFileVersion對(duì)象的已解析屬性設(shè)置為YES。這一步應(yīng)該始終如一地完成。

  • 刪除文檔的已解析版本。

對(duì)于您不再需要的任何版本,請(qǐng)調(diào)用NSFileVersion的removeAndReturnError:方法來(lái)回收該文件的存儲(chǔ)。刪除文檔修訂版本將保留在服務(wù)器上。

一個(gè)例子:讓用戶選擇版本

我們的基于文檔的應(yīng)用程序是一個(gè)簡(jiǎn)單的文本編輯器。這樣的應(yīng)用程序難以在文檔的沖突版本中定位和合并文本差異,即使如此,生成的文檔也可能不是用戶想要的。應(yīng)用程序可以選擇最近修改日期的文檔版本,但是再次無(wú)法確定用戶想要的版本。在這種情況下,一個(gè)很好的解決沖突的策略是讓最熟悉文檔內(nèi)容的用戶選擇自己想要的版本。

您可能會(huì)從監(jiān)控文檔狀態(tài)更改和處理錯(cuò)誤中記住清單6-1中所示的代碼。此代碼顯示文檔視圖控制器的方法,處理UIDocument在文檔狀態(tài)發(fā)生更改時(shí)發(fā)布的UIDocumentStateChangedNotification通知。如果新文檔狀態(tài)為UIDocumentStateInConflict,則視圖控制器在自定義狀態(tài)視圖中顯示“解決沖突”按鈕。 (它還將狀態(tài)指示燈的顏色設(shè)置為紅色。)

清單6-1檢測(cè)文檔版本中的沖突

-(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)用戶點(diǎn)擊按鈕時(shí),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)用戶點(diǎn)擊按鈕時(shí),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;

}

}

這些方法說(shuō)明了如何解決iOS文檔版本沖突的步驟。如果選擇的文檔是沖突版本,則代理將在傳入的NSFileVersion對(duì)象中調(diào)用replaceItemAtURL:options:error:將iCloud容器目錄中的文檔文件替換為所選文檔。然后委托枚舉包含表示文檔的所有沖突版本的NSFileVersion對(duì)象的數(shù)組,并將每個(gè)對(duì)象的已解析屬性設(shè)置為YES。然后,它要求NSFileVersion刪除與文檔的文件URL相關(guān)聯(lián)的文檔的所有其他沖突版本,并調(diào)用revertToContentsOfURL:completionHandler:將顯示的文檔還原到文檔文件的新內(nèi)容。

選擇當(dāng)前文檔文件時(shí)調(diào)用的第二個(gè)委托方法要簡(jiǎn)單得多。它將表示沖突版本的所有NSFileVersion對(duì)象的已解析屬性設(shè)置為YES,并刪除與文檔文件URL相關(guān)聯(lián)的所有沖突版本。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容