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

寫在前面

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

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

準(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頁(yè)簽中找到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)說(shuō)明如下:
名稱 說(shuō)明
Key-value storage 鍵值對(duì)的存儲(chǔ)服務(wù)蕾殴,用于一些簡(jiǎn)單的數(shù)據(jù)存儲(chǔ)
iCloud Documents 文檔存儲(chǔ)服務(wù),用于將文件保存到iCloud中
CloudKit 云端數(shù)據(jù)庫(kù)服務(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ú)立的空間來(lái)存放在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è)事情要說(shuō)清楚的,這里僅僅討論的是使用iCloud作為登錄賬號(hào)的app从诲,如果你的app有自己的用戶系統(tǒng)左痢,那么你還需要將同步的數(shù)據(jù)進(jìn)行標(biāo)識(shí)(例如加個(gè)系統(tǒng)用戶標(biāo)識(shí)來(lái)確定那份數(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的類型來(lái)實(shí)現(xiàn)相關(guān)的操作定页。它的使用跟NSUserDefaults類似。主要提供以下的功能:

名稱 說(shuō)明
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方法

下面我們來(lái)舉個(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ì)保存下來(lái)并且上傳到iCloud上的磕昼。

該例子最好是能夠準(zhǔn)備兩臺(tái)設(shè)備(或模擬器)來(lái)進(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通知來(lái)改變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來(lái)判斷變更的key中是否包含bg這個(gè)key熊锭,如果存在則表示背景顏色有變更弧轧,再?gòu)?code>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è)備上來(lái)方便用戶的操作,不需要再次去找到這本書或者這張圖片進(jìn)行重新下載牍陌。那么擎浴,iCloud提供了這樣的服務(wù),允許你把一份文檔上傳到iCloud中毒涧,然后其他設(shè)備再同步app上傳的文檔贮预。

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

  1. UIDocument創(chuàng)建一個(gè)子類捡偏,該類型主要對(duì)app的中的文檔進(jìn)行管理唤冈。
  2. 重寫UIDocumentcontentsForType:error:loadFromContents:ofType:error:方法,讓文檔根據(jù)app內(nèi)部機(jī)制來(lái)實(shí)現(xiàn)保存和讀取银伟。
  3. 通過UIDocumentsaveToURL:forSaveOperation:completionHandler:將文檔保存到iCloud容器中你虹。
  4. 其他設(shè)備可以NSMetadataQuery來(lái)獲取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)文檔的同步谴供。

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

首先把界面給搭建起來(lái),如下圖所示:

壁紙界面演示

界面是使用UICollectionView搭建的遂蛀,代碼在這里就不貼上來(lái)了谭跨,主要關(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:方法來(lái)解決這個(gè)問題谆扎。

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

- (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:方法來(lái)獲取容器的地址状土,參數(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:方法來(lái)對(duì)文檔進(jìn)行保存墅垮,這樣就完成了文檔上傳的操作惕医。

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

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

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

- (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屬性主要用來(lái)告訴NSMetadataQuery一個(gè)有效的查詢范圍杏死。它是一個(gè)數(shù)組類型饲窿,元素可以包含NSURL或者NSString類型煌寇,其中NSURL要求是一個(gè)目錄路徑,表示需要查找的目錄逾雄,而NSString則必須為下表的取值阀溶。如果屬性為nil則從所有目錄中進(jìn)行查找.
名稱 說(shuō)明
NSMetadataQueryUbiquitousDocumentsScope 指定該key表示在iCloud容器的Documents目錄下進(jìn)行文件查詢
NSMetadataQueryUbiquitousDataScope 指定該key表示在iCloud容器根目錄進(jìn)行文件查詢
NSMetadataQueryAccessibleUbiquitousExternalDocumentsScope 指定該key表示除應(yīng)用程序容器目錄外的所有可訪問目錄(如iCloud容器目錄等)中進(jìn)行文件查詢
  • predicate主要用于匹配查找文件的條件,其中條件篩選可以與NSMetadataItem中的attribute keys相結(jié)合鸦泳,上面代碼就是使用NSMetadataItemFSNameKey來(lái)找到名稱為image.jpg的背景圖片银锻。更多的屬性可以參考下表:
名稱 說(shuō)明
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ì)的說(shuō)明献宫,但從一些其他資料了解,這可能是表示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來(lái)代替友扰。
NSMetadataUbiquitousItemDownloadingStatusKey 使用NSString來(lái)表示文件的下載狀態(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方法來(lái)進(jìn)行查詢操作。最后通過監(jiān)聽NSMetadataQueryDidFinishGatheringNotification通知來(lái)捕獲查詢完成事件來(lái)檢測(cè)是否找到背景圖片肾请,如果存在圖片則通過BackgroundImage來(lái)加載圖片并將它作為UICollectionView的背景視圖留搔。

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

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

@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)行顯示铃肯。

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

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

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

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

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

演示界面

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

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

PreOrder就是用于記錄購(gòu)物車中要購(gòu)買的商品信息仲器,gid為商品標(biāo)識(shí)(一般由服務(wù)器下發(fā))煤率,count為購(gòu)買商品的數(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ù)庫(kù)的存儲(chǔ)路徑必須是iCloud上的地址昼捍,這跟文檔同步一樣,通過URLForUbiquityContainerIdentifier:方法先取得容器地址肢扯,再生成數(shù)據(jù)庫(kù)的存儲(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ù)庫(kù)存儲(chǔ)在iCloud上的名稱(名稱自定義)蔚晨。

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

通知 說(shuō)明
NSPersistentStoreCoordinatorStoresWillChangeNotification 與持久化存儲(chǔ)變更相關(guān)(如:遷移合并數(shù)據(jù)庫(kù)乍钻,變更存儲(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的方法來(lái)合并變更內(nèi)容即可裁赠。

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

@interface CoreDataViewController ()

//...

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

/**
 購(gòu)物車商品
 */
@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];
            }
            
            // 刷新購(gòu)物車
            [self updateShoppingCart];
            
        }];
    }];
    [center addObserverForName:NSPersistentStoreDidImportUbiquitousContentChangesNotification object:self.persistentStoreCoordinator queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
        
        [self.managedObjectContext performBlock:^{
            // 合并變更的數(shù)據(jù)
            [self.managedObjectContext mergeChangesFromContextDidSaveNotification:note];
            // 刷新購(gòu)物車
            [self updateShoppingCart];
        }];
        
    }];
    
}

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

@end

最后,我們?cè)跒槊總€(gè)商品后面的購(gòu)買按鈕寫上點(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:@"購(gòu)買" 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;
}

// 購(gòu)買按鈕點(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)擊購(gòu)買按鈕一忱,就會(huì)將商品寫入數(shù)據(jù)庫(kù)并保存,一旦保存成功將會(huì)同步到iCloud中谭确,然后其他設(shè)備會(huì)得到相應(yīng)的更新通知帘营。

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

CloudKit使用

其實(shí)CloudKit并不是什么新的東西昂秃,在沒有出現(xiàn)它之前禀梳,筆者接觸過的Parse(該項(xiàng)目已經(jīng)關(guān)停,目前代碼已經(jīng)開源)和國(guó)內(nèi)仿Parse做的LeanCloud其實(shí)就是類似的產(chǎn)品肠骆,都是屬于后端管理的產(chǎn)品算途,允許通過可視化的界面來(lái)建立數(shù)據(jù)實(shí)體以及實(shí)體間的聯(lián)系,然后在客戶端可以輕松地通過一系列的SDK接口來(lái)查詢蚀腿、編輯這些實(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中購(gòu)物車的例子來(lái)進(jìn)行演示說(shuō)明蝎毡。首先,要使用CloudKit必須要在Capabilities頁(yè)簽中勾選CloudKit一項(xiàng)氧枣,然后點(diǎn)擊頁(yè)簽中的CloudKit dashboard按鈕沐兵,可以快速地打開CloudKit的管理后臺(tái)(上圖的界面)。

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

Development Data界面

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

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

數(shù)據(jù)庫(kù) 說(shuō)明
Private Database 私有數(shù)據(jù)庫(kù)胳泉,與每個(gè)iCloud用戶關(guān)聯(lián)拐叉,只用當(dāng)前iCloud用戶才能訪問其私有數(shù)據(jù)庫(kù)的數(shù)據(jù),開發(fā)者無(wú)權(quán)限訪問這些數(shù)據(jù)扇商。在開發(fā)中可以通過CKContainer實(shí)例的privateCloudDatabase屬性來(lái)操作私有數(shù)據(jù)庫(kù)巷嚣。
Shared Database 共享數(shù)據(jù)庫(kù),在用戶登錄后才能夠訪問钳吟,其用于與其他用戶共享私有數(shù)據(jù)庫(kù)的一條或多條記錄廷粒。可以通過CKContainer實(shí)例的sharedCloudDatabase屬性來(lái)操作共享數(shù)據(jù)庫(kù)红且。
Public Database 公有數(shù)據(jù)庫(kù)坝茎,與應(yīng)用關(guān)聯(lián),存儲(chǔ)的數(shù)據(jù)對(duì)所有用戶可見暇番。用戶不需要登錄也能夠讀取數(shù)據(jù)嗤放,但是寫入數(shù)據(jù)則必須用戶登錄iCloud后才能進(jìn)行。通過CKCOntainer實(shí)例的publicCloudDatabase屬性來(lái)操作公有數(shù)據(jù)庫(kù)壁酬。

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

Record Types界面

點(diǎn)擊Create New Type來(lái)創(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)簽頁(yè)就能夠發(fā)現(xiàn)新創(chuàng)建的PreOrders類型了拖吼。

注:Record Type的創(chuàng)建并不區(qū)分?jǐn)?shù)據(jù)庫(kù),一旦對(duì)Record Type進(jìn)行新增这吻、修改或刪除都會(huì)影響所有數(shù)據(jù)庫(kù)中的對(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 ()

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

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

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

@end

@implementation CloudKitViewController

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

    // 更新購(gòu)物車信息
    [self updateShoppingCart];
}

/**
 更新購(gòu)物車
 */
- (void)updateShoppingCart
{
    CKContainer *container = [CKContainer defaultContainer];
    [container accountStatusWithCompletionHandler:^(CKAccountStatus accountStatus, NSError * _Nullable error) {
        
        //只有賬戶登錄后才能訪問私有數(shù)據(jù)庫(kù)
        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:@"購(gòu)物車(%ld)", self.preOrders.count];
                        
                    });
                    
                }
                
            }];
        }
        
    }];
}

@end

上面代碼中主要是updateShoppingCart這個(gè)方法,其獲取了用戶的私有數(shù)據(jù)庫(kù)趾断,并且將所有PreOrders查詢出來(lái)。方法中首先對(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:來(lái)執(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)簽頁(yè)笆檀,選擇PreOrders類型,在右邊界面點(diǎn)擊Add Index按鈕將recordNamegid添加為Queryable索引盒至。如圖:

添加索引

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

接下來(lái),就是改寫商品的購(gòu)買按鈕樱衷,代碼如下:

- (void)buyButtonClickedHandler:(UIButton *)sender
{
    CKContainer *container = [CKContainer defaultContainer];
    [container accountStatusWithCompletionHandler:^(CKAccountStatus accountStatus, NSError * _Nullable error) {
        
        //只有賬戶登錄后才能訪問私有數(shù)據(jù)庫(kù)
        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!");
                            //刷新購(gòu)物車
                            [self updateShoppingCart];
                        }
                        
                    }];
                }
                
            }];
        }
    }];
}

上面代碼流程先取到購(gòu)買商品ID棋嘲,然后通過商品ID來(lái)查找對(duì)應(yīng)的PreOrder記錄,如果存在記錄則將count屬性增加1箫老,如果沒有對(duì)應(yīng)記錄則新建記錄封字,并將設(shè)置gidcount,然后通過SKDatabasesaveRecord:completionHandler:來(lái)對(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ù)變更的操作涣达。就目前來(lái)看在辆,筆者對(duì)這塊的了解并不多,只是簡(jiǎn)單地講述了一些基礎(chǔ)的東西度苔,同樣匆篓,往后會(huì)繼續(xù)研究這塊的內(nèi)容,在適當(dāng)?shù)臅r(shí)間再給大家分享寇窑。

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末饮笛,一起剝皮案震驚了整個(gè)濱河市咨察,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌福青,老刑警劉巖摄狱,帶你破解...
    沈念sama閱讀 221,198評(píng)論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異无午,居然都是意外死亡二蓝,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門指厌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)刊愚,“玉大人,你說(shuō)我怎么就攤上這事踩验∨阜蹋” “怎么了商玫?”我有些...
    開封第一講書人閱讀 167,643評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)牡借。 經(jīng)常有香客問我拳昌,道長(zhǎng),這世上最難降的妖魔是什么钠龙? 我笑而不...
    開封第一講書人閱讀 59,495評(píng)論 1 296
  • 正文 為了忘掉前任炬藤,我火速辦了婚禮,結(jié)果婚禮上碴里,老公的妹妹穿的比我還像新娘沈矿。我一直安慰自己,他們只是感情好咬腋,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評(píng)論 6 397
  • 文/花漫 我一把揭開白布羹膳。 她就那樣靜靜地躺著,像睡著了一般根竿。 火紅的嫁衣襯著肌膚如雪陵像。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,156評(píng)論 1 308
  • 那天寇壳,我揣著相機(jī)與錄音醒颖,去河邊找鬼。 笑死壳炎,一個(gè)胖子當(dāng)著我的面吹牛泞歉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播冕广,決...
    沈念sama閱讀 40,743評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼疏日,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼偿洁!你這毒婦竟也來(lái)了撒汉?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,659評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤涕滋,失蹤者是張志新(化名)和其女友劉穎睬辐,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體宾肺,經(jīng)...
    沈念sama閱讀 46,200評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡溯饵,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了锨用。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片丰刊。...
    茶點(diǎn)故事閱讀 40,424評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖增拥,靈堂內(nèi)的尸體忽然破棺而出啄巧,到底是詐尸還是另有隱情寻歧,我是刑警寧澤,帶...
    沈念sama閱讀 36,107評(píng)論 5 349
  • 正文 年R本政府宣布秩仆,位于F島的核電站码泛,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏澄耍。R本人自食惡果不足惜噪珊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望齐莲。 院中可真熱鬧痢站,春花似錦、人聲如沸铅搓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)星掰。三九已至多望,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間氢烘,已是汗流浹背怀偷。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留播玖,地道東北人椎工。 一個(gè)月前我還...
    沈念sama閱讀 48,798評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像蜀踏,于是被迫代替她去往敵國(guó)和親维蒙。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評(píng)論 2 359

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