属性付きラベルを作る その1


iOSのCocoa touchにはUILabelというクラスがある。ご承知の通り、画面上に文字をラベルとして表示するためのクラスだ。簡単に使えて便利なんだけど、単純な表示しかできない。単一フォントで単一サイズの表示しかできない。

プログラミングを進めていくと、当然の事ながら複数のフォントやサイズが混在した、リッチテキストを表示したい、という要求が出てくる。リッチテキストを取り扱えるクラスとしては、HTMLを表示するUIWebViewがある。まぁ、大きいテキスト表示するならそれでいいんだけど、一行とか二行だけ表示したい、ってときもあるよね。または、UITableViewCellの中で表示したいとか。セルにUIWebViewを埋め込むのはいかにもおおげさだ。

つまり、属性付き文字列を表示できるUILabelのようなクラスが欲しい訳だ。ないものは作るべし!ということで、作ってみた。

まずは、どうやってリッチテキストを描画するか?ってことを考えよう。これにはCore Textを使う。Core Textは、テキスト描画のための低レベルのフレームワークだ。リッチテキストの描画機能もこれに含まれる。使えるのはiOS 3.2以降。これを使って描画する、UILabelのサブクラスを作ろう。

AttributedLabelというクラス名にする。クラスの宣言は、こんな感じ。

@interface AttributedLabel : UILabel
{
    NSMutableAttributedString* _attrStr;
    CTFrameRef                 _ctFrame;
    BOOL                       _needsRefreshAttrStr;
}

@end

NSMutableAttributedStringっていうのが、属性付き文字列を取り扱うためのクラス。便利そうな名前のクラスであるが、こいつはそのままでは描画ができず、Core Textと組み合わせて使う必要がある。

その次にあるCTFrameというのが、Core Textのオブジェクト。画面にテキストの描画を行うには、こいつを使う。

実装の方では、まず属性付き文字列を作らなくてはいけない。そのために、_refreshAttributedStringというメソッドを作る。

- (void)_refreshAttributedString
{
    NSDictionary*   attrDict;

    // Release old attributed string and frame
    [_attrStr release], _attrStr = nil;
    if (_ctFrame) {
        CFRelease(_ctFrame), _ctFrame = NULL;
    }

    // Create attributed string
    _attrStr = [[NSMutableAttributedString alloc]
            initWithString:self.text];

    // Get length
    int length;
    length = [_attrStr length];

    //
    // Set font attribute
    //

    // Set normal font attribute
    CTFontRef   ctFont;
    ctFont = CTFontCreateWithName(
            (CFStringRef)self.font.fontName,
            self.font.pointSize,
            NULL);
    attrDict = [NSDictionary dictionaryWithObjectsAndKeys:
            (id)ctFont, (NSString*)kCTFontAttributeName,
            nil];
    [_attrStr setAttributes:attrDict range:NSMakeRange(0, length)];

    // Release font
    if (ctFont) {
        CFRelease(ctFont), ctFont = NULL;
    }

    //
    // Create frame
    //

    // Create frame
    CGMutablePathRef    path;
    CTFramesetterRef    framesetter;
    path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, self.bounds);
    framesetter = CTFramesetterCreateWithAttributedString(
            (CFMutableAttributedStringRef)_attrStr);
    _ctFrame = CTFramesetterCreateFrame(
            framesetter, CFRangeMake(0, length), path, NULL);
    CGPathRelease(path);

    // Clear flag
    _needsRefreshAttrStr = NO;
}

このメソッドでは、まず古いオブジェクトを破棄して、新しいNSMutableAttributedStringオブジェクトを作る。そして、これに属性を付加していく。とりあえず、フォント属性を指定してみた。ここで注意してほしいのは、フォントの指定にはCTFontを使う必要があること。そして属性辞書を作るんだけど、このとき指定するキーもCore Textで用意されているものを使うこと。これらに注意。

最後にCTFrameを作る。まず始めに、CGFramesetterを作る。CTFramesetterCreateWithAttributedStringという関数があるので、これを使う。これで属性付き文字列に紐付けられたCGFramesetterができる。これに、さらに描画領域を表すパスを紐付けるためにCTFramesetterCreateFrame関数を使い、CTFrameを作る。これで、描画準備完了だ。

あとは、drawRect:で描画してやればいい。

- (void)drawRect:(CGRect)rect
{
    // Refresh attributed string
    if (_needsRefreshAttrStr) {
        [self _refreshAttributedString];
    }

    // Get current context
    CGContextRef    context;
    context = UIGraphicsGetCurrentContext();

    // Get bounds
    CGRect  bounds;
    bounds = self.bounds;

    // Save context
    CGContextSaveGState(context);

    // Flip context
    CGContextTranslateCTM(context, 0, CGRectGetHeight(bounds));
    CGContextScaleCTM(context, 1.0f, -1.0f);

    // Draw frame
    CTFrameDraw(_ctFrame, context);

    // Restore context
    CGContextRestoreGState(context);
}

描画の前に、コンテキストをひっくり返してやる。Core Textは左下原点なんでね。そのまま書くと天地が逆転した文字列が描かれる。そして、CGFrameDrawを呼ぶ事で、画面に描画される。

まずは、普通に文字列を描いてみた。次回は、いろいろと属性を付加してみる。

ここまでのソースコードAttrLabel-1.zip

    • んだな
    • 2013年 1月14日

    いつも参考にしてます。
    今回のコードなのですがframesetterはリリースの必要はないんでしょうか?
    手元の環境ではリークするようです。

    • mkino
    • 2013年 1月15日

    リリースしてください。
    動作することを示すだけのサンプルなので、あまりきちんと検証していないです。

  1. 2011年 9月7日
  2. 2011年 12月11日
    トラックバック先 :iOS開発備忘録 | source lab. note
  3. 2012年 6月4日