ios uitextview nsattributedstring

ios - UITextView: deshabilitar selección, permitir enlaces



nsattributedstring (11)

Swift 3.0

Para la versión anterior de Objective-C a través de @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) } } } }

Tengo un UITextView que muestra una NSAttributedString . Las propiedades editable y selectable textView están configuradas como false .

El attribString contiene una URL y me gustaría permitir que toque la URL para abrir un navegador. Pero la interacción con la URL solo es posible si el atributo selectable se establece en true .

¿Cómo puedo permitir la interacción del usuario solo para tocar enlaces, pero no para seleccionar texto?


si su objetivo de despliegue mínimo es iOS 11.2 o más nuevo

Puede deshabilitar la selección de texto subclasificando UITextView y prohibiendo los gestos que pueden seleccionar algo.

La siguiente solución es:

  • compatible con isEditable
  • compatible con isScrollEnabled
  • compatible con enlaces

/// Class to allow links but no selection. /// Basically, it disables unwanted UIGestureRecognizer from UITextView. /// https://.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://.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 su objetivo de despliegue mínimo es iOS 11.1 o más antiguo

Los reconocimientos de gestos de enlaces nativos de UITextView están dañados en iOS 11.0-11.1 y requieren una pequeña pulsación prolongada en lugar de un toque : los enlaces Xext 9 UITextView ya no se pueden hacer clic

Puede admitir correctamente los enlaces con su propio reconocedor de gestos y puede deshabilitar la selección de texto subclasificando UITextView y prohibiendo los gestos que pueden seleccionar algo o tocar algo.

La siguiente solución no permitirá la selección y es:

  • compatible con isScrollEnabled
  • compatible con enlaces
  • limitaciones de la solución de trabajo de iOS 11.0 y iOS 11.1, pero pierde el efecto de la interfaz de usuario al tocar en archivos adjuntos de texto

/// Class to support links and to disallow selection. /// It disables most UIGestureRecognizer from UITextView and adds a UITapGestureRecognizer. /// https://.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://.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://.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) } } } }


Aquí hay una solución Swift 4 que permite que los toques pasen a través excepto cuando se pulsa un enlace;

En la vista padre

private(set) lazy var textView = YourCustomTextView() func setupView() { textView.isScrollEnabled = false textView.isUserInteractionEnabled = false let tapGr = UITapGestureRecognizer(target: textView, action: nil) tapGr.delegate = textView addGestureRecognizer(tapGr) textView.translatesAutoresizingMaskIntoConstraints = false addSubview(textView) NSLayoutConstraint.activate(textView.edges(to: self)) }

La costumbre UITextView

class YourCustomTextView: UITextView, UIGestureRecognizerDelegate { var onLinkTapped: (URL) -> Void = { print($0) } override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { guard let gesture = gestureRecognizer as? UITapGestureRecognizer else { return true } let location = gesture.location(in: self) guard let closest = closestPosition(to: location), let startPosition = position(from: closest, offset: -1), let endPosition = position(from: closest, offset: 1) else { return false } guard let textRange = textRange(from: startPosition, to: endPosition) else { return false } let startOffset = offset(from: beginningOfDocument, to: textRange.start) let endOffset = offset(from: beginningOfDocument, to: textRange.end) let range = NSRange(location: startOffset, length: endOffset - startOffset) guard range.location != NSNotFound, range.length != 0 else { return false } guard let linkAttribute = attributedText.attributedSubstring(from: range).attribute(.link, at: 0, effectiveRange: nil) else { return false } guard let linkString = linkAttribute as? String, let url = URL(string: linkString) else { return false } guard delegate?.textView?(self, shouldInteractWith: url, in: range, interaction: .invokeDefaultAction) ?? true else { return false } onLinkTapped(url) return true } }


Aquí hay una versión del Objetivo C de la respuesta publicada por 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; }


Así es como resolví este problema: hago que mi vista de texto seleccionable sea una subclase que reemplaza a canPerformAction para devolver false.

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


Así que después de algunas investigaciones he podido encontrar una solución. Es un truco y no sé si funcionará en futuras versiones de iOS, pero funciona a partir de ahora (iOS 9.3).

Solo agregue esta categoría UITextView (Gist here ):

@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]; } } }


Como ha dicho Cœur, puede subclasificar el UITextView anulando el método de selectedTextRange , configurándolo en nil . Y los enlaces seguirán siendo accesibles, pero no podrás seleccionar el resto del texto.

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


Encuentro que el concepto de juguetear con los reconocedores de gestos internos da un poco de miedo, así que traté de encontrar otra solución. Descubrí que podemos anular el point(inside:with:) para permitir efectivamente un "toque" cuando el usuario no está tocando el texto con un enlace en su interior:

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

Esto también significa que si tiene un UITextView con un enlace dentro de un UITableViewCell , tableView(didSelectRowAt:) aún se llama cuando toca la parte no vinculada del texto :)


Lo que hago para el Objetivo C es crear una subclase y sobrescribir el método textViewdidChangeSelection: delegate, por lo que en la clase de implementación:

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

No te olvides de establecer self.delegate = self


Sobreexplique UITextView como se muestra a continuación y utilícelo para hacer un enlace intercambiable con la preservación del estilo html.

clase pública 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 }

}


Swift 4, Xcode 9.2

A continuación hay un enfoque diferente para el enlace, hacer que la propiedad de UITextView sea Seleccionable como 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) } } } }

CÓMO UTILIZAR,

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 }