今回のエンジニアブログを担当します山下です。
今回は前回のTouch IDに続き、iOS 8に新しく追加されたAPIの一つ、
ウィジェット機能の実装についてご紹介します。
ウィジェットはカレンダーやタイマーといったようなシンプルな機能を持ったアプリを
通知センターからアクセス出来るようになる機能です。Androidユーザにはなじみ深い機能かと思います。
他のアプリを開いていても、ロックしていても、ウィジェット機能には素早くアクセスが可能になるため、
アプリの応用の幅が更に広がると思います。
今回はこのウィジェット機能をもったアプリをSwiftで実装してみました。
App Extensionについて
App Extensionとは、アプリケーションの垣根を越えた補助的な機能をユーザに提供するための仕組みです。
ウィジェット機能を提供するTodayや、待ち望んでいる人も多かったCustom Keyboardもこれに含まれます。
必ずホストとなるアプリがいる必要があり、Extensionのみのアプリを作ることは出来ません。
App Extensionはアプリ内に内包する形ではあるものの、Extensionごとに別ターゲットで
異なるBundle Identifierを設定するため、ほぼ独立した別のアプリケーションとして動作します。
プロジェクトの作成
では実際にアプリケーションを作ってみましょう。
今回はApp Storeから入手可能な最新版(執筆時点)であるXcode 6.0.1を使用しています。
Xcodeを開き、iOSのSingle View Applicationを選択し、新規プロジェクトを作成します。
LanguageはSwiftを選択しましょう。
ターゲットの追加
次にExteision用のターゲットを追加します。
メニューバーの File > New > Target を選択し、
Application Extension より Today Extension を選択して Next をクリックします。
Extension用にプロダクト名を設定します。
ここでホストアプリと同じ名前を指定することは出来ません。
設定できたら Finish をクリックします。
スキームの有効化を確認するメッセージには Activate を選択しましょう。
これでプロジェクト中にTodayエクステンション用のグループが作成されます。
実行
まずはこのままの状態で実行してみましょう。
スキームがTodayエクステンション用のものになっていることを確認し、左上の三角ボタンから実行します。
実行するアプリを選択する画面では Today を選択し、Run をクリックします。
するとiOS Simulatorが起動し、まもなく通知センターが表示されます。
Hello Worldと表示されるウィジェットが表示されれば成功です。
画面更新の実装
次に動きを付けていきます。
Today Extension には通常のアプリと同じように Storyboard や ViewController が存在し、
ほぼ同様な実装が可能となっています。
まずは Storyboard を開き、初期状態で既に配置されている UILabel を、
TodayViewController と IBOutlet 接続します。
続いて TodayViewController.swift を開き、以下のように updateLabel メソッドを追加し、
widgetPerformUpdateWithCompletionHandler を書き換えます。
func widgetPerformUpdateWithCompletionHandler(completionHandler: ((NCUpdateResult) -> Void)!) { self.updateLabel() completionHandler(NCUpdateResult.NewData) } func updateLabel() { var dateFormatter = NSDateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd hh:mm:ss" self.label.text = dateFormatter.stringFromDate(NSDate()) }
ウィジェットの内容は widgetPerformUpdateWithCompletionHandler が呼ばれたタイミングで更新されます。
このメソッドは通知センターが表示されているときはもちろんのこと、バックグラウンドでも呼ばれるようです。
ウィジェットはシステムによりスナップショットが定期的に取られており、
表示を最新のものに更新するまでのつなぎとして使用されます。
更新した結果は NewData, NoData, Failed のいずれかのステータスで
completionHandlerを通じてシステムに通知します。
この仕組みは以前紹介したiOS 7のBackground Fetchに似ています。
ここまで出来たらもう一度実行してみましょう。
通知センターが開かれてからワンテンポ遅れて内容が更新されるのが分かると思います。
ボタンによる操作の実装
次はボタンを設置し、ユーザによる操作を反映してみましょう。
Storyboard を開き、Button を右側に配置します。
配置できたら Touch Up Inside を TodayViewController のメソッドと IBAction 接続しておきましょう。
通知センターの表示領域や左右のマージンは、デバイスの種類や向きによってまちまちですので、
ウィジェットのUIは最初から柔軟に対応出来るものにしておくのがベターです。
今回は Button を右寄せで表示するために、AutoLayout で制約を付けていきます。
まずは Button を選択した後、右下のボタン群より Pin ボタンをクリックし、右からの間隔を指定します。
Update Frames は Items of New Constraints を選択しておくことで、制約のつじつまが合った状態に
各パーツが移動してくれるので便利です。
そしてAdd 1 Constraintをクリックすると制約が反映されます。
この時点では縦方向の位置に関する制約がないため、ボタンは親ビューの上部に張り付いた状態になりWarningが出ます。
それでは縦方向の制約をつけます。
Button が選択された状態で Align ボタンをクリックし、Vertical Center in Container にチェックを入れます。
先ほどと同様に Update Frames を Items of New Constraints に設定し、Add 1 Constraint で反映しましょう。
ボタンのタイトルをお好みで変更したら見た目は完成です。
TodayViewController に戻り、IBAction 接続したメソッドの中身を実装します。
@IBAction func buttonTouched(sender: AnyObject) { self.updateLabel() }
出来たら実行してみましょう。
ボタンが押されたタイミングでも日時が更新されるようになりました。
マージンの操作
ここまででずっと気になっていた方もいると思いますが、
特に制約を加えているわけでもないのにラベルが中央から右に寄っているように見えてしまっています。
これはウィジェットの左側にデフォルトでマージンが設定されているのが原因です。
多くのアプリはラベルの文字列を左揃えで表示するなどして統一感を出していますが、
UIによってはこのマージンを変更したい場合もあると思います。
その場合は widgetMarginInsetsForProposedMarginInsets を実装することで対処できます。
TodayViewController に以下のメソッドを追加すると、上下左右のマージンが無くなります。
func widgetMarginInsetsForProposedMarginInsets(defaultMarginInsets: UIEdgeInsets) -> UIEdgeInsets { return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) }
実行して確認してみて下さい。
おわりに
今後、特にツール系のアプリではウィジェットの有る無しが大きな差になるかもしれません。
制約はありますが実装自体は比較的簡単なので、ウィジェットを持ったアプリを是非作ってみてください。