J'ai un UITextView
qui affiche un NSAttributedString
. Les propriétés editable
et selectable
de textView sont définies sur false
.
Le attributString contient une URL et j'aimerais permettre d'appuyer sur l'URL pour ouvrir un navigateur. Cependant, l'interaction avec l'URL n'est possible que si l'attribut selectable
est défini sur true
.
Comment puis-je autoriser l'interaction utilisateur uniquement pour exploiter des liens, mais pas pour sélectionner du texte?
Je trouve le concept de manipulation des identifiants de geste internes un peu effrayant, alors j'ai essayé de trouver une autre solution. J'ai découvert que nous pouvons remplacer la fonction point(inside:with:)
afin d'autoriser efficacement un "accès direct" lorsque l'utilisateur ne touche pas au 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: .layout(.left)) else { return false }
let startIndex = offset(from: beginningOfDocument, to: range.start)
return attributedText.attribute(.link, at: startIndex, effectiveRange: nil) != nil
}
Cela signifie également que si vous avez une UITextView
avec un lien dans une UITableViewCell
, tableView(didSelectRowAt:)
est toujours appelé lorsque vous tapez sur la partie non liée du texte :)
Comme Cœur l'a dit, vous pouvez sous-classer la UITextView
en remplaçant la méthode selectedTextRange
, en la réglant sur nil . 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 { }
}
Ainsi, après quelques recherches, j'ai pu trouver une solution. C'est un hack et je ne sais pas si cela fonctionnera dans les futures versions d'iOS, mais cela fonctionne à partir de maintenant (iOS 9.3).
Ajoutez simplement cette catégorie 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];
}
}
}
Voici une version Objective C de la réponse publié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;
}
Pour la version d'Objective-C ci-dessus 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)
}
}
}
}
Vous pouvez désactiver la sélection de texte en sous-classant UITextView
et en interdisant les gestes pouvant sélectionner quelque chose.
La solution ci-dessous est:
/// 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
}
}
Les reconnaisseurs de mouvements de liens UITextView natifs sont interrompus sous iOS 11.0-11.1 et nécessitent un petit retard appuyez longuement sur au lieu d'un tap: Xcode 9 Les liens UITextView ne sont plus cliquables
Vous pouvez gérer correctement les liens avec votre propre outil de reconnaissance des gestes et vous pouvez désactiver la sélection de texte en sous-classant UITextView
et en interdisant les gestes pouvant sélectionner ou appuyer sur quelque chose.
La solution ci-dessous interdira la sélection et est la suivante:
/// 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)
}
}
}
}
Voici une solution Swift 4 qui permet aux robinets de passer au travers sauf lorsqu'un lien est pressé;
Dans la vue parent
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))
}
Le UITextView personnalisé
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
}
}
Voici comment j'ai résolu ce problème - je fais de ma vue textuelle sélectionnable une sous-classe qui substitue canPerformAction pour retourner false.
class CustomTextView: UITextView {
override public func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return false
}
}
Cela fonctionne pour moi:
@interface MessageTextView : UITextView <UITextViewDelegate>
@end
@implementation MessageTextView
-(void)awakeFromNib{
[super awakeFromNib];
self.delegate = self;
}
- (BOOL)canBecomeFirstResponder {
return NO;
}
- (void)textViewDidChangeSelection:(UITextView *)textView
{
textView.selectedTextRange = nil;
[textView endEditing:YES];
}
@end
Swift 4, Xcode 9.2
Vous trouverez ci-dessous une approche différente pour link. Définissez la propriété isSelectable
de UITextView en 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
}
Remplacez UITextView comme ci-dessous et utilisez-le pour rendre le lien pouvant être saisi en préservant le style HTML.
classe publique 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.2
Simple
class MyTextView: UITextView {
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: UITextDirection(rawValue: UITextLayoutDirection.left.rawValue)) else { return false }
let startIndex = offset(from: beginningOfDocument, to: range.start)
return attributedText.attribute(NSAttributedString.Key.link, at: startIndex, effectiveRange: nil) != nil
}
}
Ce que je fais pour Objective C, c'est créer une sous-classe et écraser la méthode textViewdidChangeSelection: delegate, 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 self.delegate = self