寫在前面
最近在一直在研究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)行一些配置:
- 在Xcode中點(diǎn)擊項(xiàng)目目錄結(jié)構(gòu)的根節(jié)點(diǎn)進(jìn)入項(xiàng)目設(shè)置
- 在Capabilities頁簽中找到iCloud一項(xiàng),然后將對(duì)應(yīng)該項(xiàng)的開關(guān)設(shè)置為開啟狀態(tài)。
- 在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下面的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ù),如圖所示:

然后在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)作。
- 為
UIDocument創(chuàng)建一個(gè)子類,該類型主要對(duì)app的中的文檔進(jìn)行管理。 - 重寫
UIDocument的contentsForType:error:和loadFromContents:ofType:error:方法,讓文檔根據(jù)app內(nèi)部機(jī)制來實(shí)現(xiàn)保存和讀取。 - 通過
UIDocument的saveToURL:forSaveOperation:completionHandler:將文檔保存到iCloud容器中。 - 其他設(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),NSFileManager的ubiquityIdentityToken如果不為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;
}
上述代碼中,主要看cell的onApply:回調(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];
}
}
這里要解釋的是NSMetadataQuery的searchScopes和predicate兩個(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)用NSMetadataQuery的startQuery方法來進(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,如下圖所示:

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è)地方需要注意:
- 數(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"";
}
- 需要在調(diào)用
addPersistentStoreWithType:方法時(shí)傳入一個(gè)包含NSPersistentStoreUbiquitousContentNameKey的options參數(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)用NSManagedObjectContext的mergeChangesFromContextDidSaveNotification的方法來合并變更內(nèi)容即可。 |
在這里我們需要監(jiān)聽NSPersistentStoreCoordinatorStoresDidChangeNotification和NSPersistentStoreDidImportUbiquitousContentChangesNotification兩個(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)截圖:

從這個(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ù)管理界面。如圖:

上面的界面只關(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)簽頁,如圖:

點(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)字段,如圖:

然后再返回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記錄都符合條件。然后通過CKDatabase的preformQuery: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按鈕將recordName和gid添加為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è)置gid和count,然后通過SKDatabase的saveRecord: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é)看到最后,如果文章里面存在什么問題歡迎指出來,如果有什么疑問也可以在這里提,最后再次感謝大家支持~