Today - widgetを作る
デバッグ
ターゲットをExtensionにして「実行」すると,アタッチするプロセスを選ぶリストがXcodeで表示される. ここで,Todayを選ぶと,iOS上で,TodayのUIが開かれる.
そこで,自分のアプリケーションのWidgetを追加すれば,デバッグできるようになる. Extensionをターゲットにして実行する前にシミュレータを起動しておかないと,プロセスにアタッチできない. Extensionをターゲットにして実行すると,デバッガやNSLogが利用できるので便利である.
widgetのためのプロトコル〜NCWidgetProviding
以下の二つのデリゲートが用意されている.
- (void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult result))completionHandler;
- (UIEdgeInsets)widgetMarginInsetsForProposedMarginInsets:(UIEdgeInsets)defaultMarginInsets;
それぞれのデリゲートメソッドの詳細は口述しますが,先に主な目的だけ簡単に説明します.
widgetPerformUpdateWithCompletionHandler:
は,widgetのコンテンツをアップデートするべきタイミングのときにシステムからコールされます.
widgetMarginInsetsForProposedMarginInsets:
は,Today上に表示されるwidgetの余白を調整するために,システム側からwidgetの余白を決定するタイミングでコールされます.このデリゲートメソッドは,viewWillAppear:
の前のタイミングでコールされるようだ.
widgetのライフサイクル
widgetに使用するビューコントローラは,Storyboardあるいはクラスを直接plistに記述して指定する. そのビューコントローラのライフサイクルをログから考察してみる. ビューコントローラのイベント毎に時刻と まず,Todayを開いてみると,以下のログが
2014-06-23 15:02:34 +0000(0x10bf981e0)viewDidLoad
2014-06-23 15:02:34 +0000(0x10bf981e0)viewWillAppear:
次に,Todayを閉じると,デリゲートメソッドが呼ばれて,ビューコントローラが開放されるようだ.
このwidgetPerformUpdateWithCompletionHandler:
が呼ばれるタイミングや回数は,beta2現在,不明瞭であり,二度呼ばれたこともログから観察された.dealloc
のタイミングも不明瞭であった.Todayを再度引き出したときに開放されたり,適当なタイミングで開放されているようだ.
2014-06-23 15:03:25 +0000(0x10bf981e0)widgetPerformUpdateWithCompletionHandler:
2014-06-23 15:03:26 +0000(0x10bf981e0)dealloc
一度,Todayを閉じた後に,再度Toadyを開いてみると,今度は異なるビューコントローラのインスタンス生成されたことがログから確認できる. Todayを開く度に新しいビューコントローラが生成されると考えてよさそうだ.
2014-06-23 15:04:20 +0000(0x10bb33250)viewDidLoad
2014-06-23 15:04:20 +0000(0x10bb33250)viewWillAppear:
widgetPerformUpdateWithCompletionHandler:
は,ドキュメントによると次回のToday表示時にプレビューとして使うビューをキャプチャするときに呼ばれるようだ.
This method is called to give a widget an opportunity to update its contents and redraw its view prior to an operation such as a snapshot. (Apple)
というわけでこのライフサイクルにしたがってビューを作ればよさそうである. メソッドにエラーがあったりすると,widgetがフリーズするようだ.
以上を考察すると,widgetPerformUpdateWithCompletionHandler
のタイミングで新しいデータを保存しておき,viewDidLoad
のタイミングでそのデータを読み込んでまずはすぐに状態を復帰させ,viewDidAppear
とかviewWillAppear
のタイミングで再度新しいデータを読み込みに行くというのが正しい流れということになるだろう.
widgetサイズ
UIContentContainerプロトコルを使って設定する. 横幅のデフォルト値は280のようだ.
self.preferredContentSize = CGSizeMake(100, 280);
さらにデフォルトのままだと,コンテンツの左と下に余白があるはずだ.
この余白は,- (UIEdgeInsets)widgetMarginInsetsForProposedMarginInsets:(UIEdgeInsets)defaultMarginInsets;
をオーバーライドして設定する.
Today上に表示できるサイズには限界があり,限界以上のサイズにはできない. またこの限界のサイズはドキュメントに示されていない. さらに,Today上でスクロールするビューを設置することもできない.
UITableViewを設置するときは
UITableViewControllerを使うことになるだろう.
そのとき,UITableViewControllerを,- (UIEdgeInsets)widgetMarginInsetsForProposedMarginInsets:(UIEdgeInsets)defaultMarginInsets;
のデフォルトの値のまま使ってしまうと,左のマージンの空間に何もレンダリングされなくなってしまう.
これを防ぐため,- (UIEdgeInsets)widgetMarginInsetsForProposedMarginInsets:(UIEdgeInsets)defaultMarginInsets;
の値をセルがTodayのビューの左端まで届くように調整する.
そして,カスタムセル上で,左マージンを調整するようにすれば,デフォルトのカレンダーのようなUIを実装することができる.
デザインガイドラインと原則
- 高さが大きくなりすぎるのは,Todayの中身が巨大になってしまうのでダメ.あまり大きいビューコントローラを利用すると自動的にサイズを切られる.
- widgetの中にスクロールビューをいれるのはダメ.スクロール操作は一切処理できない.
- 複雑な操作が必要になるのはダメ.複雑さの基準は,Appleの標準アプリをよく見て考えること.
- UIButtonなどのタップイベントを取ることはできる.
- 配布はアプリケーションにバンドルする形式で行う.
- アプリケーションとのデータ共有はApp Groupを使って行う.
- Extensionでは,UIApplicationクラスを利用できない.
- アプリケーションを起動するときは,openURLを使うが,UIApplicationではなく,NSExtensionContextクラスのメソッドを利用する.
openURLの実行は,以下のように行う.
[self.extensionContext openURL:[NSURL URLWithString:URLString] completionHandler:^(BOOL success) {
NSLog(@"fun=%s after completion. success=%d", __func__, success);
}];
Extensionがターゲットであるかの判定
UIApplicationクラスなど,Today Extensionでは使えないクラスが存在する. このため,アプリケーションとExtensionでソースコードを共有すると,コンパイルできない部分が出てくるため,プリプロセッサでコンパイル処理を分岐する必要がある. シンプルにIS_TARGET_EXTENSION=1をプリプロセッサマクロに追加して対応する.
署名
Extensionの識別子は,アプリケーションに追加するような形式で指定する.
つまり,アプリケーション本体の識別子をprefixとして持つような識別子を指定する必要がある.
たとえば,アプリケーション本体の識別子がcom.sonson.hoge
である場合,Extensionの識別子はcom.sonson.hoge.today
のように指定する必要があるということだ.
このため,当然Extensionの署名は,アプリケーション本体とは違う証明書で行う. 実機向けにビルドする前に”Certificates, Identifiers & Profiles”でアプリケーションと他にExtension用のApp IDを追加し,プロビジョニングファイルを作成しておく. アプリケーション,Extensionでそれぞれ異なるプロビジョニングファイルを使って署名するようにXcodeを設定する.