Today Extensionについて

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

#Today Extensionとは
Today Extensionとは、App Extensionの一つで、上から出てくる通知画面の今日のお知らせ的な情報を表示している部分のこと。Todayウィジェットと呼ばれる。
↓この画面。
<img width="375" alt="IMG_1417.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/68056/dabd5fcd-d52b-ee88-b6bb-fe9a5eeff896.png">

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

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

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

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


#App Extensionのおさらい
App Extensionは、文字どおりアプリを拡張してくれる部分。(下図のApp Extension)
![スクリーンショット 2017-01-06 13.50.45.png](https://qiita-image-store.s3.amazonaws.com/0/68056/f15a93b8-55ea-da11-56eb-6a938c0e6b39.png)
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のターゲットの追加
![スクリーンショット 2016-12-28 14.36.28.png](https://qiita-image-store.s3.amazonaws.com/0/68056/6307c4e5-6f3c-10b9-19c4-15b13f6aa572.png)
プロジェクトのターゲットにエクステンションを追加します。
[+]ボタンでTodayExtensionを選択して[Next]をタップ、その後ProductNameを入力して、エクステンションを作成します。
（スキーマのアクティベートについて聞かれるので、[Activate]をタップします。）
スキーマをアクティベートすることで、TodayExtensionのテストビルド用のスキーマが生成されます。

![スクリーンショット 2016-12-28 17.03.00.png](https://qiita-image-store.s3.amazonaws.com/0/68056/15fd1c47-fc3a-e127-6489-12811bcc0331.png)
アクティベートされたスキーマを選択してアプリをビルドすると、TodayExtensionが立ち上がります。

####Interface BuilderでViewの作成
プロジェクトナビゲーターにExtensionの初期フォルダが生成されます。TodayExtensionのViewは、その中の`Maininterface.storyboard`ファイルで設定します。
![スクリーンショット 2016-12-28 16.19.06.png](https://qiita-image-store.s3.amazonaws.com/0/68056/a232701f-0be8-0efe-06dc-1945da44feff.png)
TodayExtensionのViewの作成には、いくつかの制限があります。

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

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

####TodayExtensionのアップデートの処理
TodayExtensionで行いたい処理は`TodayViewController`クラスに記述します（デフォルトだと）。
**TodayExtensionが更新されるタイミングで呼ばれる関数**が用意されています。

```ruby: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な状態の時です。
![IMG_2353.PNG](https://qiita-image-store.s3.amazonaws.com/0/68056/d8428326-b339-a31b-c002-c5c13c697943.png)
高さが低い状態を`compact`、高さが広がっている状態を`expanded`と呼びます。

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

```ruby: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()`で以下を設定しておきます。

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

```


####アプリ側とデータの共有(AppGroups)
#####**AppGroupsの有効化**
プロジェクトナビゲーターからプロジェクトを選択し、アプリとExtensionそれぞれのCapabilitiesタブのApp Groupsを有効にする。使用するAppGroupのIDを追加する。
※AppGroupの生成は[Apple Developerサイト](https://developer.apple.com)(Certificates, Identifiers & Profilesの画面)から行います。グループの説明とAppGroupのIDを指定するだけ
![スクリーンショット 2016-12-28 10.08.53.png](https://qiita-image-store.s3.amazonaws.com/0/68056/a7e9f508-4d81-0e5f-da1a-454f3cc87410.png)

#####**アプリ側で、Extensionで使用したいデータをApp Groupに保存**
使用方法は、UserDefaultsと同じです。宣言の時に、defaultではなく、使用するAppGroupを指定します。

```ruby: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の宣言を行う。

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

...
}
```

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

```ruby: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を追加し、その中に共有したいコードを書きます。
[こちらのサイト](http://swift-studying.com/blog/swift/?p=508)に詳しく説明されていますので、ご参照ください:angel:


####ホーム画面でのアプリアイコンの3DTouchによるコンテンツ表示
TodayExtensionを実装すると、ホーム画面でアプリアイコンの3DTouchで、Todayウィジェットの内容が表示されるようになります。（コードを追加する必要はありません）
表示されるのは、高さが`NCWidgetDisplayMode.compact`（コンパクト）の時の内容です。
![IMG_2352.PNG](https://qiita-image-store.s3.amazonaws.com/0/68056/4ca95323-0aeb-5f76-74e0-97427d1a4bb1.png)