寫在前面
最近在一直在研究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)行一些配置:
- 在Xcode中點(diǎn)擊項(xiàng)目目錄結(jié)構(gòu)的根節(jié)點(diǎn)進(jìn)入項(xiàng)目設(shè)置
- 在Capabilities頁(yè)簽中找到iCloud一項(xiàng)兴蒸,然后將對(duì)應(yīng)該項(xiàng)的開關(guān)設(shè)置為開啟狀態(tài)视粮。
- 在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下面的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ù),如圖所示:
然后在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)作。
- 為
UIDocument
創(chuàng)建一個(gè)子類捡偏,該類型主要對(duì)app的中的文檔進(jìn)行管理唤冈。 - 重寫
UIDocument
的contentsForType:error:
和loadFromContents:ofType:error:
方法,讓文檔根據(jù)app內(nèi)部機(jī)制來(lái)實(shí)現(xiàn)保存和讀取银伟。 - 通過
UIDocument
的saveToURL:forSaveOperation:completionHandler:
將文檔保存到iCloud容器中你虹。 - 其他設(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)堂湖,NSFileManager
的ubiquityIdentityToken
如果不為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;
}
上述代碼中慰照,主要看cell
的onApply:
回調(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];
}
}
這里要解釋的是NSMetadataQuery
的searchScopes
和predicate
兩個(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)用NSMetadataQuery
的startQuery
方法來(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,如下圖所示:
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è)地方需要注意:
- 數(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"";
}
- 需要在調(diào)用
addPersistentStoreWithType:
方法時(shí)傳入一個(gè)包含NSPersistentStoreUbiquitousContentNameKey
的options
參數(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)用NSManagedObjectContext 的mergeChangesFromContextDidSaveNotification 的方法來(lái)合并變更內(nèi)容即可裁赠。 |
在這里我們需要監(jiān)聽NSPersistentStoreCoordinatorStoresDidChangeNotification
和NSPersistentStoreDidImportUbiquitousContentChangesNotification
兩個(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)截圖:
從這個(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ù)管理界面便监。如圖:
上面的界面只關(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è)档押,如圖:
點(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)字段璃搜,如圖:
然后再返回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記錄都符合條件。然后通過CKDatabase
的preformQuery: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按鈕將recordName
和gid
添加為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è)置gid
和count
,然后通過SKDatabase
的saveRecord: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)窗市,如果有什么疑問也可以在這里提,最后再次感謝大家支持~