如何将UITextView的触摸事件传递给UITableViewCell

15

我有一个UITextView在自定义的UITableViewCell中。 UITextView可以正常使用(滚动、显示文本等),但我需要用户能够点击单元格并跳转到另一个屏幕。现在,如果您点击表格单元格的边缘(即UITextView之外),下一个视图会被正确调用。但显然,在UITextView内部,触摸事件被捕获并且无法转发到单元格。

我找到了一篇关于子类化UITextView以转发触摸事件的帖子。我尝试过但没有成功。实现如下。我在想可能是a)我的textview的super并不是uitableviewcell,因此我需要通过其他方式传递触摸事件;或者b)如果super是uitableviewcell,那么我需要传递其他东西?任何帮助都将不胜感激。

#import "ScrollableTextView.h"

@implementation ScrollableTextView

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    if (parentScrollView) {
        [parentScrollView touchesBegan:touches withEvent:event];
    }
    [super touchesBegan:touches withEvent:event];
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    if (parentScrollView) {
        [parentScrollView touchesCancelled:touches withEvent:event];
    }
    [super touchesCancelled:touches withEvent:event];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    if (parentScrollView) {
        [parentScrollView touchesEnded:touches withEvent:event];
    }
    [super touchesEnded:touches withEvent:event];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    if (parentScrollView) {
        [parentScrollView touchesMoved:touches withEvent:event];
    }
    [super touchesMoved:touches withEvent:event];
}

- (BOOL)canBecomeFirstResponder {
    return YES;
}

@end

我找到了一个现代的解决方案,使用了这个答案:https://dev59.com/zHrZa4cB1Zd3GeqPyRun#41884532 - Marcus Vinicius Kuquert
4个回答

36

尝试使用[theTextView setUserInteractionEnabled:NO]; 如果用户需要编辑TextView的内容,则您可能在设计上存在问题。

Swift 3: theTextView.isUserInteractionEnabled = false

Storyboard:勾选“用户交互启用”复选框即可。


1
这可能有点老了,但我曾经遇到过完全相同的问题,这一行代码解决了它。谢谢 :) - gabaum10
3
非常感谢!顺便提一下,如果有人想在“界面生成器”中进行设置:打开 UITextView 的属性窗格,滚动到底部的“视图”窗格处,取消选中“启用用户交互”。 - AndrewO
4
我遇到的问题是,我希望用户能够点击电话号码,但我也需要进行转场。我还在寻找同时实现这两个功能的方法。如果有人有任何建议,请告诉我。如果我找到了解决方案,我会在这里发布。 - DirectX
@DirectX 为什么电话号码在可编辑字段中?也许将其放入 UILabel 中,接受触摸,执行所需操作,然后将触摸转发到响应链中的下一个响应者。如果不是这种情况,请在此处打开新问题。 - greg
@greg 电话号码显示在一个不可编辑的字段中,但它是一个UITextView,已经开启了NSDataDetectors,所以用户可以触摸电话号码并拨打电话,但同时如果点击该单元格,应该转到另一个视图,显示其他信息等。我没有找到解决方案,所以我们只好关闭数据检测器。 - DirectX
显示剩余3条评论

3

我知道这个问题已经被问了5年,但是对于一些需要使用UIDataDetectors的可点击单元格的应用程序仍然非常重要。

因此,这里是我创建的UITextView子类,以适应UITableView中的特定行为。

-(id) initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        self.delegate = self;
    }
    return self;
}

- (BOOL)canBecomeFirstResponder {
    return NO;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    UIView *obj = self;

    do {
        obj = obj.superview;
    } while (![obj isKindOfClass:[UITableViewCell class]]);
    UITableViewCell *cell = (UITableViewCell*)obj;

    do {
        obj = obj.superview;
    } while (![obj isKindOfClass:[UITableView class]]);
    UITableView *tableView = (UITableView*)obj;

    NSIndexPath *indePath = [tableView indexPathForCell:cell];
    [[tableView delegate] tableView:tableView didSelectRowAtIndexPath:indePath];
}

- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange {
    return YES;
}

你可以根据自己的需求进行修改...
希望能对某些人有所帮助。

巧妙的解决方法。不过,当用户仅滚动UITextView时,此代码也会触发代理,这不是预期的行为。在你的解决方案之前,我想到了我的解决方案,我将其放在另一个答案中。 - Ikhsan Assaat
@IkhsanAssaat,即使我小心地用手指触摸TextView开始滚动,也从未遇到过任何滚动问题...在我的应用程序中,它不会滚动并触发的唯一方法是单击TextView上存在的URL,但是拥有更多的案例研究对于不同的实现仍然很好,谢谢。 - TheSquad

2
您的解决方案存在问题,如果您将UITextView放在UITableViewCell中,那么它的父视图将不是实际的单元格。即使在iOS 7和iOS 8上,单元格的视图结构也有细微的差别。您需要做的是通过层次结构向下(或向上)钻取以获取UITableViewCell实例。
我正在使用并修改@TheSquad的while循环以获取UITableViewCell,并将其分配给属性。然后重写这些触摸方法,每当需要时使用单元格的触摸方法,并只使用超级触摸方法实现来获取默认行为。
// set the cell as property
@property (nonatomic, assign) UITableViewCell *superCell;

- (UITableViewCell *)superCell {
    if (!_superCell) {
        UIView *object = self;

        do {
            object = object.superview;
        } while (![object isKindOfClass:[UITableViewCell class]] && (object != nil));

        if (object) {
            _superCell = (UITableViewCell *)object;
        }
    }

    return _superCell;
}

#pragma mark - Touch overrides

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    if (self.superCell) {
        [self.superCell touchesBegan:touches withEvent:event];
    } else {
        [super touchesBegan:touches withEvent:event];
    }
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    if (self.superCell) {
        [self.superCell touchesMoved:touches withEvent:event];
    } else {
        [super touchesMoved:touches withEvent:event];
    }
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    if (self.superCell) {
        [self.superCell touchesEnded:touches withEvent:event];
    } else {
        [super touchesEnded:touches withEvent:event];
    }
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    if (self.superCell) {
        [self.superCell touchesEnded:touches withEvent:event];
    } else {
        [super touchesCancelled:touches withEvent:event];
    }

}

2
上面的答案并不能解决问题,如果您在UITextView中有链接并希望在用户点击链接时像往常一样工作,并且如果用户点击常规文本,则将点击传递给单元格。使用所提出的方法,无论哪种情况下都会“选择”单元格。
以下是一些可能的解决方案:
  1. https://stackoverflow.com/a/59010352/11448489 - 将轻拍手势识别器添加到单元格中,并将其设置为需要UITextInteractionNameLinkTap识别器失败。问题在于UITextInteractionNameLinkTap字符串来自内部Apple API,可能会更改。此外,我们仍然必须直接调用委托的didSelectRowAtIndexPath,因此单元格不会被动画化。

  2. 实现对文本视图中touchesEnded的重写。在其中至少延迟0.4秒后执行某个选择器。在文本视图委托中,如果发生与URL的交互,则取消此执行请求:

class TappableTextView: UITextView, UITextViewDelegate {

    var tapHandler: (() -> Void)?

    override var delegate: UITextViewDelegate? {
        get { self }
        set { }
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.perform(#selector(onTap), with: nil, afterDelay: 0.5)
    }

    func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
        Self.cancelPreviousPerformRequests(withTarget: self, selector: #selector(onTap), object: nil)
        return true
    }

    @objc func onTap() {
        self.tapHandler?()
    }
}

它能够工作,但是延迟很明显且令人烦恼。无法减少此延迟,因为 shouldInteractWith 在 touchesEnded 后 350ms 发生。 而我们仍然需要调用 didSelectRowAtIndexPath。

  1. 我想到了另一个解决方案,如果你需要可点击的链接,但没有其他交互(不可滚动、不可选择等),它似乎完美地解决了这个问题。基本上,我们需要使文本视图忽略所有不在链接区域内的触摸:
class TapPassingTextView: UITextView, UITextViewDelegate {

    var clickableRects = [CGRect]()

    override func layoutSubviews() {
        super.layoutSubviews()
        self.updateClickableRects()
    }

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        clickableRects.contains { $0.contains(point) } ? super.hitTest(point, with: event) : nil
    }

    private func updateClickableRects() {
        self.clickableRects = []
        let range = NSRange(location: 0, length: self.attributedText.string.count)
        self.attributedText.enumerateAttribute(.link, in: range) { link, range, _ in
            guard link != nil else { return }
            self.layoutManager.enumerateLineFragments(forGlyphRange: range) { rect, _, _, _, _ in
                self.clickableRects.append(rect)
            }
        }
    }
}

搞定了!链接的轻触操作正常工作,而在文本视图下方和单元格中的轻触操作则是原生选择。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接