iOS7時代のUITableView
NSAttributedTextとDynamic Typeを利用したUITableViewを書いてみました。
XCodeとか半年ぶりなので、正しいのかあまり自信はないです。
こんな感じに表示されます。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
プロパティとして渡すのではなく、NSAttributedString
をattributedText
プロパティとして渡すようにシフトしているのでしょう。
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
を使って描画しなければ実現できなかったリッチなテキスト表現(属性テキスト、動的サイズ変更、カーニング等)を、今後はUILabel
やUITextView
で実現できるようにするために用意された、中間フレームワーク群です。
iOS7以降のミニマルデザインでは、オブジェクトやメタファよりもテキストが重要なので力を入れてきたということでしょう。
動的なテキストレイアウトを可能にするためのクラスなどが追加されているみたいです。
Dynamic Type の機能と実装
Dynamic Typeに関して、OSとしてサポートしてくれることは次の二つだけです。
UIFontDescriptor
インスタンスを生成した際に、ユーザーのフォントサイズ指定を反映させる。- ユーザーがフォントサイズ指定を変更した際に、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
内でのオブジェクト生成回数を最小限とするためです。
定数定義されているものについては、生成コストを気にする必要があるとも思えないので、どの程度パフォーマンス変わるのかは分かりませんが…。