Non-selectable UITextViews and URL interactions

:  ~ 1 min read

Say we have some HTML content we want to display in a UITextView:

if let stringData = string.data(using: .utf16),
	let attributedString = try? NSAttributedString(
		data: stringData,
		options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType],
		documentAttributes: nil) {
	attributedText = attributedString
}
else {
	attributedText = nil
}

For added complexity, let's assume the UITextView is inside a UIScrollView, meaning we'll most likely want the it to be non-editable, non-selectable and, for our case, non-scrollable as well. And here comes the tricky part: if we set its isSelectable property to false, URLs won't be tappable anymore, so we can't do that; we'll have to work around having isSelectable == true.

The first steps are to set the tintColor to .clear (the caret bugs sometimes), and reset the text selection, as it happens:

func textViewDidChangeSelection(_ textView: UITextView) {
	DispatchQueue.main.async {
		textView.selectedTextRange = nil
	}
}

This will "disable" text selection, but the loupe will still show up. If we want that to go away as well, we'll need to create a subclass, and use a little hack:

override func caretRect(for position: UITextPosition) -> CGRect {
	return CGRect(x: 0, y: contentSize.height * 1.25,
		           width: 0, height: 0)
}

This will position the loupe in the given rect, which should be offscreen. In my case, I had a full-width, almost full-height UITextView, and these numbers worked just fine, but you'll need to adjust them according to your needs, based on contentSize, height and position. I'm not sure why, but giving x any value has no impact.

I've read and tried all other solutions involving canPerformAction, selectionRects, setting UIMenuController.shared.isMenuVisible to false, or intercepting touches, but I found this solution to be the most reliable one, even though it's basically a hack.

Update, Jun 4, 2017: As Saoud pointed out, there's a better approach. A bit more code, but I feel it's more appropriate, and it's hack free. Clearly this wasn't part of the things I tried in my "intercepting touches" attempts.