在iOS平台上,基于iCloud的开发不需要搭建服务器,开发个人软件的同步很方便。很早之前提过iCloud的使用:《iOS开发之iCloud开发(数据与文档的读写删除)》,那时候主要说了字符串的读取和更新,以及文档的读写,现在再来整体完善下大文档的过程。
上个文章的遗漏:没有自动同步
首先在iCloud同步过程中有两个最重要的通知:
NSMetadataQueryDidFinishGatheringNotification
iCloud的内容获取完成NSMetadataQueryDidUpdateNotification
iCloud的数据有更新
在上个文章中,只监听了iCloud的内容数据获取完成,然后手动下载,而在iCloud的数据有更新的时候,没有做任何事情,所以这样的话只能手动下载,并不能监听然后实现自动同步数据。
iCloud同步数据的过程
这张图可以很清晰的表达出真机的同步过程
在理想状态下,如果设备2同步数据到iCloud,那么应该是其他没有同步过的设备获取到更新通知。但是现实真机的情况却是设备2同步数据到iCloud之后,除了其他设备,同样自己也会接收到iCloud数据更新的通知。
这样如果不处理,自己同时监听iCloud更新和内容获取完成后使用openWithCompletionHandler
读取iCloud内容的话,就会出现:读取》更新》读取》更新》读取……这样的死循环。
读取下载iCloud数据到本地
老派的写法
在iOS6和之前的设备时,就是现在网上最多的那些写法就是在获取到数据的时候去停止刷新,更新使用之后再去开启刷新的功能
//获取数据成功
-(void)MetadataQueryDidFinishGathering:(NSNotification*)noti {
//关闭更新
[self.m_metadataQuery disableUpdates]
//读写使用数据
// ......
// ......
//开启更新
[self.m_metadataQuery enableUpdates]
}
现在搜索iCloud的相关文章,这种几年前的写法很常见,而其他的很多就是转帖,居然没有见到几个原创的新的,可能是这个需求不强烈吧。
正确的姿势
disableUpdates中说
Unless you use enumerateResultsUsingBlock: or enumerateResultsWithOptions:usingBlock:, you should invoke this method before iterating over query results that could change due to live updates.
所以既然这么麻烦,直接使用新的api接口即可。通过这两个接口去遍历即可不用调用停止更新,然后再开启更新
enumerateResultsUsingBlock:
enumerateResultsWithOptions:usingBlock:
[self.m_metadataQuery enumerateResultsUsingBlock:^(id _Nonnull result, NSUInteger idx, BOOL * _Nonnull stop) {
//遍历所有的iCloud数据
}];
获取到iCloud数据的结果遍历的时候,我们需要根据文件的状态去更新本地的数据,通过结果可以知道文件的状态
[self.m_metadataQuery enumerateResultsUsingBlock:^(id _Nonnull result, NSUInteger idx, BOOL * _Nonnull stop) {
//遍历所有的iCloud数据
NSMetadataItem*item =result;
//获取文件更新状态
NSURL *fileURL = [result valueForAttribute:NSMetadataItemURLKey];
NSString *fileStatus;
[fileURL getResourceValue:&fileStatus forKey:NSURLUbiquitousItemDownloadingStatusKey error:nil];
}];
文件有下面三个状态
NSURLUbiquitousItemDownloadingStatusNotDownloaded
: 该文件新的数据还没有下载,需要自己调用[[NSFileManager defaultManager] startDownloadingUbiquitousItemAtURL:fileURL error:&error];去下载NSURLUbiquitousItemDownloadingStatusDownloaded
: 有新的版本,马上要下载,这个意思是说iCloud有新的数据了,可能是其他设备同步上去的NSURLUbiquitousItemDownloadingStatusCurrent
: 本地已经是这个文件的最新版本,这时候就要根据之前这个文件的状态去判断,如果之前的文件状态不是NSURLUbiquitousItemDownloadingStatusCurrent
,现在是NSURLUbiquitousItemDownloadingStatusCurrent
,那就是说已经把最新版本的下载到本地了,需要去更新本地的数据。而如果一直都是NSURLUbiquitousItemDownloadingStatusCurrent
,那就说明是这个设备自己更新上去的,这时候就不需要再去更新本地数据了
通过这个文件状态的判断,就是去切断update > read > update > read >……的方式
代码展示
//获取数据成功
-(void)MetadataQueryDidFinishGathering:(NSNotification*)noti {
NSLog(@"iCloud数据查询结束");
NSArray *items = self.m_metadataQuery.results;//查询结果集
if (!self.m_documentUrl) {
NSLog(@"手机系统设置中未开启iCloud或者未登录iCloud账户");
return;
}
if (!items || items.count == 0) {
NSLog(@"iCloud没有数据");
return;
}
//要更新的数据量,比如是三个
self.m_fileNameArray = [NSMutableArray array];
//便利结果
[self.m_metadataQuery enumerateResultsUsingBlock:^(id _Nonnull result, NSUInteger idx, BOOL * _Nonnull stop) {
NSMetadataItem*item =result;
//获取文件名
NSString *fileName = [item valueForAttribute:NSMetadataItemFSNameKey];
//获取文件更新日期
NSDate *date = [item valueForAttribute:NSMetadataItemFSContentChangeDateKey];
//获取文件更新状态
NSURL *fileURL = [result valueForAttribute:NSMetadataItemURLKey];
NSString *fileStatus;
[fileURL getResourceValue:&fileStatus forKey:NSURLUbiquitousItemDownloadingStatusKey error:nil];
NSLog(@"拉取到的信息: %@,%@,%@",fileName,date,fileStatus);
if ([fileStatus isEqualToString:NSURLUbiquitousItemDownloadingStatusDownloaded]) {
// File will be updated soon
[SVProgressHUD showWithStatus:NSLocalizedString(@"iCloud数据正在更新", nil)];
//有其他设备更新了iCloud
self.shouldUpdateLocalData = true;
}
if ([fileStatus isEqualToString:NSURLUbiquitousItemDownloadingStatusCurrent]) {
if (self.shouldUpdateLocalData) {
[self.m_fileNameArray addObject:fileName];
//当三个数据全都是最新的,并且是其他设备更新了iCloud,那就更新这个设备的数据
if (self.m_fileNameArray.count == 3) {
dispatch_async(dispatch_get_main_queue(), ^{
self.shouldUpdateLocalData = false;
[self handleDataWithFileName];
NSLog(@"iCloud数据下载同步完成");
});
}
} else {
//本地是最新的,是本机更新的数据到iCloud
}
}else if ([fileStatus isEqualToString:NSURLUbiquitousItemDownloadingStatusNotDownloaded]) {
NSError *error;
BOOL downloading = [[NSFileManager defaultManager] startDownloadingUbiquitousItemAtURL:fileURL error:&error];
NSLog(@"开始下载: %d",downloading);
//有其他设备更新了iCloud
self.shouldUpdateLocalData = true;
}
}];
}
- (void)handleDataWithFileName {
for (NSString *fileName in self.m_fileNameArray) {
//读取文件内容
HDWEAKSELF;
HDCloudDocument *doc =[[HDCloudDocument alloc] initWithFileURL:[self getUbiquityContainerUrl:fileName]];
if (doc.documentState & UIDocumentStateClosed) {
NSLog(@"打开Document数据");
[doc openWithCompletionHandler:^(BOOL success) {
if (success) {
//将iCloud的数据写到本地
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf writeToFileWithFileName:fileName withData:[NSData dataWithData:doc.myData]];
});
} else {
HDDebugLog(@"下载出错");
if (weakSelf.isShowDownloadProgress) {
[SVProgressHUD showErrorWithStatus:NSLocalizedString(@"下载iCloud数据出错,请检查手机网络", nil)];
}
}
}];
} else if (doc.documentState & UIDocumentStateNormal) {
NSLog(@"Document already opened, retrieving content");
//将iCloud的数据写到本地
[self writeToFileWithFileName:fileName withData:[NSData dataWithData:doc.myData]];
} else if (doc.documentState & UIDocumentStateEditingDisabled) {
NSLog(@"Document editing disabled.");
//将iCloud的数据写到本地
[self writeToFileWithFileName:fileName withData:[NSData dataWithData:doc.myData]];
}
}
}
上传本地的数据到iCloud
上传基本没有什么需要特殊的,更改和写入即可,下面代码可以忽略回调
//上传数据
- (void)uploadDataBase:(NSData *)data completionHandler:(uploadComplete)complete {
NSURL *cloudDataUrl = [self getUbiquityContainerUrl:ListModel_db];;
if (!self.m_documentUrl) {
if (complete) {
complete(false);
}
return;
}
HDCloudDocument *doc = [[HDCloudDocument alloc] initWithFileURL:cloudDataUrl];
if (!data) {
data = [NSData data];
}
doc.myData = data;
[doc updateChangeCount:UIDocumentChangeDone];
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:[cloudDataUrl path]]) {
[doc saveToURL:doc.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:^(BOOL success) {
if (success) {
[doc closeWithCompletionHandler:^(BOOL success) {
if (complete) {
complete(success);
}
}];
} else {
if (complete) {
complete(false);
}
}
}];
} else {
[doc saveToURL:doc.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
if (success) {
[doc closeWithCompletionHandler:^(BOOL success) {
if (complete) {
complete(success);
}
}];
} else {
if (complete) {
complete(false);
}
}
}];
}
}
总结
其他的就不需要多说了,基础的内容在这个文章里面《iOS开发之iCloud开发(数据与文档的读写删除)》,希望对大家有所帮助。谢谢
参考文章
- UIDocument openWithCompletionHandler: forces NSMetadataQuery to update
- UIDocument openWithCompletionHandler not completing on iOS device
- NSMetadataQuery’s update notification interferes with (run loop?)
- iOS应用开发视频教程笔记(十七)iCloud
- Document-Based App Programming Guide for iOS (二)
版权属于:东哥笔记 - DongGe.org
本文链接:https://dongge.org/blog/858.html
自2017年12月26日起,『转载以及大段采集进行后续编辑』须注明本文标题和链接!否则禁止所有转载和采集行为!