Edit
See my answer for a full working solution:
I managed to solve this myself by using a UITextView
instead of a UILabel
I managed to solve this by using a UITextView
instead of a UILabel
. I originally, didn't want to use a UITextView
because I need the element to behave like a UILabel
and a UITextView
can cause issues with scrolling and it's intended use, is to be editable text. The following class I wrote makes a UITextView
behave like a UILabel
but with fully accurate click detection and no scrolling issues:
import UIKit
class ClickableLabelTextView: UITextView {
var delegate: DelegateForClickEvent?
var ranges:[(start: Int, end: Int)] = []
var page: String = ""
var paragraph: Int?
var clickedLink: (() -> Void)?
var pressedTime: Int?
var startTime: TimeInterval?
override func awakeFromNib() {
super.awakeFromNib()
self.textContainerInset = UIEdgeInsets.zero
self.textContainer.lineFragmentPadding = 0
self.delaysContentTouches = true
self.isEditable = false
self.isUserInteractionEnabled = true
self.isSelectable = false
}
override func touchesBegan(_ touches: Set, with event: UIEvent?) {
startTime = Date().timeIntervalSinceReferenceDate
}
override func touchesEnded(_ touches: Set, with event: UIEvent?) {
if let clickedLink = clickedLink {
if let startTime = startTime {
self.startTime = nil
if (Date().timeIntervalSinceReferenceDate - startTime <= 0.2) {
clickedLink()
}
}
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
var location = point
location.x -= self.textContainerInset.left
location.y -= self.textContainerInset.top
if location.x > 0 && location.y > 0 {
let index = self.layoutManager.characterIndex(for: location, in: self.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
var count = 0
for range in ranges {
if index >= range.start && index < range.end {
clickedLink = {
self.delegate?.clickedLink(page: self.page, paragraph: self.paragraph, linkNo: count)
}
return self
}
count += 1
}
}
clickedLink = nil
return nil
}
}
The function hitTest
get's called multiple times but that never causes a problem, as clickedLink()
will only ever get called once per click. I tried disabling isUserInteractionEnabled
for different views but didn't that didn't help and was unnecessary.
To use the class, simply add it to your UITextView
. If you're using autoLayout
in the Xcode editor, then disable Scrolling Enabled
for the UITextView
in the editor to avoid layout warnings.
In the Swift
file that contains the code to go with your xib
file (in my case a class for a UITableViewCell
, you need to set the following variables for your clickable textView:
ranges
- the start and end index of every clickable link with the UITextView
page
- a String
to identify the page or view that contains the the UITextView
paragraph
- If you have multiple clickable UITextView
, assign each one with an numberdelegate
- to delegate the click events to where ever you are able to process them.You then need to create a protocol for your delegate
:
protocol DelegateName {
func clickedLink(page: String, paragraph: Int?, linkNo: Int?)
}
The variables passed into clickedLink
give you all the information you need to know which link has been clicked.