なるようになるかも

力は多くの場合、その人の思いを超えない。

iOS7時代のUITableView

NSAttributedTextとDynamic Typeを利用したUITableViewを書いてみました。

NSAttributedText&Dynamic Type

XCodeとか半年ぶりなので、正しいのかあまり自信はないです。

f:id:quesera2:20130930222044p:plain

こんな感じに表示されます。tableViewのmarginやpaddingはかなり適当な値で、ちゃんと調べてません。

TableViewの作法

iOS6からいろいろ変わってます。「ついてこれる奴だけついてこいッ!」って勢いでいろいろ変えやがります。適合者への道は遠いです。

UITableViewCell の生成と再利用

iOS5までのdequeueReusableCellWithIdentifier:では、キャッシュのCellが存在すれば再利用し、なければ生成していましたが、iOS6以降はUITableViewの描画を行う時点で必要なCell数を計算し、事前にキャッシュCellを作り置きしておく、という方法に変わりました。

まずviewDidLoadでCellのクラスと再利用IDを登録します。

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self.tableView registerClass:[CustomCell class] forCellReuseIdentifier:CellIdentifier];
}

実際にCellを利用するには、dequeueReusableCellWithIdentifier:forIndexPath:を呼び出します。

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
     CustomCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
     cell.textLabel.numberOfLines = 0;
     [self _updateCell:cell atIndexPath:indexPath];
     return cell;
}

見ての通り、非常にシンプルな記述となり、カスタムCellも利用しやすくなった反面、UITableViewCellStyleの指定は不可能になりました。

プリセットのStyleを使いたい場合、initWithStyle:reuseIdentifier:を呼び出すカスタムCellクラスを作る、という残念な方法を取ることになります。

UITableViewCell の高さの計算

iOS7でsizeWithFont系のメソッドはDeprecatedとなりました。

NSStringをUI要素に対して、直接textプロパティとして渡すのではなく、NSAttributedStringattributedTextプロパティとして渡すようにシフトしているのでしょう。

NSStringから計算する場合はboundingRectWithSize:options:attributes:context:を、NSAttributedStringから計算する場合はboundingRectWithSize:options:context:を使うように変わりました。

AttributedString

NSAttributedString自体はスタイルをNSDictionaryとして渡すだけなので、そこまで難しいわけではないのですが、問題は「スタイルを指定するNSDictionaryをどこで生成するか」、です。

文字生成の度に辞書を生成していると、XCode4.3辺りで簡易記法が追加されたからとはいえ、ひどく読みにくいコードになります。毎回辞書を生成することになるので、パフォーマンスも良くないでしょう。

後述するpreferredFontForTextStyle:更新の関係もあって、UIViewControllerのプロパティとして持たせる方法を取りましたが、実際にはどういう方法がいいんでしょう…。AndroidのStyleリソースみたいな簡単な方法はないんですか…。

Dyanmic Type

iOS7から設定にフォントの大きさを設定する機能が追加されました。

これはText Kitの機能の一つです。

Text Kitとは

これまでCore Textを直接叩くか、あるいはUIWebViewを使って描画しなければ実現できなかったリッチなテキスト表現(属性テキスト、動的サイズ変更、カーニング等)を、今後はUILabelUITextViewで実現できるようにするために用意された、中間フレームワーク群です。

iOS7以降のミニマルデザインでは、オブジェクトやメタファよりもテキストが重要なので力を入れてきたということでしょう。

動的なテキストレイアウトを可能にするためのクラスなどが追加されているみたいです。

Dynamic Type の機能と実装

Dynamic Typeに関して、OSとしてサポートしてくれることは次の二つだけです。

  1. UIFontDescriptorインスタンスを生成した際に、ユーザーのフォントサイズ指定を反映させる。
  2. ユーザーがフォントサイズ指定を変更した際に、Notificationを発行する。(実際にはアプリが再びフォアグラグンドになった瞬間に通知が来る)

たった二つだけです…。

Dynamic Typeを利用するためには、UIFontの取得にpreferredFontForTextStyle:もしくは、fontWithDescriptor:を使う必要があります。

TextStyleとして渡すのはNSStringで、WithDescriptorとして渡すのはUIFontDescriptorですが、前者の文字列はUIFontDescriptorの定数であり、Dynamic Type理解の鍵となるのはUIFontDescriptorクラスです。

Class Referenceは斜め読みしかしていないけれど、そのフォントをどう描画したいのかをまずUIFontDescriptorで定義し、DescriptorからUIFontを生成する、という二手間が必要になった…と理解しておけば、おおよそ問題ないと思います。

実際にはFontDescriptorを直接生成することは稀で、UIFontTextStyleHeadlineなどのプリセットされたスタイルを、preferredFontForTextStyle:で渡すことになるのでしょう。

ユーザーが設定からフォントサイズの変更を行った場合、UIContentSizeCategoryDidChangeNotificationの通知が発行されるのですが、本当に通知が発行されるだけなので、それを受け取って再レイアウトを行うのは開発者の義務です。

Dynamic Type 更新のポイント

重要なのは、UIFont不変クラス ということです。

そのため、ユーザーが設定でフォントサイズの変更を行った場合、preferredFontForTextStyle:UIFontを再生成する必要があるのです。

/**
 フォントサイズ変更の通知を受け取った際に、
 フォント属性を更新した上で、テーブルを再描画する
 */
- (void)preferredContentSizeChanged:(NSNotification *)aNotification {
    [self _refreshFontAttributes];
    [self.tableView reloadData];
}

/**
 フォント属性を更新する
 */
- (void)_refreshFontAttributes
{
    self.bodyAttribute = @{NSFontAttributeName : [UIFont preferredFontForTextStyle:UIFontTextStyleBody]};
    self.subTitleAttribute = @{NSFontAttributeName : [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline],
                           NSForegroundColorAttributeName : [UIColor darkGrayColor]};
}

通知を受け取ったら、UITableViewを再描画する前にフォント属性を更新し、UIViewControllerのプロパティとして保持する手法をとっています。

一度だけUIFontDescriptorからUIFontを生成し、取得した辞書を描画やCellの高さの計算に使いまわすことで、UITableView内でのオブジェクト生成回数を最小限とするためです。

定数定義されているものについては、生成コストを気にする必要があるとも思えないので、どの程度パフォーマンス変わるのかは分かりませんが…。

ものすごく参考にしました