面向 Extension 开发 🌞 Today Extension

3,252 阅读6分钟

app extension 让我们在用户正在使用其他 app 的时候, 拓展我们 app 的功能。

Today Extension 也叫做 widget。 它能够让一些重要的消息更快速的到达你的用户。比如说, 用户可以通过它查看天气,或者股票价格, 查看日程表等等。苹果在官方文档中说到, 一个 widget 应该有以下的特点。

  • 确保内容是最新的
  • 响应的用户事件
  • 性能好(在iOS上占用大量内存,系统可能会kill掉这个widget)

创建 Today Extension

Xcode -> File -> New -> Target -> TodayExtension

跟创建一个新的项目一样, 设置创建好之后, 项目中会多一个 Target, 修改Scheme 为你刚刚创建的 Extension 再运行, 就能在 通知中心的 Today 里面看到你刚刚创建的 widget 了, 上面写着“Hello world”

另外 Xcode 给你创建了默认的模版文件。

  • TodayViewController.swift(如果是 OC 对应会是 .h.m 文件)
  • MainInterface.storyboard
  • Info.plist

注意: 默认是使用这个 storyboard 作为这个 widget 的入口。如果不需要使用storyboard 可以删除掉这个storyboard并且将Info.plist 中的

  • NSExtensionMainStoryboard 改成 NSExtensionPrincipalClass
  • MainInterface 改成 TodayViewController

设置界面

完成了上面的步骤之后, 不论你是选择用 stroyboard 作为你 widget 的入口, 还是选择用代码来做这件事情。都是一样的。

由于不知道什么原因, 我在网上看到的文章都是使用代码来做的这件事情。所以在这篇文章以及后面的示例代码中都将使用 Xcode 默认的 storyboard 来做这个 widget 的布局。

我将解决的问题

  • 在 widget 中打开主 app 并传递参数
  • widget 和 主 app 共享数据
  • widget 和 主 app 共用资源
  • widget 的打开和折叠

我遇到的坑

也没什么坑, 毕竟 Today Extension 并不是什么很难的东西。

  • 测试的时候, 由于 widget 和 主app 是两个不同的 target, 所以在传递参数的时候, 在 appdelegate 中打印对应的值没有效果。最开始我还以为是因为设置的 scheme 是 widget 所以在 主 app 中的修改是无效的。但是实际是并不是这样。将参数以 alert 的形式表现出来, 这时候能够发现, 其实主 app 是跑起来了的。

先说说我做的准备工作吧

为了不扯那么多没用的东西。先说说我做了那些跟今天主题没什么关系的事情。

写主app

在主 app 中我写了一个 UITableView, 并使用 Userdefault 将我要持久化的数据保存下来。然后对应给 Todo list 做了,添加,和删除的功能。

widget

在 widget 中我也下了同样的一个 UITableView 只有查看的功能。

要做的事情

widget 和 主 app 共用资源

widget 和 主app 共享代码和资源。作为一个工程师, 我们在任何事情的时候都要想到高类聚低耦合着句不变的真理。所以我们还是要尽可能的让 widget 和 主 app 共享代码。

主要有两个方案:

  • framework
  • 直接共享

framework 的话,就拿 cocoapods 来说吧, 由于 widget 是一个新的target, 所以只需要在 podfile 中对应添加代码就能够在 widget 中使用。

另外一个是 直接共享, 这个就很简单了。我在示例中让主app 和 widget 共享了一张图片,一个 TodoCell 类(包括xib 文件)。我做的唯一的一件事情就是在 Xcode 中选中这个文件,然后在 Xcode右边的 TargetMenberShip 中勾选对应的 target.

widget 和 主 app 共享数据

严格来说 widget 和 app 是不同的两个 app 了, 他们之间要共享数据的话只能使用 App Groups 了。

首先在主 app

target -> capabilities -> app groups

打开 app groups 功能, 点击 + , 设置 id 。如果重复了就改一个。

widget app

target -> capabilities -> app groups

这时候的 group 列表就能够看到对应的 group 了。勾选即可。

这时候已经完成了widget 和 主app共享数据的前提条件。

接下来还需要做的事情, 就是将我们准备工作里面Userdefault相关代码进行调整。

UserDefaults.standard 改成

UserDefaults(suiteName: "your group id")

这样就可以在 widget 中 使用

let userdefault = UserDefaults(suiteName: "group.com.sunny.group")

获得在主 app 中持久化的数据了。关于 app groups 其他的用法,可以继续深入研究。

widget 的折叠和展开

苹果的官方文档里面明确的说了,widget 的界面是不能滑动的。毕竟 widget 和通知中心的滑动不能冲突啊。

所以有时候我们需要将 widget 折叠起来,毕竟太长的 widget 实在是令人讨厌啊。

主要还是说说iOS10 上怎么做的吧,毕竟没有iOS10 以下的设备。

在 TodayViewController 的 didLoad 中添加

        // iOS10 添加折叠按钮
        if #available(iOSApplicationExtension 10.0, *) {
            extensionContext?.widgetLargestAvailableDisplayMode = .expanded
        } else {
// iOS8 、iOS9 上需要自己添加折叠按钮
        }

然后实现 NCWidgetProviding 协议中的方法

    func widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) {
// 由于 iOS8 、iOS9 上没有这个代理。需要对自己添加的按钮设置 target-action 然后进行修改
        switch activeDisplayMode {
        case .compact:
            preferredContentSize = maxSize
        case .expanded:
            preferredContentSize = CGSize(width: 0.0, height: 60 * CGFloat(dataSource.count))
        }
    }

在 iOS8 和 iOS9 中, 由于系统没有这个功能。我们只能自己写一个按钮然后再来做这些事情了。

widget 打开 主app

widget 打开主 app 还是老思路,openurl 就可以了,然后在url 中添加对应需要的参数。

准备工作

主app -> target -> info -> UrlTypes

添加一个 URlType 然后设置 URL Scheme 为你自定义的字符串。 比如 “sunny”。

在 widget 中需要跳转的地方写这样的代码

self.extensionContext?.open(NSURL(string: "sunny://action=\(dataSource[indexPath.row])")

参数传递也就是按照上文, 在url中拼接了。上文有提到, widget 和 app 可以共享数据。这也可能是一种传递参数的方式。

这个时候打开主要 app 就是直接进入主要界面了。如果我们需要做一些其他的事情应该怎么做呢?

想想以前做微信或者支付宝支付的时候, 都要在 appdelegate 中写一些代码。

    func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
        let prefix = "sunny://"// 判断是否是可靠的地方传递过来的
        if url.absoluteString.hasPrefix(prefix) {
        // 参数过来了! 做对应的事情
            let a = UIAlertController(title: url.absoluteString, message: nil, preferredStyle: .alert)
            a.addAction(UIAlertAction(title: "取消", style: .cancel, handler: nil))
            self.window?.rootViewController?.present(a, animated: true, completion: nil)
            return true
        }
        return false
        
    }

others

高度

widget的默认高度是有限制的。

compact 下:

  • max = 110
  • mim = 110

expanded 下:

  • min = 110
  • max = 根据不同的机型二不同。

无论怎么设置, 都不回超出这个范围

widgetPerformUpdate
    func widgetPerformUpdate(completionHandler: (@escaping (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)
    }

这个方法用来选择 widget 再出现的时候会不会重新刷新。

通知

NSExtensionContext 中看到的几个通知貌似不是给 TodayExtension 用的。

NSExtensionContext 中能看到几个通知他们都是监听 host app 的状态的。所以对于widget 来说, host app 就是 Today 这个东西啦。

最后

抛砖引玉,本文用Today Extension做了一个很简单的功能。 当然, 我们能用他做的事情可不止这些。这就需要我们发动我们的聪明才智了。

示例代码下载链接由于使用swift写的, 由于众所周知的原因, 你发现编译不过了。可以联系我, 我将做适配。