WWDC23 SALE: Save 50% on all my Swift books and bundles! >>

How to add advanced text styling using AttributedString

Paul Hudson    @twostraws   

Updated for Xcode 14.2

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

Download this as an Xcode project

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

Download this as an Xcode project

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

Download this as an Xcode project

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

Download this as an Xcode project

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://www.hackingwithswift.com")
        return result
    }

    var body: some View {
        Text(message)
    }
}

Download this as an Xcode project

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

Download this as an Xcode project

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

Download this as an Xcode project

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

Download this as an Xcode project

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

Download this as an Xcode project

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.

Save 50% in my WWDC23 sale.

SAVE 50% To celebrate WWDC23, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

Similar solutions…

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 3.8/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.