激ニコぷんぷん丸のソースコード公開

先日リリースしたiPhoneアプリ「激ニコぷんぷん丸」のソースコードをGitHubにアップした。
特にいいアイデアも浮かばなかったので、多少インパクトがあって実績になりそうなものを作った。
が…色々迷走した挙句、よく分からないアプリが完成した。
使い道がないのでソースコードをGitHubにアップすることにした。
テストコードも書いてなく、あまりいいコードではないけど何かの参考になれば…。

Pinterestのようなインタフェースで、ツイッター上に流れているニコ動を表示するアプリ。
ツイッター上の動画情報は、ツイッターのストリームAPIを使ってリアルタイムに取得している。
クライアント部はObjective-C、サーバ部はRails3(Unicorn + NginX) + MySQL + Redisで出来ている。

とりあえず、クライアントアプリだけ。
pontago/objc-NicoTwi · GitHub

iOS7対応

面倒くさがりなのでブログを書く頻度が少なかったのだが、なるべく細かい記事でも書くことにした。
iOS7がリリースしてしばらく経つのだが、ようやくひと通りiOS7対応が終わった。
後手後手の対応で、アップデートが遅くなってしまいスミマセン。

今回はメジャーアップデートということもあり、大幅なUI変更が加わった。
流行のフラットUIに変わったことで、iOS6以前と両方サポートするアプリは少し大変かもしれない。

ステータスバーが一体化した

一番やっかいな問題が、ステータスバーの一体化だ。
ステータスバーをアプリから操作(色の変更や透過、表示・非表示)出来るようになったため、UINavigationBarなどと違和感なく一体化させることが可能になった。

今まではステータスバーを除いた座標から取得出来ていたのだが、iOS7からはステータスバーの20px分が含まれなくなった。このせいでデザインが崩れてしまうアプリがけっこうあると思う。

手っ取り早いのは、UIWindowをステータスバー分ずらしてしまうことだ。こうすることでiOS6以前と同じように処理することができる。

    if ([[UIDevice currentDevice].systemVersion floatValue] >= 7.0f) {
[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent];
self.window.clipsToBounds = YES;
self.window.frame = CGRectMake(0, 20.0f, self.window.frame.size.width, self.window.frame.size.height - 20.0f);
self.window.bounds = CGRectMake(0, 20.0f, self.window.frame.size.width, self.window.frame.size.height);
}

UINavigationBarのbarTintColorやtintColorを変更することで、ステータスバーの色も変わる。

Objective-C – iOS7でナビゲーションバーやステータスバーの文字色を変える – Qiita [キータ]

UINavigationBarの戻るが変わった

前のビューに戻るときのボタンが「<」みたいなのになった。
そのため、backBarButtonItemなどにイメージ付きのUIBarButtonItemを設定すると、「<」分ズレて表示がおかしくなる。

僕は手っ取り早く、backBarButtonItemではなく、leftBarButtonItemに設定してしまった。
具体的には、UINavigationControllerを派生したクラスを作成し、pushViewControllerをオーバーロードする。
その中で、viewControllersをカウントし、0以上の場合(一つ以上ビューがプッシュされている)に、戻るボタンを設定する。例えば以下のようなコードになる。

- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated {
if ([self.viewControllers count] > 0) {
viewController.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc]
initWithImage:[UIImage imageNamed:@"back.png"] style:UIBarButtonItemStylePlain
target:self action:@selector(popViewControllerAnimated_)];
}
[super pushViewController:viewController animated:animated];
}
- (void)popViewControllerAnimated_ {
[self popViewControllerAnimated:YES];
}

カスタムUIBarButtonItemのアイコン色が反映されない

iOS7になってから、ボタンに設定した画像がtinColor(青色とか)で塗りつぶされるようになった。
UIBarButtonItem内にUIButtonをカスタムビューとして設定していたのだが、tintColorで塗りつぶされなかった。
どうやら、UIButtonの種類が、UIButtonTypeSystemまたは、UIButtonTypeRoundedRectの場合にのみtinColorで塗りつぶしされるようだ。

EZ-NET: iOS 7 では標準ボタンの画像が tintColor で塗りつぶされる : iPhone プログラミング

UITableViewのセル背景色が反映されない

UITableViewCellの背景色は、デフォルトで白?になったようだ。
変更する方法は、tableView:willDisplayCell:forRowAtIndexPath:か、tableView:cellForRowAtIndexPath:のどちらかから、backgroundColorをclearColor(もしくは好きな色)に変更するといい。

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
cell.backgroundColor = [UIColor clearColor];
}

疲れたのでこのへんで。

CoreDataのFetchRequestテンプレートで配列を指定

クライアントアプリのコード内にSQL分を書くのがどうもしっくりこないので、FetchRequestのテンプレを使うようにしている。
単純なデータを格納することが多いせいか、テンプレだけで問題になったことはない。

fetchRequestFromTemplateWithNameメソッドでテンプレ名とプレースホルダの変数を渡すのだけど、IN句を使う必要が出てきて少し悩んだ。

テンプレのSQL分には最初、field_name IN ($hoge) のような形で設定してみたのだけど勝手に括弧が外れて、 field_name IN $hoge になった。
こういうもんなのかと思い、プレースホルダにカンマ区切りの文字列を渡してみたがエラー・・・。

どうやらNSArrayをそのまま渡せばいいだけのようだ。

NSArray *ids = @[@1, @2, @3, @4];
NSDictionary *subs = @{@"hoge":ids};
NSFetchRequest *fetchRequest = [managedObjectModel fetchRequestFromTemplateWithName:@"findByIds"
substitutionVariables:subs];

何度も使う場合、テンプレを使うことでSQL文のパース処理を省くことができるので若干高速化するようだ。
NSPredicateなんてのもあるんだなぁ・・・

PSCollectionViewでハマる

Pinterest風の表示にしたいと思って色々ライブラリをあさって見たところ、PSCollectionViewという便利そうなライブラリを発見した。
iOS6以上であれば、UICollectionViewというそのものズバリのものがあるようだ。

heightForRowAtIndexメソッドの戻り値に個々の高さを指定してやることで、高さを個別に設定することも可能だった。
戻り値にCGFloatで値を返すのだけど、小数部の桁数が増えると誤差が生じて、上手くタップを認識しなくなることがあるみたい。

調べてみると、セルの大きさをNSStringFromCGRectでNSString化して管理しているようだ。

// Calculate index to rect mapping
self.colWidth = floorf((self.width - kMargin * (self.numCols + 1)) / self.numCols);
for (NSInteger i = 0; i < numViews; i++) {
NSString *key = PSCollectionKeyForIndex(i);
// Find the shortest column
NSInteger col = 0;
CGFloat minHeight = [[colOffsets objectAtIndex:col] floatValue];
for (int i = 1; i < [colOffsets count]; i++) {
CGFloat colHeight = [[colOffsets objectAtIndex:i] floatValue];
if (colHeight < minHeight) {
col = i;
minHeight = colHeight;
}
}
CGFloat left = kMargin + (col * kMargin) + (col * self.colWidth);
CGFloat top = [[colOffsets objectAtIndex:col] floatValue];
CGFloat colHeight = [self.collectionViewDataSource collectionView:self heightForRowAtIndex:i];
CGRect viewRect = CGRectMake(left, top, self.colWidth, colHeight);
// Add to index rect map
[self.indexToRectMap setObject:NSStringFromCGRect(viewRect) forKey:key];
// Update the last height offset for this column
CGFloat heightOffset = colHeight > 0 ? top + colHeight + kMargin : top;
[colOffsets replaceObjectAtIndex:col withObject:[NSNumber numberWithFloat:heightOffset]];
}

セルのタップは、UITapGestureRecognizerを使っているようで、shouldReceiveTouchメソッドで、どのセルがタップされたか判定して、didSelectメソッドが呼びされるようだ。
heightForRowAtIndexで返される高さの小数部が増えてくると、下記のrectStringと値が一致しなくなり、タップを認識しなくなる。

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
if (![gestureRecognizer isMemberOfClass:[PSCollectionViewTapGestureRecognizer class]]) return YES;
NSString *rectString = NSStringFromCGRect(gestureRecognizer.view.frame);
NSArray *matchingKeys = [self.indexToRectMap allKeysForObject:rectString];
NSString *key = [matchingKeys lastObject];
if ([touch.view isMemberOfClass:[[self.visibleViews objectForKey:key] class]]) {
return YES;
} else {
return NO;
}
}

解決方法は簡単で、heightForRowAtIndexで値を返す際に、小数部が増えないようにしたり、丸めてやればいい。

- (CGFloat)collectionView:(PSCollectionView *)collectionView heightForRowAtIndex:(NSInteger)index {
return floorf(100.123456789f);
}

投稿しようとして気づいたのだけど、Objective-C(Cocoa)ネタ書いたの初めてだった。
iPhoneアプリを作っていると、ブログに書くようなネタがなかなか見つからない…
ちょっとググれば情報がたくさん見つかるし書くまでもないかなーとか思っちゃうせいだろうか。