43

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.

Adam Nelson
  • 7,932
  • 11
  • 44
  • 64

11 Answers11

85

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
]
Code Different
  • 90,614
  • 16
  • 144
  • 163
  • UITextItemInteraction is iOS10+ but I need iOS9+. I'll see if I can fix and vote for this in a second. – Adam Nelson Aug 31 '16 at 14:26
  • 3
    In swift 4, the attributes have been renamed from NSLinkAttributedName to .link and NSForegroundColorAttributeName to .foregroundColor – Barlow Tucker Jan 11 '18 at 04:10
  • 3
    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. – Martin Feb 05 '20 at 15:08
  • I need following answer too to make the link clickable. https://stackoverflow.com/a/14387132/5845039 – Muhammad Yusuf Jul 07 '20 at 09:21
23

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"])
GoRoS
  • 5,183
  • 2
  • 43
  • 66
Letaief Achraf
  • 600
  • 1
  • 7
  • 14
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!

Tejas
  • 437
  • 5
  • 12
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)
elarcoiris
  • 1,914
  • 4
  • 28
  • 30
  • You must set "TextView's Data Detector" --> "Link" property from storyboard "Attribute Inspector". Then only this code will work. – pallavi Feb 06 '20 at 07:14
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:us@example.com"))
            .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)
  }
}
Northern Captain
  • 1,147
  • 3
  • 25
  • 32
5

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)
Taiwosam
  • 469
  • 7
  • 13
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
        }

}
Eric Aya
  • 69,473
  • 35
  • 181
  • 253
Chetan Dobariya
  • 792
  • 8
  • 14
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 }()
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
    }
}
Nicolai Harbo
  • 1,064
  • 12
  • 25
-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)
        }
    }
Armali
  • 18,255
  • 14
  • 57
  • 171
-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)
    }
}
dawid
  • 374
  • 5
  • 9
  • 2
    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. – Tejas Jun 13 '18 at 07:43