はじめに
Dynamic Type とはフォントサイズをユーザが調整できるようにする OS の機能です。
アプリ内設定ではなく、設定アプリから端末全体に対して設定するやつですね。
公式が用意している Font.TextStyle
を使用することで簡単に対応できます。
ただし、Font.TextStyle
はフォントサイズがガチガチに決められているためデザイン指示がある場合などは対応しきれないことが多いでしょう。
というわけで、この記事ではカスタムのテキストスタイルの Dynamic Type 対応方法について紹介します。
実装方法2パターン
上記に対応する場合、パターンとしては以下の二つがあります。
-
UIFontMetrics
を使用するパターン -
ScaledMetric
を使用するパターン
UIFontMetrics
のパターンで対応している方が多い印象です。(自分の周りだけかもしれませんが)
ただ、使い始めると気付くと思うのですが UIFontMetrics
はその仕組み上、 SwiftUI プレビューの Dynamic Type 設定を変更しても反映されません。
ちなみにプレビューの Dynamic Type 設定場所はこちらです。
せっかくプレビューがあるのに Dynamic Type の細かなレイアウト調整のために Run が必要になるのはもったいないですよね。
そのため SwiftUI プレビューの Dynamic Type 変更に追従できる ScaledMetric
を使用したパターン がおすすめです。
というわけで、この記事では主に ScaledMetric
を使用した実装例を紹介します。
UIFontMetrics
の方も気になるという方はぜひ下記の記事を読んでください。
めちゃくちゃ勉強になります。
ソースコード
完成系のコードは下記に格納しています。
GitHub
ScaledMetric を使用した実装方法
1. カスタムのテキストスタイルを定義
まずはカスタムのテキストスタイルを定義します。
enum MyTextStyle {
case title
case body
case subheadline
case caption
}
2. テキストスタイルのデフォルトサイズを定義
MyTextStyle
に対してデフォルトのサイズを定義します。
extension MyTextStyle {
var defaultSize: Double {
switch self {
case .title:
return 21
case .body:
return 17
case .subheadline:
return 15
case .caption:
return 12
}
}
}
ちなみにデフォルトサイズとは DynamicTypeSize.large で表示されるサイズになります。
.large が DynamicTypeSize のデフォルト値なので、設定アプリから文字サイズを一度も変更していない場合は .large が設定されているはずです。
3. 文字の拡大率を設定
Font.TextStyle
には「Dynamic Type 変化時にどれぐらい文字サイズが変わるのか」のパラメータがそれぞれ設定されています。
例えば Font.TextStyle.caption
は元々文字サイズが小さいため DynamicTypeSize.medium
以下は文字サイズが変わらないように設定されていたりします。
全てのテキストスタイルの拡大率を同じにするのも良いですが、
元々小さい文字が小さくなりすぎないようにするなどのちょっとした配慮がアプリの品質を高めてくれます。
そのため、カスタムテキストスタイルにもそれぞれの用途に合った拡大率を設定してあげるのが良いでしょう。
設定方法としては標準の Font.TextStyle
を使用します。
どの Font.TextStyle
をあてるべきか迷う場合もありますが、基本的にはデフォルトサイズが近いものを選ぶのが良いと思います。
extension MyTextStyle {
var relativeStyle: Font.TextStyle {
switch self {
case .title:
return .title
case .body:
return .body
case .subheadline:
return .subheadline
case .caption:
return .caption
}
}
}
4. ViewModifier を定義する
MyTextStyle
で定義したパラメータを実際にフォント設定として View
に適用するための ViewModifier
を用意します。
struct ScaledFontModifier: ViewModifier {
@ScaledMetric var scale: Double
init(_ style: MyTextStyle) {
self._scale = .init(
wrappedValue: style.defaultSize,
relativeTo: style.relativeStyle
)
}
func body(content: Content) -> some View {
content
.font(.system(size: scale))
}
}
ここで本命の ScaledMetric
が登場します。
ScaledMetric.init(wrappedValue:relativeTo:)
の引数は
- wrappedValue ... デフォルトのフォントサイズ
- relativeTo ... 文字の拡大率をどの Font.TextStyle に合わせるか
となっています。
ScaledMetric
について補足
1. ViewModifier
の実装解説
ScaledMetric
は property wrapper なので ScaledMetric
の wrappedValue
の型(今回は Double)の stored property として定義します。
初期化時に self._scale
としてアクセスしていますがこれは property wrapper の仕様の一つで、アンダースコアをつけると本体の struct ScaledMetric
にアクセスすることができます。
逆にアンダースコアをつけない場合は wrappedValue
に直接アクセスすることができます。
今回は ScaledMetric.init(wrappedValue:relativeTo:)
が肝なので self._scale
としてアクセスしています。
2. @ScaledMetric
の再描画の仕組み
内部はブラックボックス化されているのであくまでも推測ですが、
@ScaledMetric
は @Environment
と同じ仕組みで
EnvironmentValues
の DynamicTypeSize
(or SizeCategory
)を参照しています。
そして DynamicTypeSize
変更時に自身が定義されている View
や ViewModifier
を再描画させています。
はじめに UIFontMetrics
はプレビューの Dynamic Type 設定が反映されないと書きましたが、上記の仕組みが関係しています。
おそらくプレビューは EnvironmentValues
として Dynamic Type を設定しており、端末の設定には一切関与していません。
逆に UIFontMetrics
は端末の設定を参照しEnvironmentValues
を参照していないため、プレビューのパラメータ変更が反映されないのでしょう。
呼び出し用のメソッドを View extension
に定義します。
extension View {
func scaledFont(_ style: MyTextStyle) -> some View {
modifier(ScaledFontModifier(style))
}
}
// 使用時
Text("hogehoge")
.scaledFont(.title)
「Dynamic Type に対応したカスタムテキストスタイルの実装」という点ではこの状態で十分なのですが、実際に使用する際は一緒に Weight も指定したくなります。
以降で指定方法を紹介します。
ただしあくまでも一例であり、それぞれ好みの使用感を追求するのが良いと思います。
5. Weight を指定できるようにする
MyTextStyle
の associatedValue
として Weight
を持たせるのがおすすめです。
デフォルト値を設定できるという点と、.scaledFont(.body(.bold))
のように直感的に使用することができるという点がポイントです。
enum MyTextStyle {
// associatedValue に Font.Weight を追加
// デフォルト値も設定する
case title(Font.Weight = .bold)
case body(Font.Weight = .regular)
case subheadline(Font.Weight = .regular)
case caption(Font.Weight = .regular)
}
extension MyTextStyle {
// Font.Weight を MyTextStyle から取得する処理を追加
var weight: Font.Weight {
switch self {
case .title(let weight),
.body(let weight),
.subheadline(let weight),
.caption(let weight):
return weight
}
}
}
struct ScaledFontModifier: ViewModifier {
@ScaledMetric var scale: Double
// weight をプロパティとして追加
var weight: Font.Weight
init(_ style: MyTextStyle) {
self._scale = .init(
wrappedValue: style.defaultSize,
relativeTo: style.relativeStyle
)
// style から weight を取得して self.weight に設定する
self.weight = style.weight
}
func body(content: Content) -> some View {
content
// 引数の weight に値を設定
.font(.system(size: scale, weight: weight))
}
}
// 使用時
// style: body, weight: regular
Text("hogehoge")
.scaledFont(.body())
// style: body, weight: bold
Text("hogehoge")
.scaledFont(.body(.bold))
まとめ
ここまでのコードの全貌はこんな感じです↓
enum MyTextStyle {
case title(Font.Weight = .bold)
case body(Font.Weight = .regular)
case subheadline(Font.Weight = .regular)
case caption(Font.Weight = .regular)
}
private extension MyTextStyle {
var defaultSize: Double {
switch self {
case .title:
return 21
case .body:
return 17
case .subheadline:
return 15
case .caption:
return 12
}
}
var relativeStyle: Font.TextStyle {
switch self {
case .title:
return .title
case .body:
return .body
case .subheadline:
return .subheadline
case .caption:
return .caption
}
}
var weight: Font.Weight {
switch self {
case .title(let weight),
.body(let weight),
.subheadline(let weight),
.caption(let weight):
return weight
}
}
}
private struct ScaledFontModifier: ViewModifier {
@ScaledMetric var scale: Double
var weight: Font.Weight
init(_ style: MyTextStyle) {
self._scale = .init(
wrappedValue: style.defaultSize,
relativeTo: style.relativeStyle
)
self.weight = style.weight
}
func body(content: Content) -> some View {
content
.font(.system(size: scale, weight: weight))
}
}
extension View {
func scaledFont(_ style: MyTextStyle) -> some View {
modifier(ScaledFontModifier(style))
}
}
プレビューで見てみましょう。
いい感じにプレビューでも反映できていますね。
AX3 あたりから title より body の方が大きくなるのが面白いですね。
この倍率にした理由が気になる
おまけ
ScaledMetric
はカスタムのフォントファミリーの Dynamic Type 対応にも使用できます。
ここでは説明は省きますが、カスタムフォントファミリー付きの実装例も GitHub にアップロードしています。
ぜひ参考にしてください。