Search code examples
uitextviewnsattributedstringios14xcode12

How to get links ranges from UITextView in Swift


I have a chat app with the UITextView with a dynamic text (comes from the server or is typed by the user). I needed to detect all types of links, and the UITextView does a great job in detecting phone numbers, addresses, hyperlinks and calendar events. Where UITextView falls short is styling, there are multiple problems there:

  1. The address links are not colored with the tintColor or with the color from linkTextAttributes property, or using the appearance() proxy.
  2. All of the links are underlined by default.
  3. Some of the links are colored properly, but I fail to see a pattern why certain links are omitted (looks like it may have something to do with the word wrap).
  4. When setting custom underlineStyle attribute, the text simply disappears.

So I thought the best thing to do would be to still use the UITextView for detecting the links, but then create my own attributed string and style everything the way I want. For this I need the ranges for the links, but I can't find the way to get them.

I tried this:

open func enumerateAttributes(in enumerationRange: NSRange, options opts: NSAttributedString.EnumerationOptions = [], using block: ([NSAttributedString.Key : Any], NSRange, UnsafeMutablePointer<ObjCBool>) -> Void)

But there are never any .link elements, nor any underline attributes if I look for them. So the question is, is there a way to get the link ranges from the UITextView somehow? Or perhaps there is a way to reliably style the links that I'm missing?


Solution

  • To detect the Phone Numbers, addresses etc, you are in fact using the NSDataDetector.

    That's what you in fact set in InterfaceBuilder there:

    enter image description here

    Then:

    let types: NSTextCheckingResult.CheckingType = [.address, .link, .phoneNumber, .date]
    let detector = try? NSDataDetector(types: types.rawValue)
    let matches = detector.matches(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count))
    matches.forEach {
        guard let range = Range($0.range, in: text) else { return }
        let sub = String(text[range])
        print(sub)
    }