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:

preview

The solution depends on which SwiftUI version you have to support:

  1. SwiftUI 3 (iOS 15, macOS 12) supports hyperlinks natively from any AttributedString source.
  2. 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:

  1. Use code from this recipe to render attributed strings in Text views.
  2. If the attributes for a particular part of the text contain NSAttributedString.Keys.link, add an onTapGesture that opens the URL to it.
  3. 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).")

Next Post Previous Post