UITextView
iOS Development
Disable Selection
Enable Links
Swift Programming

UITextView Disable selection, allow links

Master System Design with Codemia

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

Introduction

UITextView has an awkward default for read-only text that contains links. If you leave selection enabled, users can long-press, select text, and see edit actions. If you disable selection entirely, link taps stop working. The practical solution is to keep the text view non-editable, keep it selectable for link interaction, and then block the selection behavior you do not want.

Start with the Right Base Configuration

Link interaction in UITextView depends on isSelectable, so the baseline configuration matters:

  • 'isEditable = false'
  • 'isSelectable = true'
  • attributed text or data detectors for the links
  • a delegate if you want to intercept taps
swift
1import UIKit
2
3final class LinksViewController: UIViewController {
4    private let textView = NonSelectableTextView()
5
6    override func viewDidLoad() {
7        super.viewDidLoad()
8        view.backgroundColor = .systemBackground
9
10        textView.translatesAutoresizingMaskIntoConstraints = false
11        textView.isEditable = false
12        textView.isSelectable = true
13        textView.delegate = self
14        textView.isScrollEnabled = true
15        textView.backgroundColor = .clear
16        textView.textContainerInset = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
17        textView.linkTextAttributes = [
18            .foregroundColor: UIColor.systemBlue,
19            .underlineStyle: NSUnderlineStyle.single.rawValue
20        ]
21
22        let text = NSMutableAttributedString(
23            string: "Read the documentation or visit the support site."
24        )
25        text.addAttribute(
26            .link,
27            value: "https://example.com/docs",
28            range: NSRange(location: 9, length: 13)
29        )
30        text.addAttribute(
31            .link,
32            value: "https://example.com/support",
33            range: NSRange(location: 27, length: 12)
34        )
35        textView.attributedText = text
36
37        view.addSubview(textView)
38        NSLayoutConstraint.activate([
39            textView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
40            textView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
41            textView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
42            textView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
43        ])
44    }
45}

At this point, links are tappable, but users can still trigger selection.

One small subclass is usually enough. The goal is not to disable every gesture. The goal is to neutralize selection-related behavior while leaving tap handling intact.

swift
1import UIKit
2
3final class NonSelectableTextView: UITextView {
4    override var selectedTextRange: UITextRange? {
5        get { nil }
6        set {}
7    }
8
9    override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
10        false
11    }
12
13    override func addGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
14        if let longPress = gestureRecognizer as? UILongPressGestureRecognizer {
15            longPress.isEnabled = false
16        }
17        super.addGestureRecognizer(gestureRecognizer)
18    }
19}

That approach preserves ordinary taps, including taps on attributed links, while removing the usual long-press selection path. It is intentionally small, which makes it easier to revisit if iOS gesture internals change.

Even if the default browser behavior is acceptable, it is usually worth handling link taps yourself. That gives you one place for validation, analytics, or in-app navigation.

swift
1import UIKit
2
3extension LinksViewController: UITextViewDelegate {
4    func textView(
5        _ textView: UITextView,
6        shouldInteractWith URL: URL,
7        in characterRange: NSRange,
8        interaction: UITextItemInteraction
9    ) -> Bool {
10        print("Tapped:", URL.absoluteString)
11        UIApplication.shared.open(URL)
12        return false
13    }
14}

Returning false tells the text view that you handled the tap. If you want the system default behavior instead, return true.

Accessibility and Layout Still Matter

Disabling selection should not make the content harder to use. Link styling needs to stay legible, Dynamic Type should still work, and VoiceOver users should still be able to navigate to the links.

Two practical habits help here:

  • keep the text concise enough that each link has a clear label
  • test on a real device, because long-press timing and interaction details are easier to judge there than in the simulator

If the screen is just a legal disclaimer or rich help text, you may also consider UILabel plus a custom interaction layer or a web view. UITextView is the best fit when you want rich attributed text, scrolling, and built-in link handling.

Common Pitfalls

  • Setting isSelectable to false, which disables link interaction along with text selection.
  • Disabling every gesture recognizer and accidentally breaking scrolling or normal taps.
  • Forgetting to assign the delegate, so custom link routing never runs.
  • Testing only in the simulator and missing a device-specific interaction issue.
  • Removing selection in a way that harms accessibility without rechecking VoiceOver behavior.

Summary

  • Keep the text view non-editable but selectable so links remain tappable.
  • Use attributed links or detectors, then suppress selection behavior in a subclass.
  • Override only the parts of interaction you need to change.
  • Handle URLs in the delegate if you want custom navigation logic.
  • Recheck scrolling, real-device gestures, and accessibility after the change.

Course illustration
Course illustration

All Rights Reserved.