標準フレームワークだけでUITableViewに未読バッジを表示 [Xcode](Dynamic Type対応版)

 最近は、なんだか記事も書かずにコードばっかり書いてるので、iOS用アプリ開発(Objective-Cの方)のTips的なことでも記事にして、お茶を濁したいと思います。

 えぇと、iPhone やら iPad のアプリを開発していると、UITableView に標準メーラーの未読数(バッジ/バッヂ)みたいなのを表示したくなる時ってあるじゃないですか?

 要するに、こんな感じにです。

 ただ、それっぽい単語で検索をかけると、検索結果が上位の記事には「標準の UITableViewCell ではバッジ表示機能は提供されていないので、TDBadgedCellのような外部ライブラリを利用すると楽ちんですよ」的なことが書かれていることが多いんですね。

 もちろん、それで全然問題ありませんし、実際「バッジを表示する」という機能そのものはデフォルトでは提供されていないんですが、「え、別に標準フレームワークだけで普通にできるくね」と思ったので、適当にサンプルを書いてみました。
 この方法だとサブクラス化する必要すら無いので、とてもお気楽に扱えるんじゃないでしょうか。

 最近は、どこかの誰かが公開している便利なクラスがあったら、それを利用して迅速にかつスマートに開発するのがトレンドだと思いますが(車輪の再発明をすると怒られちゃいますし)、こんな単純なことにまで外部ライブラリを持ち出したくないだとか、外部のものを使い過ぎるとバージョン管理が面倒臭いとか、いちいちサブクラスを書くのすらかったるいとか、そのような最近の時流に乗り切れない昔気質の方のお役に立てましたら。

 ちなみに、通常の UITableView の実装に関してはある程度理解されている方を対象とした記事であり、コピペで動くことを目的としておりません。もしかしたら、あまり感心しない書き方であるかも知れません。最終的な実装は、ご自身の判断で行ってください。

スポンサーリンク

読み込み中です。少々お待ち下さい

サンプルと説明

 ホントはサンプルプロジェクトを github にあげようかと思ったんですが、色々めんどうだったのでそこまでするほど大層なものでもなかったので止めておきました。

 サンプルで作ったアプリは、新規ブロジェクトから「Single View Application」を選んで、ストーリーボードに「UITableView」をべとんと貼っつけた状態のデフォルトの「ViewController」に、tableViewプロパティと以下のコードを追加しただけのものです。

 つまり、最低限である「numberOfRowsInSection」と「cellForRowAtIndexPath」しか実装していません。だって、めんどくさいんだもん。単なるサンプルだし、充分ですよね。

追記:(2014/10/24)
 なんか、文字列の描画サイズを取得する sizeWithFont って、iOS 7 以降は非推奨になってたんですね(いまさら)。気付かず使っちゃってたので、boundingRectWithSize を使うように修正しました。

 あと、ついでに iOS の設定アプリから「文字サイズを変更」できる Dynamic Type 機能に対応してみました。その都合上、仕方なく「heightForRowAtIndexPath」も追加で実装しました。ぐーたらなのに、偉いですね(自画自賛)

ViewController.m

  1. #import "ViewController.h"
  2. @interface ViewController ()
  3. @end
  4. @implementation ViewController
  5. - (void)viewDidLoad
  6. {
  7.     [super viewDidLoad];
  8.     
  9.     // iOS の設定アプリで「文字サイズを変更」されたことを監視
  10.     [[NSNotificationCenter defaultCenter] addObserver:self
  11.                                              selector:@selector(didContentSizeChanged:)
  12.                                                  name:UIContentSizeCategoryDidChangeNotification
  13.                                                object:nil];
  14.     [self.tableView setAllowsSelection:YES];
  15. }
  16. - (void)didReceiveMemoryWarning
  17. {
  18.     [super didReceiveMemoryWarning];
  19. }
  20. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
  21. {
  22.     return 100; // 適当です。
  23. }
  24. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
  25. {
  26.     // 「いつもの」なので、説明は省略
  27.     static NSString *CellIdentifier = @"cell";
  28.     UITableViewCell *cell = nil;
  29.     switch ([indexPath section]) {
  30.         case 0:
  31.         {
  32.             cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
  33.             if (!cell) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; }
  34.             
  35.             [[cell textLabel] setText:[NSString stringWithFormat:@"項目その %@", @([indexPath row])]];
  36.             [[cell textLabel] setTextColor:[UIColor colorWithRed:1.0 green:0.3 blue:0.5 alpha:1.0]];
  37.             [[cell textLabel] setFont:[UIFont preferredFontForTextStyle:UIFontTextStyleBody]]; // Dynamic Type 対応
  38.             // UITableViewCell の accessoryView にバッジっぽい UILabel を設定して、適当にそれっぽく見せる。
  39.             UILabel *badge = [[UILabel alloc] initWithFrame:CGRectZero];
  40.             // 微妙にボーダーの有り無しや、角丸のR違いとかも、なんとなく表現してみました。
  41.             if ([indexPath row] % 4 == 1) {
  42.                 // 角丸のボーダーつき
  43.                 [self applyBadge:badge withText:[NSString stringWithFormat:@"%@", @([indexPath row])] withRadius:12.0 withBorder:2.0];
  44.             } else if ([indexPath row] % 4 == 2) {
  45.                 // ちょっと長めの文字列で、微妙な角丸&塗り潰してフラットな感じに
  46.                 [self applyBadge:badge withText:[NSString stringWithFormat:@"未読分が%@", @([indexPath row])] withRadius:4.0 withBorder:0.0];
  47.             } else if ([indexPath row] % 4 == 3) {
  48.                 // ボーダーも背景色も無しで、数字のみ
  49.                 [self applyBadge:badge withText:[NSString stringWithFormat:@"%@", @([indexPath row])] withRadius:0.0 withBorder:0.0];
  50.             } else {
  51.                 // バッジ無し
  52.                 badge = nil;
  53.             }
  54.             [cell setAccessoryView:badge];
  55.             break;
  56.         }
  57.         default:
  58.             break;
  59.     }
  60.     return cell;
  61. }
  62. - (void)applyBadge:(UILabel *)badge withText:(NSString *)text withRadius:(CGFloat)radius withBorder:(CGFloat)borderWidth
  63. {
  64.     /**
  65.      * バッジっぽい UILabel を適当に設定。
  66.      * iOS の設定アプリから「文字サイズを変更」できる Dynamic Type 機能対応版。
  67.      *
  68.      * ハイライト時の色を指定したい場合も、通常の didHighlightRowAtIndexPath と didUnhighlightRowAtIndexPath で対応可能です。
  69.      * didUnhighlightRowAtIndexPath の indexPath がおかしい場合は didHighlightRowAtIndexPath で
  70.      * 前回のハイライト位置を覚えておいたりするといいと思います。別の問題なので、ここでは省略します。
  71.      **/
  72.     UIFont *font = [UIFont preferredFontForTextStyle:UIFontTextStyleCaption1];
  73.     CGFloat adjust = [font lineHeight] / [[UIFont systemFontOfSize:[UIFont smallSystemFontSize]] lineHeight];
  74.     UIColor *foregroundColor = [UIColor colorWithRed:1.0 green:0.3 blue:0.5 alpha:1.0];
  75.     if (radius > 0.0) {
  76.         // 角丸の設定
  77.         [[badge layer] setCornerRadius:roundf(radius * adjust)];
  78.         [[badge layer] setMasksToBounds:YES];
  79.     }
  80.     if (borderWidth > 0.0) {
  81.         // ボーダーの設定
  82.         [[badge layer] setBorderWidth:roundf(borderWidth * adjust)];
  83.         [[badge layer] setBorderColor:[foregroundColor CGColor]];
  84.         [badge setBackgroundColor:[UIColor whiteColor]];
  85.         [badge setTextColor:foregroundColor];
  86.     } else {
  87.         [badge setBackgroundColor:radius == 0.0 ? [UIColor whiteColor] : foregroundColor];
  88.         [badge setTextColor:radius == 0.0 ? foregroundColor : [UIColor whiteColor]];
  89.     }
  90.     // 並びとかも、適当に変えられるようにすればいいと思います。
  91.     [badge setFont:font];
  92.     [badge setTextAlignment:NSTextAlignmentCenter];
  93.     [badge setText:text];
  94.     // 幅を自動で伸縮する小細工。sizeWithFont が iOS 7.0 から非推奨になったみたいなので、boundingRectWithSize を使う。
  95.     CGRect rect = [text boundingRectWithSize:CGSizeMake([[self tableView] bounds].size.width * 0.5, [[badge font] lineHeight] * 2.0)
  96.                                      options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading)
  97.                                   attributes:@{NSFontAttributeName:font}
  98.                                      context:nil];
  99.     // Padding とかも、適当に変えられるようにすれば(略)
  100.     CGFloat minWidth = roundf(32.0 * adjust);
  101.     [badge setFrame:CGRectMake(0, 0, ceilf((rect.size.width < minWidth ? minWidth : rect.size.width) + (minWidth * 0.5)), ceilf(rect.size.height) + (minWidth * 0.25))];
  102. }
  103. - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
  104. {
  105.     // UITableViewCell の高さを、文字の高さを基準にして適当に調整
  106.     static NSString *sample = @"Sample";
  107.     UIFont *font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
  108.     CGRect rect = [sample boundingRectWithSize:CGSizeMake([[self tableView] bounds].size.width, [font lineHeight] * 2.0)
  109.                                        options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading)
  110.                                     attributes:@{NSFontAttributeName:font}
  111.                                        context:nil];
  112.     return ceilf(rect.size.height + (rect.size.height * 0.8));
  113. }
  114. - (void)didContentSizeChanged:(NSNotification *)notification
  115. {
  116.     // iOS の設定アプリから「文字サイズを変更」されたので、UITableViewCell の高さを調整する為に reloadData を呼ぶ。
  117.     [[self tableView] reloadData];
  118. }
  119. @end

 特に難しいことはしてないと思いますが、詳しくはソース内のコメントで。

 そして、2時間寝かせた結果が、こちらになります。

実機でのスクリーンショットです

 パターンとして、ボーダーの有り無しや、角丸のR違いなども、なんとなく用意してみました。

 それから、ごく簡単になので実用性は微妙ですが、いちおう文字列の長さによってバッジの横幅を伸縮するようにしています。

 その他については、ソースのコメントを参照してください。

 基本的に上のコードは、そのまま使うという性質のものではないので、不要な部分は削除して、必要な機能は追加してください。自分なりに実装する際の出発点というか、お手軽なサンプルとして参照いただければと思います。

Dynamic Type 対応

 上でも触れましたが、修正ついでに Dynamic Type 機能に対応しました(2014/10/24)。

  

文字を一番小さくすると、こんな感じです。

  

文字を一番大きくすると、こんな感じです。

 対応しているアプリをあまり見かけないので、ちょっとだけ差別化が図れるかも知れません。

蛇足

 いやー、それにしても、Objective-C って見た目がキモいですよね。絵面がとにかくヒドい。

 何を唐突にとんでもない戯言をほざいておるのだと叱責されそうですが、VBに比肩するくらい嫌い。書き方が幾通りもありすぎるのも、どうかと思う。まぁ、それもあって Swift が出てきたんでしょうけど。

 そもそも、Xcode 自体が使い難い。なんであんなに直感的じゃないの。ストーリーボードとか、使う価値を見い出せない(言い過ぎ)。いや、Xcode 5、6 と進むにつれ、操作性を含めてかなり改善されてきたとは思いますが、それでもまだ色々中途半端だと思います。

 ていうか、かなり前から日本における AppStore の売上は世界でもトップクラスなのに、いまだに Xcode の日本語ローカライズをする気配すら見受けられない態度が理解できない。
 個人としては別に英語でも構わないんですが、ナメられているようで気分はよろしくないです。Apple は、全体的にそう。そんくらいのコストはかけられるでしょうに、時価総額世界トップレベルなんだから。

 あ、いや、あの、違うんです。私、Apple様、大好きです。

 いや、ホントに。iPhone 5s最高、iPad mini Retina最高です。このブログでも、ちゃんと記事内で度々そのように表明しております。iPhone 6も、発売されたらSIMフリー版を直ちに買わせていただきます。ですのでペナルティだけは、なにとぞご勘弁を(人聞きの悪い)。
買いました!ちゃんと買いましたよ、Apple 様!物凄いシブシブでしたけど(笑)

 てゆうか、Xcode を使っていると、Visual Studio の完成度の高さを思い知らされますよね。もちろん、人によって評価は異なるでしょうが、やっぱりソフトに関しては Microsoft は流石だと思います。
 ただ、最近の Visual Studio は、ちょっと何でも出来すぎて肥大化し過ぎな気もしますけど。2008辺りが、一番使い易かったような。なんて、こんなこと言ってたら、意識高い系の人達に怒られちゃいますね。

 てゆうか、なんだこの、我ながら「それ、いま言う必要あった?」と言うしかない、ひっどい蛇足は(笑)

素晴らしき iPad mini Retina

この記事をシェア
  • このエントリーをはてなブックマークに追加
  • Share on Google+
  • この記事についてツイート
  • この記事を Facebook でシェア