Is there a way to force a SwiftUI Text
element to have the height of the actual used characters instead of the font height?
Example:
var body: some View {
VStack {
Text("Week")
.font(.title2)
.foregroundStyle(.secondary)
.border(.red)
Text("44")
.font(.system(size: 200))
.minimumScaleFactor(0.1)
//.padding(.vertical, -20)
.border(.red)
}
}
This will look like this:
I would really like the text "Week" to be close to the large text "44". I can get there with a negative padding but that will not automatically adjust for changes in view size and will require manual tuning for all sizes.
The answer to the post Aligning SwiftUI text to the baseline and top of another text has a useful explanation of the way a font is sized. You can get this information from an instance of UIFont
.
It sounds like you want to set the height of the Text
to the capHeight
of the font. However, you also need to disable scaling-down by commenting out the modifier .minimumScaleFactor
, otherwise when you set a smaller frame then the font shrinks.
private func heightForFontSize(size: CGFloat) -> CGFloat {
let font = UIFont.systemFont(ofSize: size)
return font.capHeight
}
var body: some View {
VStack {
Text("Week")
.font(.title2)
.foregroundStyle(.secondary)
.border(.red)
Text("44")
.font(.system(size: 200))
// .minimumScaleFactor(0.1)
.frame(height: heightForFontSize(size: 200))
.border(.red)
}
}
The gap could be eliminated completely by using spacing: 0
for your VStack
.
Another way to approach the problem would be to fix the height of the text and then use a font that exactly fits the height. The answer to How to make text fill the height of the container in SwiftUI shows how this is done.
This approach could be used to fit the font to the size that .minimumScaleFactor
was giving you in your original code. The footprint for the text can be established using a hidden version of the scaled text, then show the actual text in an overlay in a larger font.
I have borrowed the function fontForHeight
from the other answer to show this working (it was my answer):
private func fontForHeight(targetHeight: CGFloat) -> Font {
let approxFontSize = targetHeight * 1.4
let refFont = UIFont.systemFont(ofSize: approxFontSize)
let computedFontSize = (targetHeight / refFont.capHeight) * approxFontSize
let tweakedFontSize = computedFontSize - (targetHeight * 0.05)
return Font(UIFont.systemFont(ofSize: tweakedFontSize))
}
var body: some View {
VStack {
Text("Week")
.font(.title2)
.foregroundStyle(.secondary)
.border(.red)
Text("44")
.font(.system(size: 200))
.minimumScaleFactor(0.1)
.hidden()
.overlay {
GeometryReader { proxy in
Text("44")
.font(fontForHeight(targetHeight: proxy.size.height))
.lineLimit(1)
.fixedSize()
.frame(width: proxy.size.width, height: proxy.size.height)
}
}
.border(.red)
}
}
As you can see, the danger of this approach is that the larger font might be too wide for the available width.
EDIT Following from your comment, here is another possible approach. You were saying, it is important that the font can be scaled down to fit the available space. So I would suggest, you find the font size that matches the scaled font and then show this with its height constrained to capHeight
.
This will leave some height unused, so there would be space to show the "Week" label in the same overlay. This way, the final result will be vertically centered.
Like this:
private func uiFontForLineHeight(height: CGFloat) -> UIFont {
let approxFontSize = height
let refFont = UIFont.systemFont(ofSize: approxFontSize)
let computedFontSize = (height / refFont.lineHeight) * approxFontSize
return UIFont.systemFont(ofSize: computedFontSize)
}
var body: some View {
VStack {
Text("44")
.font(.system(size: 200))
.lineLimit(1)
.minimumScaleFactor(0.1)
.hidden()
.overlay {
GeometryReader { proxy in
let uiFont = uiFontForLineHeight(height: proxy.size.height)
VStack(spacing: 0) {
Text("Week")
.font(.title2)
.foregroundStyle(.secondary)
.border(.red)
Text("44")
.font(Font(uiFont))
.lineLimit(1)
.fixedSize()
.frame(height: uiFont.capHeight)
}
.frame(height: proxy.size.height)
}
}
.border(.red)
}
}