iOS
UITextView
attributed text
tap detection
Swift programming

Detecting taps on attributed text in a UITextView in iOS

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

UITextView can display rich text, links, and custom formatting, but tap handling is not automatic for every attributed range. The best solution depends on whether you are dealing with real links or with arbitrary highlighted text that should behave like a button.

If the tapped text represents a URL, email address, or app-specific action, the simplest approach is to add the .link attribute to that range. UITextView already knows how to detect taps on links, and you can intercept them through the delegate.

swift
1import UIKit
2
3final class TermsViewController: UIViewController, UITextViewDelegate {
4    private let textView = UITextView()
5
6    override func viewDidLoad() {
7        super.viewDidLoad()
8
9        textView.delegate = self
10        textView.isEditable = false
11        textView.isScrollEnabled = false
12        textView.backgroundColor = .clear
13
14        let text = NSMutableAttributedString(
15            string: "By continuing, you agree to the Terms of Service."
16        )
17
18        let range = (text.string as NSString).range(of: "Terms of Service")
19        text.addAttribute(.link, value: URL(string: "myapp://terms")!, range: range)
20
21        textView.attributedText = text
22        view.addSubview(textView)
23    }
24
25    func textView(
26        _ textView: UITextView,
27        shouldInteractWith url: URL,
28        in characterRange: NSRange,
29        interaction: UITextItemInteraction
30    ) -> Bool {
31        if url.absoluteString == "myapp://terms" {
32            print("Open terms screen")
33            return false
34        }
35
36        return true
37    }
38}

This is the cleanest option because UITextView handles layout, hit testing, and accessibility for you.

Detect Taps On Arbitrary Attributed Ranges

Sometimes the text is not a real link. You may want a colored username, a mention, or a highlighted phrase to trigger a custom action. In that case, add a tap gesture recognizer and convert the tap location into a character index.

The core idea is:

  1. build NSTextStorage, NSLayoutManager, and NSTextContainer
  2. translate the tap point into text-container coordinates
  3. ask the layout manager for the tapped character index
  4. check whether that index falls inside the attributed range you care about
swift
1import UIKit
2
3final class MentionViewController: UIViewController {
4    private let textView = UITextView()
5    private let mentionAttribute = NSAttributedString.Key("MentionID")
6
7    override func viewDidLoad() {
8        super.viewDidLoad()
9
10        textView.isEditable = false
11        textView.isSelectable = false
12
13        let text = NSMutableAttributedString(string: "Created by @markqian in the mobile team")
14        let mentionRange = (text.string as NSString).range(of: "@markqian")
15        text.addAttribute(.foregroundColor, value: UIColor.systemBlue, range: mentionRange)
16        text.addAttribute(mentionAttribute, value: "user-42", range: mentionRange)
17        textView.attributedText = text
18
19        let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
20        textView.addGestureRecognizer(tap)
21        view.addSubview(textView)
22    }
23
24    @objc private func handleTap(_ gesture: UITapGestureRecognizer) {
25        guard let attributedText = textView.attributedText else { return }
26
27        let layoutManager = NSLayoutManager()
28        let textContainer = NSTextContainer(size: textView.bounds.size)
29        let textStorage = NSTextStorage(attributedString: attributedText)
30
31        textContainer.lineFragmentPadding = 0
32        textContainer.maximumNumberOfLines = textView.textContainer.maximumNumberOfLines
33        textContainer.lineBreakMode = textView.textContainer.lineBreakMode
34
35        layoutManager.addTextContainer(textContainer)
36        textStorage.addLayoutManager(layoutManager)
37
38        let location = gesture.location(in: textView)
39        let characterIndex = layoutManager.characterIndex(
40            for: location,
41            in: textContainer,
42            fractionOfDistanceBetweenInsertionPoints: nil
43        )
44
45        let value = attributedText.attribute(mentionAttribute, at: characterIndex, effectiveRange: nil)
46        if let mentionID = value as? String {
47            print("Tapped mention:", mentionID)
48        }
49    }
50}

This works well for custom actions because you control the attributed range, not just visible styling.

Configure The Text View Properly

Several UITextView properties affect gesture behavior:

  • set isEditable = false unless the user should type into the view
  • set isSelectable = true when you rely on built-in link interactions
  • set isSelectable = false when you use your own recognizer and want to avoid selection handles
  • make sure the text view has its final frame before translating tap points

In practice, developers often get false negatives not because the index calculation is wrong, but because scrolling, padding, or selection settings were ignored.

Common Pitfalls

The biggest mistake is styling a range to look like a link and assuming it becomes tappable automatically. Visual styling alone does not create interaction.

Another common issue is forgetting about text-container padding or insets when mapping touch locations to character indexes. That shifts the hit test and makes taps look unreliable.

Developers also often leave the text view editable when they really want interaction-only behavior. The result is text-selection behavior competing with your tap handling.

Finally, if the content is truly a link, do not reimplement Text Kit hit testing unnecessarily. The built-in .link path is simpler and more robust.

Summary

  • Use .link attributes and the text view delegate when the tappable text behaves like a link.
  • For arbitrary attributed ranges, map the tap point to a character index with Text Kit.
  • Configure UITextView editing and selection behavior to match your interaction model.
  • Account for layout details such as padding and final bounds when hit testing.
  • Prefer the built-in link path unless you genuinely need custom tap targets.

Course illustration
Course illustration

All Rights Reserved.