デバッグ

ターゲットをExtensionにして「実行」すると,アタッチするプロセスを選ぶリストがXcodeで表示される. ここで,Todayを選ぶと,iOS上で,TodayのUIが開かれる.

image

そこで,自分のアプリケーションのWidgetを追加すれば,デバッグできるようになる. Extensionをターゲットにして実行する前にシミュレータを起動しておかないと,プロセスにアタッチできない. Extensionをターゲットにして実行すると,デバッガやNSLogが利用できるので便利である.

image

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を実装することができる.

デザインガイドラインと原則

  1. 高さが大きくなりすぎるのは,Todayの中身が巨大になってしまうのでダメ.あまり大きいビューコントローラを利用すると自動的にサイズを切られる.
  2. widgetの中にスクロールビューをいれるのはダメ.スクロール操作は一切処理できない.
  3. 複雑な操作が必要になるのはダメ.複雑さの基準は,Appleの標準アプリをよく見て考えること.
  4. UIButtonなどのタップイベントを取ることはできる.
  5. 配布はアプリケーションにバンドルする形式で行う.
  6. アプリケーションとのデータ共有はApp Groupを使って行う.
  7. Extensionでは,UIApplicationクラスを利用できない.
  8. アプリケーションを起動するときは,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をプリプロセッサマクロに追加して対応する.

image

署名

Extensionの識別子は,アプリケーションに追加するような形式で指定する. つまり,アプリケーション本体の識別子をprefixとして持つような識別子を指定する必要がある. たとえば,アプリケーション本体の識別子がcom.sonson.hogeである場合,Extensionの識別子はcom.sonson.hoge.todayのように指定する必要があるということだ.

このため,当然Extensionの署名は,アプリケーション本体とは違う証明書で行う. 実機向けにビルドする前に”Certificates, Identifiers & Profiles”でアプリケーションと他にExtension用のApp IDを追加し,プロビジョニングファイルを作成しておく. アプリケーション,Extensionでそれぞれ異なるプロビジョニングファイルを使って署名するようにXcodeを設定する.