
App Extension 讓我們在用戶正在使用其他 App 的時(shí)候, 拓展我們 App 的功能。
Today Extension 也叫做 widget。 它能夠讓一些重要的消息更快速的到達(dá)你的用戶。比如說, 用戶可以通過它查看天氣,或者股票價(jià)格, 查看日程表等等。蘋果在官方文檔中說到, 一個(gè) widget 應(yīng)該有以下的特點(diǎn)。
- 確保內(nèi)容是最新的
- 響應(yīng)的用戶事件
- 性能好(在iOS上占用大量內(nèi)存,系統(tǒng)可能會(huì)kill掉這個(gè)widget)
創(chuàng)建 Today Extension
Xcode -> File -> New -> Target -> TodayExtension
跟創(chuàng)建一個(gè)新的項(xiàng)目一樣, 設(shè)置創(chuàng)建好之后, 項(xiàng)目中會(huì)多一個(gè) Target, 修改Scheme 為你剛剛創(chuàng)建的 Extension 再運(yùn)行, 就能在 通知中心的 Today 里面看到你剛剛創(chuàng)建的 widget 了, 上面寫著“Hello world”
另外 Xcode 給你創(chuàng)建了默認(rèn)的模版文件。
- TodayViewController.swift(如果是 OC 對應(yīng)會(huì)是
.h和.m文件) - MainInterface.storyboard
- Info.plist
注意: 默認(rèn)是使用這個(gè) storyboard 作為這個(gè) widget 的入口。如果不需要使用storyboard 可以刪除掉這個(gè)storyboard并且將Info.plist 中的
-
NSExtensionMainStoryboard改成NSExtensionPrincipalClass -
MainInterface改成TodayViewController
設(shè)置界面
完成了上面的步驟之后, 不論你是選擇用 stroyboard 作為你 widget 的入口, 還是選擇用代碼來做這件事情。都是一樣的。
由于不知道什么原因, 我在網(wǎng)上看到的文章都是使用代碼來做的這件事情。所以在這篇文章以及后面的示例代碼中都將使用 Xcode 默認(rèn)的 storyboard 來做這個(gè) widget 的布局。
我將解決的問題
- 在 widget 中打開主 App 并傳遞參數(shù)
- widget 和 主 App 共享數(shù)據(jù)
- widget 和 主 App 共用資源
- widget 的打開和折疊
我遇到的坑
也沒什么坑, 畢竟 Today Extension 并不是什么很難的東西。
- 測試的時(shí)候, 由于 widget 和 主 App 是兩個(gè)不同的 target, 所以在傳遞參數(shù)的時(shí)候, 在 appdelegate 中打印對應(yīng)的值沒有效果。最開始我還以為是因?yàn)樵O(shè)置的 scheme 是 widget 所以在 主 App 中的修改是無效的。但是實(shí)際是并不是這樣。將參數(shù)以 alert 的形式表現(xiàn)出來, 這時(shí)候能夠發(fā)現(xiàn), 其實(shí)主 App 是跑起來了的。
先說說我做的準(zhǔn)備工作吧
為了不扯那么多沒用的東西。先說說我做了那些跟今天主題沒什么關(guān)系的事情。
寫主 App
在主 App 中我寫了一個(gè) UITableView, 并使用 Userdefault 將我要持久化的數(shù)據(jù)保存下來。然后對應(yīng)給 Todo list 做了,添加,和刪除的功能。
widget
在 widget 中我也下了同樣的一個(gè) UITableView 只有查看的功能。
要做的事情
widget 和 主 App 共用資源
widget 和主 App 共享代碼和資源。作為一個(gè)工程師, 我們在任何事情的時(shí)候都要想到高類聚低耦合著句不變的真理。所以我們還是要盡可能的讓 widget 和主 App 共享代碼。
主要有兩個(gè)方案:
- framework
- 直接共享
framework 的話,就拿 cocoapods 來說吧, 由于 widget 是一個(gè)新的target, 所以只需要在 podfile 中對應(yīng)添加代碼就能夠在 widget 中使用。
另外一個(gè)是 直接共享, 這個(gè)就很簡單了。我在示例中讓主 App 和 widget 共享了一張圖片,一個(gè) TodoCell 類(包括xib 文件)。我做的唯一的一件事情就是在 Xcode 中選中這個(gè)文件,然后在 Xcode右邊的 TargetMenberShip 中勾選對應(yīng)的 target.
widget 和 主 App 共享數(shù)據(jù)
嚴(yán)格來說 widget 和 App 是不同的兩個(gè) App 了, 他們之間要共享數(shù)據(jù)的話只能使用 App Groups 了。
首先在主 App
target -> capabilities -> App Groups
打開 App Groups 功能, 點(diǎn)擊 + , 設(shè)置 id 。如果重復(fù)了就改一個(gè)。
widget App
target -> capabilities -> App groups
這時(shí)候的 group 列表就能夠看到對應(yīng)的 group 了。勾選即可。
這時(shí)候已經(jīng)完成了widget 和 主 App 共享數(shù)據(jù)的前提條件。
接下來還需要做的事情, 就是將我們準(zhǔn)備工作里面Userdefault相關(guān)代碼進(jìn)行調(diào)整。
將 UserDefaults.standard 改成
UserDefaults(suiteName: "your group id")
這樣就可以在 widget 中 使用
let userdefault = UserDefaults(suiteName: "group.com.sunny.group")
獲得在主 App 中持久化的數(shù)據(jù)了。關(guān)于 App Groups 其他的用法,可以繼續(xù)深入研究。
widget 的折疊和展開
蘋果的官方文檔里面明確的說了,widget 的界面是不能滑動(dòng)的。畢竟 widget 和通知中心的滑動(dòng)不能沖突啊。
所以有時(shí)候我們需要將 widget 折疊起來,畢竟太長的 widget 實(shí)在是令人討厭啊。
主要還是說說iOS10 上怎么做的吧,畢竟沒有iOS10 以下的設(shè)備。
在 TodayViewController 的 didLoad 中添加
// iOS10 添加折疊按鈕
if #available(iOSApplicationExtension 10.0, *) {
extensionContext?.widgetLargestAvailableDisplayMode = .expanded
} else {
// iOS8 、iOS9 上需要自己添加折疊按鈕
}
然后實(shí)現(xiàn) NCWidgetProviding 協(xié)議中的方法
func widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) {
// 由于 iOS8 、iOS9 上沒有這個(gè)代理。需要對自己添加的按鈕設(shè)置 target-action 然后進(jìn)行修改
switch activeDisplayMode {
case .compact:
preferredContentSize = maxSize
case .expanded:
preferredContentSize = CGSize(width: 0.0, height: 60 * CGFloat(dataSource.count))
}
}
在 iOS8 和 iOS9 中, 由于系統(tǒng)沒有這個(gè)功能。我們只能自己寫一個(gè)按鈕然后再來做這些事情了。
widget 打開 主 App
widget 打開主 App 還是老思路,openurl 就可以了,然后在url 中添加對應(yīng)需要的參數(shù)。
準(zhǔn)備工作
主 App -> target -> info -> UrlTypes
添加一個(gè) URlType 然后設(shè)置 URL Scheme 為你自定義的字符串。 比如 “sunny”。
在 widget 中需要跳轉(zhuǎn)的地方寫這樣的代碼
self.extensionContext?.open(NSURL(string: "sunny://action=\(dataSource[indexPath.row])")
參數(shù)傳遞也就是按照上文, 在url中拼接了。上文有提到, widget 和 App 可以共享數(shù)據(jù)。這也可能是一種傳遞參數(shù)的方式。
這個(gè)時(shí)候打開主要 App 就是直接進(jìn)入主要界面了。如果我們需要做一些其他的事情應(yīng)該怎么做呢?
想想以前做微信或者支付寶支付的時(shí)候, 都要在 appdelegate 中寫一些代碼。
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
let prefix = "sunny://"http:// 判斷是否是可靠的地方傳遞過來的
if url.absoluteString.hasPrefix(prefix) {
// 參數(shù)過來了! 做對應(yīng)的事情
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的默認(rèn)高度是有限制的。
compact 下:
- max = 110
- mim = 110
expanded 下:
- min = 110
- max = 根據(jù)不同的機(jī)型二不同。
無論怎么設(shè)置, 都不回超出這個(gè)范圍
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)
}
這個(gè)方法用來選擇 widget 再出現(xiàn)的時(shí)候會(huì)不會(huì)重新刷新。
通知
在 NSExtensionContext 中看到的幾個(gè)通知貌似不是給 TodayExtension 用的。
NSExtensionContext 中能看到幾個(gè)通知他們都是監(jiān)聽 host App 的狀態(tài)的。所以對于widget 來說, host App 就是 Today 這個(gè)東西啦。
最后
拋磚引玉,本文用Today Extension做了一個(gè)很簡單的功能。 當(dāng)然, 我們能用他做的事情可不止這些。這就需要我們發(fā)動(dòng)我們的聰明才智了。
示例代碼下載鏈接由于使用swift寫的, 由于眾所周知的原因, 你發(fā)現(xiàn)編譯不過了??梢月?lián)系我, 我將做適配。