NSAttributedString in SwiftUI

If you came across this blog post and you’re happy because you didn’t think it was possible to use NSAttributedString in SwiftUI I have a bad news: it isn’t supported yet. Text in SwiftUI accept a String and there isn’t the equivalent of attributedText you can find in UILabel.
So what is this article about? It is a quick introduction to NSAttributedString, and how to use them in SwiftUI by using UIKit components like a UILabel. That gives me the possibility to write about UIViewRepresentable, something we’ll get used to until SwiftUI will have everything we need.

The use case

Suppose we have a text, for the sake of the example I used the famous Lorem ipsum and we want to search for words. Usually a text editor highlight the found word, so you can easily spot it inside the whole text.
How do we accomplish something similar in SwiftUI? Suppose we put some text in a Text view, what next? UIKit has supported NSAttributedString for years, and that class has just everything we need to solve the problem. We can initialise it with a string and add attributes to a particular range of characters, so for example we can change the background and foreground colour of a particular word, or change its font. We can even have properties for the a paragraph, like the alignment, line spacing and so on.
As I mentioned UIKit is capable of dealing with this kind of strings, so we can use a UILabel for that purpose. So first we’ll see how to build an NSAttributedString, then we’ll add a UILabel to our SwiftUI view in order to display it.
The sample project can be found here

NSAttributedString

NSAttributedString has been part of Foundation for quite a while, according to the documentation it was available on iOS 3.2 so even before I began coding for iOS, back when 4.0 was available. It makes me wonder why it wasn’t supported by SwiftUI right from the beginning, maybe one of the reasons is this NS prefix, and the fact that the API still uses NSRange instead of Range. Hopefully a more “swifty” version of NSAttributedString will make its way in SwiftUI next WWDC, finger crossed for that.
Let’s see how we can implement an NSAttributedString.


let attributedString = NSMutableAttributedString(string: string)
let wholeRange = string.startIndex ..< string.endIndex

let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .justified
paragraphStyle.firstLineHeadIndent = 10.0
paragraphStyle.lineBreakMode = .byTruncatingTail
paragraphStyle.lineSpacing = 3.0

attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(wholeRange, in:string))

So the original string can be used to initialise the NSAtttributedString. In my example I use the mutable version, so I can add attributes after initialisation. Otherwise you can only specify a set of attributed while creating it with a string.
The first thing I need is to set a range, and as I want some attributes to be common to the entire string I'm interested in the whole range, so I can define one from startIndex to endIndex.
NSMutableParagraphStyle allows to set some properties about a paragraph, for example the text alignment, line spacing, and word wrapping. The property firstLineHeadIndent allows to have the first word of a new line to be indented, like in a book.
As you can see we can add an attributed, in this case the paragraphStyle, to an NSRange and in this case we use wholeRange as the alignment and line spacing is common to the entire text.
Next, let's see how we can highlight a particular word


string.enumerateSubstrings(in: wholeRange, options:.byWords) {subString, subrange,_,_ in
            if subString?.lowercased() == matching.lowercased() {
                attributedString.addAttribute(.backgroundColor, value: UIColor.yellow, range: NSRange(subrange, in: string))
            }
        }

First, we can enumerate all the words in the original string with enumerateSubstrings. We need to specify a range, so we use wholeRange to get the entire text, then we use the option .byWords. You can also use .byLines to get an entire line, or byParagraph. The closure contains the substring, so a single word, and its subrange inside the string. The other two parameters I didn't specify are an enclosing range (with spaces) and an inout variable to stop the enumeration if set to true.
Once we have a single word we can see if it matches the one we're looking for, and if so, add a new attribute. This is done by setting the .backgroundColor key in the range provided by the closure, not that we need to convert Range to NSRange.

Integrate in SwiftUI

We saw how to create an NSAttributedString and how to highlight a particular word. Now we need to do two things: use a UILabel to display the string and make sure the view is updated every time we enter a new word to search.
Let's start with integrating a UILabel. In order to use a UIKit view with SwiftUI we need to implement a struct conforming to UIViewRepresentable. So instead of having a struct conforming to View, with the body property, we have the struct conforming to UIViewRepresentable, that needs two functions: makeUIView and updateUIView. SwiftUI will use those functions to display the view and keep it updated, see the code here



class ViewWithLabel : UIView {
    private var label = UILabel()
    
    override init(frame: CGRect) {
        super.init(frame:frame)
        self.addSubview(label)
        label.numberOfLines = 0
        label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    }
    
    func setString(_ attributedString:NSAttributedString) {
        self.label.attributedText = attributedString
    }
}

So this is the UIKit view containing a UILabel that takes the entire space of its parent. Setting numberOfLines to 0 allows to have multiple lines of text.


struct TextWithAttributedString: UIViewRepresentable {
    
    var attributedString:NSAttributedString
    
    func makeUIView(context: Context) -> ViewWithLabel {
        let view = ViewWithLabel(frame:CGRect.zero)
        return view
    }
    
    func updateUIView(_ uiView: ViewWithLabel, context: UIViewRepresentableContext) {
        uiView.setString(attributedString)
    }
}

And TextWithAttributedString is the component we can use in SwiftUI. makeUIView returns the custom UIView we made with the label, and updateUIView is called every time the view needs to be updated, we only need to set the attributedString.

Finally we can take a look at SwiftUI (here is the code).
Adding the new view is straightforward, we only need to pass the attributedString as a parameter. And how do we keep it updated? We can simply use a TextField with a variable, and every time the variable changes as the user types something SwiftUI will try to refresh our view, calling the updateUIView function we just saw



private var myString = "Lorem ipsum..."
@State private var inputText = ""

var body: some View {
    VStack {
        TextField("Search text", text: $inputText)
        TextWithAttributedString(attributedString: highlightText(inString: myString, matching: inputText))
    }
}

highlightText is the function we saw in previous chapter when describing how to create an NSAttributedString.

Before I end this post a few tidbits about NSAttributedString. You can create them with an HTML string, and even with an RTF file, and once you have one of them there is the possibility to iterate through its attributes. As I said, they're quite powerful and I wish they'll become first class citizens in SwiftUI in the future, but for now you can easily use them with UIViewRepresentable.
Many more UIKit views can be imported that way, for example you may need to display web content an WKWebView can be embedded into SwiftUI by having it wrapped inside a UIViewRepresentable struct, and that's true for any custom view that you have and don't want, or cannot, rewrite in SwiftUI.