Today Extensionについて


App Extensionについて勉強中で、Today Extensionの基礎についてまとめました。

Today Extensionとは

Today Extensionとは、App Extensionの一つで、上から出てくる通知画面の今日のお知らせ的な情報を表示している部分のこと。Todayウィジェットと呼ばれる。
↓この画面。

自分のアプリにこのTodayExtensionを追加すると、以下のことができます。

  • 最新情報を取得する
  • ごく単純なタスクを実行する

複雑な処理向けではないようです。

いくつかの制限があります。例えば・・・
iOS向けのTodayウィジェットでは、キーボード入力を行うことはできない。などが挙げられます。
ロック画面でも、同じExtensionが表示されます。
アプリのアイコンを3Dタッチすると、Todayウィジェットのコンテンツが表示されます。(heightがcompactの時のコンテンツ)

App Extensionのおさらい

App Extensionは、文字どおりアプリを拡張してくれる部分。(下図のApp Extension)

AppExtensionの種類は、TodayExtensionの他に、
・カスタムキーボード
・SiriのExtension(Siriで自分のアプリの処理をする)
・・・などがある。

Containing App

AppExtensionで拡張するための元となるアプリ。

Embedded FrameworkとAppGroups

Containg AppとExtensionは、同一バンドルだが、別のセキュリティサンドボックスが割り当てられるため、コードやデータの共有ができない。
この問題を解決するための仕組みが以下の二つ。

Embedded Framework
共通で使用したいコードをフレームワーク化することでアプリとExtensionで同じコードを利用できるようにする仕組み。
ただし、Extensionの制限のため、フレームワーク化すればすべてのコードがエクステンションでも使用できるというわけではない。(例えば、SharedApplicationを使用している場合など)

AppGroups
データをアプリとExtensionの間で同じ領域に格納できるという仕組み。

AppExtensionの制限事項の例

  • shared Applicationにアクセスできない。
  • 一部のAPIが利用できない。
  • カメラ・マイクの利用ができない。
  • バックグラウンドタスクができない(ただし、NSURLSessionに限り例外)
  • AirDropによるデータ受信ができない
  • ビルド設定でarm64(mac用)アーキテクチャを含む
  • メモリに使用制限あり
  • 余計なUI部品は使用しないようにする(例えば、TextField、ScrollViewは使用不可)

TodayExtensionの実装

TodayExtensionのターゲットの追加


プロジェクトのターゲットにエクステンションを追加します。
[+]ボタンでTodayExtensionを選択して[Next]をタップ、その後ProductNameを入力して、エクステンションを作成します。
(スキーマのアクティベートについて聞かれるので、[Activate]をタップします。)
スキーマをアクティベートすることで、TodayExtensionのテストビルド用のスキーマが生成されます。


アクティベートされたスキーマを選択してアプリをビルドすると、TodayExtensionが立ち上がります。

Interface BuilderでViewの作成

プロジェクトナビゲーターにExtensionの初期フォルダが生成されます。TodayExtensionのViewは、その中のMaininterface.storyboardファイルで設定します。

TodayExtensionのViewの作成には、いくつかの制限があります。

  • UITextField, UIScrollViewなど、高度なユーザーインタラクションを求める部品は使用できない。
  • Viewの高さが高くならないように注意する。

その他は、通常通り、AutoLayoutなどでUI部品を配置することができます。

TodayExtensionのアップデートの処理

TodayExtensionで行いたい処理はTodayViewControllerクラスに記述します(デフォルトだと)。
TodayExtensionが更新されるタイミングで呼ばれる関数が用意されています。

TodayViewController.swift
    // コンテンツを更新するべきタイミングの時にシステムからコールされる
    func widgetPerformUpdate(completionHandler: (@escaping (NCUpdateResult) -> Void)) {
        //表示内容のアップデートなどを行う。
        if self.update().isSuccess {
            //更新処理が成功した場合
            completionHandler(NCUpdateResult.newData)
        } else {
            //更新処理が失敗した場合
            completionHandler(NCUpdateResult.failed)
        }

    }

表示内容更新などの処理を行ったあと、completionHandler(NCUpdateResult)を呼び出します。
渡す引数は、NCUpdateResultのパラメーターです。
NCUpdateResult.failed :更新処理に失敗した場合
NSUpdateResult.noData :更新データがない場合
NSUpdateResult.newData :無事更新がされた場合

高さの調整

TodayExtensionでは、「表示を増やす」、「表示を減らす」の二つの高さの状態を管理してくれる関数が用意されています。
下図は「表示を増やす」ボタンがあり、これはcompactな状態の時です。

高さが低い状態をcompact、高さが広がっている状態をexpandedと呼びます。

Widgetの高さを調整してくれるのが、widgetActiveDisplayModeDidChange関数です。

TodayViewController
// 高さの調整
func widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) {
    if activeDisplayMode == NCWidgetDisplayMode.compact {
        //compact
        self.preferredContentSize = maxSize
    } else {
        //extended(CGSizeで高さ指定)
        self.preferredContentSize = CGSize(width: 0, height: 200)
    }
}

また、この方法で高さの制御を使用する場合、viewDidLoad()で以下を設定しておきます。

TodayViewController
override func viewDidLoad() {
    super.viewDidLoad()
    // NCWidgetDisplayModeexpandedにしておく
    self.extensionContext?.widgetLargestAvailableDisplayMode = NCWidgetDisplayMode.expanded
}

アプリ側とデータの共有(AppGroups)

AppGroupsの有効化

プロジェクトナビゲーターからプロジェクトを選択し、アプリとExtensionそれぞれのCapabilitiesタブのApp Groupsを有効にする。使用するAppGroupのIDを追加する。
※AppGroupの生成はApple Developerサイト(Certificates, Identifiers & Profilesの画面)から行います。グループの説明とAppGroupのIDを指定するだけ

アプリ側で、Extensionで使用したいデータをApp Groupに保存

使用方法は、UserDefaultsと同じです。宣言の時に、defaultではなく、使用するAppGroupを指定します。

SettingTableViewController.swift
// 宣言
var defaults = UserDefaults(suiteName: "group.appgroupName")

private var replyWords: Array<String>? {
    didSet {
        // 保存
        defaults?.set(replyWords, forKey: "replyWords")
        defaults?.synchronize()
    }
}

アプリで保存されたデータをExtensionで使用

まず、TodayViewControllerクラスのプロパティとしてAppGroupを指定したUserDefaultsの宣言を行う。

TodayViewController.swift
class TodayViewController: UIViewController, NCWidgetProviding {
    // 宣言 
    var defaults = UserDefaults(suiteName: "group.appgroupName")

...
}

Extension側(TodayViewControllerクラス)データを使用するタイミングでUserDefaultsからデータを取得する。

TodayViewController.swift
class TodayViewController: UIViewController, NCWidgetProviding {
     // データ取得の関数
    func getData() -> Array<String>? {
        // データ取得
        if let array = defaults?.array(forKey: "replyWords") as? Array<String> {
            return array
        } 
        return nil
    }

    // コンテンツを更新するべきタイミングの時にシステムからコールされる
    func widgetPerformUpdate(completionHandler: (@escaping (NCUpdateResult) -> Void)) {
        if let labels = getData() {
            self.updateLabels(labels)
            completionHandler(NCUpdateResult.newData)
        } else {
            self.showErrorMessage(error: .userDefaultError)
            completionHandler(NCUpdateResult.failed)
        }  
     }
}

アプリ側とコードの共有(EmbeddedFramework)

ターゲットにCocoaTouchFrameworkを追加し、その中に共有したいコードを書きます。
こちらのサイトに詳しく説明されていますので、ご参照ください

ホーム画面でのアプリアイコンの3DTouchによるコンテンツ表示

TodayExtensionを実装すると、ホーム画面でアプリアイコンの3DTouchで、Todayウィジェットの内容が表示されるようになります。(コードを追加する必要はありません)
表示されるのは、高さがNCWidgetDisplayMode.compact(コンパクト)の時の内容です。