玩转iOS开发:iOS 11 新特性《基于文档管理的App》
文章转至我的个人博客: https://cainluo.github.io/15132142909284.html
众所周知iOS
是一个封闭的系统, 每个App
都有一个属于它们自己的沙盒, App
与App
之间是不可以互相访问的, 也由于这一点, iOS
也可以被称为安全的系统.
但这又会带来另一个问题, 就是在管理文件的方式上比较复杂, 用户无法自由的浏览文件, 我们需要专门的工具与流程才能在iOS
上的应用程序之间共享文件.
为了解决这个问题, 苹果爸爸在iOS 11
里加入了一个用于管理文件的新工具:UIDocumentBrowserViewController
.
这个全新的视图控制器可以让我们创建文档浏览器的App
, 就像iOS 11
上的新的文件App
一样, 可以让我们自由管理所有可用位置上的文档.
PS: 这篇文章所演示的Demo
在模拟器上是不能正常工作的, 最好备好iOS 11
的设备来构建和测试该App
.
转载声明:如需要转载该文章, 请联系作者, 并且注明出处, 以及不能擅自修改本文.
开始前的知识补充
在开始创建这个项目之前, 我们要了解iOS Document
的工作原理, UIDocument
是一个几乎运行在所有基于文档的应用的强大类.
UIDocument
是一个物理文件的包装, 它可以在我们的设备, iCloud
上异步读取/写入文件, 自动保存, 文件版本控制等等.
虽然我们可以不用强制性使用UIDocument
, 但我还是强烈建议去使用, 因为它可以帮我们省下很多事情, 比如我们需要在iPhone
和MacBook
上使用同一个iCloud
文件, UIDocument
就可以帮我们处理所有的事情.
文档提供者
提供文档的App
扩展允许其他App
在你的文件上进行修改, 避免沙盒化.
而UIDocumentPickerViewController
就是该扩展的一部分, 这是其他App
将显示你的App
中选择文档的可视化界面.
提供文档的App
允许其他App
从它本身内导入文件, 这也就说明这个文件会被拷贝一份, 而原始文件则会保持不变.
当然我们也可以允许其他App
直接打开提供文档的App
, 直接选择文件进行处理并覆盖源文件.
文档浏览器
这里我们就讲UIDocumentBrowserViewController
, 它是一种App
的类型, 并不是我们所写的App
的扩展名.
我们定制的UIDocumentBrowserViewController
子类必须是App
的根视图, 换一句话说, 这就是iOS 11
文档类型App
的自定义实现.
开始写代码
这里我们就拿颜色管理来作为场景, 在文件浏览器上去管理这些RGB
颜色值, 它的扩展名也定义为.color
.
我们在创建好工程之后, 需要在Info.plist
里把UISupportsDocumentBrowser
设置为YES
.
自定义扩展
我们在使用.color
扩展名的文件时, 需要用逗号分隔RGB
颜色值, 比如白色的话, 我们就会声明为255,255,255
.
在此, 如果要使得文档浏览器中支持自定义的.color
扩展名, 我们需要注册一个新的同意类型标识符(UTI这个东西在上一章文章的末尾就有介绍, )
, 然后我们需要将新的文件与我们的App
关联.
打开我们的项目找到Info
这个选项, 然后把我们自定义的内容填好:
Exported UTIs
所填写的内容:
- Description: 文件名的描述, 这里输入
RGB Color File
- Identifier: 文件唯一标识符, 这里我们输入为
com.cainluo.colorExtension
- Conforms To:
UTI
可以符合其他类型, 就好像父类子类一样, 这里我们就写public.text
, 因为我们的.color
也是简单的文本类型, 如果你是HTML
的话, 那你可以写成public.html
.
写完这个之后, 我们还需要指定扩展名, 展开Additional exported UTI properties
, 填入一个UTTypeTagSpecification
字典, 然后在这个字典里创建一个名为public.filename-extension
的数组, 最后再填入一个color
对象.
定义好这个UTI
之后, 我们需要在Document Types
填入对应的Name
和Types
, Name
就填入Color Extension
, Types
就填入com.razeware.colorExtension
.
如果你想了解更多关于UTI
的内容, 可以去apple.co/2v0FiHO看看.
开始写代码
创建好对应的控制器之后, 我们要设置一下:
self.delegate = self; self.allowsDocumentCreation = YES;
然后遵守UIDocumentBrowserViewControllerDelegate
和UIViewControllerTransitioningDelegate
两个协议, 并且实现它们的代理方法:
#pragma mark - UIViewControllerTransitioningDelegate - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source { return self.documentBrowserTransitionController; } #pragma mark - UIDocumentBrowserViewControllerDelegate - (void)documentBrowser:(UIDocumentBrowserViewController *)controller didRequestDocumentCreationWithHandler:(void(^)(NSURL *_Nullable urlToImport, UIDocumentBrowserImportMode importMode))importHandler { NSURL *url = [[NSBundle mainBundle] URLForResource:@"ColorFile" withExtension:@"color"]; importHandler(url, UIDocumentBrowserImportModeCopy); } - (void)documentBrowser:(UIDocumentBrowserViewController *)controller didImportDocumentAtURL:(NSURL *)sourceURL toDestinationURL:(NSURL *)destinationURL { [self presentColorControllerWithDocument:[[ColorDocument alloc] initWithFileURL:destinationURL]]; } - (void)documentBrowser:(UIDocumentBrowserViewController *)controller failedToImportDocumentAtURL:(NSURL *)documentURL error:(NSError * _Nullable)error { [self showAlertViewControllerWithTitle:@"Failed" message:@"Failed to import"]; } - (void)documentBrowser:(UIDocumentBrowserViewController *)controller didPickDocumentURLs:(NSArray <NSURL *> *)documentURLs { [self presentColorControllerWithDocument:[[ColorDocument alloc] initWithFileURL:documentURLs[0]]]; } - (NSArray<__kindof UIActivity *> *)documentBrowser:(UIDocumentBrowserViewController *)controller applicationActivitiesForDocumentURLs:(NSArray <NSURL *> *)documentURLs { ColorDocument *colorDocument = [[ColorDocument alloc] initWithFileURL:documentURLs[0]]; return @[[[DocumentActivity alloc] initDocumentActivityWithColorDocument:colorDocument]]; }
另外我们还需要配置一些东西, 并且使得可以跳转到我们需要跳转的编辑页面:
#pragma mark - Present Controller - (void)presentColorControllerWithDocument:(ColorDocument *)colorDocument { UIStoryboard *mineStoryboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil]; ColorController *colorController = [mineStoryboard instantiateViewControllerWithIdentifier:@"ColorController"]; colorController.colorDocument = colorDocument; colorController.transitioningDelegate = self; self.documentBrowserTransitionController = [self transitionControllerForDocumentURL:colorDocument.fileURL]; [self presentViewController:colorController animated:YES completion:nil]; }
解释一下代理方法:
- 在成功导入新文档的时候会调用
- (void)documentBrowser:(UIDocumentBrowserViewController *)controller didImportDocumentAtURL:(NSURL *)sourceURL toDestinationURL:(NSURL *)destinationURL;
- 这是在将要呈现新文档时会调用, 它接受一个
ColorDocument
对象, 而ColorDocument
又是UIDocument
的子类, 负责保存和加载色彩文件, 待会我们就会ColorController
里预览和编辑颜色文件.
- (void)documentBrowser:(UIDocumentBrowserViewController *)controller didRequestDocumentCreationWithHandler:(void(^)(NSURL *_Nullable urlToImport, UIDocumentBrowserImportMode importMode))importHandler;
- 在新文档无法导入时的时候就会调用, 比如当我们无权访问
importHandler
回调中传递的文件时.
- (void)documentBrowser:(UIDocumentBrowserViewController *)controller failedToImportDocumentAtURL:(NSURL *)documentURL error:(NSError * _Nullable)error;
- 当用户选择要在文档浏览器中打开文件时, 就会调用该方法,
UIDocumentBrowserViewController
支持打开多个文件, 但我们这里只需要使用一个, 所以就只去第一个.
- (void)documentBrowser:(UIDocumentBrowserViewController *)controller didPickDocumentURLs:(NSArray <NSURL *> *)documentURLs;
预览与编辑界面
打开ColorController
之后, 我们需要设置一下, 看看是否可以读取对应的文件:
- (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; ColorDocument *colorDocument = self.colorDocument; if (!colorDocument) { return; } if (colorDocument.documentState == UIDocumentStateNormal) { [self configControllerUI]; } else { [colorDocument openWithCompletionHandler:^(BOOL success) { if (success) { [self configControllerUI]; } else { [self showAlertViewControllerWithTitle:@"Error" message:@"Can't Open Document"]; } }]; } }
为了可以保存修改后的内容, 这里用了一个保存的方法:
- (IBAction)saveColorModel:(UIButton *)sender { ColorDocument *colorDocument = self.colorDocument; if (!colorDocument) { return; } colorDocument.colorModel = [[ColorModel alloc] initColorModelWithRedValue:self.redSlider.value greenValue:self.greenSlider.value blueValue:self.blueSlider.value]; [colorDocument saveToURL:colorDocument.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:^(BOOL success) { if (success) { [self showAlertViewControllerWithTitle:@"Success" message:@"Saved file"]; } else { [self showAlertViewControllerWithTitle:@"Error" message:@"Failed to save file"]; } }]; }
从其他App中打开
如果我们直接从文件App
中打开颜色文件, 你会发现我们的App
是打开了, 但不会工作.
那是因为我们的App
和.color
扩展名关联, 但我们的编辑页面并没有显示.
由于这个文件是在别的App
里打开的, 所以我们需要在AppDelegate
里处理这个事情:
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options { if (!url.isFileURL) { return NO; } DocumentBrowserController *documentBrowserController = (DocumentBrowserController *)self.window.rootViewController; if (!documentBrowserController) { return NO; } [documentBrowserController revealDocumentAtURL:url importIfNeeded:YES completion:^(NSURL * _Nullable revealedDocumentURL, NSError * _Nullable error) { if (error) { return; } [documentBrowserController presentColorControllerWithDocument:[[ColorDocument alloc] initWithFileURL:revealedDocumentURL]]; }]; return YES; }
自定义文档浏览器
在这里面我们还可以自定义一下我们的文档浏览器样式:
#pragma mark - Custom Document Browser Controller - (void)customDocumentBrowserController { self.view.tintColor = [UIColor colorNamed:@"MarineBlue"]; self.browserUserInterfaceStyle = UIDocumentBrowserUserInterfaceStyleLight; UIDocumentBrowserAction *documentBrowserAction = [[UIDocumentBrowserAction alloc] initWithIdentifier:@"com.cainluo.action" localizedTitle:@"Lighter Color" availability:UIDocumentBrowserActionAvailabilityMenu handler:^(NSArray<NSURL *> * _Nonnull urls) { ColorDocument *colorDocument = [[ColorDocument alloc] initWithFileURL:urls[0]]; [colorDocument openWithCompletionHandler:^(BOOL success) { if (success) { colorDocument.colorModel = [colorDocument.colorModel lighterColorWithToAdd:60]; [self presentColorControllerWithDocument:colorDocument]; } }]; }]; documentBrowserAction.supportedContentTypes = @[@"com.cainluo.colorExtension"]; self.customActions = @[documentBrowserAction]; UIBarButtonItem *aboutButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"About" style:UIBarButtonItemStylePlain target:self action:@selector(openAbout)]; self.additionalTrailingNavigationBarButtonItems = @[aboutButtonItem]; } - (void)openAbout { [self showAlertViewControllerWithTitle:@"关于我们" message:@"Color Document 1.0 by Cain Luo"]; }
这个颜色值是设置在Assets.xcassets
中.
动画控制器
我们可以为演示文稿添加一个动画:
@property (nonatomic, strong) UIDocumentBrowserTransitionController *documentBrowserTransitionController;
#pragma mark - UIViewControllerTransitioningDelegate - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source { return self.documentBrowserTransitionController; }
我们刚刚的那个present
方法里就写好了对应的配置:
colorController.transitioningDelegate = self; self.documentBrowserTransitionController = [self transitionControllerForDocumentURL:colorDocument.fileURL];
自定义活动
最后面我们还可以添加多一个活动列表, 这个时候我们就需要定义一个UIActivity
的自雷, 并且将.color
文件以字符串的形式复制到粘贴板上:
- (instancetype)initDocumentActivityWithColorDocument:(ColorDocument *)colorDocument { self = [super init]; if (self) { self.colorDocument = colorDocument; } return self; } + (UIActivityCategory)activityCategory { return UIActivityCategoryAction; } - (UIActivityType)activityType { return @"ColorBrowserCopy"; } - (NSString *)activityTitle { return @"Color Copy"; } - (UIImage *)activityImage { return [UIImage imageNamed:@"copy_activity_icon"]; } - (BOOL)canPerformWithActivityItems:(NSArray *)activityItems { return YES; } - (void)performActivity { [self.colorDocument openWithCompletionHandler:^(BOOL success) { if (success) { NSString *string = [self.colorDocument stringRepresentation]; if (string) { [UIPasteboard generalPasteboard].string = string; [self activityDidFinish:YES]; } } }]; }
最后我们用一个代理方法设置对应的活动事件:
#pragma mark - Custom Activity - (NSArray<__kindof UIActivity *> *)documentBrowser:(UIDocumentBrowserViewController *)controller applicationActivitiesForDocumentURLs:(NSArray <NSURL *> *)documentURLs { ColorDocument *colorDocument = [[ColorDocument alloc] initWithFileURL:documentURLs[0]]; return @[[[DocumentActivity alloc] initDocumentActivityWithColorDocument:colorDocument]]; }
最终效果
工程
https://github.com/CainLuo/iOS-11-Characteristic/tree/master/6.Document