読者です 読者をやめる 読者になる 読者になる

CXCKeyValueObserver をパクった CSNNotificationObserver ってのを作った

表題の通りです。 2週間くらい前に id:cockscomb さんが CXCKeyValueObserver というライブラリをリリースしていました。これはどんな感じのライブラリかっていうと

  • KVO の 監視開始/終了 忘れず安心に実行できる
  • 複数 KVO を使っても通知を受けた後の処理が監視単位ごとにスッキリ分かれる

っていうシンプルながら KVO の面倒くさいところが綺麗にまとまっているものです。

このコードを見たときに

  • 確かに監視する側(感覚的には監視を管理する側)が死ぬときに自分で後始末すればいいよな
  • ARC な今どきならインスタンス変数として保持しておけば、オーナーが死ぬときに自動的に死んでくれるよな

と気づきました。

そこで、同じ方法で Notification も監視管理してしまえってことで作りました。

CSNNotificationObserver

NSNotification をオブザーブする場合は KVO ほどシビアではありませんが

  • オブザーブの開始
  • Notification の受信
  • オブザーブが不要になったときまたはオブザーバーが解放される時の解除

が必要です。

CSNNotificationObserverCXCKeyValueObserver と同様にインスタンスが解放される際に remove するようにしてあります。

CSNNotificationObserver がないとき

@implementation SampleViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(receiveDidEnterBackgroundNotification:) name:UIApplicationDidEnterBackgroundNotification object:nil];
}

- (void)receiveDidEnterBackgroundNotification:(NSNotification *)notification
{
    // do something
}

- (void)dealloc
{
    // 忘れずに remove しないといけませんが ARC 環境になると dealloc を書く習慣が薄れてきて忘れがち
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

@end

CSNNotificationObserver があるとき

@implementation SampleViewController {
    CSNNotificationObserver *_observer;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    _observer = [[CSNNotificationObserver alloc] initWithObserver:self selector:@selector(receiveDidEnterBackgroundNotification:) name:UIApplicationDidEnterBackgroundNotification object:nil];
}

- (void)receiveDidEnterBackgroundNotification:(NSNotification *)notification
{
    // do something
}

- (void)dealloc
{
    // ARC 環境下であれば _observer が解放される時に remove もしてくれるので remove 忘れがない。
}

@end

NSNotificationCenter は iOS 4 から従来までの Selector を指定して受信する方法とは別に Blocks での受信もサポートされるようになりました。通知内容とそれに対する処理がまとまり見やすい反面 remove するためには add した時の戻り値を保持しておき、remove 時の引数にする必要がありイマイチ使いにくい印象がありました。

CSNNotificationObserver がないとき (Blocks 使用時)

@implementation SampleViewController {
    // observer をインスタンス変数で管理しないといけないコレクションクラスに格納したとしても面倒
    id _didEnterBackgroundNotificationObserver;
    id _willEnterForegroundNotificationObserver;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    _didEnterBackgroundNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidEnterBackgroundNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notification) {
        // do something
    }];
    
    _willEnterForegroundNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillEnterForegroundNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notification) {
        // do something
    }];
}

- (void)dealloc
{
    // 忘れがち
    [[NSNotificationCenter defaultCenter] removeObserver:_didEnterBackgroundNotificationObserver];
    [[NSNotificationCenter defaultCenter] removeObserver:_willEnterForegroundNotificationObserver];
}

@end

CSNNotificationObserver があるとき (Blocks 使用時)

@implementation SampleViewController {
    CSNNotificationObserver *_observer;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    _observer = [[CSNNotificationObserver alloc] initWithName:UIApplicationDidEnterBackgroundNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notification) {
        // do something
    }];
    
    [_observer addObserverForName:UIApplicationWillEnterForegroundNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notification) {
        // do something
    }];
}

- (void)dealloc
{
    // ARC 環境下であれば _observer が解放される時に remove もしてくれるので remove 忘れがない。
}

@end

Blocks を使った場合には CSNNotificationObserver の旨味が生きてきます。

注意点

以下のようなコードは期待しているような動作をしません。

/// 以下のメソッドは複数回呼ばれる想定
- (void)addObserver
{
    /// 二回目以降呼ばれると Notification を受信できなくなる
    self.notificationObserver = [[CSNNotificationObserver alloc] initWithObserver:self selector:@selector(respondsNotification:) name:@"SomeNotificationName" object:nil];
}

理由

上記メソッドが複数回呼ばれた場合には、生成と既存インスタンスの解放のタイミングが前後するため最終的に self はオブザーバーとして登録されません。

流れとしては2回目以降の呼び出し時には以下のようになります

  1. initWithObserver:selector:name:object:self が NSNotificationCenter に登録される
  2. CSNNotificationObserver の新しいインスタンスが戻り値として返ってくる
  3. 戻り値が代入されることで _notificationObserver が解放される
  4. _notificationObserver の解放時に self が NotificationCenter から解除される
  5. 新しい CSNNotificationObserver インスタンス_notificationObserver に新たに保持される

Cockscomb さんの CXCKeyValueObserver はオブザーバーオブジェクトがライブラリ自身であり再生成されるため同じような問題は起こらない。 Blocks のパターンの場合も同様にオブザーバーオブジェクトは NotificationCenter が返すインスタンスのため、同様に問題が起こらりません。

回避策

1. 先に既存インスタンスを解放する

/// 以下のメソッドは複数回呼ばれる想定
- (void)addObserver
{
    /// 先に解放する
    self.notificationObserver = nil;
    self.notificationObserver = [[CSNNotificationObserver alloc] initWithObserver:self selector:@selector(respondsNotification:) name:@"SomeNotificationName" object:nil];
}

2. インスタンス生成と Notification の登録をわける

/// 以下のメソッドは複数回呼ばれる想定
- (void)addObserver
{
    /// 生成時には登録しない
    self.notificationObserver = [[CSNNotificationObserver alloc] init];
    /// インスタンス変数に格納されている `CSNNotificationObserver` オブジェクトに登録する
    [self.notificationObserver addObserver:self selector:@selector(respondsNotification:) name:@"SomeNotificationName" object:nil];
}

3. ブロックスタイルをつかう

/// 以下のメソッドは複数回呼ばれる想定
- (void)addObserver
{
    self.notificationObserver = [[CSNNotificationObserver alloc] initWithName:@"SomeNotificationName" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notification) {
        [self respondsNotification:notification];
    }];
}

インストール

CocoaPods に登録してあるので

pod 'CSNNotificationObserver', '~> 0.9.2'

でサクッとインストール。ライセンスは MIT です。

実装面

remove するためには Observer を CSNNotificationObserver 保持しなくてはなりません。先に挙げたサンプルのように大半のパターンでは

だと思います。この関係で CSNNotificationObserver インスタンスの保持者が解放される流れで CSNNotificationObserver インスタンスを解放するためには、 CSNNotificationObserver インスタンス側が Notification のオブザーバーを retain してしまっていては循環参照でいっこうに解放されなくなってしまいます。

あまり複数のオブザーバーが登録されることはないとは思いますが念のために今回初めて NSHashTable を使って見ました。NSHashTable は NSMutableSet と似たような動きをしますが、格納したオブジェクトを弱参照のまま保持することも可能です。今回はこの機能を使うことで複数のオブザーバーを循環参照をさけて保持するようにしています。

最後に

よかったら使って見てください。id:cockscomb さんありがとうございます。

App Switcher に表示される View を差し替える補助ライブラリ MMAppSwitcher

以前アプリの画面を開いているアプリケーションのプレビュー画面から隠すというものを書きました。先日たまたま見つけたライブラリがこのような処理を補助するものがあったので一応ご紹介。

MMAppSwitcher というライブラリです。使い方的には MMAppSwitcherDataSource プロトコルを実装して App Switcher に表示して欲しい View を返す感じです。

ライブラリの内部的には UIApplicationWillBeginSuspendAnimationNotification という非公開な Notification をきっかけに App Switcher 用の View を表示させているようです。非公開なものなので若干審査だったり将来のバージョンアップとか大丈夫かな?って気がしますね。

こんなことをしている理由として推測するには、applicationDidEnterBackground: のタイミングだと

  1. アプリ最前面
  2. ホームボタン1回押し
  3. ホーム画面が表示されたと同時にホームボタン2回押し

というような操作をされた場合には applicationDidEnterBackground: がまだ呼ばれていないっぽいので App Switcher 用の View を用意できないんですね。だからこのライブラリの作者は willEnterBackground 相当のタイミングを探った結果 UIApplicationWillBeginSuspendAnimationNotification にたどり着いたのだと思います。

僕がやったような "ほぼ期待する動作をする" アプローチをとるのか、MMAppSwitcher のように攻めるのかは自己責任で。

どうでもいいけど

この「ホームボタン2回押し」で表示される機能ないし画面の Apple 的正式名称ってなんなんでしょうね?個人的に AppSwitcher って気持ち悪い。

iOS アプリ開発に関わる人にぜひ読んで欲しい本[の宣伝]

僕は iOS アプリのコーディングをやっていて主にそっち方面のブログエントリを書いていますが今日は本の紹介というかタイトル通り宣伝です。

アプリ開発をやってる僕ですが結構 UI / UX は気になるタイプで仕事中もデザイナーにいちゃもんつける面倒くさいやつなんですが今回同僚が本を書きました。

iOS 7デザインスタンダード 最新のフラットデザインに対応-iPhoneに最適なUI・UXを徹底的に解説!

iOS 7デザインスタンダード 最新のフラットデザインに対応-iPhoneに最適なUI・UXを徹底的に解説!

この本は主にデザイナー向けに書かれた本です。特に今まで UI / UX デザインを手がけた経験があまりない方や別プラットフォームがメインだったデザイナー向けです。僕の思ったターゲット読者別のおすすめポイントを簡単に紹介します。

デザイナー向け

先に挙げたように iOS アプリの UI / UX をバリバリやっている人向けというよりはこれから学ぶ人に最適だと思います。別プラットフォームが主戦場だった人にはチャプター 02 の iOS 標準 UI の紹介とその使いどころの説明で基礎的な知識を学べます。チャプター 03 はこの本の特徴だと思いますが実際に筆者がアプリの企画/デザインをする際にどのようなことを行っているのかを垣間見ることができる実践的な話が詰め込まれています。業務として UI / UX デザインの経験が浅い方はもちろん経験豊富な方も他のデザイナーがどのような作業フローをやっているのか?どのような視点で考えているのか?という事を知る機会は限られていると思うので1度読んでみると視野が広がるかもしれません。

開発者向け

iOS 6 から iOS 7 への標準 UI の変更点なども記載されていて iOS 7 向けのアプリ開発になれていない開発者の人でもその差異を知るのに役立ちます。巻末には各種画像のサイズや UI パーツの標準的なサイズがまとまっているのでちょっとリファレンス的に読むのもいいかもしれません。デザイナーがどんなことを考えてどのような視点でアプリのデザインを行っているか?ということは開発だけを行っていると知る機会の少ないのでこの機会に知ってみるのもいいと思います。

企画側・開発依頼側向け

デザイナーでも開発者でもないアプリの企画側の方や主にアプリ開発を依頼する側(ex: 既存 Web サービスのアプリ化を考えているような方)にも最適だと思います。デザイナーがどのような視点でアプリを良い物にするために考えているかを知ることができます。モバイルアプリの特性も知らずに考えているとあれもこれも載せたくなることは往々にしてありますが、この本を読めば本当にモバイルアプリとして iOS アプリとして必要な要素とは何なのか?ということに気づけると思います。

最後に

iOS アプリ開発に関わる人全てに1度読んでもらいたい本です。とても読みやすくすいすい読み進められる本なので是非。

iOS 7デザインスタンダード 最新のフラットデザインに対応-iPhoneに最適なUI・UXを徹底的に解説!

iOS 7デザインスタンダード 最新のフラットデザインに対応-iPhoneに最適なUI・UXを徹底的に解説!

おまけ: 別の同僚も iOS のデザインについて本を書いたのでそっちも読まないと。iOSフラットデザインの作法

dispatch_source の DISPATCH_SOURCE_TYPE_TIMER で timeout 処理を実装する

先日、x秒たったらある処理をキャンセルするといういわゆるタイムアウト処理を実装する必要があったときに dispatch_source を使ってハマったので備忘録。

当時ググっても繰り返し一定間隔で処理を動かすサンプルはすぐ見つかったのでそれをベースやっても期待する動きならなかった。結局は「エキスパート Objective-C プログラミング」にサンプルがのってて助かりましたという話。

Xcode のスニペット形式だとこんな感じ

    /// dispatch_source を生成。timer を回す queue を指定。
    dispatch_source_t timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, <#dispatch_queue#>);
    
    /// timeout の時間を "seconds" プレースホルダーに "15" のように入力。"leeway_seconds" にはズレの許容範囲を入力
    dispatch_source_set_timer(timerSource, dispatch_time(DISPATCH_TIME_NOW, <#seconds#>ull * NSEC_PER_SEC), DISPATCH_TIME_FOREVER, <#leeway seconds#>ull * NSEC_PER_SEC);

    /// timer 発火時の挙動を実装
    dispatch_source_set_event_handler(timerSource, ^{
        
        <#code#>
        
        /// 後始末
        dispatch_source_cancel(timerSource);
        /* もしくは
#if !OS_OBJECT_USE_OBJC
        dispatch_release(timerSource);
#endif
        */
    });
    
    /// timer キャンセル時の挙動を実装
    dispatch_source_set_cancel_handler(timerSource, ^{
        <#something_clean_up#>
#if !OS_OBJECT_USE_OBJC
        dispatch_release(timerSource);
#endif
    });
    
    /// timer の動作開始
    dispatch_resume(timerSource);

素直に NSTimer を使えばいいじゃん?というのがあったんだけど、その処理全体がバックグランドディスパッチキュー上で実行されてたので Runloop を自分で回すのは嫌だったり、メインスレッドにタイマーをセットするとスレッドをまたぐ事になるのでシビアなタイミング(実行順序)を求められてたので dispatch_source を使いたかったというわけです。

ホントに「エキスパート Objective-C プログラミング」はすばらしい。

UICollectionView で UITableView のセクションヘッダー風の SupplementaryView を実装する

UICollectionView は昔なら UITableView を使って頑張って実装していようなグリッドレイアウトな UI を UITableView ライクな I/F で実装できる素敵なやつです。UITableView ライクな I/F とは言いましたが実は細かい挙動が UITableView とは違っています。UICollectionView で UITableView のセクションヘッダーのようなものを実装するには SupplementaryView を使います。でも普通に UICollectionViewFlowLayout を使っても SupplementaryView はスクロールすると Cell と同じようにそのまま通り過ぎてしまいます。UITableView のセクションヘッダーみたいに同一セクション内の場合に上端に居続けてくれません。UITableView のセクションヘッダーみたいなフローティングした(上端に張り付いたような)セクションヘッダーを実装したい。じゃあ、似たような挙動になるようにしようっていうのが今回の話です。

とりあえずググる

特にライブラリとしてまとまっているものが見つけられませんでした。(あとで調べたら vast-eng/uicollectionview-gridlayoutというのがありました。)しかし、stackoverflow で同じようなことをしたいという質問が見つかりそこから gist に上げられたコードを見つけました。

Sticky Headers at the top of a UICollectionView

やや難あり

さっきの gist ですが、確かに UITableView のセクションヘッダーみたいに同一セクション内の場合に上端に居続けてくれるのですが、全体的に位置が下がり気味。何かが違う。

どノーマルの UICollectionViewFlowLayout を使ったものがこちら

f:id:griffin-stewie:20131020120023p:plain

gist のコードをそのまま適用したのがこちら

f:id:griffin-stewie:20131020120021p:plain

修正

ずれる理由は UICollectionViewFlowLayoutsectionInset の値が考慮されていませんでした。修正後のコードはこちら。

UIEdgeInsets sectionInset = self.sectionInset;

origin.y = MIN(
                MAX(
                    contentOffset.y + cv.contentInset.top,
                    (CGRectGetMinY(firstObjectAttrs.frame) - topHeaderHeight - sectionInset.top)
                    ),
                (CGRectGetMaxY(lastObjectAttrs.frame) - bottomHeaderHeight + sectionInset.bottom)
                );

公開

修正したコードを UICollectionViewFlowLayout のサブクラスとして github にあげています。もし使えそうなら使って見てください。

CSNFloatingHeaderViewFlowLayout

CocoaPods でインストールできます。

pod 'CSNFloatingHeaderViewFlowLayout', '~> 0.0'

Thank you toblerpwn.

はてなインターンのサンプルコードを読んでの感想

はてなさんが

はてなインターンで利用したiOSアプリ等のサンプルコードを公開しました - Hatena Developer Blog

という素敵なサンプルコードを公開してくれたので、好き勝手に感想を書いてみたいと思う。本来は Web アプリ側も動かしたかったんだけど、ウマく環境が作れなかったのでアプリはコードを読んだだけで動かしてはないです。あと、細かい検証とかしてないので勘違いとかしていると思うので指摘してください。

参考になった真似したい

NSError と UIAlertView の使い方

NSError の内容ベースでエラーアラートを出すのは理にかなっていると思った。OS 側が返す NSError は期待する値が入っていない不親切な場合があるので、オリジナルの NSError から一度適切な値を持った NSError を再生成して返すクラスを介したらいいのかもしれない。

すっきりした UIViewController

iOS アプリを作り慣れていない人は特に直接ネットワーク処理を書いてしまったりモデルの管理コードを書いてしまいがちで UIViewController の肥大化するのがありがちなパターンだと思う。しかし、このサンプルはネットワーク処理、モデル管理等を綺麗に分けているのは見習うべきだと思った。

通信とモデルの管理の関係

普段僕は Web API を叩くクラスとそれを元にモデル生成するのを同じクラスに書いていたけど、このコードのようにネットワークはシンプルに Web API を適切に触ることだけにし、パースやモデルの生成、管理はモデル管理クラスに分けた方が綺麗だし、のちのメンテナンス性や拡張性が確保されていいと勉強になった。

isEqual:hash をちゃんと実装

結構面倒で実装してないときがあるんだけど、モデルクラスがちゃんと isEqual と hash メソッドをオーバーライドしているのが素晴らしい。特に hash メソッドを実装するのを忘れていると NSSet に突っ込んだ時の振る舞いが期待通りにならずに軽くハマるので注意。

KVO をウマく使っている

KVO を使って ViewController がモデルを見ているあたりは Mac アプリ実装経験者っぽくてなんか素敵。NSErrorlocalizedFailureReason とか使っている点も同様。KVO を使って、TableView の更新をかけているのは凄く勉強になる。mutableArrayValueForKey: は知らなかった。勉強になった。

NSIndexSet の使い方

indexSet をうまく使っているのは素敵だと思った。 mattt が WWDC でも言っていたので使う機会があったら自分も使いたいと思ってる。

気になったところ

UIAlertView+NSError

self.message = [[NSArray arrayWithObjects:[error localizedFailureReason], [error localizedRecoverySuggestion], nil] componentsJoinedByString:@"\n"];

一瞬、[error localizedFailureReason]nil だったら落ちるんじゃないかと思ったけど、よく考えると arrayWithObjects:nil 終端として動作するので落ちない。[error localizedFailureReason][error localizedRecoverySuggestion]nil なら self.message はカラのままで済むのでスマート。ただ実際にあるかどうかはわからないが、逆にいうと [error localizedRecoverySuggestion] が入っていて[error localizedFailureReason]nil を返す場合には、先に渡されている nil の評価がされてしまって仮に値が入っていても [error localizedRecoverySuggestion]self.messagenil となる。この挙動を忘れているとある日ハマる。もっとマジメに実装するなら NSMutableArraynil チェックをしながら足していく感じになるのかもしれない。

NSDictionary に入れる値の nil チェック

IBKMInternBookmarkAPIClient が引数の nil チェックもせずに NSDictionary に突っ込んでいるのは落ちる可能性があるので危険。使う側が意識すれば落ちないだろうけど、渡されたデータの妥当性チェックはデータをもらう側に責任があると思うのでチェックした方がいいと思う。

addObserver:~ したら remove~ する

KVO の監視開始、NSNotificationCenter への登録をしても、それらの解除を書いていないのは良くない。

Be sure to invoke removeObserver: or removeObserver:name:object: before notificationObserver or any object specified in addObserver:selector:name:object: is deallocated.

NSNotificationCenteraddObserver:selector:name:object: にも書いてある。

NSNotificationCenter の remove は複数回読んでも害はないけど、KVO の remove は必ず add を一対でないと行けない(add していない状態で remove すると落ちる)のでライフサイクル的に一対になるようなところに入れるしないといけないので注意が必要。

24時間表示を Off でもちゃんと機能するように NSDateFormatter を使う

NSDateFormatter に locale を "en-US" でセットしていないけど、24時間表示を Off にしている端末でも期待する動作をするのか不安。timeZone を指定していると大丈夫なんだろうか?未検証。

まとめ

気になる点はあるけど、これだけ綺麗にまとまって、なおかつそれなりに実戦経験がある人でもやってなさそうだったり知らなそうなことをサラッと見せてくれているので、このコードで Objective-C / Cocoa を勉強するインターン生は幸せもんだなぁと思った。僕も来年インターンで入れないかな?

アプリの画面を開いているアプリケーションのプレビュー画面から隠す

iOS 7 以前でも、アプリ実行中にホームボタンを押してまた、そのアプリに戻ってきた場合には OS が作成した画面のスクリーンショットが表示されていました。プライバシーが気になるようなアプリでは Home に戻る直前の画面をそのまま次回前面になったときに使われると困ることがあります。家計簿等のお金を扱うアプリだと独自に暗証番号でロックできるようになっていますが、私が使っている「マネー2」は前面に戻ってきた時にちらっと前の画面がうつってからロック画面に移行してて何だかなぁと思っていました。iOS 7 になってからはホームボタンを2回押して出てくる最近使ったアプリ一覧画面にプレビュー画面が付いて Exposé 風になりました。Exposé 風になったことでますますこの問題が出てきてしまって気になったのでどうすれば良いのか試してみました。

iOS App Programming Guide

iOS App Programming Guide には

Remove sensitive information from views before moving to the background. When an app transitions to the background, the system takes a snapshot of the app’s main window, which it then presents briefly when transitioning your app back to the foreground. Before returning from your applicationDidEnterBackground: method, you should hide or obscure passwords and other sensitive personal information that might be captured as part of the snapshot.

と書かれています。アプリ側でよしなに対処するべきなんですね。

普通に実装しただけの場合

普通に前面にアプリがいるとき

f:id:griffin-stewie:20131011010803p:plain

そのまま Home ボタン2回押し

f:id:griffin-stewie:20131011010752p:plain

当然ながら凜々しい猫が丸見えです。

対策を実装

今回試したのは割と手抜きです。試した内容は以下のような感じです。

  • AppDelegate に Window と同じサイズの UIView を用意し、適当な色をつけておく
  • 各種ライフサイクルのデリゲートメソッドでその View を 表示 / 非表示 させる

View の実装

- (UIView *)privacyView
{
    if (_privacyView == nil) {
        _privacyView = [[UIView alloc] initWithFrame:self.window.frame];
        _privacyView.backgroundColor = [[UIColor orangeColor] colorWithAlphaComponent:0.95];
    }
    return _privacyView;
}

View の出し入れ

以下のようなメソッドを用意しておいて

- (void)showPrivacyView
{
    [self.window addSubview:self.privacyView];
}

- (void)removePrivacyView
{
    [self.privacyView removeFromSuperview];
}

Application のライフサイクルで出し入れを実装します。

- (void)applicationWillResignActive:(UIApplication *)application
{
    [self showBluredPrivacyWindow];
}

- (void)applicationDidBecomeActive:(UIApplication *)application
{
    [self removeBluredPrivacyWindow];
}

applicationWillResignActive: はアプリが前面にあるときに Home ボタン2回押しの時も、Home に戻った時にも呼ばれます。applicationDidBecomeActive: はアプリが前面に戻ってきたときに呼ばれます。こちらもアプリが前面にあるときに Home ボタン2回押しした時も、Home に戻った時にも呼ばれます。

これらを実装すると以下のような感じになります。このスクリーンショットはアプリが前面にあるときに Home ボタン2回押しした時のものです。

f:id:griffin-stewie:20131011010741p:plain

折角なので iOS 7 っぽい磨りガラス風に

ということで iOS 7 風にしてみます。UIImage に対してブラーを掛けた画像を生成する部分は Apple のサンプルコードにある UIImage+ImageEffects を使ってみます。ただ、実際にはアプリの画面自体(View)を磨りガラス風画像にする必要があります。iOS 7 から - (UIView *)snapshotViewAfterScreenUpdates:(BOOL)afterUpdates がありますが、これは戻り値が UIView なので用途にあっていません。- (BOOL)drawViewHierarchyInRect:(CGRect)rect afterScreenUpdates:(BOOL)afterUpdates なら同様の処理を context に draw してくれるのでこちらを使って今の画面のスクリーンショットを取るようにします。

任意の View のスクリーンショットを撮って磨りガラス風の画像を生成するコードはこんな感じになります。

- (UIImage *)blurredSnapshotWithBlurType:(BlurEffectsType)type
{
    /// Original Code: iOS 7 blurring techniques — Damir Tursunović http://damir.me/posts/ios7-blurring-techniques
    UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, self.window.screen.scale);
    
    // There he is! The new API method
    [self drawViewHierarchyInRect:self.frame afterScreenUpdates:NO];
    
    // Get the snapshot
    UIImage *snapshotImage = UIGraphicsGetImageFromCurrentImageContext();

    // Now apply the blur effect using Apple's UIImageEffect category
    UIImage *blurredSnapshotImage = nil;
    switch (type) {
        case BlurEffectsTypeLight:
            blurredSnapshotImage = [snapshotImage applyLightEffect];
            break;
        case BlurEffectsTypeExtraLight:
            blurredSnapshotImage = [snapshotImage applyExtraLightEffect];
            break;
        case BlurEffectsTypeDark:
            blurredSnapshotImage = [snapshotImage applyDarkEffect];
            break;
        default:
            break;
    }

    // Be nice and clean your mess up
    UIGraphicsEndImageContext();
    
    return blurredSnapshotImage;
}

ImageContext を作って、- (BOOL)drawViewHierarchyInRect:(CGRect)rect afterScreenUpdates:(BOOL)afterUpdates で画面を draw し、Context から画像化。その画像を磨りガラス風画像に再加工して書き出します。

実際にこれを使うのには先ほどのメソッドを少し変更します。

- (void)showPrivacyView
{
    self.privacyView.backgroundColor = [UIColor colorWithPatternImage:[self.window blurredSnapshotWithBlurType:BlurEffectsTypeLight]];
    [self.window addSubview:self.privacyView];
}

Window を磨りガラス風画像にして、UIColor にして view の backgroundColor に突っ込みます。綺麗に実装するのなら self.privacyView を UIImageView にするべきですね。この例では手抜き実装となっています。するとこんな感じになります。

f:id:griffin-stewie:20131011010920p:plain

シミュレータだと対策が無効の場合がある

ハマりどころとしては、シミュレータの場合、ホームに戻ってから最近使ったアプリ一覧画面をみても期待通りの動きをしません。実機で試せば問題ありませんでした。このせいで小一時間ハマりました。

まとめ

実際にやってみると簡単です。View を磨りガラス風の画像にするコードはカテゴリメソッド等で使い回しが効く処理なのでプライバシーに配慮するべき画面があるアプリでは実装してみてはいかがでしょうか?iOS 6 以下の場合は一部メソッドが使えないので少し違った対処が必要になるかと思いますが今回の方法をベースすればさほど難しくはないと思います。

サンプルコードをおいておきます。

griffin-stewie/PrivacyProtectSample