UITextView with hyperlink text
Asked Answered
C

12

46

With a non-editable UITextView, I would like to embed text like this in iOS9+:

Just click here to register

I can create a function and manipulate the text but is there a simpler way?

I see that I can use NSTextCheckingTypeLink so getting the text clickable without the 'click here' part is straightforward in Interface Builder:

Just http://example.com to register

I'm using Xcode 8 and Swift 3 if that's relevant.

Creatural answered 30/8, 2016 at 23:3 Comment(0)
D
88

Set isEditable = false or the text view will go into text-editing mode when user taps on it.

Swift 4 and later

let attributedString = NSMutableAttributedString(string: "Just click here to register")
let url = URL(string: "https://www.apple.com")!

// Set the 'click here' substring to be the link
attributedString.setAttributes([.link: url], range: NSMakeRange(5, 10))

self.textView.attributedText = attributedString
self.textView.isUserInteractionEnabled = true
self.textView.isEditable = false

// Set how links should appear: blue and underlined
self.textView.linkTextAttributes = [
    .foregroundColor: UIColor.blue,
    .underlineStyle: NSUnderlineStyle.single.rawValue
]
Doroteya answered 30/8, 2016 at 23:39 Comment(4)
UITextItemInteraction is iOS10+ but I need iOS9+. I'll see if I can fix and vote for this in a second.Creatural
In swift 4, the attributes have been renamed from NSLinkAttributedName to .link and NSForegroundColorAttributeName to .foregroundColorDisremember
If you have not read the question, be aware that NSMakeRange(5, 10) in the example above can be interpreted as a range going from 5 -> index of the first character ('c') to and excluded 10 -> index of the last character ('k'), but you will likely get an out of bounds error this way. NSMakeRange actually takes 5 -> index of the first character ('c') and 10 -> length of range.Woodruff
I need following answer too to make the link clickable. https://mcmap.net/q/267151/-can-39-t-make-url-clickable-in-uitextviewCashier
C
26

If you want to use multiple hyperlinks you can use this alternative for Swift 5

extension UITextView {

  func addHyperLinksToText(originalText: String, hyperLinks: [String: String]) {
    let style = NSMutableParagraphStyle()
    style.alignment = .left
    let attributedOriginalText = NSMutableAttributedString(string: originalText)
    for (hyperLink, urlString) in hyperLinks {
        let linkRange = attributedOriginalText.mutableString.range(of: hyperLink)
        let fullRange = NSRange(location: 0, length: attributedOriginalText.length)
        attributedOriginalText.addAttribute(NSAttributedString.Key.link, value: urlString, range: linkRange)
        attributedOriginalText.addAttribute(NSAttributedString.Key.paragraphStyle, value: style, range: fullRange)
        attributedOriginalText.addAttribute(NSAttributedString.Key.font, value: YourFont, range: fullRange)
    }
    
    self.linkTextAttributes = [
        NSAttributedString.Key.foregroundColor: YourColor,
        NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue,
    ]
    self.attributedText = attributedOriginalText
  }
}

Usage:

yourTextView.addHyperLinksToText(originalText: "Testing hyperlinks here and there", hyperLinks: ["here": "someUrl1", "there": "someUrl2"])
Caddric answered 4/2, 2020 at 17:10 Comment(2)
It works ! Thanks. This is super helpful with multiple links! :)Tenterhook
It works as expected. Also it is very reusable. Thanks bro for totally sorted answer.Daniels
P
13

The same solution for Swift 3 using extensions :

A. Add extension -

extension UITextView {
    func hyperLink(originalText: String, hyperLink: String, urlString: String) {
        let style = NSMutableParagraphStyle()
        style.alignment = .center
        let attributedOriginalText = NSMutableAttributedString(string: originalText)
        let linkRange = attributedOriginalText.mutableString.range(of: hyperLink)
        let fullRange = NSMakeRange(0, attributedOriginalText.length)
        attributedOriginalText.addAttribute(NSLinkAttributeName, value: urlString, range: linkRange)
        attributedOriginalText.addAttribute(NSParagraphStyleAttributeName, value: style, range: fullRange)
        attributedOriginalText.addAttribute(NSFontAttributeName, value: UIFont.systemFont(ofSize: 10), range: fullRange)
        self.linkTextAttributes = [
            NSForegroundColorAttributeName: UIConfig.primaryColour,
            NSUnderlineStyleAttributeName: NSUnderlineStyle.styleSingle.rawValue,
        ]
        self.attributedText = attributedOriginalText
    }
}

B. Add link url - let linkUrl = "https://www.my_website.com"

C. Implement UITextViewDelegate in your ViewController like this -

 class MyViewController: UIViewController, UITextViewDelegate { 
 }

D. Add delegate method to handle tap events -

func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
    if (URL.absoluteString == linkUrl) {
        UIApplication.shared.openURL(URL)
    }
    return false
    }
}

E. And finally, things to make sure for your UITextView under attribute inspector -

  1. Behaviour - Editable is turned OFF & Selectable is turned ON.
  2. Data Detectors - Link is turned ON.

Usage -

textView.hyperLink(originalText: "To find out more please visit our website", hyperLink: "website", urlString: linkUrl)

Cheers & happy coding!

Premise answered 23/3, 2018 at 13:32 Comment(1)
also don't forget to add textView.delegate = selfForespeak
F
9

Swift 5 This is based on Tejas' answer as a few items in both classes were deprecated.

extension UITextView {


func hyperLink(originalText: String, hyperLink: String, urlString: String) {

    let style = NSMutableParagraphStyle()
    style.alignment = .left

    let attributedOriginalText = NSMutableAttributedString(string: originalText)
    let linkRange = attributedOriginalText.mutableString.range(of: hyperLink)
    let fullRange = NSMakeRange(0, attributedOriginalText.length)
    attributedOriginalText.addAttribute(NSAttributedString.Key.link, value: urlString, range: linkRange)
    attributedOriginalText.addAttribute(NSAttributedString.Key.paragraphStyle, value: style, range: fullRange)
    attributedOriginalText.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.blue, range: fullRange)
    attributedOriginalText.addAttribute(NSAttributedString.Key.font, value: UIFont.systemFont(ofSize: 10), range: fullRange)

    self.linkTextAttributes = [
        kCTForegroundColorAttributeName: UIColor.blue,
        kCTUnderlineStyleAttributeName: NSUnderlineStyle.single.rawValue,
        ] as [NSAttributedString.Key : Any]

    self.attributedText = attributedOriginalText
}

Don't forget to add UITextViewDelegate to your view controller and set your let linkUrl = "https://example.com"

func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
    if (URL.absoluteString == linkUrl) {
        UIApplication.shared.open(URL) { (Bool) in

        }
    }
    return false
}

Usage stays the same:

textView.hyperLink(originalText: "To find out more please visit our website", hyperLink: "website", urlString: linkUrl)
Ferdinand answered 6/5, 2019 at 13:56 Comment(1)
You must set "TextView's Data Detector" --> "Link" property from storyboard "Attribute Inspector". Then only this code will work.Jehu
T
6

Using Swift >= 4:

let descriptionText = NSMutableAttributedString(string:"To learn more, check out our ", attributes: [:])

let linkText = NSMutableAttributedString(string: "Privacy Policy and Terms of Use", attributes: [NSAttributedString.Key.link: URL(string: example.com)!])

descriptionText.append(linkText)
Tarim answered 23/9, 2019 at 18:18 Comment(0)
E
5

Swift 4 code. May be I'm the only one who needs to set several links and color the words in one message. I created an AttribTextHolder class to accumulate all information about text inside this holder and easily pass it between objects to set text to UITextView somewhere deep inside a controller.

class AttribTextHolder {

        enum AttrType {
            case link
            case color
        }

        let originalText: String
        var attributes: [(text: String, type: AttrType, value: Any)]


        init(text: String, attrs: [(text: String, type: AttrType, value: Any)] = [])
        {
            originalText = text
            attributes = attrs
        }

        func addAttr(_ attr: (text: String, type: AttrType, value: Any)) -> AttribTextHolder {
            attributes.append(attr)
            return self
        }

        func setTo(textView: UITextView)
        {
            let style = NSMutableParagraphStyle()
            style.alignment = .left

            let attributedOriginalText = NSMutableAttributedString(string: originalText)

            for item in attributes {
                let arange = attributedOriginalText.mutableString.range(of: item.text)
                switch item.type {
                case .link:
                    attributedOriginalText.addAttribute(NSAttributedString.Key.link, value: item.value, range: arange)
                case .color:
                    var color = UIColor.black
                    if let c = item.value as? UIColor { color = c }
                    else if let s = item.value as? String { color = s.color() }
                    attributedOriginalText.addAttribute(NSAttributedString.Key.foregroundColor, value: color, range: arange)
                default:
                    break
                }
            }

            let fullRange = NSMakeRange(0, attributedOriginalText.length)
            attributedOriginalText.addAttribute(NSAttributedString.Key.paragraphStyle, value: style, range: fullRange)

            textView.linkTextAttributes = [
                kCTForegroundColorAttributeName: UIColor.blue,
                kCTUnderlineStyleAttributeName: NSUnderlineStyle.styleSingle.rawValue,
            ] as [String : Any]

            textView.attributedText = attributedOriginalText
        }
 }

Use it like this:

 let txt = AttribTextHolder(text: "To find out more visit our website or email us your questions")
            .addAttr((text: "our website", type: .link, "http://example.com"))
            .addAttr((text: "our website", type: .color, "#33BB22"))
            .addAttr((text: "email us", type: .link, "mailto:[email protected]"))
            .addAttr((text: "email us", type: .color, UIColor.red))
 ....
 ....
 txt.setTo(textView: myUITextView)

Also in this code I use simple String extension to convert String hex values into UIColor objects

extension String {
/// Converts string color (ex: #23FF33) into UIColor
func color() -> UIColor {
    let hex = self.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
    var int = UInt32()
    Scanner(string: hex).scanHexInt32(&int)
    let a, r, g, b: UInt32
    switch hex.characters.count {
    case 3: // RGB (12-bit)
        (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
    case 6: // RGB (24-bit)
        (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
    case 8: // ARGB (32-bit)
        (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
    default:
        (a, r, g, b) = (255, 0, 0, 0)
    }
    return UIColor(red: CGFloat(r) / 255, green: CGFloat(g) / 255, blue: CGFloat(b) / 255, alpha: CGFloat(a) / 255)
  }
}
Evert answered 18/7, 2019 at 18:22 Comment(1)
This is great stuff. Consider packaging it up in a library!Sunderance
X
2

The same solution for Swift 4 using extensions:

extension UITextView {


    func hyperLink(originalText: String, hyperLink: String, urlString: String) {

            let style = NSMutableParagraphStyle()
            style.alignment = .left

            let attributedOriginalText = NSMutableAttributedString(string: originalText)
            let linkRange = attributedOriginalText.mutableString.range(of: hyperLink)
            let fullRange = NSMakeRange(0, attributedOriginalText.length)
            attributedOriginalText.addAttribute(NSAttributedStringKey.link, value: urlString, range: linkRange)
            attributedOriginalText.addAttribute(NSAttributedStringKey.paragraphStyle, value: style, range: fullRange)
            attributedOriginalText.addAttribute(NSAttributedStringKey.foregroundColor, value: UIColor.blue, range: fullRange)
            attributedOriginalText.addAttribute(NSAttributedStringKey.font, value: UIFont.systemFont(ofSize: 10), range: fullRange)

            self.linkTextAttributes = [
               kCTForegroundColorAttributeName: UIColor.blue,
               kCTUnderlineStyleAttributeName: NSUnderlineStyle.styleSingle.rawValue,
            ] as [String : Any]


            self.attributedText = attributedOriginalText
        }

}
Xylene answered 19/6, 2018 at 10:30 Comment(0)
F
1

A safer solution to implement hyperlink via UITextView

var termsConditionsTextView: UITextView = {
let view = UITextView()
 view.backgroundColor = .clear
 view.textAlignment = .left
 
 let firstTitleString = "By registering for THIS_APP I agree with the "
 let secondTitleString = "Terms & Conditions"
 let finishTitleString = firstTitleString + secondTitleString
 let attributedString = NSMutableAttributedString(string: finishTitleString)
 attributedString.addAttribute(.link, value: "https://stackoverflow.com", range: NSRange(location: firstTitleString.count, length: secondTitleString.count))
 
 view.attributedText = attributedString
 view.textContainerInset = .zero
 view.linkTextAttributes = [
     .foregroundColor: UIColor.blue,
     .underlineStyle: NSUnderlineStyle.single.isEmpty
 ]
 
 view.font = view.font = UIFont(name: "YOUR_FONT_NAME", size: 16)
 view.textColor = UIColor.black
 
 return view }()
Frogman answered 7/10, 2020 at 11:32 Comment(0)
E
1

SWIFT 5 AND MORE THAN ONE LINK

import UIKit

public extension UITextView {
    
    func hyperLink(originalText: String, linkTextsAndTypes: [String: String]) {
        
        let style = NSMutableParagraphStyle()
        style.alignment = .left
        
        let attributedOriginalText = NSMutableAttributedString(string: originalText)
        
        for linkTextAndType in linkTextsAndTypes {
            let linkRange = attributedOriginalText.mutableString.range(of: linkTextAndType.key)
            let fullRange = NSRange(location: 0, length: attributedOriginalText.length)
            attributedOriginalText.addAttribute(NSAttributedString.Key.link, value: linkTextAndType.value, range: linkRange)
            attributedOriginalText.addAttribute(NSAttributedString.Key.paragraphStyle, value: style, range: fullRange)
            attributedOriginalText.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.blue, range: fullRange)
            attributedOriginalText.addAttribute(NSAttributedString.Key.font, value: UIFont.systemFont(ofSize: 10), range: fullRange)
        }
        
        self.linkTextAttributes = [
            kCTForegroundColorAttributeName: UIColor.blue,
            kCTUnderlineStyleAttributeName: NSUnderlineStyle.single.rawValue
        ] as [NSAttributedString.Key: Any]
        
        self.attributedText = attributedOriginalText
    }
}

And the usage in your viewController:

@IBOutlet weak var termsHyperlinkTextView: UITextView! {
        didSet {
            termsHyperlinkTextView.delegate = self
            termsHyperlinkTextView.hyperLink(originalText: "Check out terms & conditions or our privacy policy",
                                             linkTextsAndTypes: ["terms & conditions": LinkType.termsAndConditions.rawValue,
                                                                 "privacy policy": LinkType.privacyPolicy.rawValue])

        }
    }
enum LinkType: String {
        case termsAndConditions
        case privacyPolicy
    }
// MARK: - UITextViewDelegate
extension ViewController: UITextViewDelegate {
    func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
        if let linkType = LinkType(rawValue: URL.absoluteString) {
            // TODO: handle linktype here with switch or similar.
        }
        return false
    }
}
Equilibrant answered 21/1, 2021 at 13:1 Comment(0)
S
0

To implement such feature for several urls from raw text: Full guide:

  1. Use UITextView instead of UILabel
  2. Use such properties:
   textView.isEditable = false
   textView.isScrollEnabled = false
   textView.isSelectable = true // this line is VERY important!
  1. You needn't implement UITextViewDelegate. You needn't to implement shouldInteractWith. That should work. But if you want you can do. Do not forget you should use new API for iOS 17 and newer if you need custom url handle. Here an answer how to do it. But don't use it for common implementation.

  2. Implement URL parsing method:

extension String {
    func extractURLs() -> [URL] {
        var urls : [URL] = []
        do {
            let detector = try NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
            detector.enumerateMatches(in: self, options: [], range: NSMakeRange(0, self.count), using: { (result, _, _) in
                if let match = result, let url = match.url {
                    urls.append(url)
                }
            })
        } catch let error as NSError {
            print(error.localizedDescription)
        }
        return urls
    }
}


  1. Implement such hyperlink textview string format:
extension UITextView {

    func hyperLink(originalText: String, pairs: [(link: String, url: String)]) {

        let style = NSMutableParagraphStyle()
        style.alignment = .left

        let attributedOriginalText = NSMutableAttributedString(string: originalText)
        for pair in pairs {
            let hyperLink = pair.link
            let urlString = pair.url
/// if you want to handle the same text several times, implement here the proper code. I mean the algorithm finds the first range of the text.
            let linkRange = attributedOriginalText.mutableString.range(of: hyperLink)
            attributedOriginalText.addAttribute(NSAttributedString.Key.link, value: urlString, range: linkRange)
            attributedOriginalText.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.blue, range: linkRange)
        }
        let fullRange = NSMakeRange(0, attributedOriginalText.length)
        attributedOriginalText.addAttribute(NSAttributedString.Key.paragraphStyle, value: style, range: fullRange)
        attributedOriginalText.addAttribute(NSAttributedString.Key.font, value: UIFont.systemFont(ofSize: 15), range: fullRange)

        self.linkTextAttributes = [
            kCTForegroundColorAttributeName: UIColor.blue,
            kCTUnderlineStyleAttributeName: NSUnderlineStyle.single.rawValue,
        ] as [NSAttributedString.Key : Any]

        self.attributedText = attributedOriginalText
    }
}

Sample:

let string = """
stackoverflow: https://stackoverflow.com
google: https://www.google.com
"""
let urls = string.extractURLs()
let pairs = urls.map { (link: $0.absoluteString, url: $0.absoluteString) }
textViewContents.hyperLink(originalText: string, pairs: pairs)

The result

enter image description here

Saxecoburggotha answered 25/2 at 20:6 Comment(0)
C
-1

You could use this simple method to add a hyperlink to any set of characters starting with tag

func addLink(forString string : NSMutableAttributedString
        ,baseURL : String
        ,tag : String){
        let array = string.string.replacingOccurrences(of: "\n", with: " ").components(separatedBy: " ")
        let filterArray = array.filter { (string) -> Bool in
            return string.contains(tag)
        }
        for element in filterArray {
            let removedHashtag = element.replacingOccurrences(of: tag, with: "")
            let url = baseURL + removedHashtag
            let range = NSString.init(string: (string.string)).range(of: element)
            string.addAttributes([NSAttributedStringKey.link : url.replacingOccurrences(of: " ", with: "")], range: range)
        }
    }
Citron answered 31/10, 2018 at 10:3 Comment(0)
C
-5

I wanted to do the same thing and ended up just using a UIButton with the title "click here" surrounded by UILabels "just " and " to register", and then:

@IBAction func btnJustClickHereLink(_ sender: UIButton) {
    if let url = URL(string: "http://example.com") {
        UIApplication.shared.openURL(url)
    }
}
Churchgoer answered 22/11, 2016 at 9:17 Comment(1)
This is not a proper solution as it may not be scalable to different device sizes, Too much labels and button combination and constraints might break. Plus what if single test have multiple links? How many buttons and labels will be added then? Textfield or Textviews with attributed strings are proper solutions.Premise

© 2022 - 2024 — McMap. All rights reserved.