从iOS 8 开始Apple引入了扩展(Extension)用于加强系统应用服务和应用之间的交互。它的出现让自定义键盘、系统分享集成等这些依靠系统服务的开发变成了可能。WWDC 2016上众多更新也都是围绕扩展这一主题来进行了的,例如开发的Siri、iMessage Apps其实都是依靠扩展来工做的。在最新的Xcode 8 beta中也增长了众多的Extension 模板帮助开发者更快的实现不一样类型的扩展。所以今天有必要介绍一下扩展相关的开发内容。html
iOS对于扩展的支持已经由最初的6类到了现在iOS10的19类(相信随着iOS的发展扩展的覆盖面也会愈来愈广),固然不一样类型的扩展其用途和用法均不尽相同,可是其工做原理和开发方式是相似的。下面列出扩展的几个共同点:ios
扩展一般展示在系统UI或者其余应用中,运行应该尽量的迅速而功能单一;swift
因为目前iOS 10正式版还没有发布,官方文档仅就目前9类扩展作了详细指导说明,感兴趣的话你们能够前往查看。
官方对于应用扩展的生命周期描述以下图:数组
一般用户选择了一个扩展的操做时宿主会向扩展发出一个请求来启动此扩展,扩展的生命周期也由此开始(例如用户在分享菜单中选择了你的分享扩展),因为扩展自己由控制器组成,所以此时就会调用相似于viewDidLoad之类的方法进行界面布局和逻辑处理,执行完相应任务以后应该尽快将控制权交给宿主应用,扩展生命周期结束。缓存
尽管扩展和容器应用的生命周期之间没有直接关系,可是扩展自己就是做为容器应用的扩展而存在的,所以扩展和容器应用之间的交互又是不可避免的。一般扩展会经过自定义Scheme的形式来调用容器应用,而容器应用完成响应操做以后经过数据共享将数据共享给扩展来使用。微信
前面说过目前iOS支持19类扩展入口,如今就以Today扩展(也叫作Widget)为例进行说明,在开始以前先对Today扩展有一个简单的认识,下图是微博、墨迹天气、网易云音乐的的Today扩展截图,微博扩展能够用来发送微博、查看更新,墨迹天气则用来展现今日和明日的天气,网易云音乐则是推荐一些相关的歌单、专辑。网络
咱们今天的例子将利用Today扩展实现一个简单的“to do list”查看功能,在容器应用ToDoList中能够增长和删除待办事项,而Today插件则展现最新的几条待办事项,若是没有待办事项则展现添加按钮,点击添加或列表则导航到ToDoList应用。应用的主界面和Today扩展最终截图以下:session
在开发以前首先思考一下要实现一个这样的ToDoList扩展须要注意哪些问题:app
NS_EXTENSION_UNAVAILABLE
,其实思考一下也是合理的,扩展中的UIApplication是宿主应用并不是容器应用,若是开发人员直接操做Today的宿主应用岂不危险?)?这几个问题在下面的演示中将逐一解答,首先要简单实现一个ToDoList应用,这里就不得不考虑第一个问题,怎么样存储数据才能保证后面的扩展开发可以正常访问这些数据。事实上iOS 8 新增了App Groups
功能用于实现应用之间的数据共享问题(固然这个功能在OS X如今应该叫作macOS,早就出现了),在Xcode中开启并设置App Groups,Xcode - Capabilities中找到App Groups打开并添加一个名为“group.com.cmjstudio.todolist”组(注意组名称必须以group
开头,这一步操做至关于在iOS的开发证书中启用App Groups服务并注册分组,同时在Xcode - Build Settings - Code Signing Entitlements中配置对应的分组配置文件。从Xcode 8开始,证书配置将变得异常简单,不用过多的登陆开发者帐号管理证书)。添加完分组以后将在项目中生成一个ToDoList.entitlements
文件(这其实就是一个xml配置文件,事实上往后若是添加其余服务,其配置也会添加到这个文件中)。既然App Groups和开发证书相关,也就是说同一个开发证书下发布的应用只要配置了相同的组就能够实现数据的共享。App Groups支持的经常使用数据共享包括NSUserDefaults、NSFileManager、NSFileCoordinator、NSFilePresenter、UIPasteboard、KeyChain、NSURLSession等,这里不妨将数据存储到NSUserDefaults中。
下面将快速建立一个简单的ToDoList,使用UITableView进行展现,数据的操做逻辑放到TaskService.swift中:异步
import Foundation let TaskServiceDataKey = "TaskServiceData" public struct TaskService { public static let ToDoListGroupName = "group.com.cmjstudio.todolist" public static func addItem(title:String){ let userDefault = NSUserDefaults(suiteName: TaskService.ToDoListGroupName) var items = self.getItems() items.append(title) userDefault?.setObject(items, forKey: TaskServiceDataKey) userDefault?.synchronize() } public static func removeItem(title:String){ let items = self.getItems() let newItems = items.filter { (item) -> Bool in item != title } let userDefault = NSUserDefaults(suiteName: TaskService.ToDoListGroupName) userDefault?.setObject(newItems, forKey: TaskServiceDataKey) userDefault?.synchronize() } public static func getItems() -> [String]{ let userDefault = NSUserDefaults(suiteName: TaskService.ToDoListGroupName) var tasks = [String]() if let array = userDefault?.stringArrayForKey(TaskServiceDataKey) { tasks = array } return tasks } }
实现了ToDoList以后接下来就是进行扩展开发。首先在项目中添加一个名为“ToDoListTodayExtension”的Today Extension类型的Target,并选择激活这个Scheme以便后面测试。而后能够看到在项目根目录建立了一个“ToDoListTodayExtension”文件夹,它包含一个TodayViewController、MainInterface.storyboard和一个info.plist。在info.plist中定义了扩展入口点“com.apple.widget-extension”同时指定了MainInterface做为展现入口,固然很容易就能够猜到TodayViewController是MainInterface.storyboard中控制器对应的class。TodayViewController.swift是一个UIViewController控制器:
class TodayViewController: UIViewController, NCWidgetProviding { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view from its nib. } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } func widgetPerformUpdate(completionHandler: ((NCUpdateResult) -> Void)) { // Perform any setup necessary in order to update the view. // If an error is encountered, use NCUpdateResult.Failed // If there's no update required, use NCUpdateResult.NoData // If there's an update, use NCUpdateResult.NewData completionHandler(NCUpdateResult.newData) } }
能够看出这个类还实现了NCWidgetProviding
协议,其中最重要的两个方法就是用于自定义边距的widgetMarginInsets
方法和更新插件的widgetPerformUpdate
方法。此时若是编译运行(注意以前已经激活扩展的sheme,也就是从扩展运行)而且选择宿主程序Today就会看到一个带有“Hello World”字样的扩展,这其实就是MainInterface的默认布局(注意此时在Products中会生成一个ToDoListTodayExtension.appex就是对应的扩展包)。
接下来就能够进行扩展的界面布局了,你能够选择Storyboard或者code布局,须要注意的是Today扩展的宽度永远都会是屏幕宽度,布局时不须要过多关心,而高度则须要经过调整TodayViewController的preferredContentSize来完成。
另外,这里咱们须要思考一个问题:如何使用以前容器应用中编写的TaskService.swift,由于它已经包含了数据的读取方法,咱们没有必要在扩展中再实现一遍相同的操做。根据前面文章中关于Swift的命名空间和做用域的介绍应该能够想到将其提取到一个公共的命名空间中,而命名空间的实现一般是使用一个target实现的,这也正是官方推荐的作法。建立一个framework类型的Target而且将TaskSerivce.swift放到这个framework中,ToDoList和ToDoListTodayExtension均使用这个framework(在项目中增长一个名为“ToDoListKit”的Cocoa Touch Framework类型的Target,同时注意将TaskService.swift和对应的类和方法声明为公共方法,在使用TaskService的中使用import ToDoListKit
导入这个Framework)。
在TodayViewController中增长UITableView和UIButton,当没有数据时展现UIButton,点击按钮能够经过extensionContext
跳转到容器应用并增长新的代办事项,前面提到过在扩展中是没法直接利用UIApplication打开应用的由于扩展在宿主应用中运行,可是在控制器中增长了一个NSExtensionContext
类型的上下文来管理扩展操做,这样也就解决了上面说到的第三个问题。扩展的高度则经过preferredContentSize
来进行设置,而后根据记录数动态设置其高度,没有数据则设置为一行记录的高度来展现添加按钮。
import UIKit import NotificationCenter import ToDoListKit private let TodayViewControllerMaxCellCount = 3 private let TodayViewControllerCellHeight:CGFloat = 44.0 private let TodayViewControllerTableViewCellKey = "TodayViewControllerTableViewCell" class TodayViewController: UIViewController, NCWidgetProviding,UITableViewDataSource,UITableViewDelegate { override func viewDidLoad() { super.viewDidLoad() self.setup() self.loadData() } func widgetPerformUpdate(completionHandler: ((NCUpdateResult) -> Void)) { // Perform any setup necessary in order to update the view. // If an error is encountered, use NCUpdateResult.Failed // If there's no update required, use NCUpdateResult.NoData // If there's an update, use NCUpdateResult.NewData self.loadData() completionHandler(NCUpdateResult.NewData) } func widgetMarginInsetsForProposedMarginInsets(defaultMarginInsets: UIEdgeInsets) -> UIEdgeInsets { return UIEdgeInsetsZero } // MARK: - UITableView数据源和代理方法 func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return self.data.count } func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { var cell:UITableViewCell! = tableView.dequeueReusableCellWithIdentifier(TodayViewControllerTableViewCellKey) if cell == nil { cell = UITableViewCell(style: .Subtitle, reuseIdentifier: TodayViewControllerTableViewCellKey) cell.textLabel?.textColor = UIColor.whiteColor() cell.detailTextLabel?.textColor = UIColor.whiteColor() } let item = self.data[indexPath.row] cell.imageView?.image = UIImage(named: "calendar") cell.textLabel?.text = "Date & Time" cell.detailTextLabel?.text = item return cell } // MARK: - 事件响应 @IBAction func addButtonClick(sender: UIButton) { let url = NSURL(string: "todolist://add") self.extensionContext?.openURL(url!, completionHandler: nil) } // MARK: - 私有方法 private func setup(){ self.addButton.layer.cornerRadius = 3.0 self.tableView.rowHeight = TodayViewControllerCellHeight } private func loadData(){ self.data = [String]() let items = TaskService.getItems() // 控制最多显示条数 for i in 0..<items.count { self.data.append(items[i]) if i >= TodayViewControllerMaxCellCount { break } } self.layoutUI() self.tableView.reloadData() } private func layoutUI(){ if self.data.count > 0 { self.addButton.hidden = true self.tableView.hidden = false self.preferredContentSize.height = CGFloat(self.data.count) * TodayViewControllerCellHeight } else { self.addButton.hidden = false self.tableView.hidden = true self.preferredContentSize.height = TodayViewControllerCellHeight } } // MARK: - 私有属性 @IBOutlet weak var tableView: UITableView! @IBOutlet weak var addButton: UIButton! private var data:[String]! }
注意:官方已经明确指出Today扩展不支持UIScrollView滚动,建议显示最新数据或者更多的数据经过分页实现。
此外在扩展中使用了一个日历图标calendar
,而在容器应用ToDoList中这个图片已经存在于Assets.xcassets
中,但在扩展中没办法直接访问容器应用中的资源。一种解决方式是直接往扩展中添加一个calendar
图标;另外一种就是直接选择扩展这个Target—Build Phases—Copy Bundle Resources 而后添加容器中的资源。这么作的好处是尽管实际运行中存在两份资源,可是开发过程当中只须要维护一份。在ToDoListTodayExtension中咱们选择第二种方式(固然若是你确实须要进行资源文件共享而不是使用两份资源,你也能够经过NSFileManager.defaultManager().containerURLForSecurityApplicationGroupIdentifier(gropuName)
来读取容器应用中的文件,但在这里不太适合)。
固然接下来就是给ToDoListTodayExtension扩展配置App Groups,配置方法相似,惟一须要注意的是Group名称必须和前面保持一致,设置为“group.com.cmjstudio.todolist”。最后运行结果以下:
前面说过如今iOS支持的扩展类型愈来愈多,给开发者提供了更多的交互方式,除了Today扩展以外分享扩展应该是另外一个比较常见得扩展类型,好比经常使用的QQ、微信、微博等都实现了分享扩展。下面再以一个分享扩展为例简单介绍一下这种扩展的开发过程。
假设如今有一个图片社区应用“MyPicture”,用户能够分享各类图片和摄影做品,在系统相册中用户能够选择本身喜欢的图片直接分享到“MyPicture”。关于应用和扩展的建立过程再也不赘述,假设已经建立完应用扩展“MyPictureShareExtension”。默认状况下分享扩展编辑界面以下:
首先这个扩展的info.plist相比Today Extension多了一些配置选项,例如能够编辑扩展名称、语言等。这里进行设置以下:
Bundle display name
名称为“MyPicture”。NSExtensionActivationRule
,增长最大支持分享图片数NSExtensionActivationSupportsImageWithMaxCount
为9,若是超过九张则不显示分享按钮,同时此项配置也确保在网页分享、文件分享中再也不出现“MyPicture”扩展。更多配置参加Apple官方文档 (SystemExtensionKeys)[https://developer.apple.com/library/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/SystemExtensionKeys.html#//apple_ref/doc/uid/TP40014212-SW2],事实上激活规则还支持更为复杂的断言配置。
其次,Share Extension对应的控制器继承于SLComposeServiceViewController
,其中最经常使用的方法和属性以下:
charactersRemaining
。下图是咱们即将实现的最终效果,点击Category
能够选择图片分类:
这里重点关注图片的发送过程,在Share Extension中是没法直接获取到图片的(由于咱们分享的内容多是图片,也多是网页、视频等,所以SLComposeServiceViewController
也不太可能会直接提供图片访问接口),全部的访问数据包含进在extensionContext
的inputItems
中,这是一个NSInputItem
类型的数组。每一个NSInputItem
都包含一个attachments
集合,它的每一个元素都是NSItemProvider
类型,每一个NSItemProvider
就包含了对应的图片、视频、连接、文件等信息,经过它就能够获取到咱们须要的图片资源。可是须要注意,经过NSItemProvider
进行资源获取的过程较长,同时也会阻塞线程,若是直接在didSelectPost
方法中获取图片资源势必形成用户长时间等待,比较好的体验是在presentationAnimationDidFinish
方法中就异步调用NSItemProvider
的loadItemForTypeIdentifier
方法进行图片资源加载,并存储到数组中以便在didSelectPost
方法中使用。
此外,为了获取更好的用户体验,图片的上传过程一样须要放到后台进行,首先想到的就是使用NSURLSession的后台会话模式,值得一提的是在这个过程当中必须指定NSURLSessionConfiguration
的sharedContainerIdentifier
,由于上传的过程当中首先会将资源缓存到本地,而扩展是没办法直接访问宿主应用的缓存空间的,配置sharedContainerIdentifier
以便利经过App Group
使用容器应用的缓存空间。具体实现以下:
import UIKit import Social import MobileCoreServices import Alamofire private let ShareViewControllerContentTextMax = 200 private let ShareViewControllerDefaultCategoryTitle = "Category" class ShareViewController: SLComposeServiceViewController { override func viewDidLoad() { super.viewDidLoad() self.imageDatas = [NSData]() self.charactersRemaining = ShareViewControllerContentTextMax self.placeholder = "Please enter description" } // 显示分享界面,在此时则异步加载图片到self.images,避免在didSelectPost中再加载图片影响体验 override func presentationAnimationDidFinish() { // 用户输入项 guard let extensionItem = self.extensionContext?.inputItems.first else { return } guard let attachments = extensionItem.attachments as? [NSItemProvider] else { return } for attachment in attachments { let imageType = kUTTypeImage as String if attachment.hasItemConformingToTypeIdentifier(imageType) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { attachment.loadItemForTypeIdentifier(imageType, options: nil, completionHandler: { (coding, error) in if error == nil { guard let fileURL = coding as? NSURL else { return } guard let data = NSData(contentsOfURL: fileURL) else { return } self.imageDatas.append(data) // guard let image = UIImage(data: data) else { return } // self.images.append(image) } }) }) } } } // 内容验证,输入过程当中会不断调用此方法 override func isContentValid() -> Bool { if let text = self.contentText { let len = text.characters.count if len > ShareViewControllerContentTextMax { return false } self.charactersRemaining = ShareViewControllerContentTextMax - len } return true } // 发送分享内容 override func didSelectPost() { // 上传图片和编辑内容、分类 self.upload() // 通知host app 操做完成 self.extensionContext!.completeRequestReturningItems([], completionHandler: nil) } // 自定义分享编辑界面sheet override func configurationItems() -> [AnyObject]! { return [self.categorySheetItem] } // MARK: - 私有方法 private func selectCategory(){ let temp = CategoryTableViewController(style: .Grouped) temp.selectedCategory = self.categorySheetItem.title temp.selectedCategoryHandler = { [weak self]category in guard let weakSelf = self else { return } weakSelf.categorySheetItem.title = category } self.pushConfigurationViewController(temp) } private func upload(){ let urlStr = "http://requestb.in/v34h3lv3" self.manager.upload(.POST,urlStr, multipartFormData: { (formData) -> Void in for data in self.imageDatas { formData.appendBodyPart(data: data, name: "image", mimeType: "image/jpeg") } // add parameter if self.contentText != nil { formData.appendBodyPart(data: self.contentText.dataUsingEncoding(NSUTF8StringEncoding)!, name: "content") } if self.categorySheetItem.title != ShareViewControllerDefaultCategoryTitle { formData.appendBodyPart(data: self.categorySheetItem.title.dataUsingEncoding(NSUTF8StringEncoding)!, name: "category") } }){ encodingResult in switch encodingResult { case Manager.MultipartFormDataEncodingResult.Success(_, _, _): debugPrint("request") case let Manager.MultipartFormDataEncodingResult.Failure(error): debugPrint(error) } } } // MARK: - 私有属性 private lazy var categorySheetItem:SLComposeSheetConfigurationItem = { let temp = SLComposeSheetConfigurationItem() temp.title = ShareViewControllerDefaultCategoryTitle temp.tapHandler = self.selectCategory return temp }() // 自定义上传配置,在后台上传避免阻塞UI,注意:因为NSURLSession上传过程当中须要先限缓存到本地可是扩展应用自己是没办法使用Host App缓存控件的,所以注意设置sharedContainerIdentifier,使用容器应用的空间 private lazy var manager:Alamofire.Manager = { let configName = "com.cmjstudio.mypicture.backgroundsession" let configuration = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(configName) configuration.sharedContainerIdentifier = "group.com.cmjstudio.mypicture" // configuration.HTTPAdditionalHeaders = Alamofire.Manager.defaultHTTPHeaders let manager = Alamofire.Manager(configuration: configuration) manager.startRequestsImmediately = true manager.backgroundCompletionHandler = { debugPrint("completed.") } return manager }() private var imageDatas:[NSData]! }
注意:网络操做部分这里直接选择
Alamofire
进行上传,若是想本身实现图片上传,能够查看iOS开发系列--网络开发。另外,若是须要自定义分享编辑界面可让ShareViewController
继承自UIViewController
,具体细节参见Apple指导文档
因为使用了NSURLSession的后台会话,当执行完相关操做后会调用容器应用的application(application, identifier, completionHandler)
方法,若有必要有些操做能够在此方法中进行处理。
本文着重介绍了Today Extension和Share Extension两种扩展,其实扩展是比较大的一块内容,各种扩展实现方法也不尽相同,可是其生命周期、核心原理是相似的,本文也再也不一一探讨。相信iOS 10中更加丰富的扩展类型也会让应用之间的交互愈来愈丰富,有兴趣的朋友也能够访问下载Xcode 8 beta版进行探索,有时间咱们也会写一篇关于Intent Extension、Message Extensiond等新增扩展应用的文章。