UITextView with clickable links but no text highli

2019-02-07 20:18发布

问题:

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.

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.

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

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.

回答1:

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:

- (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:

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.

I simply had to override -canBecomeFirstResponder and return NO.

@interface MYTextView : UITextView
@end

@implementation MYTextView

- (BOOL)canBecomeFirstResponder {
    return NO;
}

@end


回答3:

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.

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.

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! :)



回答4:

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:

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.

Caution Swift below!

First, subclass UITextView and include this function:

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

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.

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:

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:

Here's a UITextView subclass approach that will analyze its gesture recognizers and only allow those that interact with linked text (using 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
    }
}


回答7:

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.



回答8:

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.

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:

@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