How to add advanced text styling using AttributedString
How to add advanced text styling using AttributedString êŽë š
Updated for Xcode 15
SwiftUI's Text
view is able to render more advanced strings created using Foundation's AttributedString
struct, including adding underlines, strikethrough, web links, background colors, and more. Sadly, it has a rather bafflingly opaque API so I want to show you a whole bunch of examples to help get you started.
We can create an AttributedString
with common properties such as font, background color, and foreground color:
struct ContentView: View {
var message: AttributedString {
var result = AttributedString("Hello, world!")
result.font = .largeTitle
result.foregroundColor = .white
result.backgroundColor = .red
return result
}
var body: some View {
Text(message)
}
}
That simple example is something you can do using just Text
and regular SwiftUI modifiers, but part of the power of AttributedString
is that customizations belong to the string rather than to the Text
view used to render it.
This means the background color is part of the string itself, so we can merge several strings together using different background colors if we want:
struct ContentView: View {
var message1: AttributedString {
var result = AttributedString("Hello")
result.font = .largeTitle
result.foregroundColor = .white
result.backgroundColor = .red
return result
}
var message2: AttributedString {
var result = AttributedString("World!")
result.font = .largeTitle
result.foregroundColor = .white
result.backgroundColor = .blue
return result
}
var body: some View {
Text(message1 + message2)
}
}
If you try that using Text
and background()
modifiers, you'll see that it just doesn't work.
There are a handful attributes we can customize, including underline pattern and color:
struct ContentView: View {
var message: AttributedString {
var result = AttributedString("Testing 1 2 3!")
result.font = .largeTitle
result.foregroundColor = .white
result.backgroundColor = .blue
result.underlineStyle = Text.LineStyle(pattern: .solid, color: .white)
return result
}
var body: some View {
Text(message)
}
}
You can adjust the baseline offset for pieces of the string, forcing it to be placed higher or lower than default:
struct ContentView: View {
var message: AttributedString {
let string = "The letters go up and down"
var result = AttributedString()
for (index, letter) in string.enumerated() {
var letterString = AttributedString(String(letter))
letterString.baselineOffset = sin(Double(index)) * 5
result += letterString
}
result.font = .largeTitle
return result
}
var body: some View {
Text(message)
}
}
And we can even attach tappable web links to our text using the link
property:
struct ContentView: View {
var message: AttributedString {
var result = AttributedString("Learn Swift here")
result.font = .largeTitle
result.link = URL(string: "https://hackingwithswift.com")
return result
}
var body: some View {
Text(message)
}
}
However, the really powerful feature of AttributedString
is that it doesn't throw away all the metadata we provide it about our strings, which unlocks a huge amount of extra functionality.
For example, we can mark part of the string as needing to be spelled out for accessibility reasons, so that things like passwords are read out correctly when using VoiceOver:
struct ContentView: View {
var message: AttributedString {
var password = AttributedString("abCayer-muQai")
password.accessibilitySpeechSpellsOutCharacters = true
return "Your password is: " + password
}
var body: some View {
Text(message)
}
}
Even more impressive is how it handles structured information.
For example, if we format a Date
instance as an attributed string it retains knowledge of what each component represents â it remembers that âNovemberâ is the month part of the string, for example.
This means we can style our strings semantically: we can say âmake the whole have a secondary color, apart from the weekday part â that should have a primary colorâ, like this:
struct ContentView: View {
var message: AttributedString {
var result = Date.now.formatted(.dateTime.weekday(.wide).day().month(.wide).attributed)
result.foregroundColor = .secondary
let weekday = AttributeContainer.dateField(.weekday)
let weekdayStyling = AttributeContainer.foregroundColor(.primary)
result.replaceAttributes(weekday, with: weekdayStyling)
return result
}
var body: some View {
Text(message)
}
}
Notice how that code has no idea where the weekday actually appears in the text â it's language and locale independent, so it will be styled correctly for everyone.
The same is true of working with the names of people using PersonNameComponents
â this makes an AttributedString
instance where the family name of someone is bold:
struct ContentView: View {
var message: AttributedString {
var components = PersonNameComponents()
components.givenName = "Taylor"
components.familyName = "Swift"
var result = components.formatted(.name(style: .long).attributed)
let familyNameStyling = AttributeContainer.font(.headline)
let familyName = AttributeContainer.personNameComponent(.familyName)
result.replaceAttributes(familyName, with: familyNameStyling)
return result
}
var body: some View {
Text(message)
}
}
You can even use it with measurements. For example, the following code creates a measurement of 200 kilometers, then formats that so that the value is presented much larger than the unit:
struct ContentView: View {
var message: AttributedString {
var amount = Measurement(value: 200, unit: UnitLength.kilometers)
var result = amount.formatted(.measurement(width: .wide).attributed)
let distanceStyling = AttributeContainer.font(.title)
let distance = AttributeContainer.measurement(.value)
result.replaceAttributes(distance, with: distanceStyling)
return result
}
var body: some View {
Text(message)
}
}
As a bonus, that will automatically honor the user's locale preference for distance, meaning that many users will see â124 milesâ rather than â200 kilometersâ.
Warning
If you explore the API using Xcode's autocomplete, you'll see all sorts of options that look like they ought to work but in fact do nothing at all.