带有手势识别器的 UITextView - 有条件地将触摸转发到父视图
UITextView with Gesture Recognizer - Conditionally Forward Touch to Parent View
我在 UITableViewCell
中嵌入了 UITextView
。
文本视图已禁用滚动,并随着其中的文本增加高度。
文本视图有一个类似于 link 的文本部分,它具有不同的颜色并带有下划线,我有一个 点击手势识别器 附加到检测用户是否点击文本的 "link" 部分的文本视图 (这是通过使用文本视图的 layoutManager
和 textContainerInset
检测是否点击来完成的是否落在'link'之内。它基本上是一个自定义命中测试功能)。
我希望 table 视图单元格在用户 "misses" 文本视图的 link 部分时接收点击并被选中, 但不知道该怎么做。
文本视图已将 userInteractionEnabled
设置为 true
。但是,当没有附加手势识别器时,这不会阻止触摸到达 table 视图单元格。
相反,如果我将其设置为 false
,由于某种原因,单元格选择会完全停止,即使在文本视图边界的 外部 (但手势识别器仍然有效... 为什么?).
我尝试过的
我已经尝试覆盖 gestureRecognizer(_ :shouldReceive:)
,但即使我 return false
,table 视图单元格也没有被选中...
我也尝试过实施 gestureRecognizerShouldBegin(_:)
,但即使我执行命中测试和 return false
,单元格也没有得到点击。
如何将错过的点击转发回单元格,以突出显示它?
保持所有视图处于活动状态(即启用用户交互)。
遍历文本视图的手势并禁用不需要的手势。
遍历 table 视图的 gestureRecognisers
数组,并使用 requireGestureRecognizerToFail 使它们依赖于文本视图的自定义点击手势。
如果它是静态 table 视图,您可以在已加载的视图中执行此操作。对于动态 table 视图,在文本视图单元格的 'willDisplayCell' 中执行此操作。
在尝试 (至少在我理解的范围内)无济于事之后,以及以下所有可能的组合:
- 实施
UIGestureRecognizerDelegate
、 的方法
- 覆盖
UITapGestureRecognizer
、
- 有条件调用
ignore(_:for:)
等
(也许在绝望中我错过了一些明显的东西,但谁知道呢...)
...我放弃并决定按照@danyapata 在对我的问题的评论中提出的建议,子类 UITextView。
部分基于 this Medium post 上的代码,我想出了这个 UITextView
子类:
import UIKit
/**
Detects taps on subregions of its attributed text that correspond to custom,
named attributes.
- note: If no tap is detected, the behavior is equivalent to a text view with
`isUserInteractionEnabled` set to `false` (i.e., touches "pass through"). The
same behavior doesn't seem to be easily implemented using just stock
`UITextView` and gesture recognizers (hence the need to subclass).
*/
class LinkTextView: UITextView {
private var tapHandlersByName: [String: [(() -> Void)]] = [:]
/**
Adds a custom block to be executed wjhen a tap is detected on a subregion
of the **attributed** text that contains the attribute named accordingly.
*/
public func addTapHandler(_ handler: @escaping(() -> Void), forAttribute attributeName: String) {
var handlers = tapHandlersByName[attributeName] ?? []
handlers.append(handler)
tapHandlersByName[attributeName] = handlers
}
// MARK: - Initialization
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
commonSetup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func awakeFromNib() {
super.awakeFromNib()
commonSetup()
}
private func commonSetup() {
self.delaysContentTouches = false
self.isScrollEnabled = false
self.isEditable = false
self.isUserInteractionEnabled = true
}
// MARK: - UIView
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let attributeName = self.attributeName(at: point), let handlers = tapHandlersByName[attributeName], handlers.count > 0 else {
return nil // Ignore touch
}
return self // Claim touch
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
// find attribute name
guard let touch = touches.first, let attributeName = self.attributeName(at: touch.location(in: self)) else {
return
}
// Execute all handlers for that attribute, once:
tapHandlersByName[attributeName]?.forEach({ (handler) in
handler()
})
}
// MARK: - Internal Support
private func attributeName(at point: CGPoint) -> String? {
let location = CGPoint(
x: point.x - self.textContainerInset.left,
y: point.y - self.textContainerInset.top)
let characterIndex = self.layoutManager.characterIndex(
for: location,
in: self.textContainer,
fractionOfDistanceBetweenInsertionPoints: nil)
guard characterIndex < self.textStorage.length else {
return nil
}
let firstAttributeName = tapHandlersByName.allKeys.first { (attributeName) -> Bool in
if self.textStorage.attribute(NSAttributedStringKey(rawValue: attributeName), at: characterIndex, effectiveRange: nil) != nil {
return true
}
return false
}
return firstAttributeName
}
}
像往常一样,我会等几天再接受我自己的答案,以防万一出现更好的东西...
我在 UITableViewCell
中嵌入了 UITextView
。
文本视图已禁用滚动,并随着其中的文本增加高度。
文本视图有一个类似于 link 的文本部分,它具有不同的颜色并带有下划线,我有一个 点击手势识别器 附加到检测用户是否点击文本的 "link" 部分的文本视图 (这是通过使用文本视图的 layoutManager
和 textContainerInset
检测是否点击来完成的是否落在'link'之内。它基本上是一个自定义命中测试功能)。
我希望 table 视图单元格在用户 "misses" 文本视图的 link 部分时接收点击并被选中, 但不知道该怎么做。
文本视图已将 userInteractionEnabled
设置为 true
。但是,当没有附加手势识别器时,这不会阻止触摸到达 table 视图单元格。
相反,如果我将其设置为 false
,由于某种原因,单元格选择会完全停止,即使在文本视图边界的 外部 (但手势识别器仍然有效... 为什么?).
我尝试过的
我已经尝试覆盖 gestureRecognizer(_ :shouldReceive:)
,但即使我 return false
,table 视图单元格也没有被选中...
我也尝试过实施 gestureRecognizerShouldBegin(_:)
,但即使我执行命中测试和 return false
,单元格也没有得到点击。
如何将错过的点击转发回单元格,以突出显示它?
保持所有视图处于活动状态(即启用用户交互)。
遍历文本视图的手势并禁用不需要的手势。
遍历 table 视图的 gestureRecognisers
数组,并使用 requireGestureRecognizerToFail 使它们依赖于文本视图的自定义点击手势。
如果它是静态 table 视图,您可以在已加载的视图中执行此操作。对于动态 table 视图,在文本视图单元格的 'willDisplayCell' 中执行此操作。
在尝试
- 实施
UIGestureRecognizerDelegate
、 的方法
- 覆盖
UITapGestureRecognizer
、 - 有条件调用
ignore(_:for:)
等
(也许在绝望中我错过了一些明显的东西,但谁知道呢...)
...我放弃并决定按照@danyapata 在对我的问题的评论中提出的建议,子类 UITextView。
部分基于 this Medium post 上的代码,我想出了这个 UITextView
子类:
import UIKit
/**
Detects taps on subregions of its attributed text that correspond to custom,
named attributes.
- note: If no tap is detected, the behavior is equivalent to a text view with
`isUserInteractionEnabled` set to `false` (i.e., touches "pass through"). The
same behavior doesn't seem to be easily implemented using just stock
`UITextView` and gesture recognizers (hence the need to subclass).
*/
class LinkTextView: UITextView {
private var tapHandlersByName: [String: [(() -> Void)]] = [:]
/**
Adds a custom block to be executed wjhen a tap is detected on a subregion
of the **attributed** text that contains the attribute named accordingly.
*/
public func addTapHandler(_ handler: @escaping(() -> Void), forAttribute attributeName: String) {
var handlers = tapHandlersByName[attributeName] ?? []
handlers.append(handler)
tapHandlersByName[attributeName] = handlers
}
// MARK: - Initialization
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
commonSetup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func awakeFromNib() {
super.awakeFromNib()
commonSetup()
}
private func commonSetup() {
self.delaysContentTouches = false
self.isScrollEnabled = false
self.isEditable = false
self.isUserInteractionEnabled = true
}
// MARK: - UIView
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let attributeName = self.attributeName(at: point), let handlers = tapHandlersByName[attributeName], handlers.count > 0 else {
return nil // Ignore touch
}
return self // Claim touch
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
// find attribute name
guard let touch = touches.first, let attributeName = self.attributeName(at: touch.location(in: self)) else {
return
}
// Execute all handlers for that attribute, once:
tapHandlersByName[attributeName]?.forEach({ (handler) in
handler()
})
}
// MARK: - Internal Support
private func attributeName(at point: CGPoint) -> String? {
let location = CGPoint(
x: point.x - self.textContainerInset.left,
y: point.y - self.textContainerInset.top)
let characterIndex = self.layoutManager.characterIndex(
for: location,
in: self.textContainer,
fractionOfDistanceBetweenInsertionPoints: nil)
guard characterIndex < self.textStorage.length else {
return nil
}
let firstAttributeName = tapHandlersByName.allKeys.first { (attributeName) -> Bool in
if self.textStorage.attribute(NSAttributedStringKey(rawValue: attributeName), at: characterIndex, effectiveRange: nil) != nil {
return true
}
return false
}
return firstAttributeName
}
}
像往常一样,我会等几天再接受我自己的答案,以防万一出现更好的东西...