ALAssetsLibrary を触るときの権限取得を事前に確認する
ちょっと不便
ALAssetsLibrary
を触るときにはユーザーに AssetsLibary へのアクセスを許可してもらわないといけません。個人的には先にアクセス許可の伺いを立ててからその結果に応じてアプリとしての振る舞いを切り分けたいと思います。しかし、ALAssetsLibary
にはそのようなメソッドは提供されていません。似たようなアクセス許可を得るものには ACAccountStore
がありますが、こちらはこのようなメソッドがあり結果に応じて振り分け可能になっています。
- (void)requestAccessToAccountsWithType:(ACAccountType *)accountType options:(NSDictionary *)options completion:(ACAccountStoreRequestAccessCompletionHandler)completion
ACAccountStore Class Reference
カテゴリメソッドを書いてみた
ALAssetsLibrary でも同様の振る舞いができるようなカテゴリメソッドを書いてみました。
Category method to get permission before using ALA ...
使用例
- (void)viewDidLoad { [super viewDidLoad]; [ALAssetsLibrary csn_requestAccessToAssetsLibraryWithCompletionBlock:^(BOOL granted, NSError *error) { dispatch_async(dispatch_get_main_queue(), ^{ if (granted && error == nil) { UIImagePickerController *picker = [[UIImagePickerController alloc] init]; [self presentViewController:picker animated:YES completion:NULL]; } else { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Needs Permission" message:nil delegate:nil cancelButtonTitle:@"close" otherButtonTitles:nil]; [alert show]; } }); }]; }
エラー処理は適当に書いていますがこれで事前にアクセス権を確認したのちに任意の処理にすすめられます。
実装のポイント
アクセス許可のアラートは実際に ALAssetsLibrary に触らないと出ません。ですので、- (void)enumerateGroupsWithTypes:(ALAssetsGroupType)types usingBlock:(ALAssetsLibraryGroupsEnumerationResultsBlock)enumerationBlock failureBlock:(ALAssetsLibraryAccessFailureBlock)failureBlock
で触ってしまいます。アラートを出してもらうのが目的なので得られた ALAssetsGroup には何もせずに即座に *stop に YES を代入して enumeration を止めつつ呼び出し元にコールバックします。
ユーザーが拒否した場合には failureBlock が呼ばれるのでそこで呼び出し元にコールバックします。
ハマったのが、この enumeration は NSArray
の enumeration と違い stop に YES を代入しても即座にループは止まらず都合2回 usingBlock
が呼び出されます。推測ではありますが、- (void)enumerateGroupsWithTypes:(ALAssetsGroupType)types usingBlock:(ALAssetsLibraryGroupsEnumerationResultsBlock)enumerationBlock failureBlock:(ALAssetsLibraryAccessFailureBlock)failureBlock
は「列挙すべき group
がなくなった場合には group
が nil の状態で usingBlock
が呼び出される」というデザインになっています。この API デザインを踏襲するために stop に YES が代入されてももう一度 usingBlock
が呼び出されるのだと思います。
まとめ
少しトリッキーな実装になっていますが、リファレンスやヘッダファイルに書かれていることをベースにして実装しているので特に害はないと思います。使いやすいと思うので良かったら使ってみてください。
Xcode Plugin が盛り上がっているらしい
しばらく前に簡単なプレゼン資料を作って一部の人にブログに書く書く詐欺をしていたので、以下のクラスメソッドさんの記事に便乗して書いておこうと思います。
基本的には重複してるし自分のエントリの方が適当なのでクラスメソッドさんの記事を参照してください。
Xcode Plugin とは
- Xcode は元々それ自身が複数の Plugin の塊
- Xcode 5.0.1 内部には 54 個の Plugin がある
- 3rd Perty が勝手にその Plugin の仕組みを使って自身のコードを Xcode にロードさせることで自分が欲しい機能を追加している。
- 公式な方法ではないため API はない。
- おのおのが探しだして追加する
ロードの仕組み
Info.plist の "Principal class" に読み込んでもらいたいクラス名を記載します。ここがエントリポイントとなります。Principal class となるクラスの + pluginDidLoad:(NSBundle *)plugin
メソッドを実装します。ここでインスタンスを生成してゴニョゴニョという感じです。
とかは面倒くさいので以下の便利なテンプレを組み込んでおいたら新規プロジェクトを作成するときに Xcode Plugin のひな形ができるでそっちの方が楽だと思います。これでビルドするだけで Menu の中に独自定義のメニューが足されます。
どこに何をどうやって足す
- class-dump
- otool
とかを使って Xcode にあるクラスやメソッドをごそっと書き出して、それらしいクラスやメソッドを探す。 試します。このあたりは Cocoa 全般の知識やプラグイン作成経験が生きてくる作業です。
自分で class-dump するのも面倒くさいのでここを参照
あとは既に Xcode Plugin が多数出ているしそれらのソースコードはたいてい Github に上がってるから自分がやりたいことと似ていることをやっているのを探してコード読むって感じです。
今回作ったやつ
- Xcode のコンソールに出力されているテキストを
- プロジェクト名+日時 なファイルとして書き出す
- それだけ。
ハマりどころや Tips
iOS 開発者視点で。
Xcode はマルチウィンドウ
プラグインのロードは1回だけどウインドウは複数ある
- ウインドウに足した UI のインスタンスをプロパティとかでそのまま持つとおかしくなる
- ウインドウごとにプラグイン側で独自に管理するか都度取得で対処。
- 今回はウインドウがアクティブになったタイミングで必要なインスタンスを取得。
- Xcode 側に足した UI は tag で管理して,存在しなければ追加する感じで対処。
UserDefaults は親の物
ちょっとした情報を保存したいときの典型
[[NSUserDefaults standardUserDefaluts] setObject:設定情報 forKey:なんか設定];
これをやるとどこに保存されると思いますか?
これは Xcode の Preference に保存されます。
~/Library/Preferences/com.apple.dt.Xcode.plist
ほとんどの Xcode Plugin は普通に Defaults に書いてしまってるんですが、行儀が悪いので自分のプラグイン用の plist を用意してそこに設定情報を書き込みましょう。
[NSBundle bundleForClass:[self class]];
これで自分のBundleのインスタンスがとれるので
NSBundle *bundle = [NSBundle bundleForClass:[self class]]; NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); NSString *path = nil; if ([paths count]) { NSString *lastPathComponent = [[bundle bundlePath] lastPathComponent]; NSString *filepath = [NSString stringWithFormat:@"%@/%@.plist", [lastPathComponent stringByDeletingPathExtension], [bundle bundleIdentifier]]; path = [[paths objectAtIndex: 0] stringByAppendingPathComponent:filepath]; }
とかで
~/Library/Application Support/プラグイン名/プラグイン名.plist
というパスがとれます。Xcode のバージョンによって微妙に逆ドメインになったりならなかったりしてましたが。
あとは適当に NSMutableDictionary に値突っ込んで writeToFile:atomically: で書き込めばいいと思います。NSUserDefaults でもっとエレガントに書ける方法がある気がしないでもないですけど。
Key イベントって結構簡単にとれるんですね
NSWindow の sendEvent: とか Swizzling しないととれないのかと思ってました。
NSEvent + (id)addLocalMonitorForEventsMatchingMask:(NSEventMask)mask handler:(NSEvent* (^)(NSEvent*))block NS_AVAILABLE_MAC(10_6);
で自身のアプリがアクティブな時に登録した種類のイベントだけ handler が呼ばれます。昔、Swizzling で頑張ってたのにいつの間にかこんな便利な機能が足されてました。
ということで
みんなもどんどん Xcode Plugin を作って快適なコーディングをしましょう。 そして Xcode の Defaults は汚さずにいきましょう。
Ono '斧' を触ってみた
先日、AFNetworking や NSHipster で有名な Mattt が Ruby の Nokogiri 風の XML & HTML パーサー Ono 斧 を公開しています。
早速少しだけ触ってみた
CSS Selector でのパースをサポート
XPath での指定なら今もいくつかライブラリがありますが CSS Selector をサポートしたものは私は知りませんでした。個人的には Nokogiri を連想させる最大のポイントです。
Subscripting 対応
エレメントのアトリビュートへのアクセスは Subscripting 対応になっています。
element[@"href"];
という書き方で href アトリビュートの値が取得出来ます。
NSDate のサポート
XML が対象になるかと思いますが dateValue メソッドで NSDate 型を返してくれます。 "yyyy-MM-dd'T'HH:mm:ssZ" のみのサポートですがお手軽ですね。
Block を使った列挙が可能
- (void)enumerateElementsWithXPath:(NSString *)XPath block:(void (^)(ONOXMLElement *element))block; - (void)enumerateElementsWithCSS:(NSString *)CSS block:(void (^)(ONOXMLElement *element))block;
このような列挙もサポートされています。また NSFastEnumeration に対応させた実装のサンプルにもなっているので勉強になります。
シンプルな実装
ファイルとしては実質1組の .h .m ファイルでクラスの数はそれ以上ありますが規模の小さいコードなので先に挙げたように Subscripting サポートや NSFastEnumeration 対応などなど実装の参考になりそうなちょうど良いサンプルです。
まだまだこれから
できたばかりということもあるのか CSS での id 指定が以下のようなエラーを吐いてしまってだめでした。
XPath error : Invalid expression
//[@id = 'header-container']
Nokogiri と同じ指定をしても他にもページによってはエレメントが見つからないこともありました。Mattt は Nokogiri と同等のものを目指しているのか名称だけリスペクトしてるのかわかりませんが個人的には Nokogiri と同等の動きをするライブラリになってくれることを期待しています。
今週末に CocoaPods 対応やドキュメント等が揃ってくるようなのでこれからも楽しみにしたいと思います。
簡単な動作確認ができるサンプルを置いておくので気になる方は試してみてください。
CXCKeyValueObserver をパクった CSNNotificationObserver ってのを作った
表題の通りです。 2週間くらい前に id:cockscomb さんが CXCKeyValueObserver というライブラリをリリースしていました。これはどんな感じのライブラリかっていうと
- KVO の 監視開始/終了 忘れず安心に実行できる
- 複数 KVO を使っても通知を受けた後の処理が監視単位ごとにスッキリ分かれる
っていうシンプルながら KVO の面倒くさいところが綺麗にまとまっているものです。
このコードを見たときに
- 確かに監視する側(感覚的には監視を管理する側)が死ぬときに自分で後始末すればいいよな
- ARC な今どきならインスタンス変数として保持しておけば、オーナーが死ぬときに自動的に死んでくれるよな
と気づきました。
そこで、同じ方法で Notification も監視管理してしまえってことで作りました。
CSNNotificationObserver
NSNotification をオブザーブする場合は KVO ほどシビアではありませんが
- オブザーブの開始
- Notification の受信
- オブザーブが不要になったときまたはオブザーバーが解放される時の解除
が必要です。
CSNNotificationObserver も CXCKeyValueObserver と同様にインスタンスが解放される際に 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回目以降の呼び出し時には以下のようになります
initWithObserver:selector:name:object:
でself
が NSNotificationCenter に登録されるCSNNotificationObserver
の新しいインスタンスが戻り値として返ってくる- 戻り値が代入されることで
_notificationObserver
が解放される _notificationObserver
の解放時にself
が NotificationCenter から解除される- 新しい
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 保持しなくてはなりません。先に挙げたサンプルのように大半のパターンでは
- Notification のオブザーバー ≒ 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回押し
というような操作をされた場合には applicationDidEnterBackground:
がまだ呼ばれていないっぽいので App Switcher 用の View を用意できないんですね。だからこのライブラリの作者は willEnterBackground 相当のタイミングを探った結果 UIApplicationWillBeginSuspendAnimationNotification
にたどり着いたのだと思います。
僕がやったような "ほぼ期待する動作をする" アプローチをとるのか、MMAppSwitcher
のように攻めるのかは自己責任で。
どうでもいいけど
この「ホームボタン2回押し」で表示される機能ないし画面の Apple 的正式名称ってなんなんでしょうね?個人的に AppSwitcher って気持ち悪い。
iOS アプリ開発に関わる人にぜひ読んで欲しい本[の宣伝]
僕は iOS アプリのコーディングをやっていて主にそっち方面のブログエントリを書いていますが今日は本の紹介というかタイトル通り宣伝です。
アプリ開発をやってる僕ですが結構 UI / UX は気になるタイプで仕事中もデザイナーにいちゃもんつける面倒くさいやつなんですが今回同僚が本を書きました。
iOS 7デザインスタンダード 最新のフラットデザインに対応-iPhoneに最適なUI・UXを徹底的に解説!
- 作者: 荻野博章,丸山弘詩
- 出版社/メーカー: インプレスジャパン
- 発売日: 2013/11/22
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る
この本は主にデザイナー向けに書かれた本です。特に今まで 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を徹底的に解説!
- 作者: 荻野博章,丸山弘詩
- 出版社/メーカー: インプレスジャパン
- 発売日: 2013/11/22
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る
おまけ: 別の同僚も 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 プログラミング」はすばらしい。