-
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 SwiftUI 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. 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 varies by device type. For example, the Pro fonts are used on iOS, iPadOS, macOS, and tvOS. However, watchOS uses the Compact fonts. The Flutter Text widget must display the correct font family for every device and platform when no custom font is specified by the developer.
Note: The license of the SF family fonts only allows for use on Apple devices. The swift_ui port will have to use alternative fonts on Android and other platforms.
Apple has many different system fonts that come built in besides the SF fonts. 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))
Here is a screenshot of a list of Flutter Text widgets and SwiftUI Text views going up to a font size of 96:

The SwiftUI size is a few pixels shorter than the Flutter version at "size 96". The red lines in the following image are set at the max/min of the "z" and the max/min of the "96" of the Flutter version. The image below is the SwiftUI side and shows the height difference in pixels.
This height difference probably falls within the range of "close enough" since the pixel difference is less at smaller font sizes. We can assume that Flutter font size and SwiftUI font point size are equivalent.
However, the kerning/tracking is significantly different between the two. The following image shows SwiftUI on top and Flutter on bottom:
There are also very minor differences between the fonts themselves so it's unclear whether Flutter is using the default system font or not.
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))Font design is a unique category in SwiftUI. It's used as a means of choosing the default system font in the following four ways:
-
default: SF Pro sans-serif font on iOS. -
rounded: SF Pro Rounded font on iOS. -
serif: New York font on iOS. -
monospaced: SF mono font on iOS.
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))The Flutter API should use the use correct system font on Apple devices and either use a similar font on non-Apple devices or warn the developer to supply an appropriate font.
Fonts have the following widths defined in order of increasing thickness.
compressedcondensedstandardexpanded
The height for all of these is the same.
They are implemented in SwiftUI like so:
Text("compressed").fontWidth(.compressed)
Text("condensed").fontWidth(.condensed)
Text("standard").fontWidth(.standard)
Text("expanded").fontWidth(.expanded)The font widths appear to be a selector within a family of related system fonts, not just dynamically stretched. This is visible in the SF font video:
The stem thickness stays roughly the same width while the inner spacing changes. This wouldn't happen if a font was dynamically stretched during painting.
If we want a near pixel perfect replica of SwiftUI, we need access to the San Francisco fonts on native Apple devices. For non-Apple devices we can approximate things like width using some sort of dynamic stretching and/or kerning/tracking.
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 proactively implement it in Flutter.
You can set the text foreground style using foregroundStyle, which takes a ShapeStyle. ShapeStyle is a protocol used to define how some shape is painted. It is a color or a pattern. It's a style for painting any shape and that includes text and text backgrounds. Implementations of ShapeStyle include:
- Single color
- Gradient colors
- Hierarchy styles (primary, secondary, etc.)
- Selection style
- Background style
- Foreground style (perhaps similar to onPrimary color theme in Flutter)
- Tint
- Material (a translucent layer applied over something)
- Image paint (a repeated image painted over a shape)
This article goes into more details about ShapeStyle and this article goes into more detail about Materials.
As a color, ShapeStyle is applied like so:
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, like this:
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 as a material, like the following:
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)The background modifier also takes a ShapeStyle:
Text("background color").background(.yellow)
Text("background gradient").background(.linearGradient(
colors: [.red, .blue],
startPoint: .leading,
endPoint: .trailing
))Background painting is a plain rectangle the size of the entire Text view, including with multi-line text. However, placing padding before or after the background affects the size of the background.
Text("background color\nhello")
.padding()
.background(.yellow)
Text("background gradient\nhello").background(.linearGradient(
colors: [.red, .blue],
startPoint: .leading,
endPoint: .trailing
))
Text("background color\nhello")
.background(.yellow)
.padding()
Text(attributedString)
.background(.green)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) (pronounced /'lɛdɪŋ/ and meaning line spacing, like the lead metal that used to be used in typesetting)
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 and accessibility are basically the same. Together they form a 12-part scale (see the next section). The question is, do we have access to the iOS dynamic type from Flutter? We do have access to the platform. After that it should just be a matter of mapping the published SwiftUI typography specs to some sort of table within swift_ui.
Dynamic Type refers to text that is scaled according to the device's system settings. Users should be able to choose a desired font size and have that size reflected in every app on their device. Dynamic type is not available on macOS, but in 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 system 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)Note: leading here is pronounced /'lidɪŋ/, not /'lɛdɪŋ/. It refers to the padding at beginning (left side) of the view and is in contrast to trailing (ending or right side).
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, more work needs to be done 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 out 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 below 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(true)
Text("Brevity is the soul of wit.")
.frame(width: 150)
.border(.black)
.lineLimit(1)
.allowsTightening(false)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)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. (A glyph is a particular rendering of a character or combination of characters in a font.) Some glyphs look better when closer together and some look better when further apart. Because of this, glyph spacing is built into the font itself. You can see this in the example below. The "V" and "A" partially overlap vertically 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.
The following SuiftUI code demonstrates this:
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. 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 accomplish 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
}Then you give the attributed string 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 aligned. -
center: The text is centered on every line. -
trailing: The text is right aligned.
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 alignments, 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.
The following image shows one, two, and all 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)The Text widget is just one of the many places where localization plays an important role. Well, one could argue that it is one of the most important, since it is text that is translated. Localization does not affect the UI or fidelity of the app, instead it plays a subtler role: good localization is inviting, and makes the user feel at home when using the app.
The most important part in getting localization right is the developer experience: the easier it is to export, translate, import strings in the app, the handling of plurals, the correct use of pronouns and genres, the better the user experience will be. SwiftUI and Xcode offer many tools to help improve the localization experience for the developer, but there are also some legacy standards that should be avoided.
SwiftUI is not the only UI framework developed by Apple to build natively on Apple platforms. UIKit and AppKit are older frameworks, but still widely used. They can even be integrated inside SwiftUI. Some localization constructs are shared between these frameworks and since SwftUI is the youngest of the family, there are some limitations or awkward edge cases when using some of them. They will be noted where relevant.
Supporting multiple languages in your app is a good overview of the tools available for localization. Look at Xcode's localization documentation for more information and examples.
If you initialize a text view with a string literal, the view uses the init(_:tableName:bundle:comment:) initializer, which interprets the string as a localization key and looks up its localization value for the app's locale in the given table. If the locale is not supported, the table is not found or there is no value for the key, its value is displayed in the UI as is.
Text("pencil") // displays "pencil" if no localization is foundIf you intialize a text view with a variable value, the view uses the init(_:) initializer, which doesn’t localize the string. However, you can request localization by creating a LocalizedStringKey instance first, which triggers the init(_:tableName:bundle:comment:) initializer instead:
let writingImplement = "pencil"
Text(writingImplement) // displays "pencil" for all locales
Text(LocalizedStringKey(writingImplement)) // uses localizationTo explicitly bypass localization for a string literal, use the init(verbatim:) initializer.
Text(verbatim: "pencil") // displays "pencil" for all localesLocalizedStringResource can be used to specify the Text initializer parameters outside of UI code, for example:
struct SomeController {
static var title = LocalizedStringResource(
"pencil",
table: "MyTable",
comment: "my comment",
)
}
// these display the same thing
Text(SomeController.title)
Text("pencil", table: "MyTable", comment: "my comment")Note: One of the uses for a LocalizedStringResource is "to provide localizable strings with lookups you defer to a later time". In Swift this refers to the passing of localizable strings to other processes, which may have different locales from the process where they originated from. It's unclear if this capability can be useful in the context of the swift_ui package.
String, in the context of localization, has initializers of the form init(localized:...) that take similar parameters to the String literal initializer. For example: init(localized:defaultValue:options:table:bundle:locale:comment:).
The new parameters are:
-
defaultValue: can be used to specify the localized value for the default locale. Useful when the value oflocalizedisn't intended to be displayed. -
options: used to specify formatting options. -
locale: can be used to explicitly specify a locale.
Text(LocalizedStringKey(String(localized: "pencil")))Note: we need to use LocalizedStringKey because String is not a string literal (see variable section).
AttributedString can be used to associate style and attributes to runs of text. Like String, it has various initializers of the type init(localized:options:table:bundle:locale:comment:), with the parameters playing the same roles.
let attributed = AttributedString(localized: "pencil")
Text(attributed)Note: With AttributedString the localized value can only be displayed in the language currently set in the system and cannot be specified as a specific language, even setting the locale attribute does not work. This is one of the incompatibilities I was referring to before.
String and AttributedString are awkward to work with in swiftUI (localization-wise, at least), and don't seem to be completely supported. Besides, all the features offered by them can be achieved with the Text view and string literals directly, albeit with a different API.
This needs to be investigated further.
In the initializers mentioned untile now there is the table parameter. This refers to the name of the file that contains the localization values for the Text's string. There are various file formats available to create these tables: Strings and Stringsdict files (legacy), and String catalogs.
The latter are the recommended format for new projects in Xcode 15 (example here). They can be auto-generated for swiftUI code from Xcode using the build setting SWIFT_EMIT_LOC_STRINGS = YES.
A String catalog is a json file with the .xcstrings extension, and the convention is to name them LocalizableXXX (eg. LocalizableFoo, LocalizableBar). The same file contains the localization for its keys in all the supported locales.
Xcode renders them with an interactive UI:
Another parameter of the initializers is bundle. It is used to lookup localizations in a bundle that is external the application. This functionality is not useful for the swift_ui package.
For some supported languages, swiftUI can automatically choose the correct gender, plural form or pronoun for a sentence, without the need to specify anything in the string catalogs.
In the example, note that the displayed string has been adapted ("grandi" instead of "grande", "insalate" instead of "insalata"), just by adding the inflect: true attribute.
let quantity = 2
let size = LocalizedStringResource("large") // grande in italian
let food = LocalizedStringResource("salad") // insalata in italian
Text("Add ^[\(quantity) \(size) \(food)](inflect: true) to your order")
// Displays "Aggiungi 2 grandi insalate al tuo ordine" in italianSee the WWDC video for more info.
Note: There are other mechanisms like this, that affect all localizable strings, even outside the Text view, that need their own specification. It's unclear how we can achieve something similar in the swift_ui package.
TODO (Vince):
TODO (Matt):