上一篇Key-value说的是以键值对的形式存入,但是这种存储形式是有限制的,不能进行大数据的存储,最多只能是1MB。
这一篇,通过官方介绍,获取到一个词:文档存储。觉得完全可以理解为是针对文档进行操作的,可以将文件或目录写入iCloud。
官方介绍
ps:
这里有一个问题,想着先理清楚比较好一些。
1.iCloud只是一片存储空间,完全可以通过任何方式对这篇空间进行数据的增 删 改 查
2.下文提到使用到的UIDocument NSFileManager NSFileWarpper。只是希望可以更加方便且安全的做到,针对数据的增 删 改 查
官方介绍中涉及到几个词:文件协调与文件展示。
文件协调:解决的是云端与本地数据的同步问题。关于这个官方推荐我们使用UIDocument这个类的,这个类有一个好处,会自动去协调云端与本地,而且在上传和更新文件的时候,还会去检测各设备相应文件针对更改是否会有冲突,是安全的。
文件展示:就字面意思吧,这个自己设计就好。ps:遵守了NSFilePresenter协议的对象,理解成一个文件展示器,可以监听文件的变更。关于这个,配合NSFileCoordinator使用,看看NSFileCoordinator这个类,可以嵌套学习下
关于iCloud数据的传输,是分两步的且分不同情况的(这个可以看官方的图文介绍,很清晰):
1.首次传输数据到iCloud:>1.传输文档的元数据,其中包括文档名称,修改日期,文件大小和文件类型等信息 >2.传输文档数据(ps:完整的数据)
2.更改文件:>1.协调文档的元数据 >2.传输文档数据(ps:更新的部分)
3.首次协调文件到本地:>1.协调文档的元数据,其中包括文档名称,修改日期,文件大小和文件类型等信息 >2.传输文档数据(ps:完整的数据)
4.协调到本地更改:>1.协调文档的元数据 >2.传输文档数据(ps:更新的部分)
在协调更改本地文档的时候,当收到元数据后,设备会在适当的时间自动提取更改。
文档冲突:这里就直接抄录官方的一段介绍。
在iOS中,根据需要响应并解决文档版本冲突。当在两个不同设备上运行的iOS应用程序的两个实例尝试更改文档时,会发生冲突。例如,如果两个设备未连接到网络,用户对两个设备进行更改,然后将两个设备重新连接到网络,就会发生这种情况。使用NSFileVersion对象向您的应用报告冲突。iOS文档体系结构通过提名获胜NSFileVersion
对象来管理冲突解决,但是iOS应用程序有责任接受建议的版本或指定其他版本。你可以在大多数时候依靠这种自动提名; 但是,您的应用应准备好根据需要提供帮助。当iOS文档的状态发生变化时,它会发布UIDocumentStateChangedNotification通知。收到此通知后,查询文档的documentState属性并检查值是否为UIDocumentStateInConflict。如果确定需要显式解析,请使用NSFileVersion该类解决冲突。如有必要,请寻求用户的帮助; 但在可能的情况下,解决冲突而无需用户参与 请记住,在连接到同一iCloud帐户的其他设备上运行的另一个应用实例可能会在本地实例执行之前解决冲突。
完成解决冲突后,请务必删除任何过时的文档版本; 如果不这样做,则会在用户的iCloud存储中不必要地消耗容量。
ps:个人理解:
1.系统会自动帮你解决:仔细看会看到自动提名
这个词
2.可以自己处理这种冲突,如果需要做一些额外的处理
现在看看我们要用到的类,这里介绍性文字,只介绍在本章干了什么事。其实还有很多强大的功能,具体看官方介绍结合实践了解更多
1.UIDocument:主要进行文档的存储与提取
2.NSFileManager:空间路径的获取 文档的删除
+++>PS:NSFileManager之前写了一些使用性的API总结 --> 链接
3.NSFileCoordinator:文件协调器,本章用来配合NSFileManager删除文档
4.NSFileWrapper:目录的包装
还是简单介绍下用到的类:
>1.UIDocument
这个就是负责文件读取和存储的类,并不是说是针对iCloud的,可以用在其它地方。
建议学习的时候,可以将初始化的存储空间设置为沙盒。可以看的更清晰,方便验证。
在官方介绍里,可看到在利用UIDocument进行文档操作的时候,是需要使用子类的。
在子类注释的描述中,可以看到子类的基本要求还是比较简单的,需要重写两个方法:
>- (nullable id)contentsForType:(NSString *)typeName error:(NSError **)outError;//写入操作
>- (BOOL)loadFromContents:(id)contents ofType:(nullable NSString *)typeName error:(NSError **)outError;//读取操作
>2.NSFileManager
这里可以做的事情还是蛮多的,可以获取到iCloud空间路径,也可以针对某个文档进行删除操作。
同样,这是一个操作文件/目录的类。并不针对iCloud,可以使用在其它地方,
学习 验证的时候,尽量找一个方便的空间。例如:沙盒空间,主要是查看起来很方便
>3. NSFileCoordinator
用来协调文件和目录的读写,协调在同一进程中读取和写入多个进程和对象之间的文件和目录。通知文件演示者(遵守了NSFilePresenter协议的对象),文件的相关修改。
本章,用来配置NSFileManager进行文档的删除
>4.NSFileWarpper
这个玩意,在个人demo里面占的分量还挺重的,主要是进行数据的包装。
这里涉及到的问题是,不愿意直接将文档存储到iCloud的根目录下
UIDocument初始化提供的URL地址必须是一个确定存在的路径,然后可以在后面拼接一层,指定文档/目录存储的位置
那么,如果想要配置更深层次怎么办,这个时候请参考NSFileWrapper的相关使用。结合UIDocument的contentsForType:error 方法
可以很方便的进行处理,个人也是建议最好把所有的数据都用NSFileWrapper包一层,可以统一数据类型,方便以后的解析
接下来,进入代码阶段
1.首先是针对UIDocument配置一个子类
======>.h
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface PYHDocument : UIDocument
/*
这里有一个点:你是否愿意把文档数据直接放到iCoud的根目录下,而不是自定义目录
个人是希望,将文档数据放到自定义的目录的,所以这里folderName是自定义目录名
这里是一个建议性字段 个人把文件夹名直接拼到了初始化的时候 initWithFileURL:
ps:这里个人只做了一层 可以做多层
例:
document/myApp/test.txt
document/myApp/page1/test.txt
关键点在于:
1.UIDocument初始化时的url必须是已知路径,最多在后面拼接一层
2.在确定了UIDocument可以存储的位置之后,想要更深层级储存。请在 contentsForType:error: 方法中使用NSFileWrapper进一步定制
*/
//@property (nonatomic, copy) NSString *folderName;
//文档的二进制数据
@property (nonatomic, strong) NSData *saveData;
//文档存入空间的名字
@property (nonatomic, copy) NSString *saveFileName;
@end
NS_ASSUME_NONNULL_END
=======>.m
#import "PYHDocument.h"
@interface PYHDocument()
//存储空间地址
@property (nonatomic, strong) NSURL *container;
@end
@implementation PYHDocument
- (instancetype)initWithFileURL:(NSURL *)url {
if (self = [super initWithFileURL:url]) {
self.container = url;
}
return self;
}
- (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError * _Nullable __autoreleasing *)outError {
//提取数据 这里的数据解析和存入数据的时候配套
NSLog(@"%@-%@",contents,typeName);
//1.根据存入处理 这里得到取出数据格式 严谨一些当然是进行相关判断
NSFileWrapper *apper = (NSFileWrapper *)contents;
//2.对数据进行解析
[apper.fileWrappers enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSFileWrapper * _Nonnull obj, BOOL * _Nonnull stop) {
NSLog(@"%@-%@",obj.regularFileContents,obj.preferredFilename);
}];
return YES;
}
- (id)contentsForType:(NSString *)typeName error:(NSError * _Nullable __autoreleasing *)outError {
//写入数据
NSLog(@"%@",typeName);
//1.获取目录
NSFileWrapper *wrapper = [[NSFileWrapper alloc] initWithURL:self.container options:NSFileWrapperReadingImmediate error:nil];
//1-1.目录获取失败 证明没有指定目录 那么创建一个
if (!wrapper) {
wrapper = [[NSFileWrapper alloc]initDirectoryWithFileWrappers:@{}];
}
//2.配置储存文档及文档名
if (self.saveData && self.saveFileName) {
//这里遇到的问题是重复文件不会覆盖,然后系统进行了文件名的处理。使目录中,存入了同样的数据。
NSFileWrapper *preWrapper = [wrapper.fileWrappers objectForKey:self.saveFileName];
if (preWrapper) {
[wrapper removeFileWrapper:preWrapper];
}
NSFileWrapper *dataWrapper = [[NSFileWrapper alloc] initRegularFileWithContents:self.saveData];
dataWrapper.preferredFilename = self.saveFileName;
[wrapper addFileWrapper:dataWrapper];
}
return wrapper;
}
@end
2.这里,把相关的增删改查写一起了,为了看起来方便,复制到项目中的时候,记得把增删改查的相关代码单独拎出来,还算蛮清晰的。相关说明行文字,在注释里面有。还有下面的代码,是一整个.m文件的复制
#import "DocumentsView.h"
#import "PYHDocument.h"
@interface DocumentsView()
//用来查询的类
@property (nonatomic, strong) NSMetadataQuery *query;
@end
@implementation DocumentsView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.backgroundColor = [UIColor yellowColor];
self.query = [[NSMetadataQuery alloc]init];
//注册iCloud可用性更改通知 iCloud登录状态的改变
// [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(iCloudAccountAvailabilityChanged:) name:NSUbiquityIdentityDidChangeNotification object:nil];
//查询的时候 实现的两个通知 还有其他状态 可以进入官方文档查看
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(finishedGetNewDocument:) name:NSMetadataQueryDidFinishGatheringNotification object:self.query];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(startGetNewDocument:) name:NSMetadataQueryDidStartGatheringNotification object:self.query];
}
return self;
}
- (void)startGetNewDocument:(NSNotification *)noti {
NSLog(@"==%@",[(NSMetadataQuery *)noti.object results]);
}
- (void)finishedGetNewDocument:(NSNotification *)noti {
//查询到了数据 数组里面都是NSMetadataItem
NSArray *results = [(NSMetadataQuery *)noti.object results];
[results enumerateObjectsUsingBlock:^(NSMetadataItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
/*
这里通过attributes这个属性 你可以查看到所有的参数
文件名:NSMetadataItemFSNameKey
文件的地址:NSMetadataItemURLKey
*/
//这里返回的是一个id类型的 注意一下
NSString *name = [obj valueForKey:NSMetadataItemFSNameKey];
NSString *url = [obj valueForKey:NSMetadataItemURLKey];
NSLog(@"%@-%@",name,url);
}];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
/*
step 1:
获取iCloud令牌
ps:
1.从应用的主线程访问此属性。
2.该属性的值是表示当前活动的iCloud帐户的唯一标记。您可以比较令牌以检测当前帐户是否与以前使用的帐户不同
3.如果用户在设备上启用飞行模式,则iCloud本身将无法访问,但当前的iCloud帐户仍保持登录状态。即使在飞行模式下,
该ubiquityIdentityToken属性也包含当前iCloud帐户的令牌
4.如果用户退出iCloud,例如关闭“设置”中的“文档和数据”,则ubiquityIdentityToken属性的值将更改为nil。
要检测用户何时登录或退出iCloud -->注册监听这个通知NSUbiquityIdentityDidChangeNotification,登录状态改变会系统发出通知
*/
NSFileManager * fileManager = [NSFileManager defaultManager];
id currentiCloudToken = fileManager.ubiquityIdentityToken;
NSLog(@"iCloud令牌:%@",currentiCloudToken);
/*
step 2:
判断在用户默认数据库中存档iCloud可用性
即判断iCloud是否开启
当然你还可以进行自定义的判断:例如判断本次的iCloud令牌是否与上次用户的令牌一致,说白了就是判断这次登录iCloud的账户和上次是不是用一个
官方文档提供了方案,具体怎么处理,看具体项目,先看官方介绍。look:
if(currentiCloudToken){
NSData * newTokenData =
[NSKeyedArchiver archivedDataWithRootObject:currentiCloudToken];
[[NSUserDefaults standardUserDefaults] setObject:newTokenData forKey:@“com.apple.MyAppName.UbiquityIdentityToken”];
} else {
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@“com.apple.MyAppName.UbiquityIdentityToken”];
}
ps:以下,并没有做多余的判断,只进行是否可用的判断
*/
if(currentiCloudToken) {
/*
step 2-1:
用户登录了iCloud
但需要注意的是iCloud并不一定是可使用的。例如飞行模式,虽然可以获取到令牌,但是并不能上传文档到iCloud。
还有一个点,这里建议用异步线程去进行数据的相关操作。针对数据操作都是耗时的,这里也是官方文档提供的样例
ps:
关于飞行模式,无法将数据上传到iCloud的情况
理解:
无法上传的是iCloud云盘,因为需要进行网络请求。但是数据可以上传到本地的iCloud空间,等联网之后会进行数据上传。
这里就有一个点了,是数据冲突的问题。如果只是单设备,那没问题,多设备的话,请做好文档冲突的相关准备工作。
关于文档冲突,这里也提一嘴,iOS系统具有自己解决的能力,具体看官方介绍
*/
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0),^(void){
/*
这里有个参数identifier,iOS中可以设置三种,这个在下面的查询中有用到
链接文章有介绍这几个值的含义:https://developer.apple.com/documentation/foundation/nsmetadataquery/metadata_query_search_scopes?language=objc
NSMetadataQueryUbiquitousDocumentsScope
NSMetadataQueryUbiquitousDataScope
NSMetadataQueryAccessibleUbiquitousExternalDocumentsScope
*/
NSURL *myContainer = [[NSFileManager defaultManager]
URLForUbiquityContainerIdentifier:NSMetadataQueryUbiquitousDataScope];
if (myContainer != nil) {
NSLog(@"应用可以写入iCloud容器,iCloud容器地址:%@",myContainer);
NSURL *cloudUrl = [myContainer URLByAppendingPathComponent:@"myAPP"];
/*
step 3: 文档的增 删 改 查
增 依赖于UIDocument。最好是使用UIDocument,也可以使用其他的。但是使用UIDocument的原因:自动协调与安全。
删 改 可以依赖于NSFileManager
查 使用这个类NSMetadataQuery
ps:以下,增 删 改 查,都写在这里了,请根据实际情况进行配置
*/
// //========> step 3-1:增
//
// /*
// 这里啊,是需要用到UIDocument的子类的。子类中最起码还得重写两个方法,这个看子类
// 参数url:数据要写入的位置
// */
// PYHDocument *document = [[PYHDocument alloc]initWithFileURL:cloudUrl];
// //要保存的数据
// document.saveData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"test.txt" ofType:nil]];
// document.saveFileName = @"test.txt";
//
// /*
// url:这里其实和document实例中的fileurl是一样的,都是数据要写入的位置
// operation: UIDocumentSaveForCreating 进行新建操作
// */
// [document saveToURL:document.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
// NSLog(@"%@",success ? @"yes" : @"no");
// }];
// //========> step 3-2:删
// NSFileManager *manager = [NSFileManager defaultManager];
// NSFileCoordinator *coor = [[NSFileCoordinator alloc]initWithFilePresenter:nil];
// [coor coordinateReadingItemAtURL:cloudUrl options:NSFileCoordinatorReadingWithoutChanges error:nil byAccessor:^(NSURL * _Nonnull newURL) {
// NSLog(@"%@",newURL);
//
// //newURL获取的是到myApp这个目录点 结合前面本章配置的目录.../Documents/myAPP/
// //获取指定目录下的所有一级url
// NSArray *urls = [manager contentsOfDirectoryAtURL:newURL includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsHiddenFiles error:nil];
// [urls enumerateObjectsUsingBlock:^(NSURL * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
// NSLog(@"%@",obj);
// if (idx == 0) {//验证一下 看看删除成功没
// NSError *error;
// BOOL result = [manager removeItemAtURL:obj error:&error];
// if (result) {
// NSLog(@"++删除成功");
// }else {
// NSLog(@"++删除失败,error-%@",error);
// }
// }
// }];
// }];
// //========> step 3-3:改
// /*
// 这一块,怎么改。貌似没看到有什么提示性介绍。
// 想法么:就是删除原有的,然后将新的放上去。原有的数据可以通过iCloud路径去获取就好了
// 个人感觉,官方设计iCloud的初心,侧重应该不是去修改数据,就只存储和提取。
// 官方是建议我们使用UIDocument去进行新建,NSMetadateQuery查询数据,NSFileManager(NSFileCoordinator配合)移动,复制和删除数据
// */
//========> step 3-4:查
/*
1.searchScopes 值比较固定,目前好像就只有三个值
2.predicate 谓词,这里我设置的全部文件
3.查询的开启,一定要放到主线程
*/
self.query.searchScopes = @[NSMetadataQueryUbiquitousDataScope];
[self.query setPredicate:[NSPredicate predicateWithFormat:@"%K LIKE '*'", NSMetadataItemFSNameKey]];
__weak typeof(self) wself = self;
//这里要回到主线程 开启搜寻 不然你会发现不管你怎么弄都不会有通知的触发
dispatch_async(dispatch_get_main_queue(),^(void){
//主线程
if ([wself.query startQuery]) {
NSLog(@"开始查询");
}else {
NSLog(@"开始查询失败");
}
});
}else {
NSLog(@"无法获取iCloud容器");
}
});
} else {
/*
step 2-2:
这里,标明用户没有开启iCloud的功能
邀请用户使用iCloud,跳转到设置中
ps:过期方法 自己注意。这里就做了一个简单的提示
*/
UIAlertController *ac = [UIAlertController alertControllerWithTitle:@"iCloud不可用" message:@"请打开iCloud以保持文稿安全存入iCloud中" preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
}];
UIAlertAction *sureAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString];//这里是跳转到应用的设置界面了,具体怎么设置请查询相关知识点。
if ([[UIApplication sharedApplication] canOpenURL:url]) {
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
}
}];
[ac addAction:cancelAction];
[ac addAction:sureAction];
//这步跳转,仅简单获取控制器,demo比较简单。不可参考这里在具体项目中实现
[[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:ac animated:YES completion:nil];
}
}
@end