ここしばらくエンジニアをしていないでデザイナーばかりしているゆこびんです、こんにちは。
Xcodeをさわるといえば、デザイン仕事がらみでSwift ChartやLiquid Glassの自由研究をしていたくらいです。
そんな開発ウラシマな私が、過去アプリのリニューアル案件にちらっと携わっているのですが、こんなものを発見しちゃいました!
そちらがなんと!カスタムフォントをDynamic Typeに対応させるやーつ! (๑´ㅂ`๑)
素晴らしいじゃん便利じゃんしかもiOS 14から使えてたんじゃん!
ということで、めちゃ久々に(記事書くリハビリも兼ねて)Qiitaを書いてみようと思います。
※このページはプログラミング初学者にも分かりやすい簡単な説明を心がけているため、厳密には少し違う表現が出てくるかもしれません。ご了承ください。
今回の主役!!!customフォントだってDynamic Typeに簡単に対応できちゃうだもんね!
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
public static func custom(
_ name: String,
size: CGFloat,
relativeTo textStyle: Font.TextStyle
) -> Font
使うとこんな感じです。
Font.custom("MyFont", size: 16, relativeTo: .body)
ぱっと見、
MyFontの16ptを .body として扱う
というように見えますよね。もう少し深堀りしてみましょう。
custom(_:size:relativeTo:)とは
Font.custom("MyFont", size: 16, relativeTo: .body)
これは、
MyFontが .body のDynamic Typeルールでスケールするようになる。基準の値は16pt
という意味です。
つまり:
-
.bodyっぽい見た目になる→ ❌違う -
.bodyと同じように、Dynamic Typeで拡大縮小される→ ✅これ
relativeToは「サイズが定義される」でなく「拡大縮小のされ方が定義される」
👉 relativeTo は見た目のサイズ指定ではなく、 スケーリングの特性を選ぶということです。
ざっくり言うと
-
size→ 基準。Dynamic Type Large(デフォルト)でのサイズ -
relativeTo→ そのtextStyleのDynamic Typeスケーリング(拡大縮小)カーブを使う
です。
大前提
この大前提を忘れてはいけないので(一応)書いておきますね!
.bodyだったらbody要素に!
例えば、こういうコードも書けます。
Font.custom("MyFont", size: 10, relativeTo: .headline)
Font.custom("MyFont", size: 18, relativeTo: .subheadline)
Font.custom("MyFont", size: 16, relativeTo: .body)
SwiftUIちゃんはこうなっていても我々を咎めたりはしません。
でも、こうなっていたらゆこびんはツッコミを入れたいです。
.headline は見出し的な役割を持つtext styleです。
.subheadline は見出しほどではない小見出しやサブ的な見出しのためのtext styleです。
- 見出しよりサブ見出しが大きい
- 意味とUIの見た目がズレる
- Dynamic Typeで拡大したときのバランスも崩れる
あかーーーーん!((((;゚Д゚))))
.headline を10ptにはできます。でもそれ、全体を見た時に見出しとしての仕事ちゃんとできてます?
👹 役割とサイズがズレるとUIが壊れる
ということで、コンテンツには役割にあったテキストスタイルと適切な基準のサイズを設定したいわけです。
sizeにはDynamic Typeのdefault値を指定
sizeは何を基準に決めるのか?
上に書いたように、適当に決めたらダメなやつですね。
ということで、Human Interface GuidelinesのTypography iOS, iPadOS Dynamic Type sizesのLargeを参考にすると良いと思います。
(もちろん、デザイナーさんがTypography組んでくれているならその通りに)
というか、まさにsizeはここでいう、default値(Large)を指定するということにほかなりません。
もう少し具体的に
HIGのTypography見ると、Large (default)で、
- Title1 = 28pt
・・・ - Body = 17pt
・・・ - Caption = 12pt
のように定義されています。
Font.custom("MyFont", size: 15, relativeTo: .body)
は、
.body(17pt)相当の役割を、MyFontの15ptで表現する
というイメージです。
※スケーリングは、Appleの定めた.body(17pt)で定義されているものが当たります。15ptではなく、です。
(余談)フォントによって見え方は変わる
同じptでも、フォントが違えば、
- 大きく見える
- 小さく見える
- 詰まって見える
など差があります。
例えば、iOSのデフォルトは17ptですが、日本語フォント(ヒラギノ角ゴシック系)は、実際は16ptくらいで調整されているというのは有名な話だったりします。英字と並んでも違和感がでないようにボリューム感をそろえているところがさすがAppleだと思います。他の言語のフォントもデフォルトのサイズを出したことがあるのですが面白かったです。(昔、インバウンドの多言語対応アプリをやっていたことがあったので)
雑に説明するとフォント自身が持っている箱のサイズやその中に文字が詰まっている感じとかで、フォントサイズが同じ値でも印象が全然違うことになります。さらにデザインツールでの文字の扱い(処理の仕方)も・・・と話が長くなってしまうので、興味のある人は近くのデザイナーさんに聞いてみるとよいです。
ということで、フォントをカスタムするのであれば、
👉 HIGを参考にしつつ、見た目で調整するのが現実的です
Dynamic Typeの倍率をコードで確認してみる!
Dynamic Typeは単純な倍率ではなく、カーブでスケールしています。
(※HIGの表を見比べればだいたい計算できるんですが)
実際に数値を出してみるのが手っ取り早い!
UIFontMetrics を使うと、指定したサイズが各Dynamic Type設定で何ptになるか確認できます。
import SwiftUI
import UIKit
let baseSize: CGFloat = 16 //例えば基準を16ptとすると
let categories: [(UIContentSizeCategory, String)] = [
(.extraSmall, "XS"),
(.small, "S"),
(.medium, "M"),
(.large, "L"),
(.extraLarge, "XL"),
(.extraExtraLarge, "XXL"),
(.extraExtraExtraLarge, "XXXL"),
(.accessibilityMedium, "AX1"),
(.accessibilityLarge, "AX2"),
(.accessibilityExtraLarge, "AX3"),
(.accessibilityExtraExtraLarge, "AX4"),
(.accessibilityExtraExtraExtraLarge, "AX5")
]
let textStyles: [(UIFont.TextStyle, String)] = [
(.largeTitle, "LargeTitle"),
(.title1, "Title1"),
(.title2, "Title2"),
(.title3, "Title3"),
(.headline, "Headline"),
(.subheadline, "Subheadline"),
(.body, "Body"),
(.callout, "Callout"),
(.footnote, "Footnote"),
(.caption1, "Caption1"),
(.caption2, "Caption2")
]
struct ContentView: View {
var body: some View {
ScrollView () {
VStack(alignment: .leading) {
Text("Dynamic Type scaling table")
.font(.title)
.padding(.bottom, 8)
ForEach(textStyles, id: \.0) { style, styleName in
VStack(alignment: .leading) {
Text(styleName)
.font(.headline)
ForEach(categories, id: \.0) { category, label in
let scaled = scaledValue(style: style, category: category)
let ratio = scaled / baseSize
let paddedLabel = label.padding(toLength: 4, withPad: " ", startingAt: 0) //ちょっと見やすくするために詰め物
Text("\(paddedLabel): \(String(format: "%.2f", scaled))pt / x\(String(format: "%.2f", ratio))"
)
.font(.system(.body, design: .monospaced)) //ちょっと見やすくするために等幅に
}
}
.padding(.bottom, 16)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 16)
}
}
func scaledValue(style: UIFont.TextStyle, category: UIContentSizeCategory) -> CGFloat {
let trait = UITraitCollection(preferredContentSizeCategory: category)
let metrics = UIFontMetrics(forTextStyle: style)
return metrics.scaledValue(for: baseSize, compatibleWith: trait)
}
}
#Preview {
ContentView()
}
-
.bodyの16ptはどれくらい拡大されるのか -
.headlineと.captionで伸び方がどう違うのか - アクセシビリティサイズでどれくらい大きくなるのか
👉 同じ16ptでも relativeTo によって結果が変わっていることが分かります。また、最初から小さい文字であろうcaption系はSにしてもフォントのサイズが小さくならないのもパッと見でわかりますね。
(おまけ)leadingについて
HIGのTypographyの表を見るに
leadingに関しては、font-sizeが大きい場合は狭め、小さい場合は可読性を考えて広めになっているようです。(これもまたAppleらしい😊)
leading自体は、styleごとに個別に決まっているものではなく、純粋にfont-sizeに対して決まっているようにみえます。表を見る限り同じfont-sizeなら、BodyでもTitleでもCaptionでも、同じleadingになっているので。
表のleadingはSFを使った場合であって、カスタムフォント使う場合はそのフォントが持っているascentや descentなどの情報によって決まるはずなので、この値は参考程度にとらえておくとよいでしょう。「(余談)フォントによって見え方は変わる」にも箱のサイズ的なことを書きましたが、もっと知りたいなという場合は、お近くのデザイナーさんに聞いてみよう!
(このあたりも実際コード書いて見てみると面白い発見があるかもですね。)
まとめ
- カスタムフォントも
custom(_:size:relativeTo:)で簡単にDynamic Type対応できる -
sizeは基準になるフォントサイズ -
relativeToはスケーリングの特性 - テキスト(コンテンツ)の役割のタイプを選ぶ
とても素晴らしいですね(o°▽°)o
久々に記事を書きましたが、フォントやデザインがらみのネタ(自由研究)はやっぱり楽しいですね〜〜!
最近アクセシビリティでも日本屈指の超絶専門家のデザイナーさんともお仕事もしているのもあるので、ちゃんとDynamic Typeに対応したモバイルアプリが作りたいなと思っています🥹
次は〜
Dynamic Typeに耐えられるデザインの話とか、iOSとAndroidでどう揃えるか、とかかしら? 最近やったLiquid Glass自由研究をまとめるものいいですねぇ🤗
