iCloud开发实践

Posted 杰嗒嗒的阿杰

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iCloud开发实践相关的知识,希望对你有一定的参考价值。

iCloud开发实践

写在前面

最近在一直在研究iCloud开发相关的东西,觉得是有必要写篇总结来整理一下近段时间的一些学习成果。之前一直听说iCloud服务不友好也不完善,开发难度也相对较大,其实个人觉得貌似也没说错,iCloud在客户端提供的框架相比于其他的功能框架来说,他是被分散到各个框架中,要使用它必须要了解各个部分的功能及使用场合,这样就增加了学习的成本。同时,框架的设计也比较松散,特别是文档同步,虽然提供了灵活功能强大的框架,但是要理清楚还是需要时间去学习和实践。

为了让大家少走弯路,我将iCloud划分了几大功能模块,下面会逐个地讲述每个模块的一些基本使用,同时配备例子进行说明:

准备工作

想要使用iCloud服务我们必须要有一个苹果的开发者账号(99$个人或者企业都可以),然后需要为项目进行一些配置:

  1. 在Xcode中点击项目目录结构的根节点进入项目设置
  2. 在Capabilities页签中找到iCloud一项,然后将对应该项的开关设置为开启状态。
  3. 在iCloud一栏下的有Services和Containers两个小栏目,其中Services中有三个选项,其对应说明如下:
名称说明
Key-value storage键值对的存储服务,用于一些简单的数据存储
iCloud Documents文档存储服务,用于将文件保存到iCloud中
CloudKit云端数据库服务

这三种服务会在后续章节为大家详细进行讲解。然后就是Container这一栏,顾名思义,其实可以简单认为他是用于存放数据的地方,因为每个应用所存放的数据应该是独立的同时也具有沙箱的限制,所以ios为每个应用开辟了一个独立的空间来存放在iCloud的文件或数据,同时也方便从iCloud上同步数据到这个地方。默认情况下,一旦开启iCloud服务,就会创建一个默认的容器,其命名为iCloud + BundleID。如果你不想要使用默认的容器又或者想跟自己开发的其他App共享文件数据,则可以选择Specify custom containers选项,然后在容器列表中选择一个指定的容器,或者点击+号创建一个新的容器。

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

注意:iCloud下面的Steps必须都打上勾才表示正常启用服务,否则需要根据提示检查你的苹果开发者账号中的一些应用设置。

在正式开始前还有一个事情要说清楚的,这里仅仅讨论的是使用iCloud作为登录账号的app,如果你的app有自己的用户系统,那么你还需要将同步的数据进行标识(例如加个系统用户标识来确定那份数据是哪个用户的),然后根据标识进行数据合并。好了,下面可以开始讲述一些具体开发过程(敲代码时间到了~)

Key-value同步

该种方式一般用于同步少量数据或者进行一些配置性质的数据同步。其使用也比较简单,iOS提供了一个NSUbiquitousKeyValueStore的类型来实现相关的操作。它的使用跟NSUserDefaults类似。主要提供以下的功能:

名称说明
defaultStore返回NSUbiquitousKeyValueStore对象,用于Key-value的存取操作
objectForKey:获取指定key的值
setObject:forKey:设置指定key的值
removeObjectForKey:移除指定键值
stringForKey:获取指定key保存的字符串,如果指定key不存在或者对应key保存的值不是NSString类型时则返回nil
setString:forKey:为指定key设置一个字符串
arrayForKey:获取指定key保存的数组,如果指定key不存在或者对应key保存的值不是NSArray类型时则返回nil
setArray:forKey:为指定key设置一个数组对象
dictionaryForKey:获取指定key保存的字典,如果指定key不存在或者对应key保存的值不是NSDictionary类型时则返回nil
setDictionary:forKey:为指定key设置一个字典对象
dataForKey:获取指定key保存的二进制数组,如果指定key不存在或者对应key保存的值不是NSData类型时则返回nil
setData:forKey:为指定key设置一个二进制数组
longLongForKey:获取指定key保存的64位整型值,如果指定key不存在或者对应key保存的值不包含数值时则返回0
setLongLong:forKey:为指定key设置一个64位整型值
doubleForKey:获取指定key保存的浮点数值,如果指定key不存在或者对应key保存的值不包含数值时则返回0
setDouble:forKey:为指定key设置一个浮点数值
boolForKey:获取指定key保存的布尔值,如果指定key不存在则返回NO
setBool:forKey:为指定key设置一个布尔值
synchronize同步数据,将在内存中的数据同步到磁盘中,并上传至iCloud
dictionaryRepresentation该属性会返回载入到内存中保存的key-value字典,如果想要最新的数据则需要先调用synchronize方法

下面我们来举个例子,看看如何使用NSUbiquitousKeyValueStore进行数据同步。

首先,我们在界面中拖入两个按钮,一个用于设置数据,一个用于获取数据,如图所示:

然后在VC中声明一个NSUbiquitousKeyValueStore类型的属性,并且把两个按钮与VC的按钮点击事件进行关联,其中VC代码如下:

@interface KeyValueViewController ()

// Key-value同步数据存储对象
@property (nonatomic, strong) NSUbiquitousKeyValueStore *keyValueStore;

@end

@implementation KeyValueViewController

- (IBAction)setValueButtonClickedHandler:(id)sender

    // 设置值按钮点击事件


- (IBAction)getValueButtonClickedHandler:(id)sender

    // 获取值按钮点击事件

然后在viewDidLoad方法中对keyValueStore进行初始化:

- (void)viewDidLoad

    [super viewDidLoad];
    self.keyValueStore = [NSUbiquitousKeyValueStore defaultStore];

然后分别实现两个按钮的点击事件:

- (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的字符串存取方法,要注意的是当你设置了数据后一定要调用synchronize方法,否则这些设置操作是不会保存下来并且上传到iCloud上的。

该例子最好是能够准备两台设备(或模拟器)来进行测试,一台进行值设置,另外一台进行值的获取。

有时候,我们需要实时知道一些配置的变更,特别是在你有多台设备时(如同时拥有iPhone和iPad),想要在其中一台设备中变更某项信息,然后另外一台设备也能够感知并作出相应的调整。那么,这时候你需要监听NSUbiquitousKeyValueStoreDidChangeExternallyNotification通知,它能够告诉你的App所保存的key-value有变更。

我们将上面的例子进行改造,将设置字符串改为设置一个背景颜色值,并且设定它的key为bg,然后通过监听NSUbiquitousKeyValueStoreDidChangeExternallyNotification通知来改变VC的视图背景颜色。

首先,我们在viewDidLoad中进行监听通知:

- (void)viewDidLoad

    [super viewDidLoad];
    
    // 初始化keyValueStore
    self.keyValueStore = [NSUbiquitousKeyValueStore defaultStore];

    // 监听通知
    [[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这个key,如果存在则表示背景颜色有变更,再从keyValueStore中取出颜色值并转换成UIColor对象并设置成视图的背景颜色。

同时,两个按钮的点击事件处理如下:

- (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;

通过上面的改动,在测试的过程中如果其中一台设备点击了set value按钮,则另外一台设备就会收到通知,并且变更视图的背景颜色。

文档数据同步

有时候会有这样的需求,如果开发的app是一款阅读类工具或者是一款壁纸工具,那么,我们会希望用户所下载的书籍或者壁纸会同步到不同的设备上来方便用户的操作,不需要再次去找到这本书或者这张图片进行重新下载。那么,iCloud提供了这样的服务,允许你把一份文档上传到iCloud中,然后其他设备再同步app上传的文档。

要想使用文档数据同步服务,就需要配合UIDocument来完成这项工作,具体的处理流程我先简单的描述一下,这样可以快速帮助到大家来理解机制的运作。

  1. UIDocument创建一个子类,该类型主要对app的中的文档进行管理。
  2. 重写UIDocumentcontentsForType:error:loadFromContents:ofType:error:方法,让文档根据app内部机制来实现保存和读取。
  3. 通过UIDocumentsaveToURL:forSaveOperation:completionHandler:将文档保存到iCloud容器中。
  4. 其他设备可以NSMetadataQuery来获取iCloud容器的文档列表,并更新到本地。

这里要注意一个问题,因为涉及到网络同步等相关的一些列操作,并不仅仅是当前应用进程在访问文件,系统的进程和其他应用进程也会对相关文件进行处理,所以不能通过NSFileManager直接对iCloud容器中的文件进行操作

同时也要弄清楚一个概念,其实UIDocument并不是为iCloud而设,它同样可以管理本地的文档。唯一区别是如果你的文档要放到iCloud,那么传给UIDocument的文档路径必须是以iCloud容器地址开始的路径,这样才能实现文档的同步

那么,下面来举例介绍如何进行文档数据的同步,假设开发的app是一款壁纸应用,在应用壁纸时会下载图片,然后讲它保存到iCloud中。另外的设备就可以自动地同步下载的图片并应用该壁纸。

首先把界面给搭建起来,如下图所示:

界面是使用UICollectionView搭建的,代码在这里就不贴上来了,主要关注设置图片背景的处理过程。

首先,继承UIDocument类型创建其一个子类BackgroundImage,并为BackgroundImage声明一个传入UIImage对象的构造方法以及重写contentsForType:error:loadFromContents:ofType:error:两个方法代码如下:

@interface BackgroundImage : UIDocument

// 图片对象
@property (nonatomic, strong, readonly) UIImage *image;

// 构造方法
- (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类型进行返回,否则会抛出错误提示:

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:方法来解决这个问题。

接下来,要取到iCloud容器的地址,在VC中新增一个方法用来获取容器路径:

- (NSURL *)icloudContainerBaseURL

    if ([NSFileManager defaultManager].ubiquityIdentityToken)
    
        return [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
    
    
    return nil;

上面代码先检测设备是否登录iCloud账号,NSFileManagerubiquityIdentityToken如果不为nil则表示已经登录账号。然后再通过URLForUbiquityContainerIdentifier:方法来获取容器的地址,参数可以传入容器的名称(即在项目配置时设置的容器,如:iCloud.cn.vimfung.app.iCloudDemo),传入nil则表示返回容器数组中的第一个容器。

如果URLForUbiquityContainerIdentifier:返回nil则表示iCloud服务不可用。

然后,就可以实现应用图片按钮的功能了,代码如下:

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath

    // 取得Cell对象
    BgCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath];
    // 让Cell现实图片
    cell.url = self.imageURLs[indexPath.row];
    
    // 应用图片按钮的点击事件回调
    __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服务不可用,可根据需求进行相关处理");
        
        
        // 将图片应用到CollectionView背景中。
        UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
        imageView.contentMode = UIViewContentModeScaleAspectFill;
        collectionView.backgroundView = imageView;
        
    ];
    
    return cell;

上述代码中,主要看cellonApply:回调方法(该方法是一个自定义的方法,主要是用于将点击应用图片按钮的事件回调到VC中),整个处理流程是先获取容器地址,如果地址不为空,就创建一个BackgrounImage的文档对象,然后再调用对象的saveToURL:forSaveOperation:completionHandler:方法来对文档进行保存,这样就完成了文档上传的操作。

对于保存方法,我一直有个想不明白的地方就是初始化的时候已经传入了URL,为什么还需要传入一个NSURL对象来确定保存的路径,这真的让人摸不着方向。

不过现在我把这两个URL区分对待了,初始化传入的URL是用于打开文档时使用(UIDocument的open方法是不需要传URL的),而保存的方法中的URL就仅仅针对保存目标路径而言,如果路径与fileURL相同那就是更新文件,如果不同那就是拷贝文档了。

最后,如果想要在其他设备上同步背景图片,那么就需要在viewDidLoad里面同步处理,主要就是使用NSMetadataQuery来查找背景文件,如果背景文件存在加载为背景。具体代码如下:

- (void)viewDidLoad

    [super viewDidLoad];
    
    // 进行文档同步
    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)
                    
                        //设置背景
                        UIImageView *imageView = [[UIImageView alloc] initWithImage:bgImage.image];
                        imageView.contentMode = UIViewContentModeScaleAspectFill;
                        theController.collectionView.backgroundView = imageView;
                    
                    
                ];
            
            
            query = nil;
            
        ];
        
        [query startQuery];
    

这里要解释的是NSMetadataQuerysearchScopespredicate两个属性,通常用这两个属性可以完成简单的查找工作:

  • searchScopes属性主要用来告诉NSMetadataQuery一个有效的查询范围。它是一个数组类型,元素可以包含NSURL或者NSString类型,其中NSURL要求是一个目录路径,表示需要查找的目录,而NSString则必须为下表的取值。如果属性为nil则从所有目录中进行查找.
名称说明
NSMetadataQueryUbiquitousDocumentsScope指定该key表示在iCloud容器的Documents目录下进行文件查询
NSMetadataQueryUbiquitousDataScope指定该key表示在iCloud容器根目录进行文件查询
NSMetadataQueryAccessibleUbiquitousExternalDocumentsScope指定该key表示除应用程序容器目录外的所有可访问目录(如iCloud容器目录等)中进行文件查询
  • predicate主要用于匹配查找文件的条件,其中条件筛选可以与NSMetadataItem中的attribute keys相结合,上面代码就是使用NSMetadataItemFSNameKey来找到名称为image.jpg的背景图片。更多的属性可以参考下表:
名称说明
NSMetadataItemFSNameKey文件名称
NSMetadataItemDisplayNameKey显示名称,不包含扩展名,跟文件名称可能不一样
NSMetadataItemURLKey文件URL,以file://开头,为NSURL类型
NSMetadataItemPathKey文件的绝对路径,为NSString类型
NSMetadataItemFSSizeKey文件大小,单位为字节
NSMetadataItemFSCreationDateKey文件创建时间,为NSDate类型
NSMetadataItemFSContentChangeDateKey内容最后一次变更时间,为NSDate类型
NSMetadataItemContentTypeKeyNSMetadataItem的内容类型,为UTI字符串
NSMetadataItemContentTypeTreeKey这个官网并没有详细的说明,但从一些其他资料了解,这可能是表示NSMetadataItem的内容类型的从属链,返回的数组最后一个元素就是当前内容的类型,再往上就是这个类型的父类型,再往上就是父级的父级类型,直到第一个元素就是根类型(跟类继承类似)。例如:一张jpg图片返回的内容如下:["public.item", "public.data", "public.image", "public.jpeg", "public.content"]
NSMetadataItemIsUbiquitousKey一个布尔值表示是否上传到iCloud中,类型为NSNumber
NSMetadataUbiquitousItemHasUnresolvedConflictsKey一个布尔值表示当前文件与该文件其他版本发生冲突,如果该属性值为YES则需要先解决文件的冲突部分才能正常更新到iCloud上,其类型为NSNumber
NSMetadataUbiquitousItemIsDownloadedKey一个布尔值表示文件是否已经下载到本地并且可用,其类型为NSNumber。iOS 7后使用NSMetadataUbiquitousItemDownloadingStatusKey来代替。
NSMetadataUbiquitousItemDownloadingStatusKey使用NSString来表示文件的下载状态,下载状态取值如下:NSMetadataUbiquitousItemDownloadingStatusNotDownloaded 表示尚未下载、NSMetadataUbiquitousItemDownloadingStatusDownloaded 表示已下载、NSMetadataUbiquitousItemDownloadingStatusCurrent 表示是文件的最新版本
NSMetadataUbiquitousItemIsDownloadingKey一个布尔值表示文件是否开始正在下载到本地,类型为NSNumber
NSMetadataUbiquitousItemIsUploadedKey一个布尔值表示文件是否已经上传到iCloud中,类型为NSNumber
NSMetadataUbiquitousItemIsUploadingKey一个布尔值表示文件是否正在上传到iCloud中,类型为NSNumber
NSMetadataUbiquitousItemPercentDownloadedKey当前下载进度,范围为0.0 - 100.0,类型为NSNumber
NSMetadataUbiquitousItemPercentUploadedKey当前上传进度,范围为0.0 - 100.0,类型为NSNumber
NSMetadataUbiquitousItemDownloadingErrorKey表示下载过程中产生的错误信息描述,类型为NSError
NSMetadataUbiquitousItemUploadingErrorKey表示上传过程中产生的错误信息描述,类型为NSError
NSMetadataUbiquitousItemDownloadRequestedKey其包含一个布尔值,用于表示MetadataItem是在否已经开始下载。YES表示已经开始请求下载,NO表示正在等待下载。其类型为NSNumber
NSMetadataUbiquitousItemIsExternalDocumentKey用于判断是否为应用容器外的文件,类型为NSNumber
NSMetadataUbiquitousItemContainerDisplayNameKey文件所处iCloud容器的显示名称,类型为NSString
NSMetadataUbiquitousItemURLInLocalContainerKey文件所处iCloud容器的本地URL,类型为NSURL
NSMetadataUbiquitousItemIsSharedKey包含一个布尔值,YES表示为共享文件。
NSMetadataUbiquitousSharedItemCurrentUserRoleKey返回共享文件的当前用户角色。如果返回nil则表示尚未共享。取之如下:NSMetadataUbiquitousSharedItemRoleOwner 表示共享文件的所有者、NSMetadataUbiquitousSharedItemRoleParticipant 表示共享文件的参与者
NSMetadataUbiquitousSharedItemCurrentUserPermissionsKey返回共享文件的当前用户权限, 如果返回nil则表示尚未共享。取值如下:NSMetadataUbiquitousSharedItemPermissionsReadOnly 表示当前用户具有只读权限、NSMetadataUbiquitousSharedItemPermissionsReadWrite 表示用户具有读写权限
NSMetadataUbiquitousSharedItemOwnerNameComponentsKey返回共享文件的所有者信息,其类型为NSPersonNameComponents。如果所有者为当前用户则返回nil
NSMetadataUbiquitousSharedItemMostRecentEditorNameComponentsKey返回共享文件的最新编辑者信息,其类型为NSPersonNameComponents,如果最新编辑者为当前用户则返回nil,该属性只读。

接着再调用NSMetadataQuerystartQuery方法来进行查询操作。最后通过监听NSMetadataQueryDidFinishGatheringNotification通知来捕获查询完成事件来检测是否找到背景图片,如果存在图片则通过BackgroundImage来加载图片并将它作为UICollectionView的背景视图。

这里要注意的是查询操作只能在应用激活时调用并执行,因此如果应用退到后台,则需要使用stopQuery来停止查询,等待应用恢复后在重新调用startQuery来进行查询。

上面的代码只实现了在视图加载将背景同步到本地并显示,如果你想在应用运行时也能够监控到背景图片的变更,那么就需要把NSMetadataQuery保留起来,让他生命周期与应用生命周期一样,然后通过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以上是关于iCloud开发实践的主要内容,如果未能解决你的问题,请参考以下文章

iOS开发-iCloud的简单使用(1)-配置iCloud环境以及key-value storage的使用

识别 iCloud coreData 更新:良好实践

-initWithDocumentTypes iCloud 仅在生产版本中崩溃

ios - 每次触发 application:openURL:sourceapplication 时,iCloud 备份大小都会增加

TestFlight 开发和生产

我可以授予我的应用程序对用户 iCloud 中所有数据的权利吗?