Widgets(Today Extension)のまとめ

# Today Extension

`App Extension Type`である`Today`は、通知センターにビューに置いて表示内容を即座に更新したり、簡単なタスクを実行することができ`Widgets`とも呼ばれる。その日の株価や天気、スケジュールやタスクなどを簡単に示し、ユーザが簡単に情報を確認することができる。

<p align="center">
<img src="https://qiita-image-store.s3.amazonaws.com/0/62451/c9ff111b-216e-3b06-f2c2-45dd22180883.png" width="200">
</p>
###### 画像引用：[iOS Human Interface Guidelines / Widgets](https://developer.apple.com/ios/human-interface-guidelines/extensions/widgets/)

## 導入

`Extension`用の`Target`作成する(`File > New > Target > Today Extension`)
作成したExtensionのディレクトリ配下に3つのファイルが作成される。

- `TodayViewController.swift`
- `MainInterface.storyboard`
- `Info.plist`

## `Info.plist`

<dl>
 <dt>NSExtensionPrincipalClass</dt>
 <dd>デフォルトではTodayViewControllerだが、独自でViewControllerを設定したい場合は、このキーにクラス名を指定する</dd><br>
 <dt>NSExtensionMainStoryboard</dt>
 <dd>TodayExtensionで使用するstoryboardファイルを設定するキー。デフォルトではMainInterface.storyboardが設定されている。storyboardファイルを使用したくない場合は、このキーを削除し、代わりにNSExtensionPrincipalClassを設定する</dd><br>
 <dt>CFBundleName</dt>
 <dd>Widgetsのタイトル名。デフォルトでは、ターゲット名が設定される。変更したい場合はこのキーを変更する</dd><br>
</dl>

## UIの作成
`Widgets`で表示するUIは`MainInterface.storyboard`で作成する。コードによって作成する場合は上記のように`NSExtensionMainStoryboard`キーを削除し、`NSExtensionPrincipalClass`のキーにクラス名を設定する。

表示される`Widgets`の高さはOSによって自動で決められているが、`widgetLargestAvailableDisplayMode`を設定することによって、`Show More`のボタンが追加され、拡大時の高さを変えられるようになる。（幅は変えられない）

<p align="center">
<img src="https://qiita-image-store.s3.amazonaws.com/0/62451/9ee2eeca-a27b-e777-6824-9bdf4b22e30a.gif" width="300">
</p>

```swift:NCWidgetTypes.h
@available(iOS 10.0, *)
public enum NCWidgetDisplayMode : Int {

    
    case compact // Fixed height

    case expanded // Variable height
}
```

```swift:TodayViewController

// expandedで拡大可能に
self.extensionContext?.widgetLargestAvailableDisplayMode = .expanded

// NCWidgetDisplayModeが変更されるたびに呼ばれるので、ここで拡大時の高さを設定する。
func widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) {
        if case .compact = activeDisplayMode {
            preferredContentSize = maxSize
        } else {
           preferredContentSize.height = 250
        }
}

```
幅を変える度に、`NCWidgetDisplayMode`が変更されるので、[widgetActiveDisplayModeDidChange(_:withMaximumSize:)](https://developer.apple.com/reference/notificationcenter/ncwidgetproviding/1649132-widgetactivedisplaymodedidchange)でそれぞれの高さの設定を行う。

## アニメーション

表示時にアニメーション処理を行いたい場合は、[viewWillTransitionToSize:withTransitionCoordinator:](https://developer.apple.com/reference/uikit/uicontentcontainer/1621466-viewwilltransitiontosize)でアニメーション処理を記述する。`NCWidgetDisplayMode`が変更され、高さが変更される際にこのメソッドが呼び出されるので、引数の`coordinator`の[animate(alongsideTransition:completion:)](https://developer.apple.com/reference/uikit/uiviewcontrollertransitioncoordinator/1619300-animate)を使ってアニメーションの処理を行う。以下のサンプルは、透過度を変更している。

```swift
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        coordinator.animate(alongsideTransition: { [weak self] context in
            self?.textLabel.alpha = 0.3
            self?.imageView.alpha = 0.3
        }, completion: { [weak self] _ in
            self?.textLabel.alpha = 1
            self?.imageView.alpha = 1
        })
}
```

<p align="center">
<img src="https://qiita-image-store.s3.amazonaws.com/0/62451/7654451f-cc36-0164-f162-997d5eadfccc.gif" width="300">
</p>






## `Widgets`の更新

システムが、`Widgets`の内容を更新すべきタイミングで、[widgetPerformUpdateWithCompletionHandler:](https://developer.apple.com/reference/notificationcenter/ncwidgetproviding/1490262-widgetperformupdatewithcompletio)を呼び出す。通知センターが表示されている時や、バックグラウンドでも呼び出される。`Widgets`は定期的にスナップショットが取られていて、`View`が更新されるまでは最新のスナップショットが表示されるようになっている。

```swift
public enum NCUpdateResult : UInt {

    //最新の情報を表示したい場合
    case newData
    //最新状態から変更がなく、更新する必要がない場合
    case noData
    //アップデートに失敗した際
    case failed
}
```
更新のステータスは、引数のクロージャに対して[NCUpdateResult](https://developer.apple.com/reference/notificationcenter/ncupdateresult)を返すようになっている。

```swift:TodayViewController.swift
func widgetPerformUpdate(completionHandler: (@escaping (NCUpdateResult) -> Void)) {

        fetchTodayData() { data, error in
            if let _ = error { 
                completionHandler(.failed) 
            } else {
                updateUI(data)
                completionHandler(.newData)
            }
        }
}

```

## Tapイベントやアプリ起動

通常のアプリ同様UIのタップイベントをキャプチャすることができる。通常のプロジェクト同様`MainInterface.storyboard`と`TodayViewController`を関連づけて設定する。

```swift
@IBAction func tappedButton(_ sender: Any) {
        let url = URL(string: "todaySample://")
        extensionContext?.open(url!, completionHandler: nil)
}
```

また、`extensionContext`を使用することで、`Widgets`からアプリを開くこともできる。
`URL schemes`は、`Show the Project navigator > TARGETS > info > URL Types`より設定する。

## `Widgets`の表示/非表示

アプリ側から`Widgets`の表示/非表示を設定することができる。
[setHasContent(_:forWidgetWithBundleIdentifier:)](https://developer.apple.com/reference/notificationcenter/ncwidgetcontroller/1456693-sethascontent)の`flag`を設定する。(`forWidgetWithBundleIdentifier`はextensionの`Bundle Identifier`を設定する。)

```swift
import NotificationCenter

let widgetController = NCWidgetController()
widgetController.setHasContent(flg, forWidgetWithBundleIdentifier: "com.Today.TodayExtension")
```


## 注意点やガイドライン

- `Widgets`は縦横のスクロールが可能なので、`ScrollView`の使用は非推奨
- 操作はシンプルなものにする（タスクはシングルタップで完結させる）
- キーボードは使用できない
- `Widgets`の高さの上限は、端末の高さである
- `Widgets`は自動でPaddingが設定される
- `Widgets`の背景色の設定は非推奨
- テキストの色は読みやすい色にする。黒か暗い灰色が推奨されている

## サンプルコード
[shoheiyokoyama / Today](https://github.com/shoheiyokoyama/swift-standard-framework/tree/master/Today)


## 参考
- [App Extension Programming Guide / Today](https://developer.apple.com/library/content/documentation/General/Conceptual/ExtensibilityPG/Today.html)
- [iOS Human Interface Guidelines / Widgets](https://developer.apple.com/ios/human-interface-guidelines/extensions/widgets/)
- [API Reference / NCWidgetProviding](https://developer.apple.com/reference/notificationcenter/ncwidgetproviding)
- [API Reference / NotificationCenter](https://developer.apple.com/reference/notificationcenter)