带有手势识别器的 UITextView - 有条件地向前触摸到父视图

Posted

技术标签:

【中文标题】带有手势识别器的 UITextView - 有条件地向前触摸到父视图【英文标题】:UITextView with Gesture Recognizer - Conditionally Forward Touch to Parent View 【发布时间】:2018-09-28 10:19:26 【问题描述】:

我在UITableViewCell 中嵌入了一个UITextView

文本视图已禁用滚动,并随着其中的文本增加高度。

文本视图有一个类似链接的文本部分,该部分具有不同的颜色并带有下划线,并且我在文本视图上附加了一个点击手势识别器,用于检测用户是否点击文本的“链接”部分是否(这是使用文本视图的layoutManagertextContainerInset 来检测点击是否落在“链接”内。它基本上是一个自定义的命中测试功能) .


我希望表格视图单元格接收点击并在用户“错过”文本视图的链接部分时被选中,但不知道该怎么做。 p>


文本视图将userInteractionEnabled 设置为true。但是,当没有附加手势识别器时,这不会阻止触摸到达表格视图单元格。

相反,如果我将其设置为false,由于某种原因,单元格选择会完全停止,即使在文本视图边界之外点击(但手势识别器仍然有效.. . 为什么?)。


我的尝试

我尝试过覆盖gestureRecognizer(_ :shouldReceive:),但即使我返回false,表格视图单元格也不会被选中...

我也尝试过实现gestureRecognizerShouldBegin(_:),但在那里,即使我执行命中测试并返回false,单元格也不会被点击。


如何将错过的点击转发回单元格以突出显示它?

【问题讨论】:

是否可以为 UITextView 创建一个自定义类并覆盖pointInside:withEvent,并且在此方法中您可以执行链接命中逻辑,如果它返回 false,则应该将触摸转发到细胞。 @danypata 是的,我想过,但希望有一个更优雅的解决方案,不需要额外的子类化...... 我认为下面的答案比我的蛮力建议更优雅。 【参考方案1】:

保持所有视图处于活动状态(即启用用户交互)。

循环浏览文本视图的手势并禁用不需要的手势。

循环遍历表格视图的gestureRecognisers 数组,并使用requireGestureRecognizerToFail 使它们依赖于文本视图的自定义点击手势。

如果它是静态表视图,您可以在视图加载时执行此操作。对于动态表格视图,请在文本视图单元格的“willDisplayCell”中执行此操作。

【讨论】:

谢谢,我试试看。 嗯,它似乎根本不起作用。理想情况下,我希望将所有代码包含在表格视图 cell...【参考方案2】:

在尝试Swapnil Luktuke's answer(至少在我理解的范围内)无济于事之后,以及所有可能的组合:

实现UIGestureRecognizerDelegate的方法, 覆盖UITapGestureRecognizer, 有条件地调用ignore(_:for:)

(也许在绝望中我错过了一些明显的东西,但谁知道......)

...我放弃了,决定按照 cmets 中 @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
    


像往常一样,我会等几天再接受我自己的答案,以防万一出现更好的情况......

【讨论】:

以上是关于带有手势识别器的 UITextView - 有条件地向前触摸到父视图的主要内容,如果未能解决你的问题,请参考以下文章

ios - 带有 UITextView 的 UITapGestureRecognizer

如何将双击手势添加到 UITextView

单击另一个 UITextView 时结束编辑

UITextView 的 UIGestureRecognizer 防止键盘在点击时出现

带有手势识别器的 UICollectionView

带有点击手势识别器的 UILabel 不起作用