UITextView具有可点击链接但没有文字突出显示

时间:2021-02-04 03:53:17

I have a UITextView displaying non-editable text. I want the text to automatically parse links, phone numbers, etc for the user, and for those to be clickable.

我有一个显示不可编辑文本的UITextView。我希望文本能够自动解析用户的链接,电话号码等,以及可点击的链接,电话号码等。

I don't want the user to be able to highlight text, though, because I want to override those long press and double-tap interactions to do something different.

我不希望用户能够突出显示文本,因为我想要覆盖那些长按和双击交互以执行不同的操作。

In order for links to be parsed in iOS7, the Selectable switch needs to be turned on for the UITextView, but Selectable also enables highlighting, which I don't want.

为了在iOS7中解析链接,需要为UITextView打开Selectable开关,但Selectable也可以启用突出显示,这是我不想要的。

I tried overriding the LongPress gesture to prevent highlighting, but that seems to have disabled ordinary taps on links as well...

我尝试重写LongPress手势以防止突出显示,但这似乎也禁用了链接上的普通水龙头......

for (UIGestureRecognizer *recognizer in cell.messageTextView.gestureRecognizers) {
    if ([recognizer isKindOfClass:[UILongPressGestureRecognizer class]]){
        recognizer.enabled = NO;
    }
    if ([recognizer isKindOfClass:[UITapGestureRecognizer class]]){
        recognizer.enabled = YES;
    }
}

There are lots of similar threads out there but none seem to address this specific question of links enabled, text not highlightable.

有很多类似的线程,但似乎没有解决这个特定的链接启用问题,文本不突出显示。

8 个解决方案

#1


23  

I am working on the exact same problem and the best I could do was to instantly clear the selection as soon as it is made by adding the following to the UITextView's delegate:

我正在研究完全相同的问题,我能做的最好的事情就是通过将以下内容添加到UITextView的委托来立即清除选择:

- (void)textViewDidChangeSelection:(UITextView *)textView {
    if(!NSEqualRanges(textView.selectedRange, NSMakeRange(0, 0))) {
        textView.selectedRange = NSMakeRange(0, 0);
    }
}

Note the check to prevent recursion. This pretty much addresses the issue because only selection is disabled -- links will still work.

请注意检查以防止递归。这几乎解决了这个问题,因为只有选择被禁用 - 链接仍然有用。

Another tangential issue is that the text view will still become first responder, which you can fix by setting your desired first responder after setting the selected range.

另一个切线问题是文本视图仍然会成为第一响应者,您可以通过在设置所选范围后设置所需的第一响应者来解决此问题。

Note: the only visual oddity that remains is that press-and-hold brings up the magnifying glass.

注意:剩下的唯一视觉怪异是按住并放大放大镜。

#2


16  

I'm not sure if this works for your particular case, but I had a similar case where I needed the textview links to be clickable but did not want text selection to occur and I was using the textview to present data in a CollectionViewCell.

我不确定这是否适用于您的特定情况,但我有一个类似的情况,我需要textview链接是可点击的,但不希望文本选择发生,我使用textview在CollectionViewCell中显示数据。

I simply had to override -canBecomeFirstResponder and return NO.

我只需要覆盖-canBecomeFirstResponder并返回NO。

@interface MYTextView : UITextView
@end

@implementation MYTextView

- (BOOL)canBecomeFirstResponder {
    return NO;
}

@end

#3


10  

As I wrote on the other post, there is another solution.

正如我在另一篇文章中所写,还有另一种解决方案。

After few tests, I found solution.

经过几次测试,我找到了解决方案。

If you want links active and you won't selection enabled, you need to edit gestureRecognizers.

如果您希望链接处于活动状态且未启用选择,则需要编辑gestureRecognizers。

For example - there are 3 LongPressGestureRecognizers. One for click on link (minimumPressDuration = 0.12), second for zoom in editable mode (minimumPressDuration = 0.5), third for selection (minimumPressDuration = 0.8). This solution removes LongPressGestureRecognizer for selecting and second for zooming in editing mode.

例如 - 有3个LongPressGestureRecognizers。一个用于点击链接(minimumPressDuration = 0.12),第二个用于放大可编辑模式(minimumPressDuration = 0.5),第三个用于选择(minimumPressDuration = 0.8)。此解决方案删除LongPressGestureRecognizer以进行选择,其次用于缩放编辑模式。

NSArray *textViewGestureRecognizers = self.captionTextView.gestureRecognizers;
NSMutableArray *mutableArrayOfGestureRecognizers = [[NSMutableArray alloc] init];
for (UIGestureRecognizer *gestureRecognizer in textViewGestureRecognizers) {
    if (![gestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]) {
        [mutableArrayOfGestureRecognizers addObject:gestureRecognizer];
    } else {
        UILongPressGestureRecognizer *longPressGestureRecognizer = (UILongPressGestureRecognizer *)gestureRecognizer;
        if (longPressGestureRecognizer.minimumPressDuration < 0.3) {
            [mutableArrayOfGestureRecognizers addObject:gestureRecognizer];
        }
    }
}
self.captionTextView.gestureRecognizers = mutableArrayOfGestureRecognizers;

Tested on iOS 9, but it should work on all versions (iOS 7, 8, 9). I hope it helps! :)

在iOS 9上测试过,但它应该适用于所有版本(iOS 7,8,9)。我希望它有所帮助! :)

#4


4  

Swift 4, Xcode 9.2

Swift 4,Xcode 9.2

Below is something different approach ,

以下是不同的方法,

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

    }
}

HOW TO USE,

如何使用,

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
}

#5


3  

Here's what worked for me.

这对我有用。

I couldn't get rid of the magnify glass, but this will allow you to keep the text view selectable (so you can tap the links), but get rid of all the selection related UI. Only tested on iOS 9.

我无法摆脱放大玻璃,但这将允许您保持文本视图可选(因此您可以点击链接),但摆脱所有选择相关的UI。仅在iOS 9上测试过。

Caution Swift below!

小心Swift下面!

First, subclass UITextView and include this function:

首先,子类UITextView并包含此函数:

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

That will disable the copy, etc menu. I then include a setup method, which I call from init, where I do a bunch of setup related tasks. (I only use these text views from a storyboard, thus the decoder init):

这将禁用复制等菜单。然后我包括一个设置方法,我从init调用,在那里我做了一堆设置相关的任务。 (我只使用故事板中的这些文本视图,因此解码器初始化):

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

private func setup() {
    selectable = true
    editable = false
    tintColor = UIColor.clearColor()
}

Selectable = true to keep the links tappable, editable = false because links aren't tappable in an editable text view. Specifying a clear tintColor hides the blue bars that appear at the beginning and end of a selection.

Selectable = true以使链接保持可用,editable = false,因为链接在可编辑的文本视图中不可插入。指定clear tintColor会隐藏出现在选区开头和结尾的蓝条。

Lastly, in the controller that is using the subclassed text view, make sure the UITextViewDelegate protocol is included, that the delegate is set textView.delegate = self, and implement this delegate function:

最后,在使用子类文本视图的控制器中,确保包含UITextViewDelegate协议,委托设置为textView.delegate = self,并实现此委托函数:

func textViewDidChangeSelection(textView: UITextView) {
    var range = NSRange()
    range.location = 0
    range.length = 0
    textView.selectedRange = range
}

Without this function, the selection bars, and contextual menu will be disabled, but a colored background will still be left behind the text you selected. This function gets rid of that selection background.

如果没有此功能,将禁用选择栏和上下文菜单,但仍会在所选文本后面留下彩色背景。此功能摆脱了选择背景。

Like I said, I haven't found a way to get rid of the magnify glass, but if they do a long tap anywhere besides a link, nothing will be left behind once the magnify glass disappears.

就像我说的那样,我还没有找到摆脱放大玻璃的方法,但是如果他们在链接之外的任何地方进行长时间点击,一旦放大玻璃消失,就不会留下任何东西。

#6


2  

This pretty much addresses the issue as text selection is disabled and hides magnifying glass -- links will still work.

这几乎解决了这个问题,因为禁用了文本选择并隐藏了放大镜 - 链接仍然有用。

func textViewDidChangeSelection(_ textView: UITextView) {
    if let gestureRecognizers = textView.gestureRecognizers {
        for recognizer in gestureRecognizers {
            if recognizer is UILongPressGestureRecognizer {
                if let index = textView.gestureRecognizers?.index(of: recognizer) {
                    textView.gestureRecognizers?.remove(at: index)
                }
            }
        }
    }
}

Note: Instead of removing, you can replace the recognizer with your desired one.

注意:您可以将识别器替换为所需的识别器,而不是删除。

#7


1  

Although it's admittedly fragile in the face of possible future implementation changes, Kubík Kašpar's approach is the only one that has worked for me.

虽然面对未来可能的实施变化,它确实是脆弱的,但KubíkKašpar的方法是唯一对我有用的方法。

But (a) this can be made simpler if you subclass UITextView and (b) if the only interaction you want to allow is link tapping, you can have the tap be recognised straight away:

但是(a)如果你是UITextView的子类,那么这可以变得更简单;(b)如果你想要允许的唯一交互是链接点击,你可以立即识别它:

@interface GMTextView : UITextView
@end

@implementation GMTextView

- (void)addGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer {

  // discard all recognizers but the one that activates links, by just not calling super
  // (in iOS 9.2.3 a short press for links is 0.12s, long press for selection is 0.75s)

  if ([gestureRecognizer isMemberOfClass:UILongPressGestureRecognizer.class] &&
      ((UILongPressGestureRecognizer*)gestureRecognizer).minimumPressDuration < 0.25) {  

    ((UILongPressGestureRecognizer*)gestureRecognizer).minimumPressDuration = 0.0;
    [super addGestureRecognizer:gestureRecognizer]; 
  }
}

@end

#8


1  

Here's a UITextView subclass approach that will analyze its gesture recognizers and only allow those that interact with linked text (using Swift 3).

这是一个UITextView子类方法,它将分析其手势识别器,并且只允许那些与链接文本交互的方法(使用Swift 3)。

class LinkTextView: UITextView {
    override func gestureRecognizerShouldBegin(_ gesture: UIGestureRecognizer) -> Bool {
        let tapLocation = gesture.location(in: self).applying(CGAffineTransform(translationX: -textContainerInset.left, y: -textContainerInset.top))
        let characterAtIndex = layoutManager.characterIndex(for: tapLocation, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
        let linkAttributeAtIndex = textStorage.attribute(NSLinkAttributeName, at: characterAtIndex, effectiveRange: nil)

        // Returns true for gestures located on linked text
        return linkAttributeAtIndex != nil
    }

    override func becomeFirstResponder() -> Bool {
        // Returning false disables double-tap selection of link text
        return false
    }
}

#1


23  

I am working on the exact same problem and the best I could do was to instantly clear the selection as soon as it is made by adding the following to the UITextView's delegate:

我正在研究完全相同的问题,我能做的最好的事情就是通过将以下内容添加到UITextView的委托来立即清除选择:

- (void)textViewDidChangeSelection:(UITextView *)textView {
    if(!NSEqualRanges(textView.selectedRange, NSMakeRange(0, 0))) {
        textView.selectedRange = NSMakeRange(0, 0);
    }
}

Note the check to prevent recursion. This pretty much addresses the issue because only selection is disabled -- links will still work.

请注意检查以防止递归。这几乎解决了这个问题,因为只有选择被禁用 - 链接仍然有用。

Another tangential issue is that the text view will still become first responder, which you can fix by setting your desired first responder after setting the selected range.

另一个切线问题是文本视图仍然会成为第一响应者,您可以通过在设置所选范围后设置所需的第一响应者来解决此问题。

Note: the only visual oddity that remains is that press-and-hold brings up the magnifying glass.

注意:剩下的唯一视觉怪异是按住并放大放大镜。

#2


16  

I'm not sure if this works for your particular case, but I had a similar case where I needed the textview links to be clickable but did not want text selection to occur and I was using the textview to present data in a CollectionViewCell.

我不确定这是否适用于您的特定情况,但我有一个类似的情况,我需要textview链接是可点击的,但不希望文本选择发生,我使用textview在CollectionViewCell中显示数据。

I simply had to override -canBecomeFirstResponder and return NO.

我只需要覆盖-canBecomeFirstResponder并返回NO。

@interface MYTextView : UITextView
@end

@implementation MYTextView

- (BOOL)canBecomeFirstResponder {
    return NO;
}

@end

#3


10  

As I wrote on the other post, there is another solution.

正如我在另一篇文章中所写,还有另一种解决方案。

After few tests, I found solution.

经过几次测试,我找到了解决方案。

If you want links active and you won't selection enabled, you need to edit gestureRecognizers.

如果您希望链接处于活动状态且未启用选择,则需要编辑gestureRecognizers。

For example - there are 3 LongPressGestureRecognizers. One for click on link (minimumPressDuration = 0.12), second for zoom in editable mode (minimumPressDuration = 0.5), third for selection (minimumPressDuration = 0.8). This solution removes LongPressGestureRecognizer for selecting and second for zooming in editing mode.

例如 - 有3个LongPressGestureRecognizers。一个用于点击链接(minimumPressDuration = 0.12),第二个用于放大可编辑模式(minimumPressDuration = 0.5),第三个用于选择(minimumPressDuration = 0.8)。此解决方案删除LongPressGestureRecognizer以进行选择,其次用于缩放编辑模式。

NSArray *textViewGestureRecognizers = self.captionTextView.gestureRecognizers;
NSMutableArray *mutableArrayOfGestureRecognizers = [[NSMutableArray alloc] init];
for (UIGestureRecognizer *gestureRecognizer in textViewGestureRecognizers) {
    if (![gestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]) {
        [mutableArrayOfGestureRecognizers addObject:gestureRecognizer];
    } else {
        UILongPressGestureRecognizer *longPressGestureRecognizer = (UILongPressGestureRecognizer *)gestureRecognizer;
        if (longPressGestureRecognizer.minimumPressDuration < 0.3) {
            [mutableArrayOfGestureRecognizers addObject:gestureRecognizer];
        }
    }
}
self.captionTextView.gestureRecognizers = mutableArrayOfGestureRecognizers;

Tested on iOS 9, but it should work on all versions (iOS 7, 8, 9). I hope it helps! :)

在iOS 9上测试过,但它应该适用于所有版本(iOS 7,8,9)。我希望它有所帮助! :)

#4


4  

Swift 4, Xcode 9.2

Swift 4,Xcode 9.2

Below is something different approach ,

以下是不同的方法,

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

    }
}

HOW TO USE,

如何使用,

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
}

#5


3  

Here's what worked for me.

这对我有用。

I couldn't get rid of the magnify glass, but this will allow you to keep the text view selectable (so you can tap the links), but get rid of all the selection related UI. Only tested on iOS 9.

我无法摆脱放大玻璃,但这将允许您保持文本视图可选(因此您可以点击链接),但摆脱所有选择相关的UI。仅在iOS 9上测试过。

Caution Swift below!

小心Swift下面!

First, subclass UITextView and include this function:

首先,子类UITextView并包含此函数:

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

That will disable the copy, etc menu. I then include a setup method, which I call from init, where I do a bunch of setup related tasks. (I only use these text views from a storyboard, thus the decoder init):

这将禁用复制等菜单。然后我包括一个设置方法,我从init调用,在那里我做了一堆设置相关的任务。 (我只使用故事板中的这些文本视图,因此解码器初始化):

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

private func setup() {
    selectable = true
    editable = false
    tintColor = UIColor.clearColor()
}

Selectable = true to keep the links tappable, editable = false because links aren't tappable in an editable text view. Specifying a clear tintColor hides the blue bars that appear at the beginning and end of a selection.

Selectable = true以使链接保持可用,editable = false,因为链接在可编辑的文本视图中不可插入。指定clear tintColor会隐藏出现在选区开头和结尾的蓝条。

Lastly, in the controller that is using the subclassed text view, make sure the UITextViewDelegate protocol is included, that the delegate is set textView.delegate = self, and implement this delegate function:

最后,在使用子类文本视图的控制器中,确保包含UITextViewDelegate协议,委托设置为textView.delegate = self,并实现此委托函数:

func textViewDidChangeSelection(textView: UITextView) {
    var range = NSRange()
    range.location = 0
    range.length = 0
    textView.selectedRange = range
}

Without this function, the selection bars, and contextual menu will be disabled, but a colored background will still be left behind the text you selected. This function gets rid of that selection background.

如果没有此功能,将禁用选择栏和上下文菜单,但仍会在所选文本后面留下彩色背景。此功能摆脱了选择背景。

Like I said, I haven't found a way to get rid of the magnify glass, but if they do a long tap anywhere besides a link, nothing will be left behind once the magnify glass disappears.

就像我说的那样,我还没有找到摆脱放大玻璃的方法,但是如果他们在链接之外的任何地方进行长时间点击,一旦放大玻璃消失,就不会留下任何东西。

#6


2  

This pretty much addresses the issue as text selection is disabled and hides magnifying glass -- links will still work.

这几乎解决了这个问题,因为禁用了文本选择并隐藏了放大镜 - 链接仍然有用。

func textViewDidChangeSelection(_ textView: UITextView) {
    if let gestureRecognizers = textView.gestureRecognizers {
        for recognizer in gestureRecognizers {
            if recognizer is UILongPressGestureRecognizer {
                if let index = textView.gestureRecognizers?.index(of: recognizer) {
                    textView.gestureRecognizers?.remove(at: index)
                }
            }
        }
    }
}

Note: Instead of removing, you can replace the recognizer with your desired one.

注意:您可以将识别器替换为所需的识别器,而不是删除。

#7


1  

Although it's admittedly fragile in the face of possible future implementation changes, Kubík Kašpar's approach is the only one that has worked for me.

虽然面对未来可能的实施变化,它确实是脆弱的,但KubíkKašpar的方法是唯一对我有用的方法。

But (a) this can be made simpler if you subclass UITextView and (b) if the only interaction you want to allow is link tapping, you can have the tap be recognised straight away:

但是(a)如果你是UITextView的子类,那么这可以变得更简单;(b)如果你想要允许的唯一交互是链接点击,你可以立即识别它:

@interface GMTextView : UITextView
@end

@implementation GMTextView

- (void)addGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer {

  // discard all recognizers but the one that activates links, by just not calling super
  // (in iOS 9.2.3 a short press for links is 0.12s, long press for selection is 0.75s)

  if ([gestureRecognizer isMemberOfClass:UILongPressGestureRecognizer.class] &&
      ((UILongPressGestureRecognizer*)gestureRecognizer).minimumPressDuration < 0.25) {  

    ((UILongPressGestureRecognizer*)gestureRecognizer).minimumPressDuration = 0.0;
    [super addGestureRecognizer:gestureRecognizer]; 
  }
}

@end

#8


1  

Here's a UITextView subclass approach that will analyze its gesture recognizers and only allow those that interact with linked text (using Swift 3).

这是一个UITextView子类方法,它将分析其手势识别器,并且只允许那些与链接文本交互的方法(使用Swift 3)。

class LinkTextView: UITextView {
    override func gestureRecognizerShouldBegin(_ gesture: UIGestureRecognizer) -> Bool {
        let tapLocation = gesture.location(in: self).applying(CGAffineTransform(translationX: -textContainerInset.left, y: -textContainerInset.top))
        let characterAtIndex = layoutManager.characterIndex(for: tapLocation, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
        let linkAttributeAtIndex = textStorage.attribute(NSLinkAttributeName, at: characterAtIndex, effectiveRange: nil)

        // Returns true for gestures located on linked text
        return linkAttributeAtIndex != nil
    }

    override func becomeFirstResponder() -> Bool {
        // Returning false disables double-tap selection of link text
        return false
    }
}