Skip to content

Spec: Text ‐ Typography

Matt Carroll edited this page Feb 24, 2024 · 1 revision

Typography

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.

Default system fonts

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.

Custom fonts

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))
Screenshot 2023-12-27 at 17 13 11

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

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))
font sizes

Here is a screenshot of a list of Flutter Text widgets and SwiftUI Text views going up to a font size of 96:

96 compared

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.

pixel difference

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:

width

There are also very minor differences between the fonts themselves so it's unclear whether Flutter is using the default system font or not.

Font weight

Fonts have the following weights defined in order of increasing thickness:

  • ultraLight
  • thin
  • light
  • regular
  • medium
  • semibold
  • bold
  • heavy
  • black
font weights

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

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.
font designs

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.

Font width

Fonts have the following widths defined in order of increasing thickness.

  • compressed
  • condensed
  • standard
  • expanded
Font widths

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:

Screenshot 2024-01-03 at 14 29 08

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.

Text style

The following styles are available to text:

  • bold
  • italic
  • 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.
text styles

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)

Text decoration

There are two different decorations that you can add to text:

  • underlined: Users can select the color and pattern of the underline decoration.
  • strikethrough: Users can select the color and pattern of 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)
underline decoration

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)
strikethrough decoration

Note: Shadow is not currently implemented in SwiftUI, but will likely be implemented in the future. We could proactively implement it in Flutter.

Foreground style

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:

foreground colors
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:

foreground gradients
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:

Screenshot 2023-12-28 at 18 34 09
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)

Background

The background modifier also takes a ShapeStyle:

background
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.

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)

Predefined text styles

SwiftUI predefines a list of text styles for various purposes within an app:

  • largeTitle
  • title
  • title2
  • title3
  • headline
  • body
  • callout
  • subheadline
  • footnote
  • caption1
  • caption2
predefined text styles

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

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:

xSmall Small Medium Large xLarge xxLarge xxxLarge AX1 AX2 AX3 AX4 AX5

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:

Device Settings: Dynamic Type

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))
Dynamic Type with system font

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))
Dynamic Type with custom font

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))
Dynamic Type relative to style

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

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.
truncation mode LTR
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.

truncation mode RTL
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.

Allow tightening

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:

tightening
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

Text.scale does not allow you to set an arbitrary scale but rather two logical scales:

  • default
  • secondary
Text.scale

They can be applied like so:

Text("default").textScale(.default)
Text("secondary").textScale(.secondary)

Minimum scale factor

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.

minimumScaleFactor
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)

Baseline offset

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:

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)
}
baselineOffset

Kerning and tracking

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.

VA kerning

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:

kerning

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)
tracking

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.

ligature maintained by kerening

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.

Attributed strings

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.

attributed string

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:

overlapping 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:

markdown
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 alignment

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.
Multiline text alignment
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:

RTL multiline text alignment
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.

Line limit

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:

line limit
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)

Line spacing

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.

lineSpacing
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)