UILabel の文字色をハイライト時に暗めの色にする
UI のインタラクションとしてテキストカラーを少し暗めにしたいことがあったのでその時に使った方法です。
HSB(HSV) で元の色の Brightness を変更する
先に書いたとおり暗くしたいだけなので RGB でどうこうしようとするのはめんどくささこのうえないです。ですので前回紹介した HSB(HSV) を素直に使いたいと思います。HSB(HSV) で元の色の Brightness を変更するアプローチです。
やりかた
まず暗くしたい元の UIColor から HSB の要素を抜き出します。これには - (BOOL)getRed:(CGFloat *)red green:(CGFloat *)green blue:(CGFloat *)blue alpha:(CGFloat *)alpha
を使います。各要素の値が抜き取れたところで Brightness の値だけ変更します。今後使いやすいように変更率で調整出来るようにします。すると以下のような感じです。
+ (UIColor *)csn_colorWithBaseColor:(UIColor *)baseColor brightnessRatio:(CGFloat)ratio { CGFloat hue = 0; CGFloat saturation = 0; CGFloat brightness = 0; CGFloat alpha = 0; BOOL converted = [baseColor getHue:&hue saturation:&saturation brightness:&brightness alpha:&alpha]; if (converted) { return [UIColor colorWithHue:hue saturation:saturation brightness:(brightness * ratio) alpha:alpha]; } return nil; }
使い方
self.label.textColor = [UIColor redColor]; self.label.highlightedTextColor = [UIColor csn_colorWithBaseColor:self.label.textColor brightnessRatio:0.65];
0.6 ~ 0.7 くらいの値を brightnessRatio に渡してやるとほどほどに暗くなっていいと思います。
まとめ
RGB でやるよりも HSB でやった方が遙かに直感的に明度の調整ができると思います。今回のメソッドを UIColor のカテゴリメソッドとしてヘッダファイルも含めて Gist に置いておきます。良かったら使って見てください。
HSB(HSV) のすすめ
みなさんは普段アプリの開発の際にどのような形で色を指定していますか?個人的な経験と予想では
[UIColor colorWithRed:0.251 green:0.514 blue:0.663 alpha:1.000]
のような RGB での指定だったり、カテゴリやマクロで拡張して #4083A9
のような Hex 指定だったりするのではないでしょうか?このような指定になっている理由としてはデザイナーさんから指定される形式が RGB だからというのが多い気がします。デザイナー不在で開発者だけで作っている場合も RGB の方が何となくわかりやすい気がするという理由だったりします。
色の調整
デザイナーさんがいる環境であれば本職であるデザイナーさんの指示を仰ぐ形がプロダクトとしてはベストだと思うので開発者だけで色調整する場合の話です。特に色に関する知識がほぼない人です。僕自身も全く色に関する知識はありません。
RGB は Red, Green, Blue, Alpha の要素の組み合わせで色を表しています。UIColor であれば R:1, G:0, B:0, A:1 であれば赤。単純です。それではこのような真っ赤ではなく少し暗い赤が欲しい場合にはどうしますか?RGB の各値をいじくり回しますか?気がつけばとんでもない色になったりしませんか?
そこで HSB
HSB は
- Hue: 色相
- Saturation: 再度
- Brightness: 明度
で色を表現します。先ほどの例の少し暗い赤が欲しい場合は HSB 表現の赤の B の値を少し下げるだけで暗い赤になります。単純です。
こちらで配布されている日本語版 PDF の P18 でも進められているように個人的にも HSB は非常に色の調整がやりやすいです。
ちなみにこの PDF はおすすめなので1度見ることをおすすめします。
デモアプリ等で適当な色が欲しい時
ランダムな色が欲しい場合は HSB のうち H の値を変えるだけでそれなりに色が変わります。特に連続した色が欲しい場合は H の値を順番にループさせてやれば非常にカラフルで小綺麗な色になります。
補助ツール
任意の色が欲しいとかはさすがに
[UIColor colorWithHue:0.560 saturation:0.621 brightness:0.663 alpha:1.000]
とかいきなり書いたりはできないですよね。色を見ながら作りたいのでアプリで補助します。個人的におすすめは
上記2つの合わせ技です。
HexColor は OS の Color Picker を単独のアプリとして動かせるアプリです。このアプリ自体にも色を Hex や NSColor に変換してPasteboardにはき出すことができますが、Developer Color Picker プラグインを入れることで UIColor にも吐き出せるようになります。これらのアプリとプラグインは iOS 開発以外でも便利なのでおすすめです。他にも sip なんかもあります。対応するフォーマットが多いのが特徴です。
まとめ
HSB は同系色の調整が割と直感的にできるのでおすすめです。HSB だけでいろんな色を作るのは難しいかもしれませんが RGB でざっくり色を作ってから HSB で微調整するとか、スポイトツール的な機能で欲しい色を抜いてきて HSB で調整とかすると色の幅が広がっていいのではないでしょうか。
Pandoc で github 風 CSS を使った standalone な html を生成
動機
ただ何となくふとやってみたいと思っただけです。
準備
~/.pandoc
ディレクトリを作成- github.css で画像を参照している箇所を https://raw.githubusercontent.com/gollum/gollum/master/lib/gollum/public/gollum/images/para.png https://raw.githubusercontent.com/gollum/gollum/master/lib/gollum/public/gollum/images/dirty-shade.png に差し替え
- ~/.pandoc 直下に上記 CSS ファイルを配置
実行例
pandoc 変換元ファイル.md -s --self-contained -t html5 -c github.css -o 生成される.html
ポイントは --self-contained
です。CSS やスクリプト、画像ファイルを全て data: URI スキームを使って埋め込んでくれます。準備のステップ 2 をやっておかないと pandoc: Could not find data file hoge/fuga/../../images/modules/styleguide/para.png
とか言われて怒られます。
パラメータがおおい
最良の方法が分かりませんでしたが、僕の場合には .zshrc
に
pandoc_embed_html () { pandoc -s --self-contained -t html5 -c github.css $@ }
というのを追加して
pandoc_embed_html 変換元.md -o 出力先.html
という形で実行出来るようにしています。
参考
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 さんありがとうございます。