SwiftUI AttributedString is not there yet

You should use it, but also be aware of some of its limitations

Thomas Ricouard
4 min readAug 18, 2022

I’ve only recently started to use AttributedString, it’s been introduced with iOS 15 and as the Medium application also support iOS 14 I was not really looking into it.

However, as we write even more parts of the app in SwiftUI, and iOS 16 is approaching, I felt it was a good opportunity to finally look into it and replace some of our UILabel representable (which display an NSAttributedString in SwiftUI) by SwiftUI Text + AttributedString.4

Our UILabel representable come with a lot of small glitched & bugs as you have to do some manual sizing etc… we have quite a few layout issues depending of the version of SwiftUI you’re the app running on. While we can workaround or fixes most of those, it’s a lot of code and compromises.

Medium being a text heavy app, we have quite a lot of requirements when it come to text rendering, so while I was able to successfully use AttributedString in a few places, I’ve also discovered some of its limitations.

On the surface, it looks like that AttributedString is a 1:1 equivalent to NSAttributedString. And sure you can actually pass attributes from and to those two classes, making all our existing style fit into it without much code changes. However, you have to understand that SwiftUI Text will not render that many attributes. And on top of that some of the attributes are split between modifiers on Text and attributes in AttributedString. Coming from the AppKit / UIKit world this can feel weird as you usually don’t apply anything on UILabel once you use an NSAttributedString.

To understand what is available as SwiftUI AttributedString attributes you can look at AttributedScope.

The gist of it is that you can easily fit a UIKit world NSAttributedString inside an AttributedString, a convenience initialiser exist for that.

let attributedStringUIKit = NSAttributedString()
let attributedString = AttributedString(attributedStringUIKit)

This will work and your new AttributedString will have all the attributes from the previous NSAttributedString.

However, only the one available in the SwiftUI scope will render once you render your AttributedString in a Text like below

Text(attributedString)

If you had for example a NSParagraphStyle with custom lineSpacing or lineHeightMultiplier, you can forget about it.

Instead what you can do is something like this

Text(attributedString).lineSpacing(attributedString.uiKit.paragraphStyle.lineSpacing)

This is the part where it get messy to me because it’s not clear what should be in the AttributedString and what should be applied as a modifier on Text. For now the only way to know what exactly will be rendered in a Text + AttributedString without any additional modifiers is to look at AttributedScope for SwiftUI.

And there you have it, it’s not that much yet. And iOS 16 doesn’t bring anything new to the table here.

The good new is that backgroundColor is properly supported and will highlight the text correctly at the desired range.

And adding an attribute at a range using the subscript API of AttributedString is really nice & easy.

attributedString[range].backgroundColor = .green

Prior to this the only way to highlight Text in SwiftUI was to use the .background(Color(.green))but then you would just get the whole rectangle painted. Not the actual text, lines, etc…

And it get even messier if you try to do any kind of factory for building and configuring Text. Because when using modifier like lineSpacing it erase the Text type to some View. You can’t get your Text back, so if in the output you would want to use nice Text specific features such as concatenation Text(“”) + Text(""), you can’t. So the only way is for to you to apply your lineSpacing etc… outside of your factory.

Prior to using AttributedString in SwiftUI, we’re using a nice helper to get the base of our style render for SwiftUI Text

public extension Text {
func styled<S: TextStyleProtocol>(by style: S) -> some View {
let attrs = style.attributes
let styledText = self.styledText(by: style)
let paragraphStyle = attrs[.paragraphStyle] as? NSParagraphStyle ?? NSParagraphStyle()
return styledText.lineSpacing(paragraphStyle.lineSpacing)
}

func styledText<S: TextStyleProtocol>(by style: S) -> Text {
let attrs = style.attributes
var styledText = self
if let f = attrs[.font] as? Font {
styledText = styledText.font(f)
} else if let f = attrs[.font] as? UIFont {
styledText = styledText.font(Font.custom(f.fontName, size: f.pointSize))
}
if let foreground = attrs[.foregroundColor] as? UIColor {
styledText = styledText.foregroundColor(Color(foreground))
}
if let kern = attrs[.kern] as? CGFloat {
styledText = styledText.kerning(kern)
}
return styledText
}
}

You can notice how the styleText can’t apply the .lineSpacing and we a separate function that erase the type for that.

With AttributedString we still use those extension functions because we still need some modifiers outside of the AttributedString. So we can have both the attributes supported by AttributedString and the modifier supported on Text.

Text(viewModel.attributedQuoteTextiOS15).styled(by: viewModel.quoteTextStyle)

And that’s a wrap, my hope is that this article will help some of you understand AttributedString, what it can do and can’t. But also I hope that Apple will improve on it. I think having paragraph style supported within AttributedString would be already a big plus, while also keeping the simpler modifier on Text like it is today.

Thanks for reading 🚀

Bonus: Some interesting Twitter discussions around the subject that helped me find the motivation and information to write this article.

--

--

Thomas Ricouard

📱 🚀 🇫🇷 [Entrepreneur, iOS/Mac & Web dev] | Now @Medium, @Glose 📖| Past @google 🔍 | Co-founded few companies before, a movies 🎥 app and smart browser one.