面向 Extension 開發 ? Today Extension

CepheusSun發表於2018-02-05

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寫的, 由於眾所周知的原因, 你發現編譯不過了。可以聯絡我, 我將做適配。

相關文章