iOS Widget小记

项目最近在调研是否需要为项目添加一个 Widget。这篇文章记录下自己学习 Widget 的过程。

  • 创建 Widget
  • 与主 App 的交互
    • 数据共享

创建 Widget

Widget 是一个依附于主 App 的插件,所以如果你想创建对应项目的 Widget 的话,在对应的项目中,新建一个target,类型选择 Today Extension。

创建

File -> New -> Target -> Today Extension 创建 Widget。

按照上述步骤创建完成之后,你的项目目录应该是下面图所示:

开发方式

Widget 界面的开发是可以使用纯代码或者 Storyboard 的,这个依据个人喜好了。

Widget 创建之后默认是使用 Storyboard 的,若需要纯代码的话,只需要到上图中 Widget 目录下的 Info.plist 中,删除如下黄框中的字段,并加入红框中的键值对就👌了。

代码编写

这里我使用了 Storyboard 来开发 Widget 界面。

在 TodayViewController 中,开始布局 Widget 的样式。通用组件都可以使用,但是 UITableView 等滚动视图是无法滚动的。

iOS10 之后,Widget 支持展开及折叠两种展现方式,通过设置 widgetLargestAvailableDisplayMode 属性可以让 Widget 程序实现展开布局,同时在左滑到 Widget 显示的时候,会调用 viewWillAppear,这时候可以去刷新数据获取最新的数据。

1
2
3
4
5
6
7
8
9
10
11
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

// .compact 无法折叠
// .expanded 可以折叠
if #available(iOS 10.0, *) {
self.extensionContext?.widgetLargestAvailableDisplayMode = .expanded
}

self.fetchData()
}

Widget 默认的 mode 是 compact,当我们设置了 expanded 之后才会有折叠选项。

右上角折叠与收起的回调,我们可以在下面的方法中处理

1
2
3
4
5
6
7
8
9
// 展开 / 折叠回调
func widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) {
if activeDisplayMode == .compact {
// 高度固定,最低高度为110
self.preferredContentSize = CGSize(width: UIScreen.main.bounds.width, height: 200)
} else {
self.preferredContentSize = CGSize(width: UIScreen.main.bounds.width, height: 500)
}
}

与主 App 交互

Widget 的目的就是使用户能够快速访问一些主 App 中的信息,当然其中也可以进行一些事件的处理,但是一般事件处理的方式都是调起主 App 来处理。为了能让 Widget 能和主 App 进行这些数据的共享,我们还需要进行一些相应的配置。

证书配置

ps: 需给 Widget 创建新的 AppID,Bundle ID 需要以主 App 的 Bundle ID 为前缀!

  1. 登录开发者账号,创建 App Groups。
  2. 进入到主 App 的 App ID 配置刚创建的 App Group。
  3. 进入到 Widget 的 App ID 配置 App Group。
  4. 进入 Xcode,分别进入主 App 及 Widget 的 target Capabilities,开启 App Group。 添加对应的 App Group。

这样我们的准备工作就做好了。

数据共享

Widget 给主 App 传值

Widget 中处理事件一般都是调起主 App,Widget 调起主 App 就相当于一个 App 调起另一个 App 的过程,所以我们这里我们需要分两步:

  1. 在主 App 的 target 中,Info -> URL Types,配置主 App 的 URL Scheme。
  1. 在 Widget 相应位置中,利用 Scheme 调起主程序。
1
2
3
4
5
6
// 打开主App
@IBAction func cleanBtnClicked(_ sender: UIButton) {
self.extensionContext?.open(URL(string: "LearnWidgetDemo://hello")!, completionHandler: { (successful) in
print("打开成功")
})
}

当然 Scheme 里面也可以传值,然后在主 App 的 Appdelegaete 的回调中处理:

1
2
3
4
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
// handle
return true
}

主 App 与 Widget 数据共享

上面的配置,主要就是为了让 Widget 能获取到主 App 的数据。

数据共享的方式有两种:

  • UserDefaults: 适合小量存储。
  • FileManager: 数据量较大时。

两种方式都可以写入和读取,一般情况下,都是主 App 写入,Widget 读取显示。

UserDefaults

因为沙盒机制,Widget 是不允许访问主 App 的沙盒路径的,因此 UserDefaults 的获取方法与平常有所区别,需要搭配 App group 完成实例化 UserDefaults。

1
2
3
4
5
// 这种方式是不行的
// let userDefault = UserDefaults.standard

// 获取实例
let userDefault = UserDefaults(suiteName: "App Group Name")
FileManager

同样的,FileManager 也有特殊的获取方式:

1
2
3
4
5
6
// 这种方式是不行的
//let fileManager = FileManager.default

// 获取实例
let fileManeger = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "App Group Name")

总结: 上述两种数据共享的方式,除了获取实例时,需要制定 App Group Name,其它数据存取方式都跟正常使用时一样。


ps: 如果你想测试,需要用模拟器去测试,因为如果你这个之前没有发过,就算能安装成功,手机上也没有显示的,但是模拟器上可以。


参考: App Extension Essentials —— Today