1、概述

从iOS 8 开始Apple引入了扩展(Extension)用于增强系统应用服务和应用之间的交互。它的出现让自定义键盘、系统分享集成等这些依靠系统服务的开发变成了可能。WWDC 2016上众多更新也都是围绕扩展这一主题来进行了的,例如开发的Siri、iMessage Apps其实都是依靠扩展来工作的。在最新的Xcode 8 beta中也增加了众多的Extension 模板帮助开发者更快的实现不同类型的扩展。因此今天有必要介绍一下扩展相关的开发内容。




2、扩展的生命周期

iOS对于扩展的支持已经由最初的6类到了如今iOS10的19类(相信随着iOS的发展扩展的覆盖面也会越来越广),当然不同类型的扩展其用途和用法均不尽相同,但是其工作原理和开发方式是类似的。下面列出扩展的几个共同点:

  • 扩展依附于应用而不能单独发布和部署;
  • 扩展和包含扩展的应用(containing app)生命周期是独立的,分别运行在两个不同的进程中;
  • 扩展的运行依赖于宿主应用(或者叫载体应用 host app,而不是containing app)其生命周期由宿主应用确定;
  • 对开发者而言扩展作为一个单独的target而存在;
  • 扩展通常展现在系统UI或者其他应用中,运行应该尽可能的迅速而功能单一;

由于目前iOS 10正式版尚未发布,官方文档仅就目前9类扩展做了详细指导说明,感兴趣的话大家可以前往查看

官方对于应用扩展的生命周期描述如下图:

iOS开发系列——App扩展开发 1

iOS开发系列——App扩展开发 - 敏捷大拇指 - iOS开发系列——App扩展开发 1


通常用户选择了一个扩展的操作时宿主会向扩展发出一个请求来启动此扩展,扩展的生命周期也由此开始(例如用户在分享菜单中选择了你的分享扩展),由于扩展本身由控制器组成,因此此时就会调用类似于viewDidLoad之类的方法进行界面布局和逻辑处理,执行完相应任务之后应该尽快将控制权交给宿主应用,扩展生命周期结束。

尽管扩展和容器应用的生命周期之间没有直接关系,但是扩展本身就是作为容器应用的扩展而存在的,因此扩展和容器应用之间的交互又是不可避免的。通常扩展会通过自定义Scheme的形式来调用容器应用,而容器应用完成响应操作之后通过数据共享将数据共享给扩展来使用。

iOS开发系列——App扩展开发 2

iOS开发系列——App扩展开发 - 敏捷大拇指 - iOS开发系列——App扩展开发 2





3、Today扩展演示

前面说过目前iOS支持19类扩展入口,现在就以Today扩展(也叫做Widget)为例进行说明,在开始之前先对Today扩展有一个简单的认识,下图是微博、墨迹天气、网易云音乐的的Today扩展截图,微博扩展可以用来发送微博、查看更新,墨迹天气则用来展示今日和明日的天气,网易云音乐则是推荐一些相关的歌单、专辑。

iOS开发系列——App扩展开发 3

iOS开发系列——App扩展开发 - 敏捷大拇指 - iOS开发系列——App扩展开发 3


我们今天的例子将利用Today扩展实现一个简单的“to do list”查看功能,在容器应用ToDoList中可以增加和删除待办事项,而Today插件则展示最新的几条待办事项,如果没有待办事项则展示添加按钮,点击添加或列表则导航到ToDoList应用。应用的主界面和Today扩展最终截图如下:

iOS开发系列——App扩展开发 4

iOS开发系列——App扩展开发 - 敏捷大拇指 - iOS开发系列——App扩展开发 4


在开发之前首先思考一下要实现一个这样的ToDoList扩展需要注意哪些问题:

  • 首先ToDoList容器应用需要思考如何存储数据,因为容器应用完成之后要在Today中展现,前面说过扩展和容器应用没有任何关系,二者处于两个不同的沙盒之中,要实现数据资源共享则必须在开发之前思考如何存储数据的问题?
  • 由于ToDoList容器应用和其扩展ToDoListTodayExtension均要访问读取数据那么两者就存在重复读取数据的操作,也就是两者可能会存在较多的重复代码,如何复用这些代码?
  • 点击扩展列表或添加按钮要回到容器应用,由于扩展中禁用了UIApplication的openURL该如何实现跳转(事实上扩展中很多类型和方法被标记为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中:

[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控制器:

[Swift] 纯文本查看 复制代码
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就是对应的扩展包)。

iOS开发系列——App扩展开发 5

iOS开发系列——App扩展开发 - 敏捷大拇指 - iOS开发系列——App扩展开发 5


接下来就可以进行扩展的界面布局了,你可以选择Storyboard或者code布局,需要注意的是Today扩展的宽度永远都会是屏幕宽度,布局时不需要过多关心,而高度则需要通过调整TodayViewController的preferredContentSize来完成。

另外,这里我们需要思考一个问题:如何使用之前容器应用中编写的TaskService.swift,因为它已经包含了数据的读取方法,我们没有必要在扩展中再实现一遍相同的操作。根据前面文章中关于Swift的命名空间和作用域的介绍应该可以想到将其提取到一个公共的命名空间中,而命名空间的实现通常是使用一个target实现的,这也正是官方推荐的做法。创建一个framework类型的Target并且将TaskSerivce.swift放到这个framework中,ToDoList和ToDoListTodayExtension均使用这个framework(在项目中增加一个名为“ToDoListKit”的Cocoa Touch Framework