UITableViewController 以外で UIRefreshControl を使う方法 [リスクあり]

Tweetie で有名になった Pull to refresh ですが iOS 6 から UIRefreshControl として OS 標準で追加されました。残念ながら UIRefreshControlUITableViewController と一緒に使うしかまっとうな方法はありません。UIViewController + UITableView のような構成で無理矢理使う方法を書いてみます。

重要

個人的におすすめしません。素直に UITableViewController で使うような方向でまず実装を検討してください。これから紹介する方法は OS バージョン等の要因に左右されて期待するような動きをしない危険性があります。

方法

UIRefreshControl を生成して UITableViewインスタンスに addSubview するだけです。

ただし、これだけではアプリが Background から Foreground に戻ってきた時やタブの切り替え等で viewWillAppear などが呼ばれたあとに UIRefreshControlUITableView よりも前面に出てくることがあります。

最前面に出てくるのをねじ伏せる方法

viewWillAppear で

[self fixRefreshControlLayout];
if (self.refreshControl.isRefreshing) {
    [self.tableView setContentOffset:self.scrollOffsetToRestore animated:NO];
}

というような感じにします。

  • 1行目で UIRefreshControl のレイアウトを調整
  • 2行目以降で refreshing 状態の時のために保持しておいた UITableViewcontentOffset を復元させています。

1行目の fixRefreshControlLayout はこんな感じ。

- (void)fixRefreshControlLayout
{
    if ([self isViewLoaded] == NO) {
        return ;
    }

    BOOL isRefreshing = self.refreshControl.isRefreshing;
    if (isRefreshing) {
        [self.refreshControl endRefreshing];
    }
    [self.refreshControl removeFromSuperview];
    self.refreshControl = nil;
    [self.tableView addSubview:self.refreshControl];
    if (isRefreshing) {
        [self.refreshControl beginRefreshing];
    }
}

最前面に出てきても再度 addSubview すれば期待する位置に出てきてくれるので念のため都度都度張り直しをしています。特にパフォーマンス上は大きな足かせにはなっていない印象です。

self.scrollOffsetToRestoreviewWillDisappear: のタイミングで現在の contentOffset を保持しておくようにしています。

まとめ

このような無理矢理な実装をしてねじ伏せるような実装を追加してもそんなに綺麗な感じじゃないです。ですので繰り返しになりますが素直な実装をとることを激しくおすすめします。Apple にはバグレポを通じて UIRefreshControlUITableViewController に限定されずに使えるように要望しましょう。

Blocks のアレゲなシンタックスのための Xcode の Snippet

Blocks のアレゲなシンタックスは有名で非常に覚えにくいです。なので有名な OSX / iOS アプリ開発者のココロの叫びを表したサイトがあるのはもはや有名ですね。ココに書かれているシンタックスを Xcode の Snippet にして置いてます。

griffin-stewie/XcodeBlockSyntaxSnippets

デフォでもいくつかは入ってますし、他にもすでにいっぱいあると思いますが自分自身が環境変更があったときに便利なように GitHub に置いておこうと思ったしだいです。

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 に置いておきます。良かったら使って見てください。

UIColor+CSNAdditions

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 を生成

動機

ただ何となくふとやってみたいと思っただけです。

準備

  1. ~/.pandoc ディレクトリを作成
  2. 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 に差し替え
  3. ~/.pandoc 直下に上記 CSS ファイルを配置

ここに改変済みの 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 がなくなった場合には groupnil の状態で 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
  • 勝手プラグインが親の Preference 汚すってダメだろ
  • アンインストールしても 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 は汚さずにいきましょう。