54

In Swift, as shown here, you can use NSMutableAttributedString to embed links in text.

How can I achieve this with SwiftUI?

I implemented it as the following, but it does not look how I want it to. this.

import SwiftUI

struct ContentView: View {
    var body: some View {
        HStack {
            Text("By tapping Done, you agree to the ")
            Button(action: {}) {
                Text("privacy policy")
            }
            Text(" and ")
            Button(action: {}) {
                Text("terms of service")
            }
            Text(" .")
        }
    }
}
mahan
  • 12,366
  • 5
  • 48
  • 83
Ika
  • 1,271
  • 1
  • 12
  • 20

12 Answers12

72

iOS 15+ (Swift 5.5 +)

SwiftUI has built-in support for rendering Markdown

To create a link, enclose the link's text in brackets (e.g., [Duck Duck Go]) and then follow it immediately with the URL in parentheses (e.g., (https://duckduckgo.com)).

 Text("[Privacy Policy](https://example.com)")

https://www.markdownguide.org/basic-syntax/#links

String variable

  1. Use init(_ value: String)

Creates a localized string key from the given string value.

let link = "[Duck Duck Go](https://duckduckgo.com)"
Text(.init(link))

String interpolation

  1. Use init(_ value: String)

Creates a localized string key from the given string value.

  let url = "https://duckduckgo.com"
  let link = "[Duck Duck Go](\(url))"
  Text(.init(link))

Attributed text

  1. Use init(_ attributedContent: AttributedString)

Creates a text view that displays styled attributed content.

let markdownLink = try! AttributedString(markdown: "[Duck Duck Go](https://duckduckgo.com)")
Text(markdownLink)

Similar question: Making parts of text bold in SwiftUI


Use the Link View

A control for navigating to a URL.

Link("Privacy Policy", destination: URL(string: "https://example.com")!)

https://developer.apple.com/documentation/swiftui/link

mahan
  • 12,366
  • 5
  • 48
  • 83
  • fantastic, this really helped me for iOS 15! – cole Oct 26 '21 at 09:02
  • 4
    `let str = "[Privacy Policy](https://example.com)" Text(str) // does not work.` – Sylar Nov 08 '21 at 13:31
  • 1
    When using iOS 15+, using the [Link](https://developer.apple.com/documentation/swiftui/link) element would probably be safer (and nicer). Example: `Link("Privacy Policy", destination: URL(string: "https://example.com")!)` – Daniel Dec 13 '21 at 11:52
  • @Daniel Thanks. Agree and updated my answer. But they want to embede link in text. – mahan Dec 13 '21 at 13:36
  • 1
    @mahan The issue here is we can't change the foreground colour. It remains blue even after adding foregroundColor property to Text – BraveEvidence Dec 31 '21 at 04:26
  • @Pritish try this for single text - Text(.init("[Link Example](https://www.google.es/)")) .accentColor(.red) – Shrikant Phadke Apr 06 '22 at 13:48
  • 1
    You might have to use `.init` for a string literal with interpolations, e.g. `Text(.init("[link text](\(url))"))`. – Lauren Yim Jul 12 '22 at 12:36
27

Just as @mahan mention, this works perfectly for iOS 15.0 and above by using markdown language:

Text("[Privacy Policy](https://example.com)")

But if you're using a String variable, and put it into the Text it wouldn't work. Example:

let privacyPolicyText = "Read our [Privacy Policy](https://example.com) here."
Text(privacyPolicyText) // Will not work

Solution for using a String variable

The reason is that Text got multiple initiations. So how do we solve this? The easiest way is just to do:

let privacyPolicyText = "Read our [Privacy Policy](https://example.com) here."
Text(.init(privacyPolicyText))

Result: Read our Privacy Policy here.

Jacob Ahlberg
  • 2,352
  • 6
  • 22
  • 44
18

it's very simple just use LocalizedStringKey for example:

let message = "Hello, www.google.com this is just testing for hyperlinks, check this out our website https://www.apple.in thank you."

Text(LocalizedStringKey(message))
MegaApp Studio
  • 189
  • 1
  • 2
6

To be on the safe side, if your text is not a string literal, you will probably want to use .init. In other words, if there's any string concatenation, interpolation, etc., you may want to use Text(.init(...)).

Just note that .init in this case actually refers to LocalizedStringKey.init, so localization will still be happening, just like when you're just passing a string literal.

Here are some examples and their rendered output in Xcode 14 previews.

let foo = "Foo"
let bar = "Bar"
let link = "link"

Group {
  Text("Foo [Bar](link) Baz") // ✅
  Text("Foo" + " [Bar](link) Baz") // ❌
  Text(foo + " [Bar](link) Baz") // ❌
  Text("\(foo) [Bar](link) Baz") // ✅
  Text("\(foo) [Bar](\(link)) Baz") // ❌
  Text("\(foo) [\(bar)](\(link)) Baz") // ❌
}

Rectangle().height(1)

Group {
  Text(.init("Foo [Bar](link) Baz")) // ✅
  Text(.init("Foo" + " [Bar](link) Baz")) // ✅
  Text(.init(foo + " [Bar](link) Baz")) // ✅
  Text(.init("\(foo) [Bar](link) Baz")) // ✅
  Text(.init("\(foo) [Bar](\(link)) Baz")) // ✅
  Text(.init("\(foo) [\(bar)](\(link)) Baz")) // ✅
}

Rendered output

damd
  • 6,116
  • 7
  • 48
  • 77
4

I tried concatenated Texts with Link in between and these are the ways for iOS 15+ and below iOS 15.

    if #available(iOS 15, *) {
        
        Text("[Seperate Link 1 ](https://www.android.com/intl/en_in/)")
            .font(.caption)
            .foregroundColor(Color.green)
        // green color is not applied.

        Text("[Seperate Link 2 ](https://www.apple.com/in/)")
            .font(.caption)
            .accentColor(Color.green)
        // green is applied.
        
        Text("By authorizing you agree
our ")
            .font(.caption)
            .foregroundColor(Color.black)
        + Text("[Terms and Conditions](https://www.android.com/intl/en_in/)")
            .font(.caption)
            .foregroundColor(Color.green) // default blue is applied
        + Text(" and ")
            .font(.caption)
            .foregroundColor(Color.black)
        + Text("[Privacy Policy](https://www.apple.com/in/)")
            .font(.caption)
            .foregroundColor(Color.green) // default blue
        // cannot use accentColor(Color.green) here
    }
    else{
        // lower iOS versions.
        VStack{
            Text("By authorizing you agree our ")
                .font(.caption)
                .foregroundColor(Color.black)
            
            HStack(spacing: 4 ) {
                Text("Terms and Conditions")
                    .font(.caption)
                    .foregroundColor(Color.green)
                    .onTapGesture {
                        let url = URL.init(string: "https://www.android.com/intl/en_in/")
                        guard let termsAndConditionURL = url, UIApplication.shared.canOpenURL(termsAndConditionURL) else { return }
                        UIApplication.shared.open(termsAndConditionURL)
                    }
                Text("and")
                    .font(.caption)
                    .foregroundColor(Color.black)
                Text("Privacy Policy")
                    .font(.caption)
                    .foregroundColor(Color.green)
                    .onTapGesture {
                        let url = URL.init(string: "https://www.apple.com/in/")
                        guard let privacyPolicyURL = url, UIApplication.shared.canOpenURL(privacyPolicyURL) else { return }
                        UIApplication.shared.open(privacyPolicyURL)
                    }
            }
            
        }
        
    }

Shrikant Phadke
  • 358
  • 2
  • 11
3

Motjaba Hosseni is right so far there is nothing that resembles NSAttributedString in SwiftUI. This should solve your problem for the time being:

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Text("By tapping Done, you agree to the ")
            HStack(spacing: 0) {
                Button("privacy policy") {}
                Text(" and ")
                Button("terms of service") {}
                Text(".")
            }
        }
    }
}
jbnunn
  • 6,161
  • 4
  • 40
  • 65
LuLuGaGa
  • 13,089
  • 6
  • 49
  • 57
  • 5
    This does work as long as the first Text() isn't longer than a single line. – Kuhlemann Apr 05 '20 at 18:12
  • 11
    You can now use `Link("Stackoverflow", destination: URL(string: "https://stackoverflow.com")!)` in iOS 14 and Xcode 12. – Mike Gorski Mar 13 '21 at 15:45
  • 4
    Why is this the answer to the question? This does not address "how to use attributed string to embed links in text in swift ui" (paraphrasing). Meanwhile this post has been viewed over 7k times, and the answer is the comment above this one with 0 upvotes – elight May 13 '21 at 15:26
2

It's always an option to wrap a UIKit view in UIViewRepresentable. Just have to go through the manual process of exposing each attribute you want to change.

struct AttributedText: UIViewRepresentable {
    var attributedText: NSAttributedString

    init(_ attributedText: NSAttributedString) {
        self.attributedText = attributedText
    }

    func makeUIView(context: Context) -> UITextView {
        return UITextView()
    }

    func updateUIView(_ label: UITextView, context: Context) {
        label.attributedText = attributedText
    }
}

//usage: AttributedText(NSAttributedString())
Joe
  • 3,664
  • 25
  • 27
  • 2
    A big limitation of this approach seems to be that UIViewRepresentable does not communicate the intrinsicContentSize of the UITextView up to SwiftUI - AttributedText will take up all the space available to it, which plays havoc with StackViews etc – sam-w Oct 16 '20 at 04:56
2

I know it's a bit late but I solved the same problem using HTML. First I created a small helper and link model.

struct HTMLStringView: UIViewRepresentable {
  let htmlContent: String

  func makeUIView(context: Context) -> WKWebView {
    return WKWebView()
  }

  func updateUIView(_ uiView: WKWebView, context: Context) {
    uiView.loadHTMLString(htmlContent, baseURL: nil)
  }
}

struct TextLink {
    let url: URL
    let title: String
}

Next I created function that changes String to HTML and replaces first occurrence of @link to my tappable link.

var content = "My string with @link."
var link = TextLink(url: URL(string: "https://www.facebook.com")!, title: "Facebook")
var body: some View {
    let bodySize = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body).pointSize
    var html = "<span style=\"font: -apple-system-body; font-size:calc(\(bodySize)px + 1.0vw)\">"

    if let linkRange = content.range(of: "@link") {
        let startText = content[content.startIndex ..< linkRange.lowerBound]
        let endText = content[linkRange.upperBound ..< content.endIndex]
        html += startText
        html += "<a href=\"\(link.url.absoluteString)\">\(link.title)</a>"
        html += endText
    } else {
        html += content
    }
    
    html += "</span>"
    
    return HTMLStringView(htmlContent: html)
}
TheLegend27
  • 741
  • 7
  • 8
1

You can try this way also. I think this is simplest form solution.

import SwiftUI

struct ContentView: View {
  var body: some View {
    VStack {
      Text(getAttriText())
    }
    .environment(\.openURL, OpenURLAction(handler: { url in
      if url.absoluteString.contains("privacy") {
        // action
      }
      
      if url.absoluteString.contains("terms") {
        // action
      }
      return .systemAction // change if you want to discard action
    }))
  }
  
  func getAttriText() -> AttributedString {
    
    var attriString = AttributedString("By tapping Done, you agree to the privacy policy and terms of service")
    attriString.foregroundColor = .black
    
    if let privacyRange = attriString.range(of: "privacy policy") {
      attriString[privacyRange].link = URL(string: "www.apple.com/privacy")
      attriString[privacyRange].underlineStyle = .single
      attriString[privacyRange].foregroundColor = .blue
    }
    
    if let termsRange = attriString.range(of: "terms of service") {
      attriString[termsRange].link = URL(string: "www.apple.com/terms")
      attriString[termsRange].underlineStyle = .single
      attriString[termsRange].foregroundColor = .blue
    }
    
    return attriString
  }
}

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}
desertnaut
  • 57,590
  • 26
  • 140
  • 166
0

iOS 15 below

import SwiftUI
import SwiftUIFlowLayout

public struct HyperlinkText: View {
    private let subStrings: [StringWithLinks]
    
    public init(html: String) {
        let newString = html.replacingOccurrences(of: "<a href=\'(.+)\'>(.+)</a>",
                                                          with: "@&@$2#&#$1@&@",
                                                          options: .regularExpression,
                                                          range: nil)
        self.subStrings = newString.components(separatedBy: "@&@").compactMap{ subString in
            let arr = subString.components(separatedBy: "#&#")
            return StringWithLinks(string: arr[0], link: arr[safe: 1])
        }
    }
    
    public var body: some View {
        FlowLayout(mode: .scrollable,
                   binding: .constant(false),
                   items: subStrings,
                   itemSpacing: 0) { subString in
            if let link = subString.link, let url = URL(string: link) {
                Text(subString.string)
                    .foregroundColor(Color(hexString: "#FF0000EE"))
                    .onTapGesture {
                        if UIApplication.shared.canOpenURL(url) {
                            UIApplication.shared.open(url)
                        }
                    }
                    .fixedSize(horizontal: false, vertical: true)
            } else {
                Text(subString.string).fixedSize(horizontal: false, vertical: true)
            }
        }
    }
}

struct StringWithLinks: Hashable, Identifiable {
    let id = UUID()
    let string: String
    let link: String?
    
    static func == (lhs: StringWithLinks, rhs: StringWithLinks) -> Bool {
        lhs.id == rhs.id
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}
-1

*** iOS 15 ***

You can add emails or phonenumbers like this too:

    var infoText = "For any queries reach out to [email] or call [phone]"
    var phone = "+45 12345678".replacingOccurrences(of: " ", with: "")
    var email = "some-email@some-host.something"
    
    var footerText: String {
        return infoText
            .replacingOccurrences(
                of: "[email]",
                with: "[\(email)](mailto:\(email))"
            )
            .replacingOccurrences(
                of: "[phone]",
                with: "[\(phone)](tel:\(phone))"
            )
    }
Text(.init(footerText))

Just remember the links (phone mainly, as emails doesn't have spaces) can't have spaces.

Also remember that this won't work on the simulator. You need a real device to test it.

Nicolai Harbo
  • 1,064
  • 12
  • 25
-7

Use built-in function +, it looks like a charm:

import SwiftUI

struct ContentView: View {
    var body: some View {
        HStack {
            Button(action: {

            }) {
                Text("By tapping Done, you agree to the ")
              + Text("privacy policy")
                  .foregroundColor(Color.blue)
              + Text(" and ")
              + Text("terms of service")
                  .foregroundColor(Color.blue)
              + Text(".")
            }
            .foregroundColor(Color.black)
        }
    }
}
Petr Syrov
  • 14,689
  • 3
  • 20
  • 30
  • 22
    This makes the entire text clickable, not just the link part. And in this case, there are 2 links and there is no way to differentiate which one was clicked. – Frankenstein Jul 18 '20 at 15:22