Dynamic Typeに対応してユーザー設定に合わせたフォントを使用する

2014年9月16日(火)
西方 夏子(にしかた・なつこ)

文字サイズ設定の変更に対応

テキストスタイルを指定して生成したフォントは、文字サイズ設定の変更には追従しません。そのため、ユーザーが文字サイズ設定を変更した場合は、新しい設定に合わせてフォントを生成し直す必要があります。
文字サイズ設定の変更は、UIContentSizeCategoryDidChangeNotification通知を利用して知ることができます。Dynamic Type対応のアプリでは、この通知を受けてフォントの再設定を行います。設定されている文字サイズは、通知のuserInfo(キーはUIContentSizeCategoryNewValueKey)より取得可能です。または、UIApplicationのpreferredContentSizeCategoryプロパティからも取得できます。
通知の登録は、viewDidLoadなど、適切なタイミングで行いましょう。

[ソースコード19] 文字サイズ設定変更通知の登録と解除(例)

// 通知の登録
- (void)viewDidLoad
{
    [super viewDidLoad];
 
    [[NSNotificationCenter defaultCenter] addObserver:self
                                 selector:@selector(contentSizeCategoryDidChange:)
                                     name:UIContentSizeCategoryDidChangeNotification
                                   object:nil];
}
 
- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
  
    if (self.isViewLoaded && [self.view window] == nil) {
        [[NSNotificationCenter defaultCenter] removeObserver:self];
        self.view = nil;
    }
}

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

通知を受けるハンドラでフォントを再設定します。このとき、フォントの設定にfontプロパティを用いているか、属性付きテキスト(NSAttributedString)のNSFontAttributeName属性を用いているかで、その処理の内容が異なります。

まずは、通常のテキスト(NSString)のフォントをfontプロパティで設定している場合の例を示します。この場合は、単純にfontプロパティの値を更新するだけです。

[ソースコード20] fontプロパティの更新処理(付加的な属性なし)

- (void)contentSizeCategoryDidChange:(NSNotification *)notification
{
    // UIFontDescriptorからテキストスタイルを取得
    UIFontDescriptor *fontDescriptor = [self.label.font fontDescriptor];
    NSString *textStyle =
    [fontDescriptor objectForKey:UIFontDescriptorTextStyleAttribute];
  
    if (textStyle) { 
        // テキストスタイルが設定されていた場合はフォントを更新
        UIFont *newFont = [UIFont preferredFontForTextStyle:textStyle];
        self.label.font = newFont;
        [self.label invalidateIntrinsicContentSize];
    }
}

設定済みのUIFontオブジェクトからディスクリプタを取得し、テキストスタイルが設定されているかを確認します。テキストスタイルの設定がある場合には、フォントを更新します。
なお、フォントの再設定後にinvalidateIntrinsicContentSizeを呼んでいますが、これは、Auto Layoutでビューの固有サイズ(Intrinsicサイズ)を調整するための処理です。なお、ビューのサイズ変更に関しては、「2.1.6コンテンツモード」も参考にしてください
使用フォントが、テキストスタイルのみを指定して生成されたフォントであれば、上記コードで十分です。しかし、フォント属性をさらにカスタマイズし、他の属性を追加している場合は、上記だけでは付加的な属性が抜け落ちてしまうことがあります。

この問題を対処するには、テキストスタイルに無関係な属性を、元のディスクリプタからコピーする必要があります。その処理の一例を以下に示します。

※本連載では未掲載です。書籍をお持ちの方はP.021を参照してください。

[ソースコード21] fontプロパティの更新処理(付加的な属性あり)

- (void)contentSizeCategoryDidChange:(NSNotification *)notification
{
    // UIFontDescriptorからテキストスタイルを取得
    UIFontDescriptor *fontDescriptor = [self.label.font fontDescriptor];
    NSString *textStyle =
    [fontDescriptor objectForKey:UIFontDescriptorTextStyleAttribute];
  
    if (textStyle) { // テキストスタイルが設定されていた場合はフォントを更新
        // テキストスタイルを元にディスクリプタを生成 // --- (1)
        UIFontDescriptor *newFontDescriptor =
        [UIFontDescriptor preferredFontDescriptorWithTextStyle:textStyle];
  
        // スタイルに影響しない既存の属性をコピーする // --- (2)
        // 既存のディスクリプタが持つ属性を取得
        NSMutableDictionary *attributes = fontDescriptor.fontAttributes.mutableCopy;
        // 新しいディスクリプタが持つ属性キー(スタイル依存のキー)を取得
        NSArray *allNewKeys = [newFontDescriptor.fontAttributes allKeys];
        // 既存の属性からスタイル依存の属性を削除
        [attributes removeObjectsForKeys:allNewKeys];
        // 新しいディスクリプタに残りの属性を追加
        newFontDescriptor = 
        [newFontDescriptor fontDescriptorByAddingAttributes:attributes];
  
        // ディスクリプタからフォントを生成 // --- (3)
        UIFont *newFont = [UIFont fontWithDescriptor:newFontDescriptor size:0.0];
        self.label.font = newFont;
        [self.label invalidateIntrinsicContentSize];
    }
}

上記コードの(2)で、付加的な属性をコピーしています。これで、テキストスタイルを採用したフォントにアフィン変換を適用(UIFontDescriptorMatrixAttribute属性を付加)している場合でも、付加的な属性を保持したままユーザーの文字サイズ設定に合わせたフォントを再生成することが可能です。

なお、公開されているテキストスタイルは6通りですが(冒頭参照)、実際にシステムが利用しているテキストスタイルはこれだけではありません。例えば、ソースコード18(1ページ目参照)では、Bodyスタイルにイタリック特性を追加すると、テキストスタイルはUICTFontTextStyleItalicBodyとなり*2、他の属性は追加されません。このような場合には、ソースコード20の方法でフォント更新が可能です。

続いて、属性付きテキストのNSFontAttributeNameに対してフォントを設定している場合の、フォント再設定例を示します(属性付きテキストの詳細は次回以降で解説)。このケースでは、テキストが持つ属性の中でも、フォントに関する属性のみを更新する必要があり、単純にfontプロパティを更新するよりも複雑な処理になります。
なお、下記の例では簡略化のため、ディスクリプタにはテキストスタイル属性以外に付加的な属性がないものとして説明します。

*2 iOS 7.1で確認

[ソースコード22] 属性付きテキストが持つフォント情報を更新(付加的な属性なし)

- (void)contentSizeCategoryDidChange:(NSNotification *)notification
{
    NSMutableAttributedString *mutableAttrStr =
    self.textView.attributedText.mutableCopy;
 
    [mutableAttrStr beginEditing];
    // フォント属性の数え上げ
    [mutableAttrStr enumerateAttribute:NSFontAttributeName
                inRange:NSMakeRange(0, mutableAttrStr.length)
                options:0
            usingBlock:^(id value, NSRange range, BOOL *stop){
                // UIFontDescriptorからテキストスタイルを取得
                UIFontDescriptor *fontDescriptor = [(UIFont *)value fontDescriptor];
  
                NSString *textStyle =
                [fontDescriptor objectForKey:UIFontDescriptorTextStyleAttribute];
 
                if (textStyle) {
                    // テキストスタイルが設定されていた場合はフォントを更新
                    UIFont *newFont = [UIFont preferredFontForTextStyle:textStyle];
                    [mutableAttrStr removeAttribute:NSFontAttributeName range:range];
                    [mutableAttrStr addAttribute:NSFontAttributeName
                                           value:newFont range:range];
                }
    }];
    [mutableAttrStr endEditing];
    self.textView.attributedText = mutableAttrStr;
}

対象となる属性付きテキストが持つフォント属性(NSFontAttributeName)を数え上げ、テキストスタイルが設定されていたらフォントを生成し直します。
上記ではattributedTextを利用していますが、Text KitのNSTextStorageを用いている場合でも、これと同じ方法でフォントの再設定が可能です。

なお、執筆時(iOS 7.1)に確認されている問題として、日本語を含む属性付きテキストに、テキストスタイルを元に生成したフォントを設定すると、テキストスタイル属性が抜け落ちてしまう場合があります。そのため、日本語テキストに対しては、テキストスタイルを別に保持するなどの対処が必要となります。

この記事のもとになった書籍
UIKit徹底解説 iOSユーザーインターフェイスの開発

西方 夏子 著/丸山 弘詩 編
価格:3,800円+税
発売日:2014年06月13日発売
ISBN:978-4-8443-3608-2
発行:インプレスジャパン

UIKit徹底解説 iOSユーザーインターフェイスの開発

ユーザーインターフェイス(UI)を制御するUIKitは、iOSアプリの中心的役割を担います。UIKitを深く理解することが、アプリ開発での多くの問題解決に繋がります。本書では、アプリの起動から画面表示、画面レイアウト、イベントハンドリングと、常に稼働しているUIKitの機能を丁寧に解説します。メインとなるコンテンツの表示からステータスバーを含むバーの制御、フォントやテキストの改良など、ビューやテキストの「外観」(アピアランス)を改善する内容も充実させています。使用頻度の高いテーブルビューとコレクションビュー、そしてユニバーサルアプリ化に有益なカスタムコンテナビューコントローラに関しては、チュートリアル形式で解説します。また、StoryboardやAuto Layoutも今後のアプリ開発でさらに重要となる状況を見越して、詳細解説に留まらず同じくチュートリアルで多くの具体例を紹介します。

Amazon詳細ページへImpress詳細ページへ

著者
西方 夏子(にしかた・なつこ)

ソフトウェアエンジニア。大手電機メーカーにおける組込みソフトウェアの開発を経て、現在は個人でiOS向けのアプリケーションを開発している。代表作であるローン計算アプリ「iLoan Calc」は、個人の方のみならず、多くの不動産業、金融業の方からも愛用されている。『iPhoneアプリ開発エキスパートガイド iOS6対応』(共著・インプレスジャパン刊)、『上を目指すプログラマーのためのiPhoneアプリ開発テクニック iOS 7編』(共著・インプレスジャパン刊)などの執筆にも携わり、現在は執筆業を中心にアプリ開発と育児を両立中。

連載バックナンバー

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています