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?
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 :)
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];
}
}
}
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 { }
}
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)
}
}
}
}
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;
}
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)
}
}
}
}
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
}
}
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
}
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
}
}
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