iCloud開發: key-value Storage,CloudKit,iCloud Documents

struggle_time發表於2022-03-15

iCloud開發

  • 使用iCloud的開發的前提是要有開發者賬號,個人或企業均可。

iCloud三種型別的儲存方式

型別 說明
key-value storage 鍵值對的儲存服務,用於一些簡單的資料儲存
iCloud Documents 文件儲存服務,用於將檔案儲存到iCloud中
CloudKit 雲端資料庫服務

專案配置

  • 這些是基本配置,三種方式都需要這些配置。

1、iCloud 官網配置

  • 配置iCloud Containers

  • 建立支援iCloud的Apple ID,並關聯上相應的iCloud容器。

  • 輸入Identifier即可建立完畢

2、本地Xcode配置

  • 新增CloudKit框架到專案
  • Xcode配置資訊: 選擇專案->targets->Capabilities->iCloud->開啟開關
  • 1、勾選自己要開啟的Services
  • 2、選擇對應的Containers,可以使用預設,也可以指定固定的
  • 3、觀察steps是否全部success
  • 4、修改entitlements

注意事項

  • demo是iOS和macos資料同步,所以配置稍微複雜點
  • 如果只是iOS裝置間進行同步,不用修改Containers,使用預設即可,第四步不用修改
  • 其中iCloud Key-Value Store 預設是用team id和bundle identifier做標識,因為mac和ios的bundle identifier不一致,所以要手動指定為統一的

一、key-value storage

  • 一般用於同步少量資料或者進行一些配置性質的資料同步,使用簡單。
  • 使用NSUbiquitousKeyValueStore物件進行資料讀寫

1、獲取預設store

	// 獲取預設的store,這就是在xxx.entitlements裡配置的`iCloud Key-Value Store`
	self.keyValueStore = [NSUbiquitousKeyValueStore defaultStore];

2、寫入資料

	NSLog(@"寫入iCloud資料:%zd",self.number);
	[self.keyValueStore setLongLong:self.number forKey:@"number"];
	// 同步資料,避免衝突
	[self.keyValueStore synchronize];

3、讀取資料

	// 在獲取到store後,讀取iCloud資料
	self.number = [self.keyValueStore longLongForKey:@"number"];

4、監聽資料改變(多臺裝置)

  • 需要實時知道一些配置的變更,特別是在你有多臺裝置時(如同時擁有iPhone和iPad)
	// 新增監聽
	[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(dataChanged:) name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification object:nil];
    
 - (void)dataChanged:(NSNotification *)noti{
    // 監聽到keyvalue值改變就會觸發這個通知
    NSLog(@"keyvalue改變了:%@",noti);
    if ([noti.userInfo[NSUbiquitousKeyValueStoreChangedKeysKey] containsObject:@"number"]) {
        self.number = [noti.object longLongForKey:@"number"];
        NSLog(@"keyvalue改變了:%zd",self.number);
        
        self.myLabel.stringValue = [NSString stringWithFormat:@"%zd",self.number];
    }
}     
  • iPhone上改變,mac上監聽

二、CloudKit

iCloud官網配置

1、選擇某個容器

2、新建Record,類似表名稱

  • 需要開啟recordName的索引,這樣客戶端才能查詢資料

3、新建Field,類似表欄位

  • Filed 型別
    Filed Type 型別

4、進入Data,建立記錄

  • 建立記錄後才能在客戶端進行增刪查改



編碼開始

  • 以上配置完畢可嘗試在客戶端進行增刪查改。
  • 初始化容器物件
// 初始化容器物件
self.container = [CKContainer containerWithIdentifier:ContainerID];

1、查詢資料

  • 判斷iCloud賬戶狀態 accountStatusWithCompletionHandler:
  • 獲取私有資料庫物件 weakSelf.container.privateCloudDatabase
  • 查詢資料 performQuery: inZoneWithID:completionHandler:
	 if(self.container){
        // 訪問私有資料庫
        __weak typeof(self) weakSelf = self;
        [weakSelf.container accountStatusWithCompletionHandler:^(CKAccountStatus accountStatus, NSError * _Nullable error) {
            // 只有登入iCloud才能讀取
            if (accountStatus == CKAccountStatusAvailable) {
                // 獲取私有資料庫例項
                CKDatabase *db = weakSelf.container.privateCloudDatabase;
                CKQuery *query  = [[CKQuery alloc] initWithRecordType:RecordType predicate:[NSPredicate predicateWithValue:YES]];
                // 查詢資料
                [db performQuery:query inZoneWithID:nil completionHandler:^(NSArray<CKRecord *> * _Nullable results, NSError * _Nullable error) {
                    if(!error){
                        weakSelf.preOrders = [NSMutableArray arrayWithArray:results];
                        NSLog(@"%@",results);

                        dispatch_async(dispatch_get_main_queue(), ^{
                            [weakSelf.tableView reloadData];
                        });
                    }else{
                        NSLog(@"Error:%@",error);
                    }
                }];
                
            }else {
                NSLog(@"登入iCloud錯誤");
            }
        }];
        
    }else {
        NSLog(@"連線iCloud錯誤");
    }
  • 返回CKRecord型別的資料,可以通過objectForKey方法直接讀取
<CKRecord: 0x101813050; recordID=FF125857-926B-4DC5-B972-9E6A6502B5A5:(_defaultZone:__defaultOwner__), recordChangeTag=jzjms8b2, values={\n    amount = 9144;\n    time = \"2021-08-20 09:33:45 +0000\";\n}, recordType=Water>


NSDate *time = [ck objectForKey:@"time"];
NSInteger count = [ck objectForKey:@"amount"];

2、新增資料

  • 判斷iCloud賬戶狀態 accountStatusWithCompletionHandler:

  • 獲取私有資料庫物件 weakSelf.container.privateCloudDatabase

  • 建立Record CKRecord *record = [[CKRecord alloc] initWithRecordType:RecordType];

  • 儲存資料 saveRecord: completionHandler:

        if(self.container){
            // 1 訪問私有資料庫
            __weak typeof(self) weakSelf = self;
            [self.container accountStatusWithCompletionHandler:^(CKAccountStatus accountStatus, NSError * _Nullable error) {
                // 1.1 只有登入iCloud才能讀取
                if (accountStatus == CKAccountStatusAvailable) {
                    // 1.2 獲取私有資料庫例項
                    CKDatabase *db = weakSelf.container.privateCloudDatabase;
                    // 新增資料
                    CKRecord *record = [[CKRecord alloc] initWithRecordType:RecordType];
                    
                    record[@"time"] =  [NSDate date];
                    record[@"amount"] = @(arc4random()%10000);
                    
                    // 1.3 儲存資料
                    [db saveRecord:record completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable error) {
                        if(error) {
                            NSLog(@"%@", error);
                        } else {
                            NSLog(@"Saved successfully:%@",record);
                            dispatch_async(dispatch_get_main_queue(), ^{
                                [weakSelf queryAction:nil];
                            });
                        }
                    }];
                }else {
                    NSLog(@"登入iCloud錯誤");
                }
            }];
        }
    
    

3、刪除資料

  • 判斷iCloud賬戶狀態 accountStatusWithCompletionHandler:
  • 獲取私有資料庫物件 weakSelf.container.privateCloudDatabase
  • 查詢Record是否存在 fetchRecordWithID: completionHandler:
  • 刪除資料 deleteRecordWithID: completionHandler:
  if(self.container){
        // 訪問私有資料庫
        __weak typeof(self) weakSelf = self;
        [weakSelf.container accountStatusWithCompletionHandler:^(CKAccountStatus accountStatus, NSError * _Nullable error) {
            // 只有登入iCloud才能讀取
            if (accountStatus == CKAccountStatusAvailable) {
                // 獲取私有資料庫例項
                CKDatabase *db = weakSelf.container.privateCloudDatabase;
                [db fetchRecordWithID:record.recordID completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable error) {
                    if(error) {
                        NSLog(@"%@", error);
                    } else {
                        NSLog(@"查詢成功:%@",record);
                        [db deleteRecordWithID:record.recordID completionHandler:^(CKRecordID * _Nullable recordID, NSError * _Nullable error) {
                            if(error) {
                                NSLog(@"%@", error);
                            } else {
                                NSLog(@"刪除成功:%@",record);
                                dispatch_async(dispatch_get_main_queue(), ^{
                                    [weakSelf.preOrders removeObjectAtIndex:indexPath.row];
                                    [weakSelf.tableView reloadData];
                                });
                            }
                        }];
                    }
                }];
                
            }else {
                NSLog(@"登入iCloud錯誤");
            }
        }];
        
    }else {
        NSLog(@"連線iCloud錯誤");
    }

4、修改資料

  • 判斷iCloud賬戶狀態 accountStatusWithCompletionHandler:
  • 獲取私有資料庫物件 weakSelf.container.privateCloudDatabase
  • 查詢Record是否存在 fetchRecordWithID: completionHandler:
  • 儲存資料 saveRecord: completionHandler:
 if(self.container){
        // 訪問私有資料庫
        __weak typeof(self) weakSelf = self;
        NSInteger count = [weakSelf.countTextField.text integerValue];
        [weakSelf.container accountStatusWithCompletionHandler:^(CKAccountStatus accountStatus, NSError * _Nullable error) {
            // 只有登入iCloud才能讀取
            if (accountStatus == CKAccountStatusAvailable) {
                // 獲取私有資料庫例項
                CKDatabase *db = weakSelf.container.privateCloudDatabase;
                [db fetchRecordWithID:weakSelf.record.recordID completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable error) {
                    if(error) {
                        NSLog(@"%@", error);
                    } else {
                        NSLog(@"查詢成功:%@",record);
                        record[@"amount"] = @(count);
                        [db saveRecord:record completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable error) {
                            if(error) {
                                NSLog(@"%@", error);
                            } else {
                                NSLog(@"更改成功:%@",record);
                                dispatch_async(dispatch_get_main_queue(), ^{
                                    [[NSNotificationCenter defaultCenter] postNotificationName:@"refreshUI" object:nil];
                                    [weakSelf.navigationController popViewControllerAnimated:YES];
                                });
                            }
                        }];
                    }
                }];
                
            }else {
                NSLog(@"登入iCloud錯誤");
            }
        }];
        
    }else {
        NSLog(@"連線iCloud錯誤");
    }

5、監聽iCloud賬戶狀態

    // 賬戶資訊狀態改變了會觸發這個資訊,可以嘗試重新整理資料
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(dataChanged:) name:CKAccountChangedNotification object:nil];


- (void)dataChanged:(NSNotification *)noti{
    NSLog(@"賬戶資訊狀態改變了:%@",noti);
    [self queryAction:nil];
}

CloudKit 知識掃盲

  • CKContainer 容器,或者沙盒,每個應用只能訪問自己的容器。
  • CKDatabase 顧名思義,資料庫了,包含私有資料庫和公有資料庫,使用者只能訪問自己的私有資料庫,一些不敏感的資料也可以儲存在公有資料庫中。
  • CKRecord 資料記錄,keyvalue形式儲存的,儲存一些基本型別(NSString,NSNumber,NSData,NSDate,CLLocation,CKAsset,CKReference等)
  • CKRecordZone 類似分割槽,是用來儲存Record的。所有的Record都是儲存在這裡,應用有一個預設的zone,也可以自定義zone。
  • CKAsset 檔案儲存記錄
  • CKQuery 資料庫查詢物件,指定查詢條件進行資料查詢

三、iCloud Documents

1、Xcode配置

  • 允許你把一份文件上傳到iCloud中,然後其他裝置再同步app上傳的文件。

2、自定義UIDocument

  • 首先繼承UIDocument,實現自己的方法,做好NSData資料的轉換

#import "MyDocument.h"

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

// 寫入資料前
- (nullable id)contentsForType:(NSString *)typeName error:(NSError **)outError{
    // 只能返回NSData 或者 NSFileWrapper ,所以這裡要轉換圖片
    return UIImageJPEGRepresentation(_myImage, 0.7);
}

// 讀取資料後
- (BOOL)loadFromContents:(id)contents ofType:(nullable NSString *)typeName error:(NSError **)outError {
    if ([contents isKindOfClass:[NSData class]]) {
        // 如果是NSData,還要轉換成圖片
        _myImage = [UIImage imageWithData:contents];
    }
    return YES;
}
@end
  

3、儲存圖片

  • 檔名可以隨機生成
  • 查詢時進行模糊查詢即可全部查出來
  • 儲存方法 saveToURL: forSaveOperation: completionHandler:
- (void)saveWithImage:(UIImage *)image{
    
    if(self.baseURL){
//        UIImage *image = [UIImage imageNamed:@"1"];
        self.localImageView.image = image;
        NSURL *bgURL = [self.baseURL URLByAppendingPathComponent:@"100JZPlg6M1DQht3xU.png"];
        MyDocument *bgImg = [[MyDocument alloc] initWithFileURL:bgURL image:image];
        [bgImg saveToURL:bgURL
        forSaveOperation:UIDocumentSaveForOverwriting
       completionHandler:^(BOOL success) {
           
           if (success)
           {
               NSLog(@"同步成功!");
           }
           else
           {
               NSLog(@"同步失敗, 可以記錄到本地等待下一次重新同步");
           }
           
       }];
    }else{
        NSLog(@"連線iCloud錯誤");
    }
}

4、下載圖片


- (IBAction)downloadClick {
    // 進行文件同步
    if(self.baseURL){
        __weak typeof(self) weakSelf = self;
        __block NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
        // 查詢資料範圍
        query.searchScopes = @[NSMetadataQueryUbiquitousDataScope];
        // 查詢條件 NSMetadataItemFSNameKey 按照檔名搜尋
//        query.predicate = [NSPredicate predicateWithFormat:@"%K == '100JZPlg6M1DQht3xU.png'", NSMetadataItemFSNameKey];
      	// 模糊查詢使用 *
        query.predicate = [NSPredicate predicateWithFormat:@"%K like '*JZPlg6M1DQht3xU.png'", NSMetadataItemFSNameKey];
        
        // 監聽查詢結果
        NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
        [center addObserverForName:NSMetadataQueryDidFinishGatheringNotification object:query queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
            
            NSLog(@"Note:%@",note);
          	// query.results 查詢結果陣列,如果模糊匹配可能有多個
            if (query.results.count > 0)
            {
                NSURL *fileURL = [(NSMetadataItem *)query.results.firstObject valueForAttribute:NSMetadataItemURLKey];
                //載入背景圖片
                MyDocument *bgImage = [[MyDocument alloc] initWithFileURL:fileURL image:nil];
                [bgImage openWithCompletionHandler:^(BOOL success) {
                    if (success)
                    {
                        NSLog(@"下載成功!");
                        weakSelf.backgroungImageView.image = bgImage.myImage;
                    }else{
                        NSLog(@"下載失敗");
                    }
                    
                }];
            }
            // 查詢完畢,關閉
            [query stopQuery];
            
        }];
        // 開啟查詢
        [query startQuery];
    }
}

5、Mac版程式碼稍微有點區別

  • 自定義NSDocument
#import "MyDocument.h"

@implementation MyDocument

// 讀取資料後
- (BOOL)readFromURL:(NSURL *)url ofType:(NSString *)typeName error:(NSError * _Nullable __autoreleasing *)outError {
    NSImage *im = [[NSImage alloc] initWithContentsOfURL:url];
    if (im) {
        self.myImage = im;
        return YES;
    }
    return NO;
}
// 寫入資料前
- (NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError {
    // 只能返回NSData 或者 NSFileWrapper ,所以這裡要轉換圖片
    NSData *data = self.myImage.TIFFRepresentation;
    return data;
}
- (instancetype)initWithFileURL:(NSURL *)url image:(NSImage *)image
{
    if (self = [super initWithContentsOfURL:url ofType:@"png" error:nil])
    {
        _myImage = image;
    }
    return self;
}
@end
  • 儲存圖片
- (void)saveWithImage:(NSImage *)image{
    
    if(self.baseURL){
        self.localImageView.image = image;
        NSURL *bgURL = [self.baseURL URLByAppendingPathComponent:@"100JZPlg6M1DQht3xU.png"];
        MyDocument *bgImg = [[MyDocument alloc] initWithFileURL:bgURL image:image];
        [bgImg saveToURL:bgURL ofType:@"png" forSaveOperation:NSSaveOperation completionHandler:^(NSError * _Nullable errorOrNil) {
            if (!errorOrNil)
            {
                NSLog(@"同步成功!");
            }
            else
            {
                NSLog(@"同步失敗, 可以記錄到本地等待下一次重新同步:%@",errorOrNil);
            }
        }];
    }else{
        NSLog(@"連線iCloud錯誤");
    }
}
  • 下載圖片
- (IBAction)downloadClick:(NSButton *)btn {
    // 進行文件同步
    if(self.baseURL){
        __weak typeof(self) weakSelf = self;
        __block NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
        // 查詢資料範圍
        query.searchScopes = @[NSMetadataQueryUbiquitousDataScope];
        // 查詢條件 NSMetadataItemFSNameKey 按照檔名搜尋
        //        query.predicate = [NSPredicate predicateWithFormat:@"%K == '100JZPlg6M1DQht3xU.png'", NSMetadataItemFSNameKey];
        // 模糊查詢使用 *
        query.predicate = [NSPredicate predicateWithFormat:@"%K like '*JZPlg6M1DQht3xU.png'", NSMetadataItemFSNameKey];
        
        // 監聽查詢結果
        NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
        [center addObserverForName:NSMetadataQueryDidFinishGatheringNotification object:query queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
            
            NSLog(@"Note:%@",note);
            // query.results 查詢結果陣列,如果模糊匹配可能有多個
            if (query.results.count > 0)
            {
                NSURL *fileURL = [(NSMetadataItem *)query.results.firstObject valueForAttribute:NSMetadataItemURLKey];
                //載入背景圖片
                MyDocument *bgImage = [[MyDocument alloc] initWithFileURL:fileURL image:nil];
                if([bgImage readFromURL:fileURL ofType:@"png" error:nil]){
                    NSLog(@"下載成功!");
                    weakSelf.localImageView.image = bgImage.myImage;
                }
                else{
                    NSLog(@"下載失敗");
                }
            }
            // 查詢完畢,關閉
            [query stopQuery];
            
        }];
        // 開啟查詢
        [query startQuery];
    }
}

6、監聽資料改變

  • 監聽通知 NSMetadataQueryDidFinishGatheringNotification
  • 查詢裡面的 [query stopQuery]; 需要註釋掉
    // 監聽資料改變
        [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];
                //載入背景圖片
                MyDocument *bgImage = [[MyDocument alloc] initWithFileURL:fileURL image:nil];
                if([bgImage readFromURL:fileURL ofType:@"png" error:nil]){
                    NSLog(@"下載成功!");
                    weakSelf.localImageView.image = bgImage.myImage;
                }
                else{
                    NSLog(@"下載失敗");
                }
            }
            
        }];

相關文章