第1話 サイズ違いのTextを下揃えで並べるとずれる!?
次のようの要件で画面をSwiftUIで組んだ際のお話です。
要件 ~ストップウォッチ風~
- 小数点以下第二位まで表示。
- 小数点以下を少し小さく。
脳筋で並べるとこんな感じ。
HStack(alignment: .bottom, spacing: 0) {
Text("22")
.font(.system(size: 200))
.monospacedDigit()
Text(".22")
.font(.system(size: 160))
.monospacedDigit()
}
そうすると上のスクリーンショットのように、下が揃いません💦
どうやら、Textの下
と数字の下
とは別のようです。
じゃ 下を合わせる とはいかに、、、
Fontの構造を知ると解決できたので、詳しく紹介していきます。
Fontの構造
下記のようにFontは様々な要素で構成されています。
UIFontを使うと必要な値が取得できる。
UIFontには各値を取得できるプロパティが用意されていて主に下記の値を見ていくと理想のレイアウトが組めそうです。
※2023/3現在はSwiftUI.Fontからは取得できなさそうです。
変数名 | 説明 |
---|---|
pointSize | いわゆるfontSize |
lineHeight | UILabelやTextのheightに相当 |
ascender | Baselineから上 |
capHeight | ローマ字の大文字の高さ |
xHeight | ローマ字の小文字のメインの高さ |
descender | Baselineから下 |
※注意: 右側の説明はただの私の脳内翻訳なので、適切な表現ではないかも。
上記の値でズレがどうなっているのか確認してみた。
- 元の2つのTextに対してUIFontを使ってFont指定。
- 各プロパティを視覚的に捉えるためのViewを並べてみる。
要件にあった実装方針が見えてきました。
- 下を合わせたい とは Baselineを揃えたい ことだった。
- 今回は
descender
を使ったoffset.y
の操作でうまくいきそう。 - 大きさの比率に明確な指定がない場合は .22 の
capHeight
が 22 のxHeight
にあたる高さになると良さそう。(おおよそローマ字の大文字と小文字の関係)
最終形
let mainFont: UIFont = .systemFont(ofSize: 200)
var subFont: UIFont {
.systemFont(ofSize: mainFont.xHeight * mainFont.pointSize / mainFont.capHeight)
}
var body: some View {
HStack(alignment: .bottom, spacing: 0) {
Text("22")
.font(.init(mainFont))
.monospacedDigit()
.offset(y: -mainFont.descender) // mainFont.descender分下げる
Text(".22")
.font(.init(subFont))
.monospacedDigit()
.offset(y: -subFont.descender) // subFont.descender分下げる
}
.offset(y: mainFont.descender) // 全体的にmainFont.descender分上げる
}
綺麗に下が合いました♪
[追記: 2022/3/13] HStack(alignment: .lastTextBaseline)で実現可能!!
twitterにてもっといい方法を教えていただきました。
この例だとHStack(alignment: .lastTextBaseline)としても揃えられますよ
— せみさぎ (@semisagi) March 13, 2023
VerticalAlignment
にはfirstTextBaselineとlastTextBaseline
Baselineを揃えるだけならこちらの方法を使うとSwiftUIのみでの実装が可能でした!!
これを使うと
HStack(alignment: .lastTextBaseline, spacing: 0) {
Text("22")
.font(.system(size: 200))
.monospacedDigit()
Text(".22")
.font(.system(size: 160))
.monospacedDigit()
}
これでも実現できてしまう!!
せみさぎさんに感謝です🙏
第2話 Figma上で指定したlineHeightの値を使うと大きさが合わない!?
せっかく正確な値でデザインしてもらってもSwiftUIでそのまま値を使うとうまくいきませんでした。
そもそもSwiftUIではlineHeightを指定することはできず、lineSpacingを設定することになります。
SwiftUI.Text.lineSpaceとは
ある行の下端と次の行の上端の間のスペースの量 (ポイント単位)
参考元
https://developer.apple.com/documentation/swiftui/view/linespacing(_:)
またも脳筋でそのまま計算するとこうなりそう。
Figma | SwiftUI |
---|---|
Size: 200 | Size: 200 |
Line height: 240 | lineSpacing: 240 - 200 = 40 |
そうすると下記のようにずれます。
青・・・Figma
黒・・・SwiftUI.Text
この差は元のlineHeightがpointSizeとは違うことを考慮していなかったため起きていたずれでした。
lineHeight
> pointSize
なので
lineHeight
+ 40 > pointSize
+ 40(FigmaのlineHeight: 240)こうなってしまうのでした。
ではFigmaのlineHeightを正しく設定するにはどうするのか。
ここでもUIFontの登場です。
UIFontで元のlineHeightからの差分をlineSpacingにする。
これが正解です。
lineSpacing = Figma.lineHeight - UIFont.lineHeight
綺麗に隙間があいました♪
だがそれだけじゃない
lineSpacingは行間・・・二行だとlineSpacing1つ分
lineHeightは行高・・・各行の上下にlineSpacing1つ分、合計2つ分
つまりleneHeightを設定する場合は行間 + 上下のpadding も増やす必要があります。
特に単行の場合はlineSpcingを設定しても何も変化はありませんが、lineHeightを設定すると全体の高さが変わリます。
下図のように、単行・複行に限らず上下のpaddingによる拡張が必要だとわかります。
結論
let lineHeight: CGFloat = 240 // figmaのLine height
let uiFont: UIFont = .systemFont(ofSize: 200)
var lineSpacing: CGFloat { lineHeight - uiFont.lineHeight }
var body: some View {
Text("Happy\nHappy")
.font(.init(uiFont))
.lineSpacing(lineSpacing)
.padding(.vertical, lineSpacing/2)
}
以上!!
現状はUIFontでしか、こうした微調整ができません。
今後SwiftUI.FontやTextに生えるメソッドを使って同じような調整ができるようになって欲しいですね。
参考記事
https://developer.apple.com/library/archive/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/CustomTextProcessing/CustomTextProcessing.html
http://akisute.com/2016/09/ios.html
http://blog.eppz.eu/uilabel-line-height-letter-spacing-and-more-uilabel-typography-extensions/