سؤال

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.

هل كانت مفيدة؟

المحلول

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.

نصائح أخرى

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

As I wrote on the other post, there is another solution I found after few tests.

If you want links active and you don't want 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! :)

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
}

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.

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.

Here's a UITextView subclass approach that will only recognize points that land on linked text.

class LinkTextView: UITextView {
    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        let tapLocation = point.applying(CGAffineTransform(translationX: -textContainerInset.left, y: -textContainerInset.top))
        let characterAtIndex = layoutManager.characterIndex(for: tapLocation, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
        let linkAttributeAtIndex = textStorage.attribute(.link, at: characterAtIndex, effectiveRange: nil)

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

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

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

I found lramirez135's and Kubík Kašpar's answers almost solved this problem. However lramirez135's answer can't handle the long press to select, whereas Kubík Kašpar's answer is iOS version dependent.

I combined their logic and created this subclass of UITextView in swift, which works for me.

class CustomUITextView: UITextView {
    override var canBecomeFirstResponder: Bool {
        return false
    }

    init() {
        super.init(frame: .zero, textContainer: nil)

        guard let textViewGestureRecognizers = self.gestureRecognizers else { return }
        for textViewGestureRecognizer in textViewGestureRecognizers {
            if textViewGestureRecognizer.isKind(of: UILongPressGestureRecognizer.self) {
                textViewGestureRecognizer.isEnabled = false
            }
        }
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Objective-c version:

@implementation CustomTextView

- (BOOL)canBecomeFirstResponder {
    return NO;
}

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.backgroundColor = UIColor.whiteColor;
        self.textContainerInset = UIEdgeInsetsZero;
        self.textContainer.lineFragmentPadding = 0;
        self.editable = NO;
        self.scrollEnabled = NO;
        self.linkTextAttributes = @{ NSForegroundColorAttributeName: UIColor.orangeColor };

        NSArray<UIGestureRecognizer *> *textViewGestureRecognizers = (NSArray<UIGestureRecognizer *> *)self.gestureRecognizers;
        for (UIGestureRecognizer *textViewGestureRecognizer in textViewGestureRecognizers) {
            if ([textViewGestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]) {
                [textViewGestureRecognizer setEnabled:NO];
            }
        }
    }
    return self;
}

@end
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top