Skip to content

Instantly share code, notes, and snippets.

@globulus
Created August 26, 2021 14:28
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save globulus/87adfb13a60f87500c28891e8cecf97c to your computer and use it in GitHub Desktop.
Save globulus/87adfb13a60f87500c28891e8cecf97c to your computer and use it in GitHub Desktop.
SwiftUI Text with NSAttributedString, HTML or Markdown with tappable Hyperlinks
// full recipe at https://swiftuirecipes.com/blog/hyperlinks-in-swiftui-text
import SwiftUI
import SwiftUIFlowLayout
import MarkdownKit
struct HyperlinkTest: View {
var body: some View {
VStack {
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>.")
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).")
#if compiler(>=5.5)
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).")
#endif
}.padding()
}
}
struct HyperlinkTest_Previews: PreviewProvider {
static var previews: some View {
HyperlinkTest()
}
}
struct HyperlinkText: View {
private let pairs: [StringWithAttributes]
init(_ attributedString: NSAttributedString) {
pairs = attributedString.stringsWithAttributes
}
init(markdown: String) {
self.init(MarkdownParser().parse(markdown))
}
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)
} else {
return nil
}
}
var body: some View {
FlowLayout(mode: .vstack,
binding: .constant(false),
items: pairs,
itemSpacing: 0) { pair in
if let link = pair.attrs[.link],
let url = link as? URL {
Text(pair)
.onTapGesture {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
}
}
} else {
Text(pair)
}
}
}
}
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)
}
}
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
}
}
extension Text {
init(_ singleAttribute: StringWithAttributes) {
let string = singleAttribute.string
let attrs = singleAttribute.attrs
var text = Text(string)
if let font = attrs[.font] as? UIFont {
text = text.font(.init(font))
}
if let color = attrs[.foregroundColor] as? UIColor {
text = text.foregroundColor(Color(color))
}
if let kern = attrs[.kern] as? CGFloat {
text = text.kerning(kern)
}
if #available(iOS 14.0, *) {
if let tracking = attrs[.tracking] as? CGFloat {
text = text.tracking(tracking)
}
}
if let strikethroughStyle = attrs[.strikethroughStyle] as? NSNumber, strikethroughStyle != 0 {
if let strikethroughColor = (attrs[.strikethroughColor] as? UIColor) {
text = text.strikethrough(true, color: Color(strikethroughColor))
} else {
text = text.strikethrough(true)
}
}
if let underlineStyle = attrs[.underlineStyle] as? NSNumber,
underlineStyle != 0 {
if let underlineColor = (attrs[.underlineColor] as? UIColor) {
text = text.underline(true, color: Color(underlineColor))
} else {
text = text.underline(true)
}
}
if let baselineOffset = attrs[.baselineOffset] as? NSNumber {
text = text.baselineOffset(CGFloat(baselineOffset.floatValue))
}
self = text
}
init(_ attributes: [StringWithAttributes]) {
self.init("")
for singleAttribute in attributes {
self = self + Text(singleAttribute)
}
}
init(_ attributedString: NSAttributedString) {
self.init(attributedString.stringsWithAttributes)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment