面向 Extension 开发 🌞 Today Extension

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 改成

1
UserDefaults(suiteName: "your group id")

这样就可以在 widget 中 使用

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

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

widget 的折叠和展开

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

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

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

在 TodayViewController 的 didLoad 中添加

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

然后实现 NCWidgetProviding 协议中的方法

1
2
3
4
5
6
7
8
9
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 中需要跳转的地方写这样的代码

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
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
1
2
3
4
5
6
7
8
9
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写的, 由于众所周知的原因, 你发现编译不过了。可以联系我, 我将做适配。

CepheusSun wechat
订阅我的公众号,每次更新我都不一定会告诉你!
坚持原创技术分享,您的支持将鼓励我继续创作!
0%