Ika
Ika

Reputation: 1773

How to make hyperlinks in SwiftUI

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(" .")
        }
    }
}

Upvotes: 91

Views: 64503

Answers (14)

Lalit kumar
Lalit kumar

Reputation: 2197

Hyperlink

Hypelink color

Bold text

Underline hyperlink Color

        Text("By tapping Done, you agree to the [privacy policy](https://www..) and [Terms and Conditions](https://www.terms.)")
             .padding()
         
         Text("By tapping Done, you agree to the \(Text("[privacy policy](https://www.)")) and \(Text("[Terms and Conditions](https://www.)"))")
             .tint(.red)
             .padding()
         
         Text("**By tapping Done:** you agree to the \(Text("[privacy policy](https://www.)")) and \(Text("[Terms and Conditions](https://www.)"))")
             .padding()
         
         Text("By tapping Done, you agree to the \(Text("[privacy policy](https://www.)").underline()) and \(Text("[Terms and Conditions](https://www.)").underline())")
             .tint(.red)
             .padding()
         
         Text("By tapping Done, you agree to the \(Text("[privacy policy](https://www.)").underline()) and \(Text("[Terms and Conditions](https://www.)").underline())")
             .padding()

enter image description here

Upvotes: 26

Claytog
Claytog

Reputation: 957

Another way to effectively do this, is to use .environment which gives you control over what happens when the user taps a link within the text. For instance, you may want to open the url within a webview inside the app, rather than open in the device browser.

@State private var isPresentedPrivacyPolicy = false
@State private var isPresentedTermsOfUse = false

private let privacyPolicyURL = "https://privacypolicy.com"
private let termsOfUseURL = "https://termsofuse.com"

HStack{
    let privacyPolicyText = "By continuing you accept our [Privacy Policy](\(privacyPolicyURL)) and [Terms of Use](\(termsOfUseURL))."
    Text(privacyPolicyText)
    .environment(\.openURL, OpenURLAction { url in
        if url.absoluteString == privacyPolicyURL {
            isPresentedPrivacyPolicy.toggle()
        } else if url.absoluteString == termsOfUseURL {
            isPresentedTermsOfUse.toggle()
        }
        return .handled
    })
}
.sheet(isPresented: $isPresentedPrivacyPolicy) {
    NavigationStack {
        WebView(url: URL(string: privacyPolicyURL)!) 
        .ignoresSafeArea()
        .navigationTitle("Privacy Policy")
        .navigationBarTitleDisplayMode(.inline)
    }
}
.sheet(isPresented: $isPresentedTermsOfUse) {
    NavigationStack {
        WebView(url: URL(string: termsOfUseURL)!)
        .ignoresSafeArea()
        .navigationTitle("Terms of Use")
        .navigationBarTitleDisplayMode(.inline)
    }
}

And WebView:

import SwiftUI
import WebKit

struct WebView: UIViewRepresentable {

    let url: URL

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

    func updateUIView(_ webView: WKWebView, context: Context) {
        let request = URLRequest(url: url)
        webView.load(request)
    }
}

Upvotes: 4

mahan
mahan

Reputation: 14925

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

Upvotes: 120

Deepak Gupta
Deepak Gupta

Reputation: 61

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()
  }
}

Upvotes: 6

damd
damd

Reputation: 6937

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

Upvotes: 8

zhijie feng
zhijie feng

Reputation: 21

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)
    }
}

Upvotes: 1

Nicolai Harbo
Nicolai Harbo

Reputation: 1169

*** 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 = "[email protected]"
    
    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.

Upvotes: 1

Shrikant Phadke
Shrikant Phadke

Reputation: 418

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)
                    }
            }
            
        }
        
    }

Upvotes: 5

Jacob Jidell
Jacob Jidell

Reputation: 2792

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.

Upvotes: 43

MegaApp Studio
MegaApp Studio

Reputation: 299

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))

Upvotes: 29

TheLegend27
TheLegend27

Reputation: 762

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)
}

Upvotes: 3

Petr Syrov
Petr Syrov

Reputation: 15253

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)
        }
    }
}

Upvotes: -10

Joe
Joe

Reputation: 3761

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())

Upvotes: 2

LuLuGaGa
LuLuGaGa

Reputation: 14378

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(".")
            }
        }
    }
}

Upvotes: 5

Related Questions