UITextView:禁用选择,允许链接

130*_*13a 34 uitextview nsattributedstring ios

我有一个UITextView显示一个NSAttributedString.textView editableselectable属性都设置为false.

attributionString包含一个URL,我想允许点击URL来打开浏览器.但只有在selectable属性设置为的 情况下才能与URL进行交互true.

如何仅允许用户交互来点击链接,而不是用于选择文本?

Max*_*mia 77

我发现摆弄内部手势识别器的概念有点吓人,所以试图寻找另一种解决方案.我发现point(inside:with:)当用户没有触及内部有链接的文本时,我们可以覆盖以有效地允许"点击":

// Inside a UITextView subclass:
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {

    guard let pos = closestPosition(to: point) else { return false }

    guard let range = tokenizer.rangeEnclosingPosition(pos, with: .character, inDirection: .layout(.left)) else { return false }

    let startIndex = offset(from: beginningOfDocument, to: range.start)

    return attributedText.attribute(.link, at: startIndex, effectiveRange: nil) != nil
}   
Run Code Online (Sandbox Code Playgroud)

这也意味着如果你有一个UITextView链接里面的a UITableViewCell,tableView(didSelectRowAt:)仍然会在点击文本的非链接部分时被调用:)

  • 这是非常有效的,你应得到1.000 SO点,以获得极好的答案!没有放大镜,没有剪切/复制/粘贴弹出菜单,没有挑剔的业务玩内部API类(手势识别器),**和**,**最好**,我可以长按一个`UITextView`好像它只是一个支持tappable链接的`UILabel`和'UITableViewCell`中的`TextKit`! (7认同)
  • 非常感谢您提供的解决方案,此方法效果很好。但是最好通过在其中放置`guard super.point(inside:point,with:event)else {return false}`(或检查`bounds.contains(point)`)来检查point是否在其bounds内。第一行,以防止链接是文本的最后一部分的情况。在这种情况下,点击文本视图下方但超出其范围将阻止其他视图接收触摸事件。 (4认同)
  • @dinesharjani为队友欢呼-我最近发现的一件事要注意:双击一个单词会突出显示它:( (2认同)
  • 到目前为止我找到的最优雅的解决方案.谢谢 ! (2认同)
  • Swift 4.1 兼容 (Xcode 9.4.1): ```override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { guard let pos =closestPosition(to: point), let range = tokenizer.rangeEnclosurePosition( pos, with: .character, inDirection: UITextLayoutDirection.left.rawValue) else { return false } let startIndex = offset(from: beginOfDocument, to: range.start) return attributesText.attribute(.link, at: startIndex, EffectiveRange: nil ) != nil }``` (2认同)
  • 您仍然可以长按链接并开始选择/打开上下文菜单 (2认同)

Pab*_*mez 9

正如Cœur所说,你可以继承UITextView覆盖方法selectedTextRange,将其设置为nil.链接仍然可以点击,但您将无法选择其余文本.

class PIUnselectableTextView: PITextView {
    override public var selectedTextRange: UITextRange? {
        get {
            return nil
        }
        set { }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 不幸的是,双击不是链接的某些文本仍会选择它 (3认同)

Car*_* Vo 8

请尝试:

func textViewDidChangeSelection(_ textView: UITextView) {
    textView.selectedTextRange = nil
}
Run Code Online (Sandbox Code Playgroud)

  • 请注意旧版 iOS 上的无限循环。至少在 iOS 11 上,我必须在设置 `selectedTextRange` 之前将委托清零,然后恢复它。iOS 14似乎不受影响,没有测试12和13。 (2认同)

Cœu*_*œur 7

如果您的最低部署目标是 iOS 11.2 或更高版本

您可以通过子类化UITextView和禁止可以选择某些内容的手势来禁用文本选择。

下面的解决方案是:

  • 与 isEditable 兼容
  • 与 isScrollEnabled 兼容
  • 与链接兼容
/// Class to allow links but no selection.
/// Basically, it disables unwanted UIGestureRecognizer from UITextView.
/// https://stackoverflow.com/a/49443814/1033581
class UnselectableTappableTextView: UITextView {

    // required to prevent blue background selection from any situation
    override var selectedTextRange: UITextRange? {
        get { return nil }
        set {}
    }

    override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        if gestureRecognizer is UIPanGestureRecognizer {
            // required for compatibility with isScrollEnabled
            return super.gestureRecognizerShouldBegin(gestureRecognizer)
        }
        if let tapGestureRecognizer = gestureRecognizer as? UITapGestureRecognizer,
            tapGestureRecognizer.numberOfTapsRequired == 1 {
            // required for compatibility with links
            return super.gestureRecognizerShouldBegin(gestureRecognizer)
        }
        // allowing smallDelayRecognizer for links
        // https://stackoverflow.com/questions/46143868/xcode-9-uitextview-links-no-longer-clickable
        if let longPressGestureRecognizer = gestureRecognizer as? UILongPressGestureRecognizer,
            // comparison value is used to distinguish between 0.12 (smallDelayRecognizer) and 0.5 (textSelectionForce and textLoupe)
            longPressGestureRecognizer.minimumPressDuration < 0.325 {
            return super.gestureRecognizerShouldBegin(gestureRecognizer)
        }
        // preventing selection from loupe/magnifier (_UITextSelectionForceGesture), multi tap, tap and a half, etc.
        gestureRecognizer.isEnabled = false
        return false
    }
}
Run Code Online (Sandbox Code Playgroud)

如果您的最低部署目标是 iOS 11.1 或更低版本

原生 UITextView 链接手势识别器在 iOS 11.0-11.1 上被破坏,需要一个小的延迟长按而不是点击Xcode 9 UITextView 链接不再可点击

您可以使用自己的手势识别器正确支持链接,并且可以通过子类化UITextView和禁止可以选择某些内容或点击某些内容的手势来禁用文本选择。

以下解决方案将禁止选择,并且是:

  • 与 isScrollEnabled 兼容
  • 与链接兼容
  • iOS 11.0 和 iOS 11.1 的解决方法限制,但在点击文本附件时失去 UI 效果
/// Class to support links and to disallow selection.
/// It disables most UIGestureRecognizer from UITextView and adds a UITapGestureRecognizer.
/// https://stackoverflow.com/a/49443814/1033581
class UnselectableTappableTextView: UITextView {

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

        // Native UITextView links gesture recognizers are broken on iOS 11.0-11.1:
        // https://stackoverflow.com/questions/46143868/xcode-9-uitextview-links-no-longer-clickable
        // So we add our own UITapGestureRecognizer.
        linkGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(textTapped))
        linkGestureRecognizer.numberOfTapsRequired = 1
        addGestureRecognizer(linkGestureRecognizer)
        linkGestureRecognizer.isEnabled = true
    }

    var linkGestureRecognizer: UITapGestureRecognizer!

    // required to prevent blue background selection from any situation
    override var selectedTextRange: UITextRange? {
        get { return nil }
        set {}
    }

    override func addGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
        // Prevents drag and drop gestures,
        // but also prevents a crash with links on iOS 11.0 and 11.1.
        // https://stackoverflow.com/a/49535011/1033581
        gestureRecognizer.isEnabled = false
        super.addGestureRecognizer(gestureRecognizer)
    }

    override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        if gestureRecognizer == linkGestureRecognizer {
            // Supporting links correctly.
            return super.gestureRecognizerShouldBegin(gestureRecognizer)
        }
        if gestureRecognizer is UIPanGestureRecognizer {
            // Compatibility support with isScrollEnabled.
            return super.gestureRecognizerShouldBegin(gestureRecognizer)
        }
        // Preventing selection gestures and disabling broken links support.
        gestureRecognizer.isEnabled = false
        return false
    }

    @objc func textTapped(recognizer: UITapGestureRecognizer) {
        guard recognizer == linkGestureRecognizer else {
            return
        }
        var location = recognizer.location(in: self)
        location.x -= textContainerInset.left
        location.y -= textContainerInset.top
        let characterIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
        let characterRange = NSRange(location: characterIndex, length: 1)

        if let attachment = attributedText?.attribute(.attachment, at: index, effectiveRange: nil) as? NSTextAttachment {
            if #available(iOS 10.0, *) {
                _ = delegate?.textView?(self, shouldInteractWith: attachment, in: characterRange, interaction: .invokeDefaultAction)
            } else {
                _ = delegate?.textView?(self, shouldInteractWith: attachment, in: characterRange)
            }
        }
        if let url = attributedText?.attribute(.link, at: index, effectiveRange: nil) as? URL {
            if #available(iOS 10.0, *) {
                _ = delegate?.textView?(self, shouldInteractWith: url, in: characterRange, interaction: .invokeDefaultAction)
            } else {
                _ = delegate?.textView?(self, shouldInteractWith: url, in: characterRange)
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


Alb*_*haw 7

启用selectable以便可以点击链接,然后在检测到选择后立即取消选择。它将在 UI 有机会更新之前生效。

yourTextView.selectable = YES;//required for tappable links
yourTextView.delegate = self;//use <UITextViewDelegate> in .h

- (void)textViewDidChangeSelection:(UITextView *)textView {
    if (textView == yourTextView && textView.selectedTextRange != nil) {
        // `selectable` is required for tappable links but we do not want
        // regular text selection, so clear the selection immediately.
        textView.delegate = nil;//Disable delegate while we update the selectedTextRange otherwise this method will get called again, circularly, on some architectures (e.g. iPhone7 sim)
        textView.selectedTextRange = nil;//clear selection, will happen before copy/paste/etc GUI renders
        textView.delegate = self;//Re-enable delegate
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,在较新的 iOS 版本中,如果您按住并拖动 UITextView,光标现在可以使用上述方法闪烁和闪烁,因此为了解决这个问题,我们只需通过调整色调颜色来清除光标和选择(高光),然后将链接颜色设置回我们想要的任何颜色(因为它之前也使用了色调颜色)。

UIColor *originalTintColor = textView.tintColor;
[textView setTintColor:[UIColor clearColor]];//hide selection and highlight which now appears for a split second when tapping and holding in newer iOS versions
[textView setLinkTextAttributes:@{NSForegroundColorAttributeName: originalTintColor}];//manually set link color since it was using tint color before
Run Code Online (Sandbox Code Playgroud)

  • 这应该是公认的答案。它是最简单、最健壮的,并且不需要子类化。 (2认同)
  • @fumoboy007 因为你最近使用了这个我想提请你注意我刚刚所做的编辑,它完善了最近的 ios 版本(如 iOS14)中的解决方案 (2认同)

Mar*_*tel 6

这是 Max Chuquimia 发布的答案的 Objective C 版本。

- (BOOL) pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    UITextPosition *position = [self closestPositionToPoint:point];
    if (!position) {
        return NO;
    }
    UITextRange *range = [self.tokenizer rangeEnclosingPosition:position
                                                withGranularity:UITextGranularityCharacter
                                                    inDirection:UITextLayoutDirectionLeft];
    if (!range) {
        return NO;
    }

    NSInteger startIndex = [self offsetFromPosition:self.beginningOfDocument
                                         toPosition:range.start];
    return [self.attributedText attribute:NSLinkAttributeName
                                  atIndex:startIndex
                           effectiveRange:nil] != nil;
}
Run Code Online (Sandbox Code Playgroud)


130*_*13a 5

经过一些研究,我已经找到了解决方案.这是一个黑客,我不知道它是否会在未来的iOS版本中运行,但它现在正在运行(iOS 9.3).

只需添加此UITextView类别(Gist here):

@implementation UITextView (NoFirstResponder)

- (void)addGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer {
    if ([gestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]) {

        @try {
            id targetAndAction = ((NSMutableArray *)[gestureRecognizer valueForKey:@"_targets"]).firstObject;
            NSArray <NSString *>*actions = @[@"action=loupeGesture:",           // link: no, selection: shows circle loupe and blue selectors for a second
                                             @"action=longDelayRecognizer:",    // link: no, selection: no
                                             /*@"action=smallDelayRecognizer:", // link: yes (no long press), selection: no*/
                                             @"action=oneFingerForcePan:",      // link: no, selection: shows rectangular loupe for a second, no blue selectors
                                             @"action=_handleRevealGesture:"];  // link: no, selection: no
            for (NSString *action in actions) {
                if ([[targetAndAction description] containsString:action]) {
                    [gestureRecognizer setEnabled:false];
                }
            }

        }

        @catch (NSException *e) {
        }

        @finally {
            [super addGestureRecognizer: gestureRecognizer];
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


spe*_*ict 5

仅适用于无需选择的可点击链接的解决方案。

  1. UITextView处理手势的子类,使其只能点击。基于Cœur的回答
class UnselectableTappableTextView: UITextView {

    // required to prevent blue background selection from any situation
    override var selectedTextRange: UITextRange? {
        get { return nil }
        set {}
    }

    override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {

        if let tapGestureRecognizer = gestureRecognizer as? UITapGestureRecognizer,
            tapGestureRecognizer.numberOfTapsRequired == 1 {
            // required for compatibility with links
            return super.gestureRecognizerShouldBegin(gestureRecognizer)
        }

        return false
    }

}
Run Code Online (Sandbox Code Playgroud)
  1. 设置delegate禁用.preview3D触摸。从hackingwithswift 中获取参考
class ViewController: UIViewController, UITextViewDelegate {
    @IBOutlet var textView: UITextView!

    override func viewDidLoad() {
        //...
        textView.delegate = self
    }

    func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
        UIApplication.shared.open(URL)

        // Disable `.preview` by 3D Touch and other interactions
        return false
    }
}
Run Code Online (Sandbox Code Playgroud)

如果您只想UITextView在没有滚动手势的情况下嵌入链接,这可能是一个很好的解决方案。