post Image
【iOS 11】Dynamic Type でカスタムフォントに対応する

ご覧いただきありがとうございます。
この記事は Qiita Advent Calendar 2017 ゆめみ1 の第 22 日目の記事です。
前日は @clown0082 さんの記事でした。

はじめに

iOS 11 から Dynamic Type でシステムフォントに加えて
ついにカスタムフォントが設定できるようになりました。
本記事執筆時では iOS 11 だけが対応可能となっており,
なかなか採用されにくいとは思いますが,
この記事では,どのようにしたら対応できるのかについて書きます。

システムフォントの場合とカスタムフォントとで一部表示が異なることがあり
もしご存知の方がいらっしゃったら教えていただきたいです。

この記事は iOS の Advent Calendar 2017 第 21 日目の記事2
関連した内容になります。合わせてご覧いただけたら嬉しいです。

Dynamic Type とは(おさらい)

Dynamic Type は iOS 7 から導入されました。
設定アプリ,iOS 11 からはコントロールセンターに追加することで
ユーザが好みのフォントサイズに変えられますよね。
ユーザが設定したフォントサイズに伴って
アプリ内のフォントサイズも変化させるというものです。

私は一番小さなフォントサイズ設定が好きです。
ただ,全ユーザがそうだとは限りません。
ユーザによってみやすいフォントサイズは異なるという意識を持つことが大事です。

Dynamic Type でカスタムフォントに対応する

ひとつ前の記事2に書いたのと同じ実装をした上でさらなる実装が必要です。
UIFontTextStyle を適用しただけではシステムフォントのままだからです。
下記が Dynamic Type 対応の要点(復習)です。

Storyboard など IB の場合

  • UI 部品を配置しフォント部分で UIFontTextStyle のどれかを選ぶ
  • Automatically Adjusts Font 部分にチェックを入れる

コードの場合

  • UI 部品のインスタンスを作り,フォント設定に UIFontTextStyle を指定
  • adjustsFontForContentSizeCategorytrue にする
swift
// body を設定する例
label.font = UIFont.preferredFont(forTextStyle: .body)
label.adjustsFontForContentSizeCategory = true

カスタムフォントの対応

カスタムフォントに対応する場合は上記の実装に加えて,
UIFontMetrics3 を用います。
カスタムフォントを scaledFont(for:) メソッドに渡すことで
ユーザが設定中のフォントサイズに合うように自動調整された
指定された UIFontTextStyle のフォントが得られます。
多分コードで書くしかないです。。。

Futura に最近ハマっているのでカスタムフォントの例として使います。
iOS 11 だけのコードでよければ下記のようになります。

swift
// Futura-Medium で body を設定する例
let customFont = UIFont(name: "Futura-Medium", size: UIFont.labelFontSize)
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: customFont)
label.adjustsFontForContentSizeCategory = true

iOS 11 未満はシステムフォントになるように場合分けします。
もっと綺麗に書けそうな気がします。

swift
if #available(iOS 11.0, *) {
    // カスタムフォントの綴り間違いには注意
    if let customFont = UIFont(name: "Futura-Medium", size: UIFont.labelFontSize) {
        label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: customFont)
    } else {
        label.font = UIFont.preferredFont(forTextStyle: .body)
    }
} else {
    // iOS 11 未満はシステムフォントにする
    label.font = UIFont.preferredFont(forTextStyle: .body)
}

簡単な実装例で確認

サンプルコード4

GitHub にサンプルアプリを用意しましたので
見づらいところ等,適宜参考にしてください。
Swift は 今まで業務で携われていないこともあり,
まともにレビューしてもらったことがないので
コード間違い・改善点等のご指摘,是非お願いいたします。

実装環境

  • Xcode 9.2
  • iOS 10 and later(実質 iOS 11 以上)
  • 説明の都合上 FatViewController でお送りします

実装例

例として下記のような文字列と UIFontTextStyle の Array を用意し,
TableView のセルに流し込んで textLabel.text に表示させてみます。

Constant.swift
struct DynamicTypeSample {
    static let textArray: [String] =
        ["Body", "Callout", "Caption1", "Caption2", "Footnote",
         "Headline", "Subhead", "Title1", "Title2", "Title3"]
    static let styleArray: [UIFontTextStyle] =
        [.body, .callout, .caption1, .caption2, .footnote,
         .headline, .subheadline, .title1, .title2, .title3]
}

UITabelViewDataSource の実装だけ書くと下記のようになります。

実装例:クリックでコード表示
CustomFontViewController.swift
extension CustomFontViewController: UITableViewDataSource {

    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return DynamicTypeSample.textArray.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "dynamicTypeCustomFontListCell")
        cell?.textLabel?.text = DynamicTypeSample.textArray[indexPath.row]
        // Custom Font は iOS 11 から
        if #available(iOS 11.0, *) {
            if let customFont = UIFont(name: "Futura-Medium", size: UIFont.labelFontSize) {
                cell?.textLabel?.font = UIFontMetrics(forTextStyle: DynamicTypeSample.styleArray[indexPath.row]).scaledFont(for: customFont)
            } else {
                cell?.textLabel?.font = UIFont.preferredFont(forTextStyle: DynamicTypeSample.styleArray[indexPath.row])
            }
        } else {
            cell?.textLabel?.font = UIFont.preferredFont(forTextStyle: DynamicTypeSample.styleArray[indexPath.row])
        }
        cell?.textLabel?.adjustsFontForContentSizeCategory = true
        cell?.selectionStyle = .none
        return cell!
    }
}

セクション(Header)がある場合は文字列を流し込むだけでは
システムフォントのままになるのでカスタムフォントに対応するために
UITableViewDelegate の Header 系の Delegate メソッドで個別で書く必要があります。

実装例:クリックでコード表示
CustomFontViewController.swift
extension CustomFontViewController: UITableViewDelegate {

    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let headerView = self.generateHeaderView()
        return headerView
    }

    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        let headerView = self.generateHeaderView()
        return headerView.frame.size.height
    }
}

// Header の UIView を生成し返す
func generateHeaderView() -> UIView {
    let sectionLabel = UILabel(frame: CGRect(x: self.dynamicTypeListTableView.separatorInset.left,
                                             y: 4.0,
                                             width: (self.dynamicTypeListTableView.frame.size.width - self.dynamicTypeListTableView.separatorInset.left),
                                             height: 0.0))
    sectionLabel.text = "Custom Font"
    if #available(iOS 11.0, *) {
        if let customFont = UIFont(name: "Futura-Medium", size: UIFont.labelFontSize) {
            sectionLabel.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: customFont)
        } else {
            sectionLabel.font = UIFont.preferredFont(forTextStyle: .headline)
        }
    } else {
        sectionLabel.font = UIFont.preferredFont(forTextStyle: .headline)
    }
    sectionLabel.sizeToFit()
    let sectionView = UIView(frame: CGRect(origin: .zero,
                                           size: CGSize(width: self.dynamicTypeListTableView.frame.size.width,
                                                        height: sectionLabel.frame.size.height + 8.0)))
    sectionView.backgroundColor = UIColor(red: 232/255, green: 233/255, blue: 237/255, alpha: 1)
    sectionView.addSubview(sectionLabel)

    return sectionView
}

実行例

実行し,Accessibility Inspector を使って
フォントサイズを変えてみると下記のような感じになります。
Accessibility Inspector の使い方についてはこの記事2をご覧ください。

dynamictype_exe_02.gif

UIFontTextStyle ごとにちゃんと変化していることがわかります🎉

がしかし,システムフォント側となんか違う・・・
特に Caption2… 明らかに大きい。
🤔

dyna_01.png

システムフォントとの挙動の違いについて

この動作だけ見ると問題なくフォントサイズが変わっているように
見えるのですが,一部の UIFontTextStyle で大きさが
システムフォントと異なってしまっています。

このことについて調べてみたのですがよくわからずでした。
下記の記事にもこのことが一部触れられていて .caption2 の代わりに
.largeTitle (iOS 11からの UIFontTextStyle)が使われているようで
バグではないかということでした。

Note: I am not sure if it is a bug or a “feature” but the .caption2 style seems to scale larger than the .caption1 style even though it uses a smaller point size at the .large size.

via
Using A Custom Font With Dynamic Type

とりあえず,Technical Support Incident(TSI)5で質問してみようと思います。
こういうのを WWDC のラボで聞けばいいんだろうなー

おわりに

iOS 11 から Dynamic Type でカスタムフォントを指定できるように
なったので今回はその対応方法について記事を書きました。
ありそうでなかった機能なので WWDC 17 のときは嬉しかったのですが,
iOS 11 以上かそれ未満かで場合分けなどいざ実装してみると色々大変で
iOS 10 のサポート切ってもいいかなというタイミング(iOS 13くらい?)まで
本格的な採用はないのでは?というのが現時点の私見です。

Dynamic Type 自体が商用アプリとして採用されにくい部分があるので,
まずは社内ハッカソンや社内の便利ツール(受付アプリなど)など
小規模な単位で対応してみたら面白いかもしれないですね!
Apple が WWDC 17 の色々なセッションで言及しているだけに
知見はためておいて損はない気がします。

来年も引き続き UI 周りに力入れて,新しい技術習得をしながら,
日々の業務・個人アプリの制作を頑張っていこうと思います。
ここまでご覧いただきありがとうございました!

明日は @u-minor さんが担当します!!

参考

引用


『 Swift 』Article List
Category List

Eye Catch Image
Read More

Androidに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

AWSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Bitcoinに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

CentOSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

dockerに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

GitHubに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Goに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Javaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

JavaScriptに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Laravelに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Pythonに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Rubyに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Scalaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Swiftに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Unityに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Vue.jsに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Wordpressに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

機械学習に関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。