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

时间:2018-09-28 10:19:27

标签: ios uitableview uitextview uitapgesturerecognizer

我在UITextView中嵌入了UITableViewCell

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

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


我希望表格视图单元格在用户“缺少”文本视图的链接部分时接收点击并被选中,但无法弄清楚该怎么做。 >


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

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


我尝试过的东西

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

我也尝试实现gestureRecognizerShouldBegin(_:),但是即使在我执行点击测试并返回false的情况下,单元也无法获得点击。


如何将错过的水龙头转发回单元格以突出显示?

2 个答案:

答案 0 :(得分:1)

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

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

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

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

答案 1 :(得分:1)

尝试Swapnil Luktuke's answer(至少据我所知)无济于事,并且尝试了以下所有可能的组合:

  • 实施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
    }
}

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