34

Is there any way in SwiftUI to open browser, when tapping on some part of the text.

I tried the above solution but it doesn't work because onTapGesture returns View which you cannot add to Text

Text("Some text ").foregroundColor(Color(UIColor.systemGray)) +
Text("clickable subtext")
   .foregroundColor(Color(UIColor.systemBlue))
   .onTapGesture {

   }

I want to have tappable subtext in the main text that's why using HStack will not work

Karen Karapetyan
  • 704
  • 1
  • 9
  • 18

9 Answers9

45

Update for iOS 15 and higher: There is a new Markdown formatting support for Text, such as:

Text("Some text [clickable subtext](some url) *italic ending* ")

you may check WWDC session with a timecode for details

The old answer for iOS 13 and 14:

Unfortunately there is nothing that resembles NSAttributedString in SwiftUI. And you have only a few options. In this answer you can see how to use UIViewRepresentable for creating an old-school UILabel with click event, for example. But now the only SwiftUI way is to use HStack:

struct TappablePieceOfText: View {
    
    var body: some View {
        
        HStack(spacing: 0) {
            Text("Go to ")
                .foregroundColor(.gray)

            Text("stack overflow")
                .foregroundColor(.blue)
                .underline()
                .onTapGesture {
                    let url = URL.init(string: "https://stackoverflow.com/")
                    guard let stackOverflowURL = url, UIApplication.shared.canOpenURL(stackOverflowURL) else { return }
                    UIApplication.shared.open(stackOverflowURL)
                }
            
            Text(" and enjoy")
                .foregroundColor(.gray)
        }
        
        
    }
}

UPDATE Added solution with UITextView and UIViewRepresentable. I combined everything from added links and the result is quite good, I think:

import SwiftUI
import UIKit

struct TappablePieceOfText: View {
    
    var body: some View {
        TextLabelWithHyperlink()
            .frame(width: 300, height: 110)
    }
    
}

struct TextLabelWithHyperlink: UIViewRepresentable {
    
    func makeUIView(context: Context) -> UITextView {
        
        let standartTextAttributes: [NSAttributedString.Key : Any] = [
            NSAttributedString.Key.font: UIFont.systemFont(ofSize: 20),
            NSAttributedString.Key.foregroundColor: UIColor.gray
        ]
        
        let attributedText = NSMutableAttributedString(string: "You can go to ")
        attributedText.addAttributes(standartTextAttributes, range: attributedText.range) // check extention
        
        let hyperlinkTextAttributes: [NSAttributedString.Key : Any] = [
            NSAttributedString.Key.font: UIFont.systemFont(ofSize: 20),
            NSAttributedString.Key.foregroundColor: UIColor.blue,
            NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue,
            NSAttributedString.Key.link: "https://stackoverflow.com"
        ]
        
        let textWithHyperlink = NSMutableAttributedString(string: "stack overflow site")
        textWithHyperlink.addAttributes(hyperlinkTextAttributes, range: textWithHyperlink.range)
        attributedText.append(textWithHyperlink)
        
        let endOfAttrString = NSMutableAttributedString(string: " end enjoy it using old-school UITextView and UIViewRepresentable")
        endOfAttrString.addAttributes(standartTextAttributes, range: endOfAttrString.range)
        attributedText.append(endOfAttrString)
        
        let textView = UITextView()
        textView.attributedText = attributedText
        
        textView.isEditable = false
        textView.textAlignment = .center
        textView.isSelectable = true
        
        return textView
    }

    func updateUIView(_ uiView: UITextView, context: Context) {}
    
}

result of HStack and Text: HStack and Text

result of UIViewRepresentable and UITextView:

enter image description here

UPDATE 2: here is a NSMutableAttributedString little extension:

extension NSMutableAttributedString {
    
    var range: NSRange {
        NSRange(location: 0, length: self.length)
    }
    
}
Hrabovskyi Oleksandr
  • 3,070
  • 2
  • 17
  • 36
13

I didn't have the patience to make the UITextView and UIViewRepresentable work, so instead I made the whole paragraph tappable but still kept the underscored URL look/feel. Especially helpful if you are trying to add Terms of Service URL link to your app.

enter image description here

The code is fairly simple:

Button(action: {
    let tosURL = URL.init(string: "https://www.google.com")! // add your link here
    if UIApplication.shared.canOpenURL(tosURL) {
        UIApplication.shared.open(tosURL)
    }
}, label: {
    (Text("Store.ly helps you find storage units nearby. By continuing, you agree to our ")
        + Text("Terms of Service.")
            .underline()
        )
        .frame(maxWidth: .infinity, alignment: .leading)
        .font(Font.system(size: 14, weight: .medium))
        .foregroundColor(Color.black)
        .fixedSize(horizontal: false, vertical: true)
})
    .padding([.horizontal], 20)

Zorayr
  • 23,770
  • 8
  • 136
  • 129
6

Base on Dhaval Bera's code, I put some struct.

struct TextLabelWithHyperLink: UIViewRepresentable {
  
  @State var tintColor: UIColor
  
  @State var hyperLinkItems: Set<HyperLinkItem>
  
  private var _attributedString: NSMutableAttributedString
  
  private var openLink: (HyperLinkItem) -> Void
  
  init (
    tintColor: UIColor,
    string: String,
    attributes: [NSAttributedString.Key : Any],
    hyperLinkItems: Set<HyperLinkItem>,
    openLink: @escaping (HyperLinkItem) -> Void
  ) {
    self.tintColor = tintColor
    self.hyperLinkItems = hyperLinkItems
    self._attributedString = NSMutableAttributedString(
      string: string,
      attributes: attributes
    )
    self.openLink = openLink
  }
  
  
  func makeUIView(context: Context) -> UITextView {
    let textView = UITextView()
    textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
    textView.isEditable = false
    textView.isSelectable = true
    textView.tintColor = self.tintColor
    textView.delegate = context.coordinator
    textView.isScrollEnabled = false
    return textView
  }
  
  func updateUIView(_ uiView: UITextView, context: Context) {
   
    for item in hyperLinkItems {
      let subText = item.subText
      let link = item.subText.replacingOccurrences(of: " ", with: "_")
      
      _attributedString
        .addAttribute(
          .link,
          value: String(format: "https://%@", link),
          range: (_attributedString.string as NSString).range(of: subText)
        )
    }
    
    uiView.attributedText = _attributedString
  }
  
  func makeCoordinator() -> Coordinator {
    Coordinator(parent: self)
  }
  
  class Coordinator: NSObject, UITextViewDelegate {
    var parent : TextLabelWithHyperLink
    
    init( parent: TextLabelWithHyperLink ) {
      self.parent = parent
    }
    
    func textView(
      _ textView: UITextView,
      shouldInteractWith URL: URL,
      in characterRange: NSRange,
      interaction: UITextItemInteraction
    ) -> Bool {
      
      let strPlain = URL.absoluteString
        .replacingOccurrences(of: "https://", with: "")
        .replacingOccurrences(of: "_", with: " ")
      
      if let ret = parent.hyperLinkItems.first(where: { $0.subText == strPlain }) {
        parent.openLink(ret)
      }
      
      return false
    }
  }
}

struct HyperLinkItem: Hashable {
    
  let subText : String
  let attributes : [NSAttributedString.Key : Any]?
  
  init (
    subText: String,
    attributes: [NSAttributedString.Key : Any]? = nil
  ) {
    self.subText = subText
    self.attributes = attributes
  }
  
  func hash(into hasher: inout Hasher) {
    hasher.combine(subText)
  }
    
  static func == (lhs: HyperLinkItem, rhs: HyperLinkItem) -> Bool {
    lhs.hashValue == rhs.hashValue
  }
}

Usage:


TextLabelWithHyperLink(
  tintColor: .green,
  string: "Please contact us by filling contact form. We will contact with you shortly.  Your request will be processed in accordance with the Terms of Use and Privacy Policy.",
  attributes: [:],
  hyperLinkItems: [
    .init(subText: "processed"),
    .init(subText: "Terms of Use"),
  ],
  openLink: {
  (tappedItem) in
    print("Tapped link: \(tappedItem.subText)")
  }
)
seulbeom kim
  • 61
  • 1
  • 2
6

Starting from iOS 15 you can use AttributedString and Markdown with Text.

An example of using Markdown:

Text("Plain text. [This is a tappable link](https://stackoverflow.com)")

AttributedString gives you more control over formatting. For example, you can change a link color:

var string = AttributedString("Plain text. ")
        
var tappableText = AttributedString("I am tappable!")
tappableText.link = URL(string: "https://stackoverflow.com")
tappableText.foregroundColor = .green

string.append(tappableText)

Text(string)

Here is what it looks like:

tappable text

A side note: if you want your tappable text to have a different behavior from opening a URL in a browser, you can define a custom URL scheme for your app. Then you will be able to handle tap events on a link using onOpenURL(perform:) that registers a handler to invoke when the view receives a url for the scene or window the view is in.

1

Tappable String using UITextView

struct TextLabelWithHyperlink: UIViewRepresentable {

@State var tintColor: UIColor = UIColor.black
@State var arrTapableString: [String] = []

var configuration = { (view: UITextView) in }
var openlink = {(strtext: String) in}

func makeUIView(context: Context) -> UITextView {
    
    let textView = UITextView()
    textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
    textView.isEditable = false
    textView.isSelectable = true
    textView.tintColor = self.tintColor
    textView.delegate = context.coordinator
    textView.isScrollEnabled = false
    return textView
}

func updateUIView(_ uiView: UITextView, context: Context) {
    configuration(uiView)
    let stringarr  = NSMutableAttributedString(attributedString: uiView.attributedText)
    for strlink in arrTapableString{
        let link = strlink.replacingOccurrences(of: " ", with: "_")
        stringarr.addAttribute(.link, value: String(format: "https://%@", link), range: (stringarr.string as NSString).range(of: strlink))
    }
    uiView.attributedText = stringarr
}

func makeCoordinator() -> Coordinator {
    Coordinator(parent: self)
}

class Coordinator: NSObject,UITextViewDelegate {
    var parent : TextLabelWithHyperlink
    init(parent: TextLabelWithHyperlink) {
        self.parent = parent
    }
    
    func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
        let strPlain = URL.absoluteString.replacingOccurrences(of: "https://", with: "").replacingOccurrences(of: "_", with: " ")
        if (self.parent.arrTapableString.contains(strPlain)) {
            self.parent.openlink(strPlain)
        }
        return false
    }
    
}}

Implementation in swiftui

TextLabelWithHyperlink(arrTapableString: ["Terms of Use", "Privacy Policy"]) { (textView) in
                            let string = "Please contact us by filling contact form. We will contact with you shortly.  Your request will be processed in accordance with the Terms of Use and Privacy Policy."
                            
                            let attrib = NSMutableAttributedString(string: string, attributes: [.font: UIFont(name: Poodlife_Font.oxygen_regular, size: 14)!,.foregroundColor:  UIColor.black])
                            
                            attrib.addAttributes([.font: UIFont(name: Font.oxygen_bold, size: 14)!,
                                                  .foregroundColor:  UIColor.black], range: (string as NSString).range(of: "Terms of Use"))
                            
                            attrib.addAttributes([.font: UIFont(name: Font.oxygen_bold, size: 14)!,
                                                  .foregroundColor:  UIColor.black,
                                                  .link: "Privacy_Policy"], range: (string as NSString).range(of: "Privacy Policy"))
                            
                            textView.attributedText = attrib
                        } openlink: { (tappedString) in
                            print("Tapped link:\(tappedString)")
                        }
Dhaval Bera
  • 109
  • 4
1

Below is my fully SwiftUI solution. With the below solution, any container you put this in will nicely be formatted and you can make the specific text you want clickable.

struct TermsAndPrivacyText: View {
  @State private var sheet: TermsOrPrivacySheet? = nil
  let string = "By signing up, you agree to XXXX's Terms & Conditions and Privacy Policy"
  
  
  enum TermsOrPrivacySheet: Identifiable {
    case terms, privacy
    
    var id: Int {
      hashValue
    }
  }
  
  
  func showSheet(_ string: String) {
    if ["Terms", "&", "Conditions"].contains(string) {
      sheet = .terms
    }
    else if ["Privacy", "Policy"].contains(string) {
      sheet = .privacy
    }
  }
  
  
  func fontWeight(_ string: String) -> Font.Weight {
    ["Terms", "&", "Conditions", "Privacy", "Policy"].contains(string) ? .medium : .light
  }

  
  private func createText(maxWidth: CGFloat) -> some View {
    var width = CGFloat.zero
    var height = CGFloat.zero
    let stringArray = string.components(separatedBy: " ")
    
    
    return
      ZStack(alignment: .topLeading) {
        ForEach(stringArray, id: \.self) { string in
          Text(string + " ")
            .font(Theme.Fonts.ofSize(14))
            .fontWeight(fontWeight(string))
            .onTapGesture { showSheet(string) }
            .alignmentGuide(.leading, computeValue: { dimension in
              if (abs(width - dimension.width) > maxWidth) {
                width = 0
                height -= dimension.height
              }
              
              let result = width
              if string == stringArray.last {
                width = 0
              }
              else {
                width -= dimension.width
               }
              
              return result
            })
            .alignmentGuide(.top, computeValue: { dimension in
              let result = height
              if string == stringArray.last { height = 0 }
              return result
            })
          }
      }
      .frame(maxWidth: .infinity, alignment: .topLeading)
    }
  
  
  var body: some View {
      GeometryReader { geo in
        ZStack {
          createText(maxWidth: geo.size.width)
        }
      }
      .frame(maxWidth: .infinity)
      .sheet(item: $sheet) { item in
        switch item {
        case .terms:
          TermsAndConditions()
        case .privacy:
          PrivacyPolicy()
        }
      }
  }
}

enter image description here

Darkisa
  • 1,899
  • 3
  • 20
  • 40
1

For iOS 14

I used a third party library like Down. It's a lot simpler than creating your own parsing engine.

import SwiftUI
import Down

struct ContentView: View {
    @State var source = NSAttributedString()

    var body: some View {
        VStack {
            TextView(attributedText: source)
                .padding(.horizontal)
                .padding(.vertical, 10)
                .frame(maxWidth: .infinity, minHeight: 64, maxHeight: 80, alignment: .leading)
                .background(Color( red: 236/255, green: 236/255, blue: 248/255))
                .cornerRadius(10)
                .padding()
        }
        .onAppear {
            let down = Down(markdownString: "Work hard to get what you like, otherwise you'll be forced to just like what you get! [tap here](https://apple.com)")
            source = try! down.toAttributedString(.default, stylesheet: "* {font-family: 'Avenir Black'; font-size: 15}")
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

struct TextView: UIViewRepresentable {
    var attributedText: NSAttributedString

    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()
        textView.autocapitalizationType = .sentences
        textView.isSelectable = true
        textView.isEditable = false
        textView.backgroundColor = .clear
        textView.isUserInteractionEnabled = true
        return textView
    }

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

enter image description here

Ардак
  • 11
  • 2
0

I used @АлександрГрабовский answer, but I also had to do some configs to make it work for me. I have 2 links in my text field, both of them have a custom colour and directs the user to different pages. I also didn't want the scroll to be enabled, but if I disabled it the height wouldn't get adjusted and it would stretch to the outside of the view. I tried SO MANY different things and I found, for the moment, a solution that works for me, so I thought I might as well share it here.

This is what I was looking for

Again, thanks to @АлександрГрабовский answer I managed to do it. The only tweaks I had to do were:

  1. set the links attributes related to the text colour to another var and set the "linkTextAttributes" property on the UITextView to that, in order to change the text colour, while the font and link destination I used what was suggested in his response. The text colour didn't change if I set the colour attributes to the link itself.

    let linkAttributes: [NSAttributedString.Key : Any] = [ NSAttributedString.Key.foregroundColor: UIColor(named: "my_custom_green") ?? UIColor.blue ] textView.linkTextAttributes = linkAttributes

  2. I didn't want the UITextView to scroll and the only way I found to keep the multi line height and not scroll (setting isScrollEnabled to false didn't work for me) was to set scrollRangeToVisible to the last string range I had.

    textView.scrollRangeToVisible(ppWithHyperlink.range)

I don't know if this is the best alternative, but it is what I found... hope in the future there's a better way to do this in swiftUI!!!

0

In Ios 15 you can just try

Text("Apple website: [click here](https://apple.com)")
Alexander
  • 127
  • 2
  • 10