JSON を jq で確認しやすくする CSNJQFormatter

普段アプリを作っていたアプリが取得した JSON データを Console に出力させています。このままでは改行がなくなっていたりシンタックスハイライトもなくて見づらかったのでこんなものを作りました。

griffin-stewie/CSNJQFormatter

以下のような文字列を返すだけの単純なもので他に機能はありません。

cat <<'END' | jq '.' 
{"foo":"bar"}
END

利用シーンとしては NSLog にこの文字列をくわせてやると、あとで Console に出力されている文字列を Terminal にコピペしてやる感じです。実装的にも単純に文字列連結させているだけです。

デバッグ用途で気が向いたら使ってみてください。

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 さんありがとうございます。

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.

Placeholder 付き UITextView

UITextView って UITextField みたいに Placeholder がないんですね。つい先日まで気づきませんでした。必要とするようなシチュエーションがないとかそのような UI が iOS 的にナシなのかなぁと思っていたらカレンダーアプリのイベント追加画面で Apple 自身が実装してました。しょうがないので作りました。良かったら使ってみてください。CSTextView から CSNPlaceholderTextView に名称を変更しています。

CSNPlaceholderTextView

CSNPlaceholderTextView

特徴

  • 挙動は Calendar.app の イベント追加画面 (EKEventEditViewController)のメモの部分に似せている
  • placeholder の表示位置は Caret の位置に自動調整
  • placeholder の font は textView 側の font プロパティに連動
  • UITextView のリプレイスとして使えるように UITextViewDelegate を汚染しない

ということで、Placeholder として使っている Label の textColor は UITextField の placeholder プロパティの説明にあるように

The placeholder string is drawn using a 70% grey color.

な色にしてあったり、UITextViewDelegate を汚染しないように UITextiView が準拠している UITextInput プロトコルのメソッドをオーバーライドするようにして表示位置や出し入れを調整しています。

ハマったのはplaceholder の表示の On/Off を

- (CGRect)caretRectForPosition:(UITextPosition *)position

だけでやっていましたが、入力済みのテキストを Cut してから Undo Cut して Undo Typing したあとに placeholder が消えないことでした。

- (UITextRange *)selectedTextRange

メソッドはこの問題の動作でも呼ばれることがわかったのでここでも On/Off を行うことで回避しました。ただ、この方法が UITextInput とかのテキスト入力系 の振る舞いとして適切かどうかはちょっとわかりません。

追記

「text プロパティでテキストを代入すると placeholder が消えない」というバグがあったので修正しておきました。

CocoaPods 対応

pod 'CSNPlaceholderTextView', '~> 0.0'

iOS で「LINE で送る」を実装する

iOS で「LINE で送る」を実装する

昨年末頃、LINE より公式に「LINE で送る」の仕様が公開されました。私の知る範囲では Objective-C で書かれた iOS 向けのライブラリは以下の2つです。

LineKit

実装されている内容

  • LINE.app のインストール確認
  • テキストを送る
  • 画像を送る

上記の通り非常にシンプルなライブラリになっています。

LineActivity

実装されている内容

  • LINE.app のインストール確認
  • 未インストールの場合 App Store を開く
  • テキストを送る
  • 画像を送る

UIActivity のサブクラスとして実装されています。アイコン用の画像ファイルも添付されています。

練習がてら自分も作ってみた

CSNLINEOpener

実装されている内容

  • LINE.app のインストール確認
  • 未インストールの場合 App Store を開く
  • テキストを送る
  • 画像を送る
  • 単機能クラスとUIActivityの二本立て

LINE アプリに対して文字列もしくは画像を送る CSNLINEOpener クラスと UIActivity のサブクラス CSNLINEOpenerActivity があります。ちょうど LineKitLineActivity の間ですね。

CSNLINEOpenerActivity の方はアイコン画像を用意できないのでイニシャライザで渡してもらうような形にしています。

とはいえ、私自身 LINE ユーザーじゃないので送信テストまではできてませんw ごめんなさい。

ご利用には ガイドライン に従う必要があります。

インストール

CocoaPods で

pod 'CSNLINEOpener', '~> 0.0'

ハマった・迷った・困ったポイント

実装例や既存ライブラリが探しにくい

"LINE" というキーワードが一般的すぎてなかなか探しにくいです。最終的には具体的なメソッド名やスキーマ名等で探したら見つかったのが上述の2つのライブラリです。

UIPasteboard を使った UIImage の渡し方

UIPasteboard を使って UIImage を渡すんですが、

pasteboard.image = image;

とかすると LINE 側で落ちます。 結論から言うと

[pasteboard setData:UIImagePNGRepresentation(image) forPasteboardType:@"public.png"];

としないとダメのようです。プロパティで渡せたらもうちょっと楽だったんですが。

画像の共有方法変更

iOS 7 から同一チーム ID のアプリ間でしか名前付き Pasteboard を使えなくなっています。最新の CSNLINEOpener では generalPasteboard を使うように変更しています。これにより画像の共有は可能ですがユーザーがコピーしていたデータは上書きされて消えるのでご注意ください。

UIActivity の使い方

私のドキュメントの読み間違いだったら申し訳ないんですが、私の理解はこんな感じです。

  • ViewController を使う系か使わない系か?
    • 使う系の場合
      • activityViewController を実装して ViewController を返す
    • 使わない系の場合
      • performActivity を実装する
  • いずれの場合も処理完了時には activityDidFinish: を呼ぶ
  • prepareWithActivityItems: は data への参照を確保したり表示させたい ViewController の準備など、あくまでも自身が提供したい動作の準備のみを行う

たまに上記の私の理解とは違う実装をしているのを見かけますが、とりあえずは今回は自分の解釈で実装しました。

LINE のアイコン

UIActivity として提供する場合アイコンも一緒に配布したいわけですが、LINE の公式に配布されている画像リソースはそのままでは使いにくいですし、改変は NG っぽい。それに勝手に同梱するわけにもいかなそう。ということでこのライブラリを使う人に自分で用意してもらう形にしてみました。

まとめ

LINE で送る機能を作るのは簡単ですし、わざわざ自分で作らなくても既存のものを使わせてもらえばいいのですが、簡単な故に良い練習材料かなぁと思いました。気が向いたら使ってみてください。