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


前回の続き。Cocoa touchで属性付き文字列を表示するラベルを作ろう。

今回は、属性を追加するためのメソッドを追加してみる。太字の属性。それと、文字の前景色の属性。

これらの設定を行うために、専用のメソッドを用意してみた。addBoldFontAttrWithRange:と、addForegroundColorAttrWithColor:range:だ。範囲と、前景色の場合は色とを指定する。

// Attributes
- (void)addBoldFontAttrWithRange:(NSRange)range;
- (void)addForegroundColorAttrWithColor:(UIColor*)color range:(NSRange)range;

これらのメソッドの実装は、指定した範囲と色をインスタンス変数に格納するだけ。実際に文字列に属性を付加するのは、前回説明した_refreshAttributedStringメソッドの中になる。

では、その拡張された実装を。最初の部分は変化なし。フォント属性の追加のところで、太字属性に関する記述が新たに追加されている。

    ...

    //
    // Set font attribute
    //

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

    // Get bold font ranges
    UIFont*     boldFont;
    CTFontRef   ctBoldFont;
    boldFont = [UIFont boldSystemFontOfSize:12.0f];
    ctBoldFont = CTFontCreateWithName(
            (CFStringRef)boldFont.fontName, self.font.pointSize, NULL);
    for (NSValue* boldFontRangeValue in _boldFontRanges) {
        // Add bold font
        NSRange boldFontRange;
        boldFontRange = [boldFontRangeValue rangeValue];
        if (NSMaxRange(boldFontRange) <= length) {
            [_attrStr addAttribute:(NSString*)kCTFontAttributeName
                    value:(id)ctBoldFont range:boldFontRange];
        }
    }

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

    ...

太字設定は、まず太字用のCTFontオブジェクトを作る。今回は、システムのボールドフォントからフォント名を取得した。インスタンス変数の_boldFontRangesという配列に太字を設定する範囲を格納している。そこから範囲を取り出して、addAttribute:value:range:メソッドを使って属性を追加している。

続いて、前景色のための実装。

    ...

    //
    // Set foreground color attribute
    //

    // Set normal body forgrond color
    UIColor*    bodyForegroundColor;
    if (self.highlighted) {
        bodyForegroundColor = self.highlightedTextColor;
    }
    else {
        bodyForegroundColor = self.textColor;
    }
    [_attrStr addAttribute:(NSString*)kCTForegroundColorAttributeName
            value:(id)bodyForegroundColor.CGColor range:NSMakeRange(0, length)];

    // Get foreground colors
    int i;
    for (i = 0; i < [_foregroundColors count]; i++) {
        // Get foreground color and range
        UIColor*    foregroundColor;
        NSRange     foregroundColorRange;
        foregroundColor = [_foregroundColors objectAtIndex:i];
        foregroundColorRange = [[_foregroundColorRanges objectAtIndex:i] rangeValue];

        // Add foreground color attribute
        if (NSMaxRange(foregroundColorRange) <= length) {
            [_attrStr addAttribute:(NSString*)kCTForegroundColorAttributeName
                    value:(id)foregroundColor.CGColor range:foregroundColorRange];
        }
    }

    ...

基本的な流れは、フォントの設定のときと同じだ。色の属性は、CGColorオブジェクトとして設定することに注意。

新たに追加されたメソッドは、次のように呼び出す。

    // Set text
    _attrLabel.text = @"これは太字や色付き文字が表示できるラベルです。";

    // Add attributes
    [_attrLabel addBoldFontAttrWithRange:NSMakeRange(3, 2)];
    [_attrLabel addForegroundColorAttrWithColor:[UIColor blueColor]
            range:NSMakeRange(6, 5)];

で、実行結果がこれ。

確かに属性付き文字列が表示された!これで便利なラベルの出来上がり。

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

属性付きラベルを作る その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

uListeningのDeveloper’s Voiceを公開


HMDTで開発した製品の秘話を語る「Developer’s Voice」。今回新たに、「uListening」のDeveloper’s Voiceを公開。

uListeningは、英会話教材の第一人者アルクさんと一緒にやらせてもらっているアプリ。リスニング機能をメインに、iPhoneでの英会話学習をサポートするよ。

Developer’s Voiceでは、担当開発者がこだわりの機能の数々を語っております。ぜひ、ご一読を。

iOS 5 beta 7登場


iOS 5 beta 7登場。着々とバージョンアップを積み重ねているねぇ。GM到達も近いかな。

ここ数年のAppleの新OSリリースに関するスケジュールの遵守ぶりは素晴らしい。はっきりいって、本当に素晴らしいと思う。入れる、といった新機能がちゃんと入って、この辺りに出す、といった日付にちゃんと出しているから。

なんでだろうなと考えると、まず思いつくのはOSが成熟していること。OS XおよびiOSは同じカーネルを基盤として、その上に積み重ね続けている。すぐに新OSをホイホイ作りたがるどっかのマイクロソフトとは違う。ずっと使っていれば、成熟してバグも出にくくなる。

あと、これが重要だと思うんだけど、Appleは出来ないことを言わなくなった。出来たことだけを言うようになった。つまり、未来に対するホラを吹かない。その代わり、出来上がったものの発表を効果的にする。

Appleって、ビジョナリーの会社だと思われがちだけど、実はあんまり未来を語らない。メディアを使って、こんな素敵な近未来が来るよ、って喧伝することをしない。もちろん、考えていないわけではなくて、それを外にはなかなか見せない。みんなが知るのは、そのビジョンを実現した製品を発表するときだ。だから、吹いてしまったホラを実現するために四苦八苦することがない。出さなければいいだけのことだからね。

OSの新機能追加についても、ベータリリースという名目で世に出したときは、既にその実装は完了している、または目処がついている。あとは製品レベルまで仕上げるのと、開発者に使ってもらうための時間を確保するだけだ。だから大きな遅れがなくなったんだと思う。

この辺がやっぱりジョブズの考え方なんだろうなぁ。

iOS 4でUIPageViewControllerみたいなものを


iOS 5から、UIPageViewControllerというビューコントローラのクラスが追加される。一言で言うと、iBooksみたいなページめくりを実現してくれるクラスだ。これを使えば、誰でも簡単に書籍みたいなページめくりができる。

便利なんだけど、これを使っちゃうとiOS 4はどうするよ?って問題が出てくる。iOS 5専用にしちゃうという手もあるが、一世代前のOSくらいまではサポートしたいよな。

うーむ、と悩んだ結果、無いものは作れ!といういつもの結論に到達。ということで、iOS 4でも動くUIPageViewControllerの代替クラスを作った。APIはいっしょ。滑らかにページめくりしてくれる。

いまのところ、とりあえず動いている段階だけど、ソースコードが整理できたらどこかで公開するかも。

「iMandalArt – Developer’s Voice」を公開


HMDT Webページにて、「iMandalArt – Developer’s Voice」を公開しました。

HMDTのWebページは先週リニューアルして、うちで開発したプロダクトの紹介をするページを設けた。でも、ただ紹介するだけじゃつまんないよね、ということで、開発者による座談会を企画。どういったことを考えながらこのプロダクトを開発したのか?という生の声を届けたいと思ってます。

iMandalArtのDeveloper’s Voiceでは、マンダラート初期のHyperCard版やNewton版の画像なども公開。また、アプリの仕様やユーザインタフェースを、どうやって決めていくかというプロセスについても語ってるよ。iMandalArtに興味のある方だけでなく、アプリ開発のプロセスに関心のある方もぜひ。

ジョブズCEO退任


ジョブズがCEO退任の手紙を、Apple取締役と社員に送る

ついに来るべきものが来たという感じが。覚悟していたより早かったけど。

うちの会社、というよりも私個人の活動は、学生のときに初めてMacを触ってからずっとそれに支配されてきた訳で。いまの仕事をやれているのは、ジョブズのおかげといっても全然言い過ぎではない。

とりあえず思いつく言葉は、お疲れさまでした。かな。

MacでARMでUniversal Binaryの悪夢再び?


マイコミジャーナルより、『加速する「MacでARM採用」の噂、議論の存在をIntelが認める』。

マジすかー。PowerPCとIntelのUniversal Binary対応が終わったと思ったら、次はIntelとARMのUniversal Binaryすか。3つ同時にサポートすることは、共通して動くOS Xのバージョンがないから、考えなくてもいいな。

しかし、PowerPCからARMだったら、基本のエンディアンが同じだからまだ楽だったろうに。間にIntel挟むから面倒。ま、上位の方しか作らないアプリ開発屋は、あんまりエンディアンの影響受けないけどね。

iPhoneアプリでUDIDを使いたいケースその2


追記による注意:この記事は、はなはだしい勘違いに基づいていて、内容が破綻しています。修正しようにも、最も大事なところが勘違いしているためほぼ全部書き換えになってしまうので、しょうがないのでそのままにしておきます。最後に何が問題だったか書いておくので、そちらを参照して下さい。ということで、そのままの中身を信じないで下さい。

前回に続いて、iPhoneアプリでUDIDを使いたくなるケーススタディ。

あるiOSアプリがあったとして、自宅ではiPadの広い画面でゆうゆうと使いたい。通勤中はiPhoneで使いたい。それぞれのデバイスで、作業を別々に進めていく。どこかで同期を行う必要がある。こういった状況を考えよう。

まず大前提として、iOSのモデルは1アカウントに対して複数デバイスを運用する。これらのデバイスは、iTunesを通じてアプリおよび書類を同期できる。逆に言うと、複数デバイスで異なるデータを保持し続けることを想定していない。

もちろん、ほとんどの状況において、データの同期は善だ。iPhoneとiPadとで違うデータがたまり続けたら目もあてられない。そのためにiTunesを使ったり、同期のための独自サーバを立てたりする。iOS 5からはiCloudも提供される。

でも実際にアプリを使っていると、同期したくないデータも出てくる。たとえば、ブックビューワみたいなアプリを考える。iPhoneでもiPadでも同じ本棚を共有して本が読めるとする。このとき、iPhoneで呼んだ続きをそのままiPadで読みたい、という要求があるだろう。それと同時に、iPhoneで読んだ本はiPhoneで読み続けて、iPadではiPadのものを読み続けたい、という要求も発生するだろう。

現在の同期モデルだと、前者しかできない。すべての書類が同期されてしまうので、「前回読んだ本」を表すデータもただ一つしか存在できないからだ。となると、そのデータをデバイス毎にひも付けた形で保存しておきたいことになる。、、、UDIDさんの出番ですよー。

UDIDが使えれば、それをキーとしてデータを保存しておけばいい。生のデータを使うのが嫌であれば、ハッシュ値でも何でもとればいい。UDIDが使えないとなると、デバイスを識別する方法がなくなっちゃう。仮に自前のUUID作ったとしても、それがデバイス間で同期されたら意味ないし。

デバイスのモデル名を使うという方法があるかもしれない。UIDeviceクラスからは、@”iPhone”や@”iPad”という現在使っているデバイスのモデル名が取得できる。なるほど、これならiPhoneとiPadは識別できるな。、、、一人でiPad二台使ってたらどうすんの?

というわけで、やっぱりUDIDは欲しい。ま、これに関してはMACアドレスで代用は可能だけど。ネットにつながない「閉じた」アプリケーションであっても、複数デバイスを運用する限り、どのデバイス上で動いているか識別したい要求は存在する。

追記による注意:はい、どこが勘違いしていたでしょうか?「iTunesの同期によって、デバイス間の書類も同期される」というところです。実際は、同期されません。したがってUDIDを取得する必要もありませんでした。

もともとこの記事は、UDIDが必要になるケースって何があるかな?と考えて、複数デバイスを使い回しているときにUDIDを意識することがあるかも、というところから書いていきました。その結果、都合のいい勘違いを自分の中で作ってしまったようです。ということで、迷惑をかけた人がいたら、申し訳ありませんでした。

UIDeviceのuniqueIdentifierがdeprecatedに


iOS 5 beta 6で、UIDeviceクラスのuniqueIdentifierプロパティが非推奨(deprecated)になった。将来なくなるってこと。んな、beta 6まで進んだこのタイミングで言われても、という気はする。

uniqueIdentifierプロパティは、デバイス個体のID(UUID)を取得するためのメソッド。これを使うと個体を識別できる。値自体は特に秘密ではなくて、iPhoneをMacにつないで、iTunesを使って確認できる。個体識別番号っていうと、ちょっとでもIT系のニュースを聞きかじっている人なら、即セキュリティやプライバシーの問題を思い浮かべるよね。実際、UUIDを許可無しに流出させるアプリがあるってニュースもあったし、穴はふさいどけ、ってことなんでしょう。

開発側からすると、別にプライバシーを暴くためでもなんでもなくて、UUIDを使いたいっていうケースは存在する。それは実行環境の識別を行いたいから。もっと細かく言うと、デバイスの個体識別と、ユーザ識別がある。

デバイスの個体識別は、ユーザがアプリを複数のデバイス(たとえばiPhoneとiPad)で使っているとき、いまどっちのデバイスで動いているの?というのを知りたいとき。これをやるには、デバイスのUUIDが不可欠なんだよね。これが取れないとなると、MACアドレスを使うか、そもそも個体識別が必要ないように仕様を変えるしかない。

ユーザ識別は、いま使っているユーザを他のユーザと区別したいとき。これをやるにはUUIDは完全ではないんだけど、手軽なソリューションだった。

ユーザ識別をしたいときってのはIn App Purchaseが絡むときが多いので、そのAPIを使うこともできる。でも、あれを使うとユーザにApple IDの入力を頼むことになるんで、警戒されるんだよね。

まぁ、どっちにしろUUID以外のきちんとしたソリューションは考えておかなくちゃいけなかった訳で。今回は先延ばしにしていたものに対して、尻を叩かれたかって感じだ。 the domain shops