Hyperlinks in SwiftUI Text
Reading time: 3 min
This recipe shows how to display text with tappable hyperlinks in SwiftUI, and, in doing so, fully support attributed strings, HTML and Markdown in text views on any SwiftUI version.
The end result looks like this:
The solution depends on which SwiftUI version you have to support:
- SwiftUI 3 (iOS 15, macOS 12) supports hyperlinks natively from any
AttributedString
source. - SwiftUI 1 and 2 (iOS 13-14, macOS 10.15-11) don't support
NSAttributedString
by default, but there are ways to work around it. Read on to see a working solution for all SwiftUI versions.
Recipe for all versions
TL;DR You can find the full source in this gist.
The recipe goes like this:
- Use code from this recipe to render attributed strings in
Text
views. - If the attributes for a particular part of the text contain
NSAttributedString.Keys.link
, add anonTapGesture
that opens the URL to it. - Use the SwiftUI Flow Layout component to order the tappable and non-tappable text items sequentially.
You might ask what's the difference between code in this recipe and the one here. The code in SwiftUI Text with HTML via NSAttributedString always results in a Text
, on which you can then apply usual text attributes. Code in this recipe, while more powerful, doesn't leave you with a single Text
at the end.
First, add this simple struct that represents a portion of text with associated attributes:
struct StringWithAttributes: Hashable, Identifiable {
let id = UUID()
let string: String
let attrs: [NSAttributedString.Key: Any]
static func == (lhs: StringWithAttributes, rhs: StringWithAttributes) -> Bool {
lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
Then, add this extension that breaks an NSAttributedString
down into StringWithAttributes
array:
extension NSAttributedString {
var stringsWithAttributes: [StringWithAttributes] {
var attributes = [StringWithAttributes]()
enumerateAttributes(in: NSRange(location: 0, length: length), options: []) { (attrs, range, _) in
let string = attributedSubstring(from: range).string
attributes.append(StringWithAttributes(string: string, attrs: attrs))
}
return attributes
}
}
Now it's time to add the Text
extensions that allow it to work with StringWithAttributes
. You can find that code starting at line 91 of the gist. It's just a slightly reworked version of the code from this recipe.
Lastly, here's the code for the HyperlinkText
component:
import SwiftUIFlowLayout
struct HyperlinkText: View {
private let strings: [StringWithAttributes]
init(_ attributedString: NSAttributedString) {
strings = attributedString.stringsWithAttributes
}
var body: some View {
FlowLayout(mode: .vstack, // change to scrollable if embedding in ScrollView
binding: .constant(false), // don't need a refresh binding
items: strings,
itemSpacing: 0) { string in
if let link = string.attrs[.link],
let url = link as? URL {
Text(string)
.onTapGesture {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
}
}
} else {
Text(string)
}
}
}
}
Supporting HTML
Converting HTML to NSAttributedString
is easy enough, allowing us to add this simple initializer that immediately inits HyperlinkText
from HTML code:
extension HyperlinkText {
init?(html: String) {
if let data = html.data(using: .utf8),
let attributedString = try? NSAttributedString(data: data,
options: [.documentType: NSAttributedString.DocumentType.html],
documentAttributes: nil) {
self.init(attributedString, font: font)
} else {
return nil
}
}
}
And you can test it with:
HyperlinkText(html: "To <b>learn more</b>, <i>please</i> feel free to visit <a href=\"https://swiftuirecipes.com\">SwiftUIRecipes</a> for details, or check the <code>source code</code> at <a href=\"https://github.com/globulus\">Github page</a>.")
Supporting Markdown
To support markdown, all we have to do is be able to convert Markdown text to NSAttributedString
. MarkdownKit is a great module that does exactly that, so add it to your dependencies list, and then use it like this:
import MarkdownKit
extension HyperlinkText {
init(markdown: String) {
self.init(MarkdownParser().parse(markdown))
}
}
And you can test it with:
HyperlinkText(markdown: "To **learn more**, *please* feel free visit [SwiftUIRecipes](https://swiftuirecipes.com) for details, or check the `source code` at [Github page](https://github.com/globulus).")
SwiftUI 3 solution
SwiftUI 3 supports attributed string natively, including links that are tappable by default in any Text
. Here's some sample code that utilizes native markdown support:
// yep, this just works out of the box
Text("To **learn more**, *please* feel free to visit [SwiftUIRecipes](https://swiftuirecipes.com) for details, or check the `source code` at [Github page](https://github.com/globulus).")