iCloud開發(fā)實(shí)踐

寫在前面

最近在一直在研究iCloud開發(fā)相關(guān)的東西,覺得是有必要寫篇總結(jié)來整理一下近段時(shí)間的一些學(xué)習(xí)成果。之前一直聽說iCloud服務(wù)不友好也不完善,開發(fā)難度也相對(duì)較大,其實(shí)個(gè)人覺得貌似也沒說錯(cuò),iCloud在客戶端提供的框架相比于其他的功能框架來說,他是被分散到各個(gè)框架中,要使用它必須要了解各個(gè)部分的功能及使用場(chǎng)合,這樣就增加了學(xué)習(xí)的成本。同時(shí),框架的設(shè)計(jì)也比較松散,特別是文檔同步,雖然提供了靈活功能強(qiáng)大的框架,但是要理清楚還是需要時(shí)間去學(xué)習(xí)和實(shí)踐。

為了讓大家少走彎路,我將iCloud劃分了幾大功能模塊,下面會(huì)逐個(gè)地講述每個(gè)模塊的一些基本使用,同時(shí)配備例子進(jìn)行說明:

準(zhǔn)備工作

想要使用iCloud服務(wù)我們必須要有一個(gè)蘋果的開發(fā)者賬號(hào)(99$個(gè)人或者企業(yè)都可以),然后需要為項(xiàng)目進(jìn)行一些配置:

  1. 在Xcode中點(diǎn)擊項(xiàng)目目錄結(jié)構(gòu)的根節(jié)點(diǎn)進(jìn)入項(xiàng)目設(shè)置
  2. 在Capabilities頁簽中找到iCloud一項(xiàng),然后將對(duì)應(yīng)該項(xiàng)的開關(guān)設(shè)置為開啟狀態(tài)。
  3. 在iCloud一欄下的有Services和Containers兩個(gè)小欄目,其中Services中有三個(gè)選項(xiàng),其對(duì)應(yīng)說明如下:
名稱 說明
Key-value storage 鍵值對(duì)的存儲(chǔ)服務(wù),用于一些簡(jiǎn)單的數(shù)據(jù)存儲(chǔ)
iCloud Documents 文檔存儲(chǔ)服務(wù),用于將文件保存到iCloud中
CloudKit 云端數(shù)據(jù)庫服務(wù)

這三種服務(wù)會(huì)在后續(xù)章節(jié)為大家詳細(xì)進(jìn)行講解。然后就是Container這一欄,顧名思義,其實(shí)可以簡(jiǎn)單認(rèn)為他是用于存放數(shù)據(jù)的地方,因?yàn)槊總€(gè)應(yīng)用所存放的數(shù)據(jù)應(yīng)該是獨(dú)立的同時(shí)也具有沙箱的限制,所以iOS為每個(gè)應(yīng)用開辟了一個(gè)獨(dú)立的空間來存放在iCloud的文件或數(shù)據(jù),同時(shí)也方便從iCloud上同步數(shù)據(jù)到這個(gè)地方。默認(rèn)情況下,一旦開啟iCloud服務(wù),就會(huì)創(chuàng)建一個(gè)默認(rèn)的容器,其命名為iCloud + BundleID。如果你不想要使用默認(rèn)的容器又或者想跟自己開發(fā)的其他App共享文件數(shù)據(jù),則可以選擇Specify custom containers選項(xiàng),然后在容器列表中選擇一個(gè)指定的容器,或者點(diǎn)擊+號(hào)創(chuàng)建一個(gè)新的容器。

配置完成后,效果如圖所示:

開啟iCloud服務(wù)

注意:iCloud下面的Steps必須都打上勾才表示正常啟用服務(wù),否則需要根據(jù)提示檢查你的蘋果開發(fā)者賬號(hào)中的一些應(yīng)用設(shè)置。

在正式開始前還有一個(gè)事情要說清楚的,這里僅僅討論的是使用iCloud作為登錄賬號(hào)的app,如果你的app有自己的用戶系統(tǒng),那么你還需要將同步的數(shù)據(jù)進(jìn)行標(biāo)識(shí)(例如加個(gè)系統(tǒng)用戶標(biāo)識(shí)來確定那份數(shù)據(jù)是哪個(gè)用戶的),然后根據(jù)標(biāo)識(shí)進(jìn)行數(shù)據(jù)合并。好了,下面可以開始講述一些具體開發(fā)過程(敲代碼時(shí)間到了~)

Key-value同步

該種方式一般用于同步少量數(shù)據(jù)或者進(jìn)行一些配置性質(zhì)的數(shù)據(jù)同步。其使用也比較簡(jiǎn)單,iOS提供了一個(gè)NSUbiquitousKeyValueStore的類型來實(shí)現(xiàn)相關(guān)的操作。它的使用跟NSUserDefaults類似。主要提供以下的功能:

名稱 說明
defaultStore 返回NSUbiquitousKeyValueStore對(duì)象,用于Key-value的存取操作
objectForKey: 獲取指定key的值
setObject:forKey: 設(shè)置指定key的值
removeObjectForKey: 移除指定鍵值
stringForKey: 獲取指定key保存的字符串,如果指定key不存在或者對(duì)應(yīng)key保存的值不是NSString類型時(shí)則返回nil
setString:forKey: 為指定key設(shè)置一個(gè)字符串
arrayForKey: 獲取指定key保存的數(shù)組,如果指定key不存在或者對(duì)應(yīng)key保存的值不是NSArray類型時(shí)則返回nil
setArray:forKey: 為指定key設(shè)置一個(gè)數(shù)組對(duì)象
dictionaryForKey: 獲取指定key保存的字典,如果指定key不存在或者對(duì)應(yīng)key保存的值不是NSDictionary類型時(shí)則返回nil
setDictionary:forKey: 為指定key設(shè)置一個(gè)字典對(duì)象
dataForKey: 獲取指定key保存的二進(jìn)制數(shù)組,如果指定key不存在或者對(duì)應(yīng)key保存的值不是NSData類型時(shí)則返回nil
setData:forKey: 為指定key設(shè)置一個(gè)二進(jìn)制數(shù)組
longLongForKey: 獲取指定key保存的64位整型值,如果指定key不存在或者對(duì)應(yīng)key保存的值不包含數(shù)值時(shí)則返回0
setLongLong:forKey: 為指定key設(shè)置一個(gè)64位整型值
doubleForKey: 獲取指定key保存的浮點(diǎn)數(shù)值,如果指定key不存在或者對(duì)應(yīng)key保存的值不包含數(shù)值時(shí)則返回0
setDouble:forKey: 為指定key設(shè)置一個(gè)浮點(diǎn)數(shù)值
boolForKey: 獲取指定key保存的布爾值,如果指定key不存在則返回NO
setBool:forKey: 為指定key設(shè)置一個(gè)布爾值
synchronize 同步數(shù)據(jù),將在內(nèi)存中的數(shù)據(jù)同步到磁盤中,并上傳至iCloud
dictionaryRepresentation 該屬性會(huì)返回載入到內(nèi)存中保存的key-value字典,如果想要最新的數(shù)據(jù)則需要先調(diào)用synchronize方法

下面我們來舉個(gè)例子,看看如何使用NSUbiquitousKeyValueStore進(jìn)行數(shù)據(jù)同步。

首先,我們?cè)诮缑嬷型先雰蓚€(gè)按鈕,一個(gè)用于設(shè)置數(shù)據(jù),一個(gè)用于獲取數(shù)據(jù),如圖所示:

Key-value Storage演示界面

然后在VC中聲明一個(gè)NSUbiquitousKeyValueStore類型的屬性,并且把兩個(gè)按鈕與VC的按鈕點(diǎn)擊事件進(jìn)行關(guān)聯(lián),其中VC代碼如下:

@interface KeyValueViewController ()

// Key-value同步數(shù)據(jù)存儲(chǔ)對(duì)象
@property (nonatomic, strong) NSUbiquitousKeyValueStore *keyValueStore;

@end

@implementation KeyValueViewController

- (IBAction)setValueButtonClickedHandler:(id)sender
{
    // 設(shè)置值按鈕點(diǎn)擊事件
}

- (IBAction)getValueButtonClickedHandler:(id)sender
{
    // 獲取值按鈕點(diǎn)擊事件
}

然后在viewDidLoad方法中對(duì)keyValueStore進(jìn)行初始化:

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.keyValueStore = [NSUbiquitousKeyValueStore defaultStore];
}

然后分別實(shí)現(xiàn)兩個(gè)按鈕的點(diǎn)擊事件:

- (IBAction)setValueButtonClickedHandler:(id)sender
{
    [self.keyValueStore setString:@"Hello iCloud" forKey:@"data"];
    [self.keyValueStore synchronize];
}

- (IBAction)getValueButtonClickedHandler:(id)sender
{
    NSString *string = [self.keyValueStore stringForKey:@"data"];
    NSLog(@"data = %@", string);
}

上面的代碼用到了NSUbiquitousKeyValueStore的字符串存取方法,要注意的是當(dāng)你設(shè)置了數(shù)據(jù)后一定要調(diào)用synchronize方法,否則這些設(shè)置操作是不會(huì)保存下來并且上傳到iCloud上的。

該例子最好是能夠準(zhǔn)備兩臺(tái)設(shè)備(或模擬器)來進(jìn)行測(cè)試,一臺(tái)進(jìn)行值設(shè)置,另外一臺(tái)進(jìn)行值的獲取。

有時(shí)候,我們需要實(shí)時(shí)知道一些配置的變更,特別是在你有多臺(tái)設(shè)備時(shí)(如同時(shí)擁有iPhone和iPad),想要在其中一臺(tái)設(shè)備中變更某項(xiàng)信息,然后另外一臺(tái)設(shè)備也能夠感知并作出相應(yīng)的調(diào)整。那么,這時(shí)候你需要監(jiān)聽NSUbiquitousKeyValueStoreDidChangeExternallyNotification通知,它能夠告訴你的App所保存的key-value有變更。

我們將上面的例子進(jìn)行改造,將設(shè)置字符串改為設(shè)置一個(gè)背景顏色值,并且設(shè)定它的key為bg,然后通過監(jiān)聽NSUbiquitousKeyValueStoreDidChangeExternallyNotification通知來改變VC的視圖背景顏色。

首先,我們?cè)?code>viewDidLoad中進(jìn)行監(jiān)聽通知:

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    // 初始化keyValueStore
    self.keyValueStore = [NSUbiquitousKeyValueStore defaultStore];

    // 監(jiān)聽通知
    [[NSNotificationCenter defaultCenter] addObserverForName:NSUbiquitousKeyValueStoreDidChangeExternallyNotification object:self.keyValueStore queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
       
        if ([note.userInfo[NSUbiquitousKeyValueStoreChangedKeysKey] containsObject:@"bg"])
        {
            long long bgColorValue = [self.keyValueStore longLongForKey:@"bg"];
            UIColor *bgColor = [UIColor colorWithRed:(bgColorValue & 0xff) / 0xff
                                               green:(bgColorValue >> 8 & 0xff) / 0xff
                                                blue:(bgColorValue >> 16 & 0xff) / 0xff
                                               alpha:1];
            self.view.backgroundColor = bgColor;
        }
        
    }];
}

在上面代碼中的通知處理,我們先通過NSUbiquitousKeyValueStoreChangedKeysKey來判斷變更的key中是否包含bg這個(gè)key,如果存在則表示背景顏色有變更,再從keyValueStore中取出顏色值并轉(zhuǎn)換成UIColor對(duì)象并設(shè)置成視圖的背景顏色。

同時(shí),兩個(gè)按鈕的點(diǎn)擊事件處理如下:

- (IBAction)setValueButtonClickedHandler:(id)sender
{
    [self.keyValueStore setLongLong:0x00ff00 forKey:@"bg"];
    [self.keyValueStore synchronize];
}

- (IBAction)getValueButtonClickedHandler:(id)sender
{
    long long bgColorValue = [self.keyValueStore longLongForKey:@"bg"];
    UIColor *bgColor = [UIColor colorWithRed:(bgColorValue & 0xff) / 0xff
                                       green:(bgColorValue >> 8 & 0xff) / 0xff
                                        blue:(bgColorValue >> 16 & 0xff) / 0xff
                                       alpha:1];
    self.view.backgroundColor = bgColor;
}

通過上面的改動(dòng),在測(cè)試的過程中如果其中一臺(tái)設(shè)備點(diǎn)擊了set value按鈕,則另外一臺(tái)設(shè)備就會(huì)收到通知,并且變更視圖的背景顏色。

文檔數(shù)據(jù)同步

有時(shí)候會(huì)有這樣的需求,如果開發(fā)的app是一款閱讀類工具或者是一款壁紙工具,那么,我們會(huì)希望用戶所下載的書籍或者壁紙會(huì)同步到不同的設(shè)備上來方便用戶的操作,不需要再次去找到這本書或者這張圖片進(jìn)行重新下載。那么,iCloud提供了這樣的服務(wù),允許你把一份文檔上傳到iCloud中,然后其他設(shè)備再同步app上傳的文檔。

要想使用文檔數(shù)據(jù)同步服務(wù),就需要配合UIDocument來完成這項(xiàng)工作,具體的處理流程我先簡(jiǎn)單的描述一下,這樣可以快速幫助到大家來理解機(jī)制的運(yùn)作。

  1. UIDocument創(chuàng)建一個(gè)子類,該類型主要對(duì)app的中的文檔進(jìn)行管理。
  2. 重寫UIDocumentcontentsForType:error:loadFromContents:ofType:error:方法,讓文檔根據(jù)app內(nèi)部機(jī)制來實(shí)現(xiàn)保存和讀取。
  3. 通過UIDocumentsaveToURL:forSaveOperation:completionHandler:將文檔保存到iCloud容器中。
  4. 其他設(shè)備可以NSMetadataQuery來獲取iCloud容器的文檔列表,并更新到本地。

這里要注意一個(gè)問題,因?yàn)樯婕暗骄W(wǎng)絡(luò)同步等相關(guān)的一些列操作,并不僅僅是當(dāng)前應(yīng)用進(jìn)程在訪問文件,系統(tǒng)的進(jìn)程和其他應(yīng)用進(jìn)程也會(huì)對(duì)相關(guān)文件進(jìn)行處理,所以不能通過NSFileManager直接對(duì)iCloud容器中的文件進(jìn)行操作。

同時(shí)也要弄清楚一個(gè)概念,其實(shí)UIDocument并不是為iCloud而設(shè),它同樣可以管理本地的文檔。唯一區(qū)別是如果你的文檔要放到iCloud,那么傳給UIDocument的文檔路徑必須是以iCloud容器地址開始的路徑,這樣才能實(shí)現(xiàn)文檔的同步

那么,下面來舉例介紹如何進(jìn)行文檔數(shù)據(jù)的同步,假設(shè)開發(fā)的app是一款壁紙應(yīng)用,在應(yīng)用壁紙時(shí)會(huì)下載圖片,然后講它保存到iCloud中。另外的設(shè)備就可以自動(dòng)地同步下載的圖片并應(yīng)用該壁紙。

首先把界面給搭建起來,如下圖所示:

壁紙界面演示

界面是使用UICollectionView搭建的,代碼在這里就不貼上來了,主要關(guān)注設(shè)置圖片背景的處理過程。

首先,繼承UIDocument類型創(chuàng)建其一個(gè)子類BackgroundImage,并為BackgroundImage聲明一個(gè)傳入UIImage對(duì)象的構(gòu)造方法以及重寫contentsForType:error:loadFromContents:ofType:error:兩個(gè)方法代碼如下:

@interface BackgroundImage : UIDocument

// 圖片對(duì)象
@property (nonatomic, strong, readonly) UIImage *image;

// 構(gòu)造方法
- (instancetype)initWithFileURL:(NSURL *)url image:(UIImage *)image;

@end

@implementation BackgroundImage

- (instancetype)initWithFileURL:(NSURL *)url image:(UIImage *)image
{
    if (self = [super initWithFileURL:url])
    {
        _image = image;
    }
    return self;
}

- (id)contentsForType:(NSString *)typeName error:(NSError * _Nullable __autoreleasing *)outError
{
    return UIImageJPEGRepresentation(_image, 0.8);
}

- (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError * _Nullable __autoreleasing *)outError
{
    if ([contents isKindOfClass:[NSData class]])
    {
        _image = [UIImage imageWithData:contents];
    }
    
    return YES;
}

@end

注意:上述代碼中的contentsForType:error:方法只允許返回NSData或者NSFileWrapper類型,不能直接把UIImage類型進(jìn)行返回,否則會(huì)拋出錯(cuò)誤提示:

The default implementation of -[UIDocument writeContents:toURL:forSaveOperation:originalContentsURL:error: only understands contents of type NSFileWrapper or NSData, not UIImage. You must override one of the write methods to support custom content types

不過可以重寫writeContents:andAttributes:safelyToURL:forSaveOperation:error:方法來解決這個(gè)問題。

接下來,要取到iCloud容器的地址,在VC中新增一個(gè)方法用來獲取容器路徑:

- (NSURL *)icloudContainerBaseURL
{
    if ([NSFileManager defaultManager].ubiquityIdentityToken)
    {
        return [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
    }
    
    return nil;
}

上面代碼先檢測(cè)設(shè)備是否登錄iCloud賬號(hào),NSFileManagerubiquityIdentityToken如果不為nil則表示已經(jīng)登錄賬號(hào)。然后再通過URLForUbiquityContainerIdentifier:方法來獲取容器的地址,參數(shù)可以傳入容器的名稱(即在項(xiàng)目配置時(shí)設(shè)置的容器,如:iCloud.cn.vimfung.app.iCloudDemo),傳入nil則表示返回容器數(shù)組中的第一個(gè)容器。

如果URLForUbiquityContainerIdentifier:返回nil則表示iCloud服務(wù)不可用。

然后,就可以實(shí)現(xiàn)應(yīng)用圖片按鈕的功能了,代碼如下:

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    // 取得Cell對(duì)象
    BgCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath];
    // 讓Cell現(xiàn)實(shí)圖片
    cell.url = self.imageURLs[indexPath.row];
    
    // 應(yīng)用圖片按鈕的點(diǎn)擊事件回調(diào)
    __weak typeof(self) theController = self;
    [cell onApply:^(UIImage * _Nonnull image) {
       
        //同步文檔
        NSURL *baseURL = [theController icloudContainerBaseURL];
        if (baseURL)
        {
            NSURL *bgURL = [baseURL URLByAppendingPathComponent:@"image.jpg"];
            BackgroundImage *bgImg = [[BackgroundImage alloc] initWithFileURL:bgURL image:image];
            [bgImg saveToURL:bgURL
            forSaveOperation:UIDocumentSaveForOverwriting
           completionHandler:^(BOOL success) {
               
               if (success)
               {
                   NSLog(@"同步成功!");
               }
               else
               {
                   NSLog(@"同步失敗, 可以記錄到本地等待下一次重新同步");
               }
               
            }];
        }
        else
        {
            NSLog(@"iCloud服務(wù)不可用,可根據(jù)需求進(jìn)行相關(guān)處理");
        }
        
        // 將圖片應(yīng)用到CollectionView背景中。
        UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
        imageView.contentMode = UIViewContentModeScaleAspectFill;
        collectionView.backgroundView = imageView;
        
    }];
    
    return cell;
}

上述代碼中,主要看cellonApply:回調(diào)方法(該方法是一個(gè)自定義的方法,主要是用于將點(diǎn)擊應(yīng)用圖片按鈕的事件回調(diào)到VC中),整個(gè)處理流程是先獲取容器地址,如果地址不為空,就創(chuàng)建一個(gè)BackgrounImage的文檔對(duì)象,然后再調(diào)用對(duì)象的saveToURL:forSaveOperation:completionHandler:方法來對(duì)文檔進(jìn)行保存,這樣就完成了文檔上傳的操作。

對(duì)于保存方法,我一直有個(gè)想不明白的地方就是初始化的時(shí)候已經(jīng)傳入了URL,為什么還需要傳入一個(gè)NSURL對(duì)象來確定保存的路徑,這真的讓人摸不著方向。

不過現(xiàn)在我把這兩個(gè)URL區(qū)分對(duì)待了,初始化傳入的URL是用于打開文檔時(shí)使用(UIDocument的open方法是不需要傳URL的),而保存的方法中的URL就僅僅針對(duì)保存目標(biāo)路徑而言,如果路徑與fileURL相同那就是更新文件,如果不同那就是拷貝文檔了。

最后,如果想要在其他設(shè)備上同步背景圖片,那么就需要在viewDidLoad里面同步處理,主要就是使用NSMetadataQuery來查找背景文件,如果背景文件存在加載為背景。具體代碼如下:

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    // 進(jìn)行文檔同步
    NSURL *baseURL = [self icloudContainerBaseURL];
    if (baseURL)
    {
        __block NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
        query.searchScopes = @[NSMetadataQueryUbiquitousDataScope];
        query.predicate = [NSPredicate predicateWithFormat:@"%K == 'image.jpg'", NSMetadataItemFSNameKey];
        
        NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
        [center addObserverForName:NSMetadataQueryDidFinishGatheringNotification object:query queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
            
            if (query.results.count > 0)
            {
                
                
                NSURL *fileURL = [(NSMetadataItem *)query.results.firstObject valueForAttribute:NSMetadataItemURLKey];
                
                //加載背景圖片
                BackgroundImage *bgImage = [[BackgroundImage alloc] initWithFileURL:fileURL image:nil];
                [bgImage openWithCompletionHandler:^(BOOL success) {
                    
                    if (success)
                    {
                        //設(shè)置背景
                        UIImageView *imageView = [[UIImageView alloc] initWithImage:bgImage.image];
                        imageView.contentMode = UIViewContentModeScaleAspectFill;
                        theController.collectionView.backgroundView = imageView;
                    }
                    
                }];
            }
            
            query = nil;
            
        }];
        
        [query startQuery];
    }
}

這里要解釋的是NSMetadataQuerysearchScopespredicate兩個(gè)屬性,通常用這兩個(gè)屬性可以完成簡(jiǎn)單的查找工作:

  • searchScopes屬性主要用來告訴NSMetadataQuery一個(gè)有效的查詢范圍。它是一個(gè)數(shù)組類型,元素可以包含NSURL或者NSString類型,其中NSURL要求是一個(gè)目錄路徑,表示需要查找的目錄,而NSString則必須為下表的取值。如果屬性為nil則從所有目錄中進(jìn)行查找.
名稱 說明
NSMetadataQueryUbiquitousDocumentsScope 指定該key表示在iCloud容器的Documents目錄下進(jìn)行文件查詢
NSMetadataQueryUbiquitousDataScope 指定該key表示在iCloud容器根目錄進(jìn)行文件查詢
NSMetadataQueryAccessibleUbiquitousExternalDocumentsScope 指定該key表示除應(yīng)用程序容器目錄外的所有可訪問目錄(如iCloud容器目錄等)中進(jìn)行文件查詢
  • predicate主要用于匹配查找文件的條件,其中條件篩選可以與NSMetadataItem中的attribute keys相結(jié)合,上面代碼就是使用NSMetadataItemFSNameKey來找到名稱為image.jpg的背景圖片。更多的屬性可以參考下表:
名稱 說明
NSMetadataItemFSNameKey 文件名稱
NSMetadataItemDisplayNameKey 顯示名稱,不包含擴(kuò)展名,跟文件名稱可能不一樣
NSMetadataItemURLKey 文件URL,以file://開頭,為NSURL類型
NSMetadataItemPathKey 文件的絕對(duì)路徑,為NSString類型
NSMetadataItemFSSizeKey 文件大小,單位為字節(jié)
NSMetadataItemFSCreationDateKey 文件創(chuàng)建時(shí)間,為NSDate類型
NSMetadataItemFSContentChangeDateKey 內(nèi)容最后一次變更時(shí)間,為NSDate類型
NSMetadataItemContentTypeKey NSMetadataItem的內(nèi)容類型,為UTI字符串
NSMetadataItemContentTypeTreeKey 這個(gè)官網(wǎng)并沒有詳細(xì)的說明,但從一些其他資料了解,這可能是表示NSMetadataItem的內(nèi)容類型的從屬鏈,返回的數(shù)組最后一個(gè)元素就是當(dāng)前內(nèi)容的類型,再往上就是這個(gè)類型的父類型,再往上就是父級(jí)的父級(jí)類型,直到第一個(gè)元素就是根類型(跟類繼承類似)。例如:一張jpg圖片返回的內(nèi)容如下:["public.item", "public.data", "public.image", "public.jpeg", "public.content"]
NSMetadataItemIsUbiquitousKey 一個(gè)布爾值表示是否上傳到iCloud中,類型為NSNumber
NSMetadataUbiquitousItemHasUnresolvedConflictsKey 一個(gè)布爾值表示當(dāng)前文件與該文件其他版本發(fā)生沖突,如果該屬性值為YES則需要先解決文件的沖突部分才能正常更新到iCloud上,其類型為NSNumber
NSMetadataUbiquitousItemIsDownloadedKey 一個(gè)布爾值表示文件是否已經(jīng)下載到本地并且可用,其類型為NSNumber。iOS 7后使用NSMetadataUbiquitousItemDownloadingStatusKey來代替。
NSMetadataUbiquitousItemDownloadingStatusKey 使用NSString來表示文件的下載狀態(tài),下載狀態(tài)取值如下:NSMetadataUbiquitousItemDownloadingStatusNotDownloaded 表示尚未下載、NSMetadataUbiquitousItemDownloadingStatusDownloaded 表示已下載、NSMetadataUbiquitousItemDownloadingStatusCurrent 表示是文件的最新版本
NSMetadataUbiquitousItemIsDownloadingKey 一個(gè)布爾值表示文件是否開始正在下載到本地,類型為NSNumber
NSMetadataUbiquitousItemIsUploadedKey 一個(gè)布爾值表示文件是否已經(jīng)上傳到iCloud中,類型為NSNumber
NSMetadataUbiquitousItemIsUploadingKey 一個(gè)布爾值表示文件是否正在上傳到iCloud中,類型為NSNumber
NSMetadataUbiquitousItemPercentDownloadedKey 當(dāng)前下載進(jìn)度,范圍為0.0 - 100.0,類型為NSNumber
NSMetadataUbiquitousItemPercentUploadedKey 當(dāng)前上傳進(jìn)度,范圍為0.0 - 100.0,類型為NSNumber
NSMetadataUbiquitousItemDownloadingErrorKey 表示下載過程中產(chǎn)生的錯(cuò)誤信息描述,類型為NSError
NSMetadataUbiquitousItemUploadingErrorKey 表示上傳過程中產(chǎn)生的錯(cuò)誤信息描述,類型為NSError
NSMetadataUbiquitousItemDownloadRequestedKey 其包含一個(gè)布爾值,用于表示MetadataItem是在否已經(jīng)開始下載。YES表示已經(jīng)開始請(qǐng)求下載,NO表示正在等待下載。其類型為NSNumber
NSMetadataUbiquitousItemIsExternalDocumentKey 用于判斷是否為應(yīng)用容器外的文件,類型為NSNumber
NSMetadataUbiquitousItemContainerDisplayNameKey 文件所處iCloud容器的顯示名稱,類型為NSString
NSMetadataUbiquitousItemURLInLocalContainerKey 文件所處iCloud容器的本地URL,類型為NSURL
NSMetadataUbiquitousItemIsSharedKey 包含一個(gè)布爾值,YES表示為共享文件。
NSMetadataUbiquitousSharedItemCurrentUserRoleKey 返回共享文件的當(dāng)前用戶角色。如果返回nil則表示尚未共享。取之如下:NSMetadataUbiquitousSharedItemRoleOwner 表示共享文件的所有者、NSMetadataUbiquitousSharedItemRoleParticipant 表示共享文件的參與者
NSMetadataUbiquitousSharedItemCurrentUserPermissionsKey 返回共享文件的當(dāng)前用戶權(quán)限, 如果返回nil則表示尚未共享。取值如下:NSMetadataUbiquitousSharedItemPermissionsReadOnly 表示當(dāng)前用戶具有只讀權(quán)限、NSMetadataUbiquitousSharedItemPermissionsReadWrite 表示用戶具有讀寫權(quán)限
NSMetadataUbiquitousSharedItemOwnerNameComponentsKey 返回共享文件的所有者信息,其類型為NSPersonNameComponents。如果所有者為當(dāng)前用戶則返回nil
NSMetadataUbiquitousSharedItemMostRecentEditorNameComponentsKey 返回共享文件的最新編輯者信息,其類型為NSPersonNameComponents,如果最新編輯者為當(dāng)前用戶則返回nil,該屬性只讀。

接著再調(diào)用NSMetadataQuerystartQuery方法來進(jìn)行查詢操作。最后通過監(jiān)聽NSMetadataQueryDidFinishGatheringNotification通知來捕獲查詢完成事件來檢測(cè)是否找到背景圖片,如果存在圖片則通過BackgroundImage來加載圖片并將它作為UICollectionView的背景視圖。

這里要注意的是查詢操作只能在應(yīng)用激活時(shí)調(diào)用并執(zhí)行,因此如果應(yīng)用退到后臺(tái),則需要使用stopQuery來停止查詢,等待應(yīng)用恢復(fù)后在重新調(diào)用startQuery來進(jìn)行查詢。

上面的代碼只實(shí)現(xiàn)了在視圖加載將背景同步到本地并顯示,如果你想在應(yīng)用運(yùn)行時(shí)也能夠監(jiān)控到背景圖片的變更,那么就需要把NSMetadataQuery保留起來,讓他生命周期與應(yīng)用生命周期一樣,然后通過NSMetadataQueryDidUpdateNotification通知來捕獲更新,下面我們來改寫剛才的代碼:

@interface iCloudDocumentViewController ()

//... 

/**
 查詢
 */
@property (nonatomic, strong) NSMetadataQuery *query;


/**
 是否需要更新背景
 */
@property (nonatomic) BOOL needUpdateBg;

@end

@implementation iCloudDocumentViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    NSURL *baseURL = [self icloudContainerBaseURL];
    if (baseURL)
    {
        self.query = [[NSMetadataQuery alloc] init];
        self.query.searchScopes = @[NSMetadataQueryUbiquitousDataScope];
        self.query.predicate = [NSPredicate predicateWithFormat:@"%K == 'image.jpg'", NSMetadataItemFSNameKey];
        [self.query enableUpdates];
        
        __weak typeof(self) theController = self;
        NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
        [center addObserverForName:NSMetadataQueryDidUpdateNotification object:self.query queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
            
            if (theController.query.results.count > 0)
            {
                NSMetadataItem *item = theController.query.results.firstObject;
                NSString *status = [item valueForAttribute:NSMetadataUbiquitousItemDownloadingStatusKey];
                if ([status isEqualToString:NSMetadataUbiquitousItemDownloadingStatusDownloaded])
                {
                    theController.needUpdateBg = YES;
                }
                
                if (theController.needUpdateBg && [status isEqualToString:NSMetadataUbiquitousItemDownloadingStatusCurrent])
                {
                    theController.needUpdateBg = NO;
                    
                    //更新背景
                    NSURL *fileURL = [item valueForAttribute:NSMetadataItemURLKey];
                    BackgroundImage *bgImage = [[BackgroundImage alloc] initWithFileURL:fileURL image:nil];
                    [bgImage openWithCompletionHandler:^(BOOL success) {
                        
                        if (success)
                        {
                            //設(shè)置背景
                            UIImageView *imageView = [[UIImageView alloc] initWithImage:bgImage.image];
                            imageView.contentMode = UIViewContentModeScaleAspectFill;
                            theController.collectionView.backgroundView = imageView;
                        }
                        
                    }];
                }
            }
            
        }];
        
        [center addObserverForName:NSMetadataQueryDidFinishGatheringNotification object:self.query queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
            
            if (theController.query.results.count > 0)
            {
                NSMetadataItem *item = theController.query.results.firstObject;
                
                //更新背景
                NSURL *fileURL = [item valueForAttribute:NSMetadataItemURLKey];
                BackgroundImage *bgImage = [[BackgroundImage alloc] initWithFileURL:fileURL image:nil];
                [bgImage openWithCompletionHandler:^(BOOL success) {
                    
                    if (success)
                    {
                        //設(shè)置背景
                        UIImageView *imageView = [[UIImageView alloc] initWithImage:bgImage.image];
                        imageView.contentMode = UIViewContentModeScaleAspectFill;
                        theController.collectionView.backgroundView = imageView;
                    }
                    
                }];
            }
            
        }];
        
        [self.query startQuery];
    }
}

@end

這里把NSMetadataQuery作為VC的屬性,主要是因?yàn)橹挥幸粋€(gè)VC,所以這樣做是沒有問題的。然后增加了一個(gè)needUpdateBg屬性,用于標(biāo)識(shí)是否需要更新背景。從代碼可以看到調(diào)用enableUpdates方法并且新增了一個(gè)NSMetadataQueryDidUpdateNotification通知的監(jiān)聽,這一步就是讓文檔有更新觸發(fā)通知回調(diào)。

對(duì)于更新的回調(diào),它只要iCloud中的文件有變更就會(huì)觸發(fā),所以這里就需要對(duì)NSMetadataItem是否已經(jīng)下載到本地進(jìn)行判斷,只有完全更新到本地后才進(jìn)行背景圖的更新。所以回調(diào)中要比對(duì)NSMetadataUbiquitousItemDownloadingStatusKey,如果狀態(tài)值為NSMetadataUbiquitousItemDownloadingStatusCurrent就表示本地的圖片是最新的,才進(jìn)行顯示。

文檔的同步更新就先說到這里吧,這部分的內(nèi)容涉及比較多,后續(xù)我會(huì)深入研究這部分內(nèi)容然后再給大家分享。

本地?cái)?shù)據(jù)庫(CoreData)同步

很多時(shí)候都會(huì)用到本地?cái)?shù)據(jù)庫來存儲(chǔ)一些配置和緩存信息。對(duì)于一個(gè)電商App,在未登錄應(yīng)用賬號(hào)時(shí),添加到購物車的商品其實(shí)也可以使用本地?cái)?shù)據(jù)庫來存儲(chǔ)。如果想要購物車的東西同步到其他設(shè)備上,那么就可以借助iCloud同步去實(shí)現(xiàn)。

對(duì)于本地?cái)?shù)據(jù)庫(SQLite)的操作,目前比較常用的有iOS原生的CoreData框架,另外就是第三方的FMDB。個(gè)人比較偏向CoreData,開發(fā)起來好處很多,一方面是實(shí)體關(guān)系都是可視化的,而且自動(dòng)生成實(shí)體類型,不需要考慮一些SQL的編寫,特別是處理復(fù)雜關(guān)系。另外一方面就是數(shù)據(jù)在后續(xù)更新,它提供了一套更新映射方案,不需要單獨(dú)進(jìn)行合并或者遷移處理。同時(shí),CoreData跟其他系統(tǒng)庫的結(jié)合更友好,iCloud就是其中一個(gè)。

那么,上面的需求我們就是采用CoreData去實(shí)現(xiàn),下面搭建一個(gè)簡(jiǎn)單的演示界面:

演示界面

再新建一個(gè)模型文件Model.xcdatamodeld,如下圖所示:

數(shù)據(jù)模型定義

PreOrder就是用于記錄購物車中要購買的商品信息,gid為商品標(biāo)識(shí)(一般由服務(wù)器下發(fā)),count為購買商品的數(shù)量。

然后在viewDidLoad中初始化CoreData。如:

@interface CoreDataViewController ()

@property (nonatomic, strong) NSManagedObjectModel *managedObjectModel;

@property (nonatomic, strong) NSPersistentStoreCoordinator *persistentStoreCoordinator;

@property (nonatomic, strong) NSPersistentStore *persistentStore;

@property (nonatomic, strong) NSManagedObjectContext *managedObjectContext;

/**
 商品列表
 */
@property (nonatomic, strong) NSArray<NSDictionary *> *goodsList;

@end

@implementation CoreDataViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    //為演示需要,創(chuàng)建一個(gè)商品列表,正常情況這部分?jǐn)?shù)據(jù)要從服務(wù)端下發(fā)
    self.goodsList = @[@{@"gid" : @0, @"name" : @"商品0"}, @{@"gid" : @1, @"name" : @"商品1"}, @{@"gid" : @2, @"name" : @"商品2"}, @{@"gid" : @3, @"name" : @"商品3"}, @{@"gid" : @4, @"name" : @"商品4"}];

    //初始化CoreData
    NSURL *baseURL = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
    NSURL *storeURL = [baseURL URLByAppendingPathComponent:@"data.sqlite"];
    NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"Model" withExtension:@"momd"];
    self.managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
    self.persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel];
    
    // 要同步iCloud必須設(shè)置存儲(chǔ)配置,并且包含NSPersistentStoreUbiquitousContentNameKey
    NSDictionary *storeOptions = @{NSPersistentStoreUbiquitousContentNameKey: @"CoreData"};
    self.persistentStore = [self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
                                                                         configuration:nil
                                                                                   URL:storeURL
                                                                               options:storeOptions
                                                                                 error:nil];

    self.managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    
    __weak typeof(self) theController = self;
    [self.managedObjectContext performBlockAndWait:^{
        [theController.managedObjectContext setPersistentStoreCoordinator:theController.persistentStoreCoordinator];
    }];
}

@end

上面代碼有兩個(gè)地方需要注意:

  1. 數(shù)據(jù)庫的存儲(chǔ)路徑必須是iCloud上的地址,這跟文檔同步一樣,通過URLForUbiquityContainerIdentifier:方法先取得容器地址,再生成數(shù)據(jù)庫的存儲(chǔ)地址。如果是本地地址可能由于沙箱權(quán)限問題就會(huì)導(dǎo)致發(fā)生下面的錯(cuò)誤:

CoreData: error: -addPersistentStoreWithType:SQLite configuration:(null) URL:file:///var/mobile/Containers/Data/Application/825F3D35-2FD7-41F5-BF7A-58B98E5E5540/data.sqlite options:{
NSPersistentStoreRebuildFromUbiquitousContentOption = 1;
NSPersistentStoreUbiquitousContentNameKey = CoreData1;
} ... returned error Error Domain=NSCocoaErrorDomain Code=513 "You don’t have permission to save the file “store” in the folder “A15BEA0D-4C18-4321-8D6C-5BFBBB0A1DAF”." UserInfo={NSFilePath=/var/mobile/Containers/Data/Application/825F3D35-2FD7-41F5-BF7A-58B98E5E5540/CoreDataUbiquitySupport/mobile~25F77E70-BFB3-475A-82E2-C84F65B59CA7/CoreData1/A15BEA0D-4C18-4321-8D6C-5BFBBB0A1DAF/store, NSUnderlyingError=0x28143e730 {Error Domain=NSPOSIXErrorDomain Code=1 "Operation not permitted"}} with userInfo dictionary {
NSFilePath = "/var/mobile/Containers/Data/Application/825F3D35-2FD7-41F5-BF7A-58B98E5E5540/CoreDataUbiquitySupport/mobile~25F77E70-BFB3-475A-82E2-C84F65B59CA7/CoreData1/A15BEA0D-4C18-4321-8D6C-5BFBBB0A1DAF/store";
NSUnderlyingError = "Error Domain=NSPOSIXErrorDomain Code=1 "Operation not permitted"";
}

  1. 需要在調(diào)用addPersistentStoreWithType:方法時(shí)傳入一個(gè)包含NSPersistentStoreUbiquitousContentNameKeyoptions參數(shù),該key的值是數(shù)據(jù)庫存儲(chǔ)在iCloud上的名稱(名稱自定義)。

接下來我們先了解一下關(guān)于CoreData同步的三個(gè)通知:

通知 說明
NSPersistentStoreCoordinatorStoresWillChangeNotification 與持久化存儲(chǔ)變更相關(guān)(如:遷移合并數(shù)據(jù)庫,變更存儲(chǔ)位置等),在變更前會(huì)派發(fā)此通知。同時(shí),該通知還會(huì)在iCloud賬號(hào)變更和刪除文檔數(shù)據(jù)之前派發(fā)消息
NSPersistentStoreCoordinatorStoresDidChangeNotification NSPersistentStoreCoordinatorStoresWillChangeNotification類似,在持久化存儲(chǔ)變更后進(jìn)行通知派發(fā),其包含userInfo信息,其中包含下面幾個(gè)Key:NSAddedPersistentStoresKey 新增的持久化存儲(chǔ)(NSArray)、NSRemovedPersistentStoresKey 移除的持久化存儲(chǔ)(NSArray)、NSUUIDChangedPersistentStoresKey 變更的持久化存儲(chǔ)(NSArray),包含新舊持久化存儲(chǔ)信息,第一個(gè)元素為舊存儲(chǔ)實(shí)例,第二個(gè)元素是新存儲(chǔ)實(shí)例,當(dāng)存在數(shù)據(jù)遷移時(shí),數(shù)組還包含第三個(gè)元素,該元素是包含所有已遷移實(shí)例的新objectID數(shù)組
NSPersistentStoreDidImportUbiquitousContentChangesNotification 當(dāng)iCloud中存儲(chǔ)的數(shù)據(jù)發(fā)生變化時(shí)會(huì)向設(shè)備派發(fā)此通知,通知中包含了增刪改的一些詳細(xì)信息,我們不需要做過多的事情,只要調(diào)用NSManagedObjectContextmergeChangesFromContextDidSaveNotification的方法來合并變更內(nèi)容即可。

在這里我們需要監(jiān)聽NSPersistentStoreCoordinatorStoresDidChangeNotificationNSPersistentStoreDidImportUbiquitousContentChangesNotification兩個(gè)通知。前一個(gè)主要時(shí)在應(yīng)用首次啟動(dòng)時(shí)同步線上數(shù)據(jù)庫版本,后一個(gè)是在其他設(shè)備調(diào)整購物車數(shù)據(jù)時(shí)可以實(shí)時(shí)監(jiān)聽并合并數(shù)據(jù)進(jìn)行UI上的更新顯示。代碼如下:

@interface CoreDataViewController ()

//...

/**
 購物車按鈕
 */
@property (weak, nonatomic) IBOutlet UIButton *carButton;

/**
 購物車商品
 */
@property (nonatomic, strong) NSArray<PreOrder *> *preOrders;

@end

@implementation CoreDataViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    //...

    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
   
    [center addObserverForName:NSPersistentStoreCoordinatorStoresDidChangeNotification object:self.persistentStoreCoordinator queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
        
        [self.managedObjectContext performBlock:^{
            
            if ([self.managedObjectContext hasChanges])
            {
                // 有變更則保存
                [self.managedObjectContext save:nil];
            }
            
            // 刷新購物車
            [self updateShoppingCart];
            
        }];
    }];
    [center addObserverForName:NSPersistentStoreDidImportUbiquitousContentChangesNotification object:self.persistentStoreCoordinator queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
        
        [self.managedObjectContext performBlock:^{
            // 合并變更的數(shù)據(jù)
            [self.managedObjectContext mergeChangesFromContextDidSaveNotification:note];
            // 刷新購物車
            [self updateShoppingCart];
        }];
        
    }];
    
}

/**
 更新購物車
 */
- (void)updateShoppingCart
{
    NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:@"PreOrder"];
    self.preOrders = [self.managedObjectContext executeFetchRequest:request error:nil];
    
    //更新顯示
    [self.carButton setTitle:[NSString stringWithFormat:@"購物車(%ld)", self.preOrders.count] forState:UIControlStateNormal];
    [self.carButton sizeToFit];
}

@end

最后,我們?cè)跒槊總€(gè)商品后面的購買按鈕寫上點(diǎn)擊事件,如下:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"GoodsCell"];
    if (!cell)
    {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"GoodsCell"];
        
        UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
        [btn setTitle:@"購買" forState:UIControlStateNormal];
        [btn sizeToFit];
        [btn addTarget:self
                action:@selector(buyButtonClickedHandler:)
      forControlEvents:UIControlEventTouchUpInside];
        
        cell.accessoryView = btn;
    }
    
    cell.textLabel.text = self.goodsList[indexPath.row][@"name"];
    cell.accessoryView.tag = indexPath.row;
    
    return cell;
}

// 購買按鈕點(diǎn)擊事件
- (void)buyButtonClickedHandler:(UIButton *)sender
{
    NSInteger index = sender.tag;
    
    int gid = [self.goodsList[index][@"gid"] intValue];
    NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:@"PreOrder"];
    request.predicate = [NSPredicate predicateWithFormat:@"gid = %d", gid];
    PreOrder *preOrder = [self.managedObjectContext executeFetchRequest:request error:nil].firstObject;
    if (!preOrder)
    {
        preOrder = [NSEntityDescription insertNewObjectForEntityForName:@"PreOrder" inManagedObjectContext:self.managedObjectContext];
        preOrder.gid = gid;
    }
    
    preOrder.count ++;
    [self.managedObjectContext save:nil];
    
    [self updateShoppingCart];
}

通過點(diǎn)擊購買按鈕,就會(huì)將商品寫入數(shù)據(jù)庫并保存,一旦保存成功將會(huì)同步到iCloud中,然后其他設(shè)備會(huì)得到相應(yīng)的更新通知。

上面例子到這里算是把CoreData同步流程完整地演示了一遍。但是從iOS 10以后這種CoreData同步形式已經(jīng)被蘋果標(biāo)注過時(shí)了,因?yàn)镃loudKit已經(jīng)可以取代這樣的操作。接下來我繼續(xù)跟大家一起探討CloudKit的使用。

CloudKit使用

其實(shí)CloudKit并不是什么新的東西,在沒有出現(xiàn)它之前,筆者接觸過的Parse(該項(xiàng)目已經(jīng)關(guān)停,目前代碼已經(jīng)開源)和國內(nèi)仿Parse做的LeanCloud其實(shí)就是類似的產(chǎn)品,都是屬于后端管理的產(chǎn)品,允許通過可視化的界面來建立數(shù)據(jù)實(shí)體以及實(shí)體間的聯(lián)系,然后在客戶端可以輕松地通過一系列的SDK接口來查詢、編輯這些實(shí)體數(shù)據(jù)。同樣,CloudKit也是這樣的一種模式,我們可以先看一下CloudKit的管理后臺(tái)截圖:

CloudKit Dashboard

從這個(gè)界面可以看到CloudKit劃分了開發(fā)(Development)和生產(chǎn)(Production)兩個(gè)環(huán)境。開發(fā)產(chǎn)品的時(shí)候就需要在開發(fā)環(huán)境下進(jìn)行,直到開發(fā)完成并進(jìn)行App發(fā)布時(shí),就可以將開發(fā)環(huán)境發(fā)布到生產(chǎn)環(huán)境中。CloudKit包含很多內(nèi)容,在這里我會(huì)先針對(duì)文章主題,將常用的流程給大家進(jìn)行演示。

還是使用CoreData中購物車的例子來進(jìn)行演示說明。首先,要使用CloudKit必須要在Capabilities頁簽中勾選CloudKit一項(xiàng),然后點(diǎn)擊頁簽中的CloudKit dashboard按鈕,可以快速地打開CloudKit的管理后臺(tái)(上圖的界面)。

在這里我們點(diǎn)擊開發(fā)環(huán)境中的Data一欄來進(jìn)入到數(shù)據(jù)管理界面。如圖:

Development Data界面

上面的界面只關(guān)注Records和Record Types兩個(gè)標(biāo)簽頁。Records頁主要展示實(shí)體數(shù)據(jù)的記錄數(shù)據(jù)以及查詢。左側(cè)用來指定哪個(gè)數(shù)據(jù)庫的哪個(gè)類型,然后篩選條件和排序規(guī)則是什么,點(diǎn)擊Query Records就可以在右邊界面顯示查詢到的記錄內(nèi)容。Record Types頁則是用于管理所有記錄的類型,這里的Record Type其實(shí)跟CoreData中的Entity一樣,包含了實(shí)體的屬性和關(guān)系。

這里需要介紹關(guān)于CloudKit中有三種不同的數(shù)據(jù)庫的類型:

數(shù)據(jù)庫 說明
Private Database 私有數(shù)據(jù)庫,與每個(gè)iCloud用戶關(guān)聯(lián),只用當(dāng)前iCloud用戶才能訪問其私有數(shù)據(jù)庫的數(shù)據(jù),開發(fā)者無權(quán)限訪問這些數(shù)據(jù)。在開發(fā)中可以通過CKContainer實(shí)例的privateCloudDatabase屬性來操作私有數(shù)據(jù)庫。
Shared Database 共享數(shù)據(jù)庫,在用戶登錄后才能夠訪問,其用于與其他用戶共享私有數(shù)據(jù)庫的一條或多條記錄??梢酝ㄟ^CKContainer實(shí)例的sharedCloudDatabase屬性來操作共享數(shù)據(jù)庫。
Public Database 公有數(shù)據(jù)庫,與應(yīng)用關(guān)聯(lián),存儲(chǔ)的數(shù)據(jù)對(duì)所有用戶可見。用戶不需要登錄也能夠讀取數(shù)據(jù),但是寫入數(shù)據(jù)則必須用戶登錄iCloud后才能進(jìn)行。通過CKCOntainer實(shí)例的publicCloudDatabase屬性來操作公有數(shù)據(jù)庫。

購物車屬于個(gè)人信息,不應(yīng)該被其他用戶查看,因此,我們這里使用私有數(shù)據(jù)庫來操作數(shù)據(jù),來保存購物車的商品信息。首先需要?jiǎng)?chuàng)建一個(gè)新的Record Type,點(diǎn)擊Record Types標(biāo)簽頁,如圖:

Record Types界面

點(diǎn)擊Create New Type來創(chuàng)建一個(gè)PreOrders的類型并為其創(chuàng)建gid和count字段,如圖:

添加類型

點(diǎn)擊右下角的Create Record Type按鈕,確認(rèn)創(chuàng)建類型。創(chuàng)建成功后會(huì)多出一些系統(tǒng)默認(rèn)字段,如圖:

創(chuàng)建類型完成

然后再返回Records標(biāo)簽頁就能夠發(fā)現(xiàn)新創(chuàng)建的PreOrders類型了。

注:Record Type的創(chuàng)建并不區(qū)分?jǐn)?shù)據(jù)庫,一旦對(duì)Record Type進(jìn)行新增、修改或刪除都會(huì)影響所有數(shù)據(jù)庫中的對(duì)應(yīng)類型。

記錄類型創(chuàng)建好后就可以回到Xcode進(jìn)行相關(guān)的編碼處理。把CoreData的界面進(jìn)行調(diào)整,新建一個(gè)CloudKitViewController視圖控制器類型與UI進(jìn)行關(guān)聯(lián),其代碼如下:

@interface CloudKitViewController ()

/**
 購物車按鈕
 */
@property (weak, nonatomic) IBOutlet UIBarButtonItem *carButtonItem;

/**
 商品列表
 */
@property (nonatomic, strong) NSArray<NSDictionary *> *goodsList;

/**
 購物車商品列表
 */
@property (nonatomic, strong) NSArray<CKRecord *> *preOrders;

@end

@implementation CloudKitViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    //...

    // 更新購物車信息
    [self updateShoppingCart];
}

/**
 更新購物車
 */
- (void)updateShoppingCart
{
    CKContainer *container = [CKContainer defaultContainer];
    [container accountStatusWithCompletionHandler:^(CKAccountStatus accountStatus, NSError * _Nullable error) {
        
        //只有賬戶登錄后才能訪問私有數(shù)據(jù)庫
        if (accountStatus == CKAccountStatusAvailable)
        {
            CKDatabase *db = container.privateCloudDatabase;
            
            CKQuery *query = [[CKQuery alloc] initWithRecordType:@"PreOrders" predicate:[NSPredicate predicateWithValue:YES]];
            [db performQuery:query inZoneWithID:nil completionHandler:^(NSArray<CKRecord *> * _Nullable results, NSError * _Nullable error) {
                
                if (!error)
                {
                    self.preOrders = results;
                    
                    dispatch_async(dispatch_get_main_queue(), ^{
                        
                        //更新顯示
                        self.carButtonItem.title = [NSString stringWithFormat:@"購物車(%ld)", self.preOrders.count];
                        
                    });
                    
                }
                
            }];
        }
        
    }];
}

@end

上面代碼中主要是updateShoppingCart這個(gè)方法,其獲取了用戶的私有數(shù)據(jù)庫,并且將所有PreOrders查詢出來。方法中首先對(duì)用戶的iCloud的登錄狀態(tài)進(jìn)行了判斷,因?yàn)?code>CKContainer的privateCloudDatabase屬性即使在用戶未登錄狀態(tài)下也會(huì)正常返回,但一旦對(duì)其進(jìn)行操作就會(huì)報(bào)錯(cuò),所以需要使用accountStatusWithCompletionHandler:方法進(jìn)行賬號(hào)狀態(tài)判斷。

這里的代碼也相對(duì)簡(jiǎn)單,主要使用了CKQuery對(duì)象進(jìn)行查詢的操作。構(gòu)建查詢需要指定RecordType和查詢條件。在這里為PreOrders,然后查詢條件設(shè)置為所有PreOrder記錄都符合條件。然后通過CKDatabasepreformQuery:inZoneWithID:comletionHandler:來執(zhí)行查詢操作。

注:如果查詢的Record Type沒有索引,則查詢就會(huì)報(bào)錯(cuò):
<CKError 0x6000028f6640: "Invalid Arguments" (12/2015); server message = "Type is not marked indexable"; uuid = C22FF9D7-C619-4EA8-95A1-EF702078927F; container ID = "iCloud.cn.vimfung.app.iCloudDemo">

如果建立的索引并沒有應(yīng)用到查詢條件時(shí),則會(huì)默認(rèn)使用Record Type的recordName屬性作為索引,如果沒有為該字段建立Queryable索引,則會(huì)導(dǎo)致如下報(bào)錯(cuò):
<CKError 0x600003623360: "Invalid Arguments" (12/2015); server message = "Field 'recordName' is not marked queryable"; uuid = 9E63EC88-9B7A-45FF-A7E9-B08471F6AED5; container ID = "iCloud.cn.vimfung.app.iCloudDemo">

之前沒有為PreOrders建立,所以查詢會(huì)報(bào)錯(cuò)。我們回到管理后臺(tái)的Indexs標(biāo)簽頁,選擇PreOrders類型,在右邊界面點(diǎn)擊Add Index按鈕將recordNamegid添加為Queryable索引。如圖:

添加索引

點(diǎn)擊右下角Save Record Type按鈕保存,然后再回到項(xiàng)目中測(cè)試就能夠正常查詢了。

接下來,就是改寫商品的購買按鈕,代碼如下:

- (void)buyButtonClickedHandler:(UIButton *)sender
{
    CKContainer *container = [CKContainer defaultContainer];
    [container accountStatusWithCompletionHandler:^(CKAccountStatus accountStatus, NSError * _Nullable error) {
        
        //只有賬戶登錄后才能訪問私有數(shù)據(jù)庫
        if (accountStatus == CKAccountStatusAvailable)
        {
            NSInteger index = sender.tag;
            
            int gid = [self.goodsList[index][@"gid"] intValue];
            
            //先查詢是否存在該商品
            CKQuery *query = [[CKQuery alloc] initWithRecordType:@"PreOrders" predicate:[NSPredicate predicateWithFormat:@"gid = %d", gid]];
            CKDatabase *db = container.privateCloudDatabase;
            [db performQuery:query inZoneWithID:nil completionHandler:^(NSArray<CKRecord *> * _Nullable results, NSError * _Nullable error) {
                
                if (!error)
                {
                    CKRecord *preOrder = nil;
                    if (results.count > 0)
                    {
                        preOrder = results.firstObject;
                        NSNumber *countNum = [preOrder objectForKey:@"count"];
                        [preOrder setObject:@(countNum.intValue + 1) forKey:@"count"];
                    }
                    else
                    {
                        preOrder = [[CKRecord alloc] initWithRecordType:@"PreOrders"];
                        [preOrder setObject:@(1) forKey:@"count"];
                        [preOrder setObject:@(gid) forKey:@"gid"];
                    }
                    
                    [db saveRecord:preOrder completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable error) {
                        
                        if (!error)
                        {
                            NSLog(@"save suc!");
                            //刷新購物車
                            [self updateShoppingCart];
                        }
                        
                    }];
                }
                
            }];
        }
    }];
}

上面代碼流程先取到購買商品ID,然后通過商品ID來查找對(duì)應(yīng)的PreOrder記錄,如果存在記錄則將count屬性增加1,如果沒有對(duì)應(yīng)記錄則新建記錄,并將設(shè)置gidcount,然后通過SKDatabasesaveRecord:completionHandler:來對(duì)記錄進(jìn)行保存。保存成功后更新UI。

整個(gè)例子到這里已經(jīng)改寫完成,通過上面的調(diào)整發(fā)現(xiàn),其實(shí)CloudKit不算復(fù)雜,但是與上一章的CoreData相比,該例子沒有實(shí)現(xiàn)實(shí)時(shí)監(jiān)聽數(shù)據(jù)變更的操作。就目前來看,筆者對(duì)這塊的了解并不多,只是簡(jiǎn)單地講述了一些基礎(chǔ)的東西,同樣,往后會(huì)繼續(xù)研究這塊的內(nèi)容,在適當(dāng)?shù)臅r(shí)間再給大家分享。

那么,所有的內(nèi)容到這里就告一段落了,感謝各位同學(xué)看到最后,如果文章里面存在什么問題歡迎指出來,如果有什么疑問也可以在這里提,最后再次感謝大家支持~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 關(guān)于Mongodb的全面總結(jié) MongoDB的內(nèi)部構(gòu)造《MongoDB The Definitive Guide》...
    中v中閱讀 32,273評(píng)論 2 89
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,741評(píng)論 25 709
  • 由于興趣所致過去一年間我花了比較多的時(shí)間在一個(gè)內(nèi)容的學(xué)習(xí)和優(yōu)化上,它就是關(guān)于星座性格的主題,有別于現(xiàn)在普遍的娛樂...
    義善書館閱讀 608評(píng)論 0 0
  • 親愛的姐妹,“聽說”你要結(jié)婚了。原諒我不能陪伴你走上紅毯,穿上白紗,做最美的新娘! 我們相識(shí)是在六年...
    流淚的青蛙閱讀 454評(píng)論 2 2
  • 我的大學(xué),一個(gè)排名倒數(shù)的二本學(xué)校,連最初的我也嫌棄的學(xué)校。但是突然開始舍不得學(xué)校,舍不得離開,舍不得結(jié)束自己的學(xué)生...
    向著月亮向日葵閱讀 215評(píng)論 0 0

友情鏈接更多精彩內(nèi)容