こんにちは、スマートバンクでアプリエンジニアをしているロクネムです。
弊社ではデザインシステムを導入しており、ColorやTypographyなどのデザイントークンやコンポーネントライブラリをiOSアプリの実装でも利用しています。 導入してから約1年ほど経ちますが、 B/43という家計簿プリカアプリ の開発を通じて、SwiftUIで1画面を実装する上でかかる時間がかなり短くなったように感じます。
本記事ではそんなデザインシステムを活用した開発の中でも Typography にフォーカスし、B/43のiOSアプリにおいてどのようにTypographyを実装しているのかについてご紹介します。
Typographyのデザイントークン
デザイントークンとは、色、タイポグラフィ、サイズ、不透明度、影などのデザインをするための最小要素のことです。
B/43のデザインシステムにおいてもこのデザイントークンは定義しており、その設計の詳細については下記の putchom san のブログにて語られています。
B/43のデザイントークンの定義において、Typographyの型はW3CのDesign Tokens Format Moduleに従って定義しており、以下のpropertyを持ちます。
fontFamily
: Typographyのフォント。fontSize
: Typographyのサイズ。fontWeight
: Typographyの太さ。letterSpacing
: Typographyの水平方向の文字間隔。lineHeight
: Typographyの垂直方向の行間隔。
// 💭 B/43におけるデザイントークンの定義イメージ { "typography": { "body": { "small": { "fontSize": "{fontSize.extraSmall}", "fontWeight": "{fontWeight.normal}", "lineHeight": "{lineHeight.normal.extraSmall}", }, "medium": {...}, "large": {...} }, "display": {...}, ... } }
B/43のiOSアプリの実装においては、 fontFamily
と letterSpacing
についてはシステムのデフォルトを使用することとしています。
Typographyの実装
ここからは、Typographyの fontSize
, fontWeight
そして lineHeight
をどう定義していくかというところを考えていきます。
fontSize
, fontWeight
の実装
各デザイントークンの fontSize
, fontWeight
の実装は以下のようになります。
public enum B43Typography { // ✅ デザインシステムで規定されたデザイントークンを定義 case bodySmall, bodyMedium, bodyLarge case displaySmall, displayMedium, displayLarge ... // ✅ 各デザイントークンに対応するsizeを返す public var fontSize: CGFloat { switch self { case .bodySmall: B43ReferenceFontSizeToken.extraSmall case .bodyMedium: B43ReferenceFontSizeToken.medium case .bodyLarge: B43ReferenceFontSizeToken.extraLarge case .displaySmall: B43ReferenceFontSizeToken.twoExtraLarge ... } } // ✅ 各デザイントークンに対応するweightを返す public var fontWeight: UIFont.Weight { switch self { case .bodySmall, .bodyMedium, .bodyLarge: .regular case .displaySmall, .displayMedium, .displayLarge: .bold ... } } } public extension B43Typography { // ✅ 各Typographyに対応するsize, weightを持つFontを返す var font: UIFont { // 📝 Font FamilyにはSystem Fontを指定 .systemFont(ofSize: fontSize, weight: fontWeight) } }
fontSize
等の数値についてはStyle Dictionaryにて生成されたコードを参照するようにしています。
この B43Typography
をSwiftUIのTextに適用できるようにModifierを定義します。
public struct TypographyModifier: ViewModifier { public let typography: B43Typography public func body(content: Content) -> some View { content .font(Font(typography.font)) } } public extension View { func typography(_ typography: B43Typography) -> some View { modifier(TypographyModifier(typography: typography)) } }
lineHeight
の実装
続いて、上記実装にて lineHeight
を考慮できるようにしていきます。
まずは B43Typography
に lineHeight
のpropertyを追加します。
public enum B43Typography { ... // ✅ 各デザイントークンに対応するline heightを返す public var lineHeight: CGFloat { switch self { case .bodySmall: B43ReferenceLineHeightToken.normalExtraSmall case .bodyMedium: B43ReferenceLineHeightToken.normalMedium case .bodyLarge: B43ReferenceLineHeightToken.normalExtraLarge case .displaySmall: B43ReferenceLineHeightToken.denseTwoExtraLarge ... } } }
次に、この lineHeight
をSwiftUIのTextに反映できるようにしていきます。
SwiftUIでは lineSpacing Modifierを用いて行間隔を指定することが可能です。
B43Typography
にてこの lineSpacing
を返すpropertyを定義します。
public extension B43Typography { ... // ✅ 各Typographyに対応するline heightを元に行間を返す var lineSpacing: CGFloat { // 📝 Typographyの期待するlineHeightからUIFont自身の持つlineHeightを引いた値を返す lineHeight - font.lineHeight } }
そして、 TypographyModifier
から参照させます。
public struct TypographyModifier: ViewModifier {
public let typography: B43Typography
public func body(content: Content) -> some View {
content
.font(Font(typography.font))
+ .lineSpacing(typography.lineSpacing)
}
}
これにより行間がTypgraphyごとに指定されるようになりました。
しかし、Textの上部と下部の余白を指定できていないため、Line Heightを完全に反映できていません。
足りない上下の余白分は TypographyModifier
にて垂直方向にpaddingを付与してあげましょう。
public struct TypographyModifier: ViewModifier {
public let typography: B43Typography
public func body(content: Content) -> some View {
content
.font(Font(typography.font))
.lineSpacing(typography.lineSpacing)
+ .padding(.vertical, typography.lineSpacing / 2.0)
}
}
ここまでで、エンジニアはFigmaを見て指定されたTypographyと同じものを実装で指定すれば、同様の見た目を再現できるようになりました。
struct HomeScreen: View { ... var body: some View { ... Text(...) .typography(.bodySmall) ... } }
よって、厳密にFigmaと見た目を揃える場合は UIデザイナーに必要なiOSのTypographyの知識 の記事にあるように1ptの小ささを考慮したテキストスタイルを定義するなど、運用の工夫が必要となります。
Dynamic Type
続いて、このTypographyの実装をDynamic Typeに対応させていきます。
Dynamic Typeとは
Dynamic Typeとは、OSの設定からテキストサイズを変更するとアプリ上のテキストもそれに合わせてサイズが調整されるようにする機能です。
Default | A11y Three Extra Large |
---|---|
iOSでは、System FontのWeight, Size, Leadingの属性のセットを表すText Styleがいくつか定義されており、このText Styleを用いて実装するとDynamic Typeに対応したテキストが表示されます。
Style | Weight | Size (points) | Leading (points) |
---|---|---|---|
Large Title | Regular | 34 | 41 |
Title 1 | Regular | 28 | 34 |
Title 2 | Regular | 22 | 28 |
Title 3 | Regular | 20 | 25 |
Headline | Semibold | 17 | 22 |
Body | Regular | 17 | 22 |
Callout | Regular | 16 | 21 |
Subhead | Regular | 15 | 20 |
Footnote | Regular | 13 | 18 |
Caption 1 | Regular | 12 | 16 |
Caption 2 | Regular | 11 | 13 |
Text("Body Text Style") .font(.body) Text("Title 1 Text Style") .font(.title1)
テキストサイズを変更するとText Styleの適用されたTextは拡大縮小されます。
実はこの拡大縮小率はText Styleによって異なります。
例えば、もともとサイズの大きいTitle 1は拡大縮小率は低く、それに比べてBodyは拡大縮小率が高く設定されています。
また、Caption 2のような既に非常に小さいText Styleの場合は縮小率がかなり低く設定されています。
Dynamic Type対応
このDynamic TypeにデザインシステムのTypographyの実装を対応させるためには UIFontMetrics.scaledFont(for:) メソッドを使用します。
このメソッドに対してDynamic Typeに対応させたいUIFontを渡すことで、テキストサイズの変更に応じて拡大縮小されたUIFontを取得できます。
public extension B43Typography { var font: UIFont { - .systemFont(ofSize: fontSize, weight: fontWeight) + UIFontMetrics.default + .scaledFont(for: .systemFont(ofSize: fontSize, weight: fontWeight)) } ... }
UIFontMetrics.default
は TextStyleのBodyに対応したFont Metricsを返します。
つまり、上記実装ではBodyと同じ倍率でテキストのサイズが拡大縮小されます。
本来的にはデザインシステムのTypographyの持つサイズに近いTextStyleと同じ倍率で拡大縮小されるように設定するのが正しいです。
よって、まずはTypographyの持つサイズに近いTextStyleを返すpropertyを追加します。
public enum B43Typography { ... // ✅ 各デザイントークンに対応するTextStyleを返す public var fontMetricsTextStyle: UIFont.TextStyle { switch self { case .bodySmall: .footnote case .bodyMedium: .callout case .bodyLarge: .title2 case .displaySmall: .title1 ... } } }
そして、この値を UIFontMetrics
の初期化時に指定します。
public extension B43Typography { var font: UIFont { - UIFontMetrics.default + UIFontMetrics(forTextStyle: fontMetricsTextStyle) + .scaledFont(for: .systemFont(ofSize: fontSize, weight: fontWeight)) } ... }
以上で、Typographyの持つサイズに近いTextStyleと同じ倍率でフォントが拡大縮小されるようになりました。
Line Heightについても同様にDynamic Typeが適用されるよう修正しておきます。
public extension B43Typography { ... var lineSpacing: CGFloat { - lineHeight - font.lineHeight + let lineHeight = UIFontMetrics(forTextStyle: fontMetricsTextStyle) + .scaledValue(for: lineHeight) + return lineHeight - font.lineHeight } ... }
最後に、テキストサイズの変更を検知して動的に更新が反映されるように、 TypographyModifier
にて @Environment(.dynamicTypeSize) を宣言します。
public struct TypographyModifier: ViewModifier {
+ @Environment(\.dynamicTypeSize) private var dynamicTypeSize
public let typography: B43Typography
public func body(content: Content) -> some View { ... }
}
@Environment(\.dynamicTypeSize)
の値をViewのbodyから参照することはありませんが、宣言しておくことでテキストサイズの変更を検知して動的にフォントを拡大縮小してくれるようになります。この宣言がなければ、該当のViewが再描画されない限りはフォントサイズは更新されません。
まとめ
以上が、B/43のiOSアプリにおけるTypographyの実装方針でした。
B/43では、アプリ起動時に preferredContentSizeCategory を取得して計測しており、Dynamic Typeの設定サイズごとのユーザー数を以下のように可視化しています。
デフォルトのLargeを設定しているユーザーは全体の約70%、デフォルトより小さいサイズを指定している方は約25%、残りの約5%がデフォルトより大きいサイズを指定していました。
Dynamic Typeは、より大きなサイズのテキストの方が読みやすいというユーザーにとっての助けになることはもちろんのこと、テキストのサイズを通常より小さく表示することで、小さいサイズのテキストを読むことができるユーザーはより多くの情報を画面に表示することが可能とします。
本記事が、そんなDynamic Typeに対応したTypographyの実装を進める上での参考になれば幸いです。
最後に
スマートバンクでは一緒にB/43のアプリを開発していくメンバーを募集しています! カジュアル面談も受け付けていますので、ぜひお気軽にご応募ください💪