如何在Swift 4的UILabel中准确检测是否单击了链接?
修改
有关完整的解决方案,请参见我的答案:
See my answer for a full working solution:
我设法通过使用UITextView
而不是UILabel
解决了这个问题.我编写了一个类,使UITextView
的行为类似于UILabel
,但具有完全准确的链接检测.
I managed to solve this myself by using a UITextView
instead of a UILabel
. I wrote a class that makes the UITextView
behave like a UILabel
but with fully accurate link detection.
我已经成功地使用NSMutableAttributedString
设置了链接样式,但是我无法准确检测到单击了哪个字符.我已经尝试过此问题(我可以将其转换为Swift 4代码),但是没有运气.
I have managed to style the links without problem using NSMutableAttributedString
but I am unable to accurately detect which character has been clicked. I have tried all the solutions in this question (that I could convert to Swift 4 code) but with no luck.
以下代码有效,但无法准确检测到单击了哪个字符并获得了错误的链接位置:
The following code works but fails to accurately detect which character has been clicked and gets the wrong location of the link:
func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool {
// Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: CGSize.zero)
let textStorage = NSTextStorage(attributedString: label.attributedText!)
// Configure layoutManager and textStorage
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
// Configure textContainer
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = label.lineBreakMode
textContainer.maximumNumberOfLines = label.numberOfLines
let labelSize = label.bounds.size
textContainer.size = labelSize
// Find the tapped character location and compare it to the specified range
let locationOfTouchInLabel = self.location(in: label)
let textBoundingBox = layoutManager.usedRect(for: textContainer)
let textContainerOffset = CGPoint(x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x, y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y)
let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x, y: locationOfTouchInLabel.y - textContainerOffset.y)
let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
print(indexOfCharacter)
return NSLocationInRange(indexOfCharacter, targetRange)
}
我设法通过使用UITextView
而不是UILabel
来解决此问题.我本来不想使用UITextView
,因为我需要该元素的行为像UILabel
一样,并且UITextView
可能会导致滚动问题,并且打算将其用作可编辑文本.我编写的以下类使UITextView
的行为类似于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<UITouch>, with event: UIEvent?) {
startTime = Date().timeIntervalSinceReferenceDate
}
override func touchesEnded(_ touches: Set<UITouch>, 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
}
}
函数hitTest
会被多次调用,但不会造成任何问题,因为clickedLink()
每次单击只会被调用一次.我尝试为不同的视图禁用isUserInteractionEnabled
,但这不是没有帮助,也是不必要的.
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.
要使用该类,只需将其添加到您的UITextView
中.如果您在Xcode编辑器中使用autoLayout
,请在编辑器中为UITextView
禁用Scrolling Enabled
以避免布局警告.
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.
在Swift
文件中,该文件包含与xib
文件一起提供的代码(在我的情况下是UITableViewCell
的类,您需要为可点击的textView设置以下变量:
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
-每个具有UITextView
的可点击链接的开始和结束索引
-
page
-一个String
来标识包含UITextView
的页面或视图
-
paragraph
-如果您有多个可点击的UITextView
,请为每个数字分配一个数字 -
delegate
-将点击事件委派到您能够对其进行处理的任何地方.
-
ranges
- the start and end index of every clickable link with theUITextView
-
page
- aString
to identify the page or view that contains the theUITextView
-
paragraph
- If you have multiple clickableUITextView
, assign each one with an number -
delegate
- to delegate the click events to where ever you are able to process them.
然后您需要为delegate
创建协议:
You then need to create a protocol for your delegate
:
protocol DelegateName {
func clickedLink(page: String, paragraph: Int?, linkNo: Int?)
}
传递给clickedLink
的变量为您提供了您需要知道单击哪个链接的所有信息.
The variables passed into clickedLink
give you all the information you need to know which link has been clicked.