UITextView:禁用选择,允许链接

问题描述 投票:27回答:14

我有一个qazxsw poi,显示了qazxsw poi。 textView的qazxsw poi和UITextView属性都设置为NSAttributedString

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

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

ios uitextview nsattributedstring
14个回答
66
投票

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

selectable

这也意味着如果你有一个truepoint(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 } 仍然会在点击文本的非链接部分时被调用:)


0
投票

Swift 4,Xcode 9.2

下面是链接的不同方法,使UITextView的class HyperlinkEnabledReadOnlyTextView: UITextView { override init(frame: CGRect, textContainer: NSTextContainer?) { super.init(frame: frame, textContainer: textContainer) isEditable = false isSelectable = false initHyperLinkDetection() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) isEditable = false isSelectable = false initHyperLinkDetection() } // MARK: - Prevent interaction except on hyperlinks // Combining https://stackoverflow.com/a/44878203/2015332 and https://stackoverflow.com/a/49443814/1033581 private var linkGestureRecognizer: UITapGestureRecognizer! private func initHyperLinkDetection() { // 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, which moreover detects taps faster than native one linkGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(textTapped)) linkGestureRecognizer.numberOfTapsRequired = 1 addGestureRecognizer(linkGestureRecognizer) linkGestureRecognizer.isEnabled = true // because previous call sets it to false } 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 point(inside point: CGPoint, with event: UIEvent?) -> Bool { // Allow only taps located over an hyperlink var location = point location.x -= textContainerInset.left location.y -= textContainerInset.top guard location.x >= bounds.minX, location.x <= bounds.maxX, location.y >= bounds.minY, location.y <= bounds.maxY else { return false } let charIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil) return attributedText.attribute(.link, at: charIndex, effectiveRange: nil) != nil } @objc private func textTapped(recognizer: UITapGestureRecognizer) { guard recognizer == linkGestureRecognizer else { return } var location = recognizer.location(in: self) location.x -= textContainerInset.left location.y -= textContainerInset.top guard location.x >= bounds.minX, location.x <= bounds.maxX, location.y >= bounds.minY, location.y <= bounds.maxY else { return } 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: characterIndex, 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) } } } } 属性为.attachment

override public var selectedTextRange: UITextRange? {
    get {
        return nil
    }
    set {}
}

public init() {
    super.init(frame: CGRect.zero, textContainer: nil)
    commonInit()
}

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

private func commonInit() {
    self.tintColor = UIColor.black
    self.isScrollEnabled = false
    self.delegate = self
    self.dataDetectorTypes = []
    self.isEditable = false
    self.delegate = self
    self.font = Style.font(.sansSerif11)
    self.delaysContentTouches = true
}


@available(iOS 10.0, *)
public func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
    // Handle link
    return false
}

public func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
    // Handle link
    return false
}

如何使用,

isSelectable

0
投票

这是我如何解决这个问题 - 我使我的可选textview成为一个子类,它覆盖了canPerformAction以返回false。

false

0
投票

我为Objective C做的是创建一个子类并覆盖textViewdidChangeSelection:delegate方法,所以在实现类中:

class TextView: UITextView {
    //MARK: Properties    
    open var didTouchedLink:((URL,NSRange,CGPoint) -> Void)?

    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
    }

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

    override func draw(_ rect: CGRect) {
        super.draw(rect)
    }

    open override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = Array(touches)[0]
        if let view = touch.view {
            let point = touch.location(in: view)
            self.tapped(on: point)
        }
    }
}

extension TextView {
    fileprivate func tapped(on point:CGPoint) {
        var location: CGPoint = point
        location.x -= self.textContainerInset.left
        location.y -= self.textContainerInset.top
        let charIndex = layoutManager.characterIndex(for: location, in: self.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
        guard charIndex < self.textStorage.length else {
            return
        }
        var range = NSRange(location: 0, length: 0)
        if let attributedText = self.attributedText {
            if let link = attributedText.attribute(NSAttributedStringKey.link, at: charIndex, effectiveRange: &range) as? URL {
                print("\n\t##-->You just tapped on '\(link)' withRange = \(NSStringFromRange(range))\n")
                self.didTouchedLink?(link, range, location)
            }
        }

    }
}

. . . . . . .

let textView = TextView()//Init your textview and assign attributedString and other properties you want.
textView.didTouchedLink = { (url,tapRange,point) in
//here goes your other logic for successfull URL location
}

别忘了设置self.delegate = self


0
投票

这是一个Swift 4解决方案,允许点击通过,除非按下链接;

在父视图中

class CustomTextView: UITextView {

override public func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
        return false
    }

}

自定义UITextView

#import "CustomTextView.h"

@interface CustomTextView()<UITextViewDelegate>
@end

@implementation CustomTextView

0
投票

Swift 4.2

简单

- (void) textViewDidChangeSelection:(UITextView *)textView
{
    UITextRange *selectedRange = [textView selectedTextRange];
    NSString *selectedText = [textView textInRange:selectedRange];
    if (selectedText.length > 1 && selectedText.length < textView.text.length)
    {
        textView.selectedRange = NSMakeRange(0, 0);
    }
}

9
投票

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

tableView(didSelectRowAt:)

5
投票

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

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

class CustomTextView: UITextView {
override public var selectedTextRange: UITextRange? {
    get {
        return nil
    }
    set { }
}

3
投票

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

您可以通过继承UITextView并禁止可以选择某些内容的手势来禁用文本选择。

以下解决方案是:

  • 与isEditable兼容
  • 与isScrollEnabled兼容
  • 兼容链接
here

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

原生UITextView链接手势识别器在iOS 11.0-11.1上被破坏,需要一个小的延迟长按而不是点击:@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]; } } }

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

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

  • 与isScrollEnabled兼容
  • 兼容链接
  • iOS 11.0和iOS 11.1的解决方法限制,但在点击文本附件时会丢失UI效果
/// 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
    }
}

2
投票

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

Xcode 9 UITextView links no longer clickable

1
投票

Swift 3.0

通过@Lukas获得上述Objective-C版本

UITextView

1
投票

这对我有用:

/// 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)
            }
        }
    }
}

1
投票

我最终结合了- (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; } extension UITextView { override open func addGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) { if gestureRecognizer.isKind(of: UILongPressGestureRecognizer.self) { do { let array = try gestureRecognizer.value(forKey: "_targets") as! NSMutableArray let targetAndAction = array.firstObject let actions = ["action=oneFingerForcePan:", "action=_handleRevealGesture:", "action=loupeGesture:", "action=longDelayRecognizer:"] for action in actions { print("targetAndAction.debugDescription: \(targetAndAction.debugDescription)") if targetAndAction.debugDescription.contains(action) { gestureRecognizer.isEnabled = false } } } catch let exception { print("TXT_VIEW EXCEPTION : \(exception)") } defer { super.addGestureRecognizer(gestureRecognizer) } } } } (iOS <11变体)的解决方案。这按预期工作:一个只读,不可选择的UITextView,超链接仍在其中工作。 Coeur解决方案的一个优点是触摸检测是即时的,不显示高亮,也不允许拖放链接。

这是结果代码:

@interface MessageTextView : UITextView <UITextViewDelegate>

@end

@implementation MessageTextView

-(void)awakeFromNib{
    [super awakeFromNib];
    self.delegate = self;
}

- (BOOL)canBecomeFirstResponder {
    return NO;
}

- (void)textViewDidChangeSelection:(UITextView *)textView
{
    textView.selectedTextRange = nil;
    [textView endEditing:YES];
}

@end

请注意,我在编译https://stackoverflow.com/a/44878203/2015332枚举时遇到了一些问题,因为我没有使用它,所以我删除了它。


0
投票

像下面一样覆盖UITextView并使用它来呈现可保存的html样式的可点击链接。

公共类LinkTextView:UITextView {

https://stackoverflow.com/a/49443814/2015332

}

© www.soinside.com 2019 - 2024. All rights reserved.