UITextView: désactiver la sélection, autoriser les liens

j'ai un UITextView qui affiche un NSAttributedString. Le textView editable et selectable propriétés sont toutes les deux à false.

l'attributedString contient une URL et j'aimerais autoriser le tapotement de L'URL pour ouvrir un navigateur. Mais l'interaction avec L'URL n'est possible que si selectable attribut est défini à true.

Comment puis-je permettre l'interaction de l'utilisateur seulement pour des liens de tapotage, mais pas pour la sélection de texte?

17
demandé sur Lukas 2016-03-24 13:44:27

10 réponses

je trouve le concept de tripoter avec les reconnaisseurs gestuels internes un peu effrayant, alors j'ai essayé de trouver une autre solution. J'ai découvert que nous pouvons passer outre!--1--> effectivement permettre un "tap" lorsque l'utilisateur n'est pas en contact avec le bas sur du texte avec un lien à l'intérieur:

// 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: UITextLayoutDirection.left.rawValue) else { return false }

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

    return attributedText.attribute(NSLinkAttributeName, at: startIndex, effectiveRange: nil) != nil 
}   

Cela signifie également que si vous avez un UITextView avec un lien à l'intérieur d'un UITableViewCell,tableView(didSelectRowAt:) devient encore appelée lorsque vous appuyez sur la non-liés portion du texte :)

34
répondu Max Chuquimia 2017-07-03 05:13:49

Donc après quelques recherches j'ai pu trouver une solution. C'est un hack et je ne sais pas si ça marchera dans les futures versions d'iOS, mais ça marche dès maintenant (iOS 9.3).

il suffit d'ajouter ce UITextView catégorie (Gist ici):

@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];
        }
    }
}
5
répondu Lukas 2016-06-21 16:04:57

comme Cœur l'a dit, Vous pouvez classer le UITextView outrepassant la méthode de selectedTextRange, la valeur nul. Et les liens seront toujours cliquables, mais vous ne pourrez pas sélectionner le reste du texte.

class CustomTextView: UITextView {
override public var selectedTextRange: UITextRange? {
    get {
        return nil
    }
    set { }
}
4
répondu Pablo Sanchez Gomez 2018-05-16 13:00:49

Swift 3.0

For above Objective-C Version via @Lukas

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

    }
1
répondu Abhishek Thapliyal 2017-07-16 04:45:30

Voici une version Objective C de la réponse Postée par Max Chuquimia.

- (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;
}
1
répondu Marcin Kuptel 2018-05-22 09:35:35

si votre cible minimale de déploiement est iOS 11.2 ou plus récente

vous pouvez désactiver la sélection de texte en sous-classant UITextView et interdire les gestes qui peuvent sélectionner quelque chose.

la solution CI-DESSOUS est:

  • compatible avec isEditable
  • compatible avec isScrollEnabled
  • compatible avec les liens
/// 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
    }
}

si votre cible minimale de déploiement est IOS 11.1 ou plus

autochtones Liens UITextView les reconnaisseurs de gestes sont brisés sur iOS 11.0-11.1 et nécessitent un petit retard à long appuyez sur au lieu d'un appuyez sur: Xcode 9 les liens UITextView ne sont plus cliquables

vous pouvez supporter correctement les liens avec votre propre reconnaissance gestuelle et vous pouvez désactiver la sélection de texte en sous-classant UITextView et interdire les gestes qui peuvent choisir quelque chose ou taper quelque chose.

la solution ci-dessous ne permet pas la sélection et est:

  • compatible avec isScrollEnabled
  • compatible avec les liens
  • contournez les limitations de iOS 11.0 et IOS 11.1, mais perd l'effet UI en tapant sur les attachements de texte
/// 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)
            }
        }
    }
}
0
répondu Cœur 2018-03-30 03:06:18

Overide UITextView comme ci-dessous et l'utiliser pour rendre tappable lien avec la préservation de style html.

public class LinkTextView: UITextView {

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
}

}

0
répondu deepax11 2018-04-10 06:57:16

Swift 4, Xcode 9.2

ci-dessous est quelque chose de l'approche différente pour le lien, faire isSelectable propriété D'UITextView à false

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

    }
}

COMMENT UTILISER,

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
}
0
répondu Vats 2018-06-09 13:31:48

Voici comment j'ai résolu ce problème - je fais mon textview sélectionnable une sous-classe qui remplace canPerformAction pour retourner false.

class CustomTextView: UITextView {

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

}
0
répondu Phillip English 2018-06-21 19:04:23

ce que je fais pour L'objectif C est de créer une sous-classe et de réécrire textViewdidChangeSelection: delegate method, donc dans la classe d'implémentation:

#import "CustomTextView.h"

@interface CustomTextView()<UITextViewDelegate>
@end

@implementation CustomTextView

. . . . . . .

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

N'oubliez pas de définir soi-même.delegate = self

0
répondu JFCa 2018-08-31 10:23:13