-
Notifications
You must be signed in to change notification settings - Fork 5
Spec: Text
"A view that displays one or more lines of read-only text."
https://developer.apple.com/documentation/swiftui/text/
The goal of the Flutter Text widget is to be a near pixel-perfect replica of SwiftUI's Text view. For typography that means that the font, size, text decorations, and alignments should be visually indistinguishable from what Swift UI gives.
To familiarize yourself with typography in Apple, view the following resources:
As you implement the API, take into account the following topics:
If no font is specified, the Text widget should use the default system font. On iOS this font is determined by the environment. Currently the default is San Francisco but this comes in different families:
- SF Pro
- SF Pro Rounded
- SF Compact
- SF Compact Rounded
- SF Mono
The family that is used is different on different device types. For example, the Pro fonts are used on iOS, iPadOS, macOS, and tvOS. However, watchOS uses the Compact fonts by default. The Flutter Text widget must display the correct font family for every device and platform when no custom font is specified by the developer.
Apple has many different system fonts that come built into to the OS. These vary depending on the platform and version. The complete list is available on the System Fonts page.
To select one of these fonts, use the Font.custom parameter on font:
Text("Arial").font(.custom("Arial", size: 30))
Text("Chalkduster").font(.custom("Chalkduster", size: 30))
Text("Party LET").font(.custom("Party LET", size: 30))
Text("Snell Roundhand").font(.custom("Snell Roundhand", size: 30))
Text("Zapfino").font(.custom("Zapfino", size: 30))
You can also include one of your own fonts in the app bundle. You would use it by referring to the name the same way as shown above.
Font size is measured in points where points are a pixel density independent value. That is, a specific point size should have roughly the same physical size regardless of the device's screen pixel density.
In SwiftUI, you set the font size like so:
Text("size 6").font(.system(size: 6))
Text("size 7").font(.system(size: 7))
Text("size 8").font(.system(size: 8))
Text("size 9").font(.system(size: 9))
Text("size 10").font(.system(size: 10))
Text("size 11").font(.system(size: 11))
Text("size 12").font(.system(size: 12))
Text("size 13").font(.system(size: 13))
Text("size 14").font(.system(size: 14))
Text("size 15").font(.system(size: 15))
Text("size 16").font(.system(size: 16))
Text("size 17").font(.system(size: 17))
Text("size 18").font(.system(size: 18))
Text("size 19").font(.system(size: 19))
Text("size 20").font(.system(size: 20))
Text("size 21").font(.system(size: 21))
Text("size 22").font(.system(size: 22))
Text("size 23").font(.system(size: 23))
Text("size 24").font(.system(size: 24))
Text("size 25").font(.system(size: 25))
Text("size 26").font(.system(size: 26))
Text("size 27").font(.system(size: 27))
Text("size 28").font(.system(size: 28))
Text("size 29").font(.system(size: 29))
Text("size 30").font(.system(size: 30))
Text("size 31").font(.system(size: 31))
Text("size 32").font(.system(size: 32))
Text("size 33").font(.system(size: 33))
Text("size 34").font(.system(size: 34))
Text("size 35").font(.system(size: 35))
Text("size 36").font(.system(size: 36))
Text("size 37").font(.system(size: 37))
Text("size 38").font(.system(size: 38))
Text("size 39").font(.system(size: 39))
Text("size 40").font(.system(size: 40))
Text("size 41").font(.system(size: 41))
Text("size 42").font(.system(size: 42))
Text("size 43").font(.system(size: 43))
Text("size 44").font(.system(size: 44))
Text("size 45").font(.system(size: 45))
Text("size 46").font(.system(size: 46))
Text("size 47").font(.system(size: 47))
Text("size 48").font(.system(size: 48))
Text("size 49").font(.system(size: 49))
Text("size 50").font(.system(size: 50))
Text("size 51").font(.system(size: 51))
Text("size 52").font(.system(size: 52))
Text("size 53").font(.system(size: 53))
Text("size 54").font(.system(size: 54))
Text("size 56").font(.system(size: 56))
Text("size 58").font(.system(size: 58))
Text("size 60").font(.system(size: 60))
Text("size 62").font(.system(size: 62))
Text("size 64").font(.system(size: 64))
Text("size 66").font(.system(size: 66))
Text("size 68").font(.system(size: 68))
Text("size 70").font(.system(size: 70))
Text("size 72").font(.system(size: 72))
Text("size 76").font(.system(size: 76))
Text("size 80").font(.system(size: 80))
Text("size 84").font(.system(size: 84))
Text("size 88").font(.system(size: 88))
Text("size 92").font(.system(size: 92))
Text("size 96").font(.system(size: 96))Flutter also has the notion of pixel density independent font sizes. However, it shouldn't be assumed that SwiftUI point sizes and Flutter fonts size values are the same. If any conversion needs to happen, this SwiftUI port should ensure that the size values produce the same sized font on the screen.
Fonts have the following weights defined in order of increasing thickness:
ultraLightthinlightregularmediumsemiboldboldheavyblack
They are implemented in SwiftUI using fontWeight like so:
Text("ultraLight").fontWeight(.ultraLight)
Text("thin").fontWeight(.thin)
Text("light").fontWeight(.light)
Text("regular").fontWeight(.regular)
Text("medium").fontWeight(.medium)
Text("semibold").fontWeight(.semibold)
Text("bold").fontWeight(.bold)
Text("heavy").fontWeight(.heavy)
Text("black").fontWeight(.black)Or by selecting the weight parameter on the font:
Text("ultraLight").font(.system(size: 30, weight: .ultraLight))
Text("thin").font(.system(size: 30, weight: .thin))
Text("light").font(.system(size: 30, weight: .light))
Text("regular").font(.system(size: 30, weight: .regular))
Text("medium").font(.system(size: 30, weight: .medium))
Text("semibold").font(.system(size: 30, weight: .semibold))
Text("bold").font(.system(size: 30, weight: .bold))
Text("heavy").font(.system(size: 30, weight: .heavy))
Text("black").font(.system(size: 30, weight: .black))There are four choices for font design:
-
default: SF Pro font. -
rounded: SF Pro Rounded font. -
serif: New York font. -
monospaced: SF mono font.
They are implemented in SwiftUI using fontDesign:
Text("default").fontDesign(.default)
Text("rounded").fontDesign(.rounded)
Text("serif").fontDesign(.serif)
Text("monospaced").fontDesign(.monospaced)Or, alternatively, using the design parameter of the font:
Text("default").font(.system(size: 30, design: .default))
Text("rounded").font(.system(size: 30, design: .rounded))
Text("serif").font(.system(size: 30, design: .serif))
Text("monospaced").font(.system(size: 30, design: .monospaced))Fonts have the following widths defined in order of increasing thickness. The height for all of these is the same.
compressedcondensedstandardexpanded
They are implemented in SwiftUI like so:
Text("compressed").fontWidth(.compressed)
Text("condensed").fontWidth(.condensed)
Text("standard").fontWidth(.standard)
Text("expanded").fontWidth(.expanded)Text.scale does not allow you to set an arbitrary scale but rather two logical scales:
defaultsecondary
They can be applied like so:
Text("default").textScale(.default)
Text("secondary").textScale(.secondary)The following styles are available to text:
bolditalic-
monospaced: All characters have the same width. -
monospacedDigit: Digits have the same width while other characters use the default font. -
textcase: Text is rendered as all uppercase or lowercase regardless of the case of the underlying string.
These are implemented in SwiftUI like so:
Text("bold").bold()
Text("italic").italic()
Text("monospaced").monospaced()
Text("monospacedDigit 123").monospacedDigit()
Text("textCase uppercase").textCase(.uppercase)
Text("textCase lowercase").textCase(.lowercase)There are two different decorations that you can add to text:
-
underlined: Users can select thecolorandpatternof the underline decoration. -
strikethrough: Users can select thecolorandpatternof the strikethrough decoration.
Underline is implemented like this:
Text("underline").underline()
Text("red").underline(color: .red)
Text("solid").underline(pattern: .solid)
Text("dot").underline(pattern: .dot)
Text("dash").underline(pattern: .dash)
Text("dashDot").underline(pattern: .dashDot)
Text("dashDotDot").underline(pattern: .dashDotDot)
And strikethrough like this:
Text("strikethrough").strikethrough()
Text("red").strikethrough(color: .red)
Text("solid").strikethrough(pattern: .solid)
Text("dot").strikethrough(pattern: .dot)
Text("dash").strikethrough(pattern: .dash)
Text("dashDot").strikethrough(pattern: .dashDot)
Text("dashDotDot").strikethrough(pattern: .dashDotDot)
Note: Shadow is not currently implemented in SwiftUI, but will likely be implemented in the future. We could preemptively implement it in Flutter.
You can set the text foreground style using foregroundStyle. This takes a ShapeStyle, which can be a system color:
Text("black").foregroundStyle(.black)
Text("blue").foregroundStyle(.blue)
Text("brown").foregroundStyle(.brown)
Text("cyan").foregroundStyle(.cyan)
Text("gray").foregroundStyle(.gray)
Text("green").foregroundStyle(.green)
Text("indigo").foregroundStyle(.indigo)
Text("mint").foregroundStyle(.mint)
Text("orange").foregroundStyle(.orange)
Text("pink").foregroundStyle(.pink)
Text("purple").foregroundStyle(.purple)
Text("red").foregroundStyle(.red)
Text("teal").foregroundStyle(.teal)
Text("white").foregroundStyle(.white)
Text("clear").foregroundStyle(.clear)Or a gradient:
Text("linearGradient")
.foregroundStyle(.linearGradient(
colors: [.red, .blue, .green, .yellow],
startPoint: .leading,
endPoint: .trailing
))
Text("AngularGradient")
.foregroundStyle(AngularGradient(
colors: [.red, .blue, .green, .yellow],
center: .center
))
Text("conicGradient")
.foregroundStyle(.conicGradient(
colors: [.red, .blue, .green, .yellow],
center: .center
))
Text("ellipticalGradient")
.foregroundStyle(.ellipticalGradient(
colors: [.red, .blue, .green, .yellow]
))
Text("RadialGradient")
.foregroundStyle(RadialGradient(
colors: [.red, .blue, .green, .yellow],
center: .center,
startRadius: 30,
endRadius: 100
))Or a material:
VStack(alignment: .leading) {
Text("ultraThinMaterial").foregroundStyle(.ultraThinMaterial)
Text("thinMaterial").foregroundStyle(.thinMaterial)
Text("regularMaterial").foregroundStyle(.regularMaterial)
Text("thickMaterial").foregroundStyle(.thickMaterial)
Text("ultraThickMaterial").foregroundStyle(.ultraThickMaterial)
}.background(.blue)You can also apply other effects like filling the text with an image. This section does not cover all of the possibilities for ShapeStyle.
The background modifier also takes a ShapeStyle:
Text("background color").background(.yellow)
Text("background gradient").background(.linearGradient(
colors: [.red, .blue],
startPoint: .leading,
endPoint: .trailing
))There are many more options for background, but this goes beyond the scope of typography.
SwiftUI predefines a list of text styles for various purposes within an app:
largeTitletitletitle2title3headlinebodycalloutsubheadlinefootnotecaption1caption2
They can be applied to a Text view like so:
Text("largeTitle").font(.largeTitle)
Text("title").font(.title)
Text("title2").font(.title2)
Text("title3").font(.title3)
Text("headline").font(.headline)
Text("body").font(.body)
Text("callout").font(.callout)
Text("subheadline").font(.subheadline)
Text("footnote").font(.footnote)
Text("caption").font(.caption)
Text("caption2").font(.caption2)Each of those styles are built from specific values for the following three components:
- Weight
- Size (points)
- Leading (points)
The values for weight, size, and leading vary depending on the following contexts:
- Dynamic Type (xSmall, Small, Medium, Large, xLarge, xxLarge, xxxLarge)
- Accessibility (AX) level (AX1, AX2, AX3, AX4, AX5)
- Platform (iOS, ipadOS, macOS, tvOS, watchOS)
The specific numerical values are provided in the SwiftUI typography specs. Flutter needs to match all of these.
Dynamic Type refers to text that is scaled within the based on user choice in the device's (not the app's) system settings. Dynamic type is not available on macOS, but on iOS it is available by going to Settings > Display & Brightness > Text Size. This gives you access to seven sizes, which in the documentation are known as:
- xSmall
- Small
- Medium
- Large
- xLarge
- xxLarge
- xxxLarge
You can gain access to five additional sizes by going to Settings > Accessibility > Display & Text Size > Larger Text > Larger Accessibility Sizes. These sizes are referred to in the documentation as:
- AX1
- AX2
- AX3
- AX4
- AX5
Dynamic Type affects all of the predefined text styles, but as an example, here is the font point size for the body style on iOS and ipadOS:
- xSmall: 14
- Small: 15
- Medium: 16
- Large: 17
- xLarge: 19
- xxLarge: 21
- xxxLarge: 23
- AX1: 28
- AX2: 33
- AX3: 40
- AX4: 47
- AX5: 53
Here are images of how the twelve Dynamic Type settings affect each of the predefined text styles:
All of those used the same code:
Text("largeTitle").font(.largeTitle)
Text("title").font(.title)
Text("title2").font(.title2)
Text("title3").font(.title3)
Text("headline").font(.headline)
Text("body").font(.body)
Text("callout").font(.callout)
Text("subheadline").font(.subheadline)
Text("footnote").font(.footnote)
Text("caption").font(.caption)
Text("caption2").font(.caption2)Only the Dynamic Type setting was adjusted.
Note: Rather than running the simulator and changing the settings to see the effect of each of these, you can view them easily in the Xcode Preview window by clicking the Device Settings button:
Dynamic Type is applied to the system font only if you specify one of the predefined text styles.
// Dynamic Type not applied
Text("body")
// Dynamic Type applied
Text("body").font(.body)
// Dynamic Type not applied
Text("body").font(.system(size: 10))
With a custom font, Dynamic Type is applied if you use the size parameter but not applied if you use the fixedSize parameter.
// Dynamic Type applied
Text("body").font(.custom("arial", size: 30))
// Dynamic Type overridden
Text("body").font(.custom("arial", fixedSize: 30))
When applied, the scaling is done in relation to the body size by default, but you can select another style to scale with by using the relativeTo parameter.
// Dynamic Type applied (relative to body)
Text("body").font(.custom("arial", size: 30, relativeTo: .body))
// Dynamic Type applied (relative to largeTitle)
Text("body").font(.custom("arial", size: 30, relativeTo: .largeTitle))
You can also adjust other values such as padding in response to the user's current Dynamic Type setting. To achieve this, use the @ScaledMetric property wrapper:
struct MyView: View {
@ScaledMetric var scale = 100.0
var body: some View {
Text("ScaledMetric: \(scale)")
.padding(.leading, scale / 10)Here are the values of how 100.0 is scaled for each of the Dynamic Type settings:
- xSmall: 86.33
- Small: 91.0
- Medium: 95.33
- Large: 100.0
- xLarge: 109.0
- xxLarge: 118.33
- xxxLarge: 131.67
- AX1: 154.67
- AX2: 181.67
- AX3: 218.33
- AX4: 254.67
- AX5: 281.67
The scale depends on the initial value and is not precisely the same scale for other initial values. For example, if the initial value is 1.0, the scale will never go below 1.0, even for xSmall.
TODO: If the ScaledMetric value is not available from the system, we will need to do more work to discover the scaling algorithm.
Truncation mode affects where ellipses are added to a string to indicate truncated text when the entire string doesn't fit in the allotted space. There are three options for truncationMode:
-
tail: Cut off the end of the string and add the ellipses there. -
middle: Cut off the middle of the string and add the ellipses there. -
head: Cut off the beginning of the string and add the ellipses there.
Text("Brevity is the soul of wit.")
.frame(width: 150)
.border(.black)
.lineLimit(1)
.truncationMode(.tail)
Text("Brevity is the soul of wit.")
.frame(width: 150)
.border(.black)
.lineLimit(1)
.truncationMode(.middle)
Text("Brevity is the soul of wit.")
.frame(width: 150)
.border(.black)
.lineLimit(1)
.truncationMode(.head)Be aware that head is on the right and tail on the left for right-to-left (RTL) languages like Arabic or Hebrew.
Text("هذا سطر طويل من النص")
.frame(width: 150)
.border(.black)
.lineLimit(1)
.truncationMode(.tail)
Text("هذا سطر طويل من النص")
.frame(width: 150)
.border(.black)
.lineLimit(1)
.truncationMode(.middle)
Text("هذا سطر طويل من النص")
.frame(width: 150)
.border(.black)
.lineLimit(1)
.truncationMode(.head)Note: That was a Google translate of "This is a long line of text." into Arabic, so it might not be grammatically correct.
You can squeeze in a little more text if you set the allowsTightening flag to true. This will reduce the space around characters in a string by just a little. See also the sections on kerning and tracking. In the following image, the first line allows tightening while the second doesn't:
Text("Brevity is the soul of wit.")
.frame(width: 150)
.border(.black)
.lineLimit(1)
.allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/)
Text("Brevity is the soul of wit.")
.frame(width: 150)
.border(.black)
.lineLimit(1)
.allowsTightening(false)Another option to make text fit in a small space is to scale it down. For this you can set the minimumScaleFactor to a value between 0.0 and 1.0. The default is 1.0. SwiftUI then attempts to make the entire string fit by scaling down the size. But only up to a point. If the string still doesn't fit, it will be truncated.
Text("Brevity is the soul of wit.")
.frame(width: 150)
.border(.black)
.lineLimit(1)
.minimumScaleFactor(1.0)
Text("Brevity is the soul of wit.")
.frame(width: 150)
.border(.black)
.lineLimit(1)
.minimumScaleFactor(0.7)
Text("Brevity is the soul of wit.")
.frame(width: 150)
.border(.black)
.lineLimit(1)
.minimumScaleFactor(0.5)
Text("Brevity is the soul of wit.")
.frame(width: 150)
.border(.black)
.lineLimit(1)
.minimumScaleFactor(0.3)
Text("Brevity is the soul of wit.")
.frame(width: 150)
.border(.black)
.lineLimit(1)
.minimumScaleFactor(0.1)The baseline is the line on which text normally sits, though some letters may go below the baseline. In the image below, you can see the "y" goes below the baseline:
In SwiftUI you can move text above or below the baseline using baselineOffset:
HStack() {
Text("lower").baselineOffset(-10)
Text("normal")
Text("higher").baselineOffset(10)
}
Kerning refers to the spacing between glyphs. Some letters look better when closer together and some look better when further apart. Because of this, spacing is built into the font itself. You can see this in the example below. The "V" and "A" partially overlap when placed next to each other.
Even though the font normally handles kerning, you can also adjust the character offsets in SwiftUI by setting the kerning modifier like so:
Text("kerning").kerning(-3)
Text("kerning").kerning(-2)
Text("kerning").kerning(-1)
Text("kerning").kerning(0)
Text("kerning").kerning(1)
Text("kerning").kerning(2)
Text("kerning").kerning(3)Here is the result:
You can accomplish something similar with the tracking modifier. Tracking adds or removes white space after every character. Take the example from above but replace kerning with tracking:
Text("tracking").tracking(-3)
Text("tracking").tracking(-2)
Text("tracking").tracking(-1)
Text("tracking").tracking(0)
Text("tracking").tracking(1)
Text("tracking").tracking(2)
Text("tracking").tracking(3)
With the text and font used in the examples above, there is no difference between kerning and tracking. Where kerning makes a difference is when the font contains ligatures. Ligatures are special glyphs used to display a combination of characters. For example, some fonts have a ligature for "fl" and "fi". Apparently the San Francisco fonts do not, but it is visible in the system Zapfino font.
Text("flight")
.font(.custom("Zapfino", size: 20))
Text("flight")
.font(.custom("Zapfino", size: 20))
.kerning(20)
Text("flight")
.font(.custom("Zapfino", size: 20))
.tracking(20)The first example is the word "flight" without kerning or tracking. The second example uses kerning to separate the characters while still maintaining the "fl" ligature. The third example uses tracking to add space after every character and ignores the ligature.
So, use kerning when you care about maintaining ligatures at the character level. Otherwise, use tracking.
Note that SwiftUI uses preset tracking values for every font point size of the default fonts for iOS, iPadOS, and visionOS, macOS, and tvOS. Flutter needs to match these.
Many of the styles above can be combined into a single string using character ranges for each attribute. This is accomplished in Swift UI using AttributedString.
To do that you first define the string with its attributed style ranges like so:
var attributedString: AttributedString {
var attributedString = AttributedString("backgroundColor, foregroundColor, baselineOffset, font, kern, tracking, strikethroughStyle, underlineStyle")
var range = attributedString.range(of: "backgroundColor")!
attributedString[range].backgroundColor = .yellow
range = attributedString.range(of: "foregroundColor")!
attributedString[range].foregroundColor = .blue
range = attributedString.range(of: "baselineOffset")!
attributedString[range].baselineOffset = 10
range = attributedString.range(of: "font")!
attributedString[range].font = UIFont(name: "Chalkduster", size: 18.0)
range = attributedString.range(of: "kern")!
attributedString[range].kern = 10
range = attributedString.range(of: "tracking")!
attributedString[range].tracking = -2
range = attributedString.range(of: "strikethroughStyle")!
attributedString[range].strikethroughStyle = .single
range = attributedString.range(of: "underlineStyle")!
attributedString[range].underlineStyle = .single
return attributedString
}And then you give it directly to the Text view:
Text(attributedString)Using the same method you can also overlap different styles.
var overlappingStyles: AttributedString {
var attributedString = AttributedString("SwiftUI Attributed Strings")
var range = attributedString.range(of: "ftUI Attributed Str")!
attributedString[range].backgroundColor = .yellow
range = attributedString.range(of: "Strings")!
attributedString[range].foregroundColor = .red
range = attributedString.range(of: "SwiftUI")!
attributedString[range].font = UIFont(name: "Chalkduster", size: 24)
return attributedString
}Even Markdown is supported:
var markdown: AttributedString {
do {
let attributedString = try AttributedString(markdown: "**Markdown!** Please visit our [website](https://example.com)")
return attributedString
} catch {
print("Error parsing markdown: \(error)")
return AttributedString("Error")
}
}Multiline text can have the following alignments in SwiftUI:
-
leading: The text is left justified. -
center: The text is centered on every line. -
trailing: The text is right justified.
Text("To be, or not to be, that is the question:")
.frame(width: 120)
.border(.black)
.multilineTextAlignment(.leading)
Text("To be, or not to be, that is the question:")
.frame(width: 120)
.border(.black)
.multilineTextAlignment(.center)
Text("To be, or not to be, that is the question:")
.frame(width: 120)
.border(.black)
.multilineTextAlignment(.trailing)One would expect that in RTL contexts, leading and trailing would have the opposite justifications, but that is apparently not the case:
Text("هذا سطر طويل من النص")
.frame(width: 150)
.border(.black)
.multilineTextAlignment(.leading)
Text("هذا سطر طويل من النص")
.frame(width: 150)
.border(.black)
.multilineTextAlignment(.center)
Text("هذا سطر طويل من النص")
.frame(width: 150)
.border(.black)
.multilineTextAlignment(.trailing)Currently justified text is not supported in SwiftUI. We could proactively support it in Flutter.
You can limit the number of lines that are shown in multiline text by using lineLimit. A default of nil will show all of the lines.
Text("To be, or not to be, that is the question.")
.frame(width: 150)
.border(.black)
.lineLimit(1)
Text("To be, or not to be, that is the question.")
.frame(width: 150)
.border(.black)
.lineLimit(2)
Text("To be, or not to be, that is the question.")
.frame(width: 150)
.border(.black)
.lineLimit(nil)You can use lineSpacing to increase the amount of space between lines. In the first example below, there is no additional spacing. The second example adds 10 points of additional spacing.
Text("To be, or not to be, that is the question.")
.frame(width: 150)
.border(.black)
.lineSpacing(0.0)
Text("To be, or not to be, that is the question.")
.frame(width: 150)
.border(.black)
.lineSpacing(10.0)TODO (Matteo):
TODO (Vince):
TODO (Matt):