web-dev-qa-db-fra.com

Détection des taps sur du texte attribué dans une UITextView dans iOS

J'ai un UITextView qui affiche un NSAttributedString. Cette chaîne contient des mots que je voudrais rendre susceptibles d’être effacés, de sorte que lorsqu’ils sont tapés, je suis rappelé afin que je puisse effectuer une action. Je me rends compte que UITextView peut détecter des taps sur une URL et rappeler mon délégué, mais ce ne sont pas des URL.

Il me semble qu'avec iOS 7 et la puissance de TextKit, cela devrait maintenant être possible, mais je ne trouve aucun exemple et je ne sais pas par où commencer.

Je comprends qu’il est maintenant possible de créer des attributs personnalisés dans la chaîne (bien que je ne l’aie pas encore fait) et que ceux-ci seront utiles pour détecter si l’un des mots magiques a été exploité. Dans tous les cas, je ne sais toujours pas comment intercepter ce tapotement et détecter sur quel mot le tapotement a eu lieu.

Notez que la compatibilité iOS 6 est non requise.

114
tarmes

Je voulais juste aider les autres un peu plus. Suite à la réponse de Shmidt, il est possible de faire exactement ce que j'avais demandé dans ma question initiale.

1) Créez une chaîne attribuée avec des attributs personnalisés appliqués aux mots cliquables. par exemple.

NSAttributedString* attributedString = [[NSAttributedString alloc] initWithString:@"a clickable Word" attributes:@{ @"myCustomTag" : @(YES) }];
[paragraph appendAttributedString:attributedString];

2) Créez un UITextView pour afficher cette chaîne et ajoutez-lui un UITapGestureRecognizer. Puis manipulez le robinet:

- (void)textTapped:(UITapGestureRecognizer *)recognizer
{
    UITextView *textView = (UITextView *)recognizer.view;

    // Location of the tap in text-container coordinates

    NSLayoutManager *layoutManager = textView.layoutManager;
    CGPoint location = [recognizer locationInView:textView];
    location.x -= textView.textContainerInset.left;
    location.y -= textView.textContainerInset.top;

    // Find the character that's been tapped on

    NSUInteger characterIndex;
    characterIndex = [layoutManager characterIndexForPoint:location
                                           inTextContainer:textView.textContainer
                  fractionOfDistanceBetweenInsertionPoints:NULL];

    if (characterIndex < textView.textStorage.length) {

        NSRange range;
        id value = [textView.attributedText attribute:@"myCustomTag" atIndex:characterIndex effectiveRange:&range];

        // Handle as required...

        NSLog(@"%@, %d, %d", value, range.location, range.length);

    }
}

Tellement facile quand vous savez comment!

114
tarmes

Détection de tapotements sur un texte attribué avec Swift

Parfois, pour les débutants, il est un peu difficile de savoir comment faire pour mettre les choses en place (ce fut pour moi de toute façon), alors cet exemple est un peu plus complet.

Ajoutez un UITextView à votre projet.

Sortie

Connectez le UITextView au ViewController avec une sortie nommée textView.

Attribut personnalisé

Nous allons créer un attribut personnalisé en faisant un Extension .

Remarque: Cette étape est techniquement optionnelle, mais si vous ne le faites pas, vous devrez éditer le code dans la partie suivante pour utiliser un attribut standard. comme NSAttributedString.Key.foregroundColor. L'utilisation d'un attribut personnalisé présente l'avantage de pouvoir définir les valeurs que vous souhaitez stocker dans la plage de texte attribuée.

Ajouter un nouveau fichier Swift avec Fichier> Nouveau> Fichier ...> iOS> Source> Swift Fichier . Vous pouvez appeler cela comme vous voulez. J'appelle le mien NSAttributedStringKey + CustomAttribute.Swift.

Collez dans le code suivant:

import Foundation

extension NSAttributedString.Key {
    static let myAttributeName = NSAttributedString.Key(rawValue: "MyCustomAttribute")
}

Code

Remplacez le code dans ViewController.Swift par le suivant. Notez le UIGestureRecognizerDelegate.

import UIKit
class ViewController: UIViewController, UIGestureRecognizerDelegate {

    @IBOutlet weak var textView: UITextView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Create an attributed string
        let myString = NSMutableAttributedString(string: "Swift attributed text")

        // Set an attribute on part of the string
        let myRange = NSRange(location: 0, length: 5) // range of "Swift"
        let myCustomAttribute = [ NSAttributedString.Key.myAttributeName: "some value"]
        myString.addAttributes(myCustomAttribute, range: myRange)

        textView.attributedText = myString

        // Add tap gesture recognizer to Text View
        let tap = UITapGestureRecognizer(target: self, action: #selector(myMethodToHandleTap(_:)))
        tap.delegate = self
        textView.addGestureRecognizer(tap)
    }

    @objc func myMethodToHandleTap(_ sender: UITapGestureRecognizer) {

        let myTextView = sender.view as! UITextView
        let layoutManager = myTextView.layoutManager

        // location of tap in myTextView coordinates and taking the inset into account
        var location = sender.location(in: myTextView)
        location.x -= myTextView.textContainerInset.left;
        location.y -= myTextView.textContainerInset.top;

        // character index at tap location
        let characterIndex = layoutManager.characterIndex(for: location, in: myTextView.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)

        // if index is valid then do something.
        if characterIndex < myTextView.textStorage.length {

            // print the character index
            print("character index: \(characterIndex)")

            // print the character at the index
            let myRange = NSRange(location: characterIndex, length: 1)
            let substring = (myTextView.attributedText.string as NSString).substring(with: myRange)
            print("character at index: \(substring)")

            // check if the tap location has a certain attribute
            let attributeName = NSAttributedString.Key.myAttributeName
            let attributeValue = myTextView.attributedText?.attribute(attributeName, at: characterIndex, effectiveRange: nil)
            if let value = attributeValue {
                print("You tapped on \(attributeName.rawValue) and the value is: \(value)")
            }

        }
    }
}

enter image description here

Maintenant, si vous appuyez sur le "w" de "Swift", vous devriez obtenir le résultat suivant:

character index: 1
character at index: w
You tapped on MyCustomAttribute and the value is: some value

Remarques

  • Ici, j'ai utilisé un attribut personnalisé, mais il aurait tout aussi bien pu être NSAttributedString.Key.foregroundColor (couleur du texte) ayant la valeur UIColor.green.
  • Auparavant, l'affichage du texte ne pouvait être ni édité ni sélectionné, mais dans ma réponse mise à jour pour Swift 4.2), il semble bien fonctionner, que ces éléments soient sélectionnés ou non.

Une étude plus approfondie

Cette réponse reposait sur plusieurs autres réponses à cette question. Outre ceux-ci, voir aussi

54
Suragch

Ceci est une version légèrement modifiée, basée sur la réponse de @tarmes. Je ne pouvais pas obtenir la variable value rapporter autre chose que null sans le Tweak ci-dessous. En outre, j'avais besoin du dictionnaire d'attributs complet renvoyé afin de déterminer l'action résultante. J'aurais mis cela dans les commentaires mais ne semble pas avoir le représentant pour le faire. Excuse-moi d'avance si j'ai violé le protocole.

Tweak spécifique est d'utiliser textView.textStorage au lieu de textView.attributedText. En tant que programmeur iOS en apprentissage, je ne sais pas trop pourquoi, mais quelqu'un d'autre peut peut-être nous éclairer.

Modification spécifique dans la méthode de traitement du robinet:

    NSDictionary *attributesOfTappedText = [textView.textStorage attributesAtIndex:characterIndex effectiveRange:&range];

Code complet dans mon contrôleur de vue

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.textView.attributedText = [self attributedTextViewString];
    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(textTapped:)];

    [self.textView addGestureRecognizer:tap];
}  

- (NSAttributedString *)attributedTextViewString
{
    NSMutableAttributedString *paragraph = [[NSMutableAttributedString alloc] initWithString:@"This is a string with " attributes:@{NSForegroundColorAttributeName:[UIColor blueColor]}];

    NSAttributedString* attributedString = [[NSAttributedString alloc] initWithString:@"a tappable string"
                                                                       attributes:@{@"tappable":@(YES),
                                                                                    @"networkCallRequired": @(YES),
                                                                                    @"loadCatPicture": @(NO)}];

    NSAttributedString* anotherAttributedString = [[NSAttributedString alloc] initWithString:@" and another tappable string"
                                                                              attributes:@{@"tappable":@(YES),
                                                                                           @"networkCallRequired": @(NO),
                                                                                           @"loadCatPicture": @(YES)}];
    [paragraph appendAttributedString:attributedString];
    [paragraph appendAttributedString:anotherAttributedString];

    return [paragraph copy];
}

- (void)textTapped:(UITapGestureRecognizer *)recognizer
{
    UITextView *textView = (UITextView *)recognizer.view;

    // Location of the tap in text-container coordinates

    NSLayoutManager *layoutManager = textView.layoutManager;
    CGPoint location = [recognizer locationInView:textView];
    location.x -= textView.textContainerInset.left;
    location.y -= textView.textContainerInset.top;

    NSLog(@"location: %@", NSStringFromCGPoint(location));

    // Find the character that's been tapped on

    NSUInteger characterIndex;
    characterIndex = [layoutManager characterIndexForPoint:location
                                       inTextContainer:textView.textContainer
              fractionOfDistanceBetweenInsertionPoints:NULL];

    if (characterIndex < textView.textStorage.length) {

        NSRange range;
        NSDictionary *attributes = [textView.textStorage attributesAtIndex:characterIndex effectiveRange:&range];
        NSLog(@"%@, %@", attributes, NSStringFromRange(range));

        //Based on the attributes, do something
        ///if ([attributes objectForKey:...)] //make a network call, load a cat Pic, etc

    }
}
32
natenash203

Faire un lien personnalisé et faire ce que vous voulez au robinet est devenu beaucoup plus facile avec iOS 7. Il existe un très bon exemple sur Ray Wenderlich

24
Aditya Mathur

exemple WWDC 201 :

NSLayoutManager *layoutManager = textView.layoutManager;
 CGPoint location = [touch locationInView:textView];
 NSUInteger characterIndex;
 characterIndex = [layoutManager characterIndexForPoint:location
inTextContainer:textView.textContainer
fractionOfDistanceBetweenInsertionPoints:NULL];
if (characterIndex < textView.textStorage.length) { 
// valid index
// Find the Word range here
// using -enumerateSubstringsInRange:options:usingBlock:
}
11
Shmidt

J'ai pu résoudre cela assez simplement avec NSLinkAttributeName

Swift 2

class MyClass: UIViewController, UITextViewDelegate {

  @IBOutlet weak var tvBottom: UITextView!

  override func viewDidLoad() {
      super.viewDidLoad()

     let attributedString = NSMutableAttributedString(string: "click me ok?")
     attributedString.addAttribute(NSLinkAttributeName, value: "cs://moreinfo", range: NSMakeRange(0, 5))
     tvBottom.attributedText = attributedString
     tvBottom.delegate = self

  }

  func textView(textView: UITextView, shouldInteractWithURL URL: NSURL, inRange characterRange: NSRange) -> Bool {
      UtilityFunctions.alert("clicked", message: "clicked")
      return false
  }

}
10
Jase Whatson

Exemple complet pour détecter des actions sur un texte attribué avec Swift

let termsAndConditionsURL = TERMS_CONDITIONS_URL;
let privacyURL            = PRIVACY_URL;

override func viewDidLoad() {
    super.viewDidLoad()

    self.txtView.delegate = self
    let str = "By continuing, you accept the Terms of use and Privacy policy"
    let attributedString = NSMutableAttributedString(string: str)
    var foundRange = attributedString.mutableString.range(of: "Terms of use") //mention the parts of the attributed text you want to tap and get an custom action
    attributedString.addAttribute(NSLinkAttributeName, value: termsAndConditionsURL, range: foundRange)
    foundRange = attributedString.mutableString.range(of: "Privacy policy")
    attributedString.addAttribute(NSLinkAttributeName, value: privacyURL, range: foundRange)
    txtView.attributedText = attributedString
}

Et puis vous pouvez voir l'action avec shouldInteractWith URL UITextViewDelegate delegate method.Assurez-vous d’avoir défini le délégué correctement.

func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let vc = storyboard.instantiateViewController(withIdentifier: "WebView") as! SKWebViewController

        if (URL.absoluteString == termsAndConditionsURL) {
            vc.strWebURL = TERMS_CONDITIONS_URL
            self.navigationController?.pushViewController(vc, animated: true)
        } else if (URL.absoluteString == privacyURL) {
            vc.strWebURL = PRIVACY_URL
            self.navigationController?.pushViewController(vc, animated: true)
        }
        return false
    }

De même, vous pouvez effectuer n'importe quelle action en fonction de vos besoins.

À votre santé!!

7
Akila Wasala

Il est possible de faire cela avec characterIndexForPoint:inTextContainer:fractionOfDistanceBetweenInsertionPoints: . Cela fonctionnera un peu différemment de ce que vous vouliez - vous devrez tester si un personnage engagé appartient à un mot magique. Mais ça ne devrait pas être compliqué.

BTW, je recommande fortement de regarder Introducing Text Kit à partir de WWDC 2013.

4
Arek Holko

Utilisez cette extension pour Swift:

import UIKit

extension UITapGestureRecognizer {

    func didTapAttributedTextInTextView(textView: UITextView, inRange targetRange: NSRange) -> Bool {
        let layoutManager = textView.layoutManager
        let locationOfTouch = self.location(in: textView)
        let index = layoutManager.characterIndex(for: locationOfTouch, in: textView.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)

        return NSLocationInRange(index, targetRange)
    }
}

Ajoutez UITapGestureRecognizer à votre affichage texte avec le sélecteur suivant:

guard let text = textView.attributedText?.string else {
        return
}
let textToTap = "Tap me"
if let range = text.range(of: tapableText),
      tapGesture.didTapAttributedTextInTextView(textView: textTextView, inRange: NSRange(range, in: text)) {
                // Tap recognized
}
1
Mol0ko

Celui-ci pourrait fonctionner correctement avec un lien court, une liaison multiple dans une vue de texte. Cela fonctionne bien avec iOS 6,7,8.

- (void)tappedTextView:(UITapGestureRecognizer *)tapGesture {
    if (tapGesture.state != UIGestureRecognizerStateEnded) {
        return;
    }
    UITextView *textView = (UITextView *)tapGesture.view;
    CGPoint tapLocation = [tapGesture locationInView:textView];

    NSDataDetector *detector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink|NSTextCheckingTypePhoneNumber
                                                           error:nil];
    NSArray* resultString = [detector matchesInString:self.txtMessage.text options:NSMatchingReportProgress range:NSMakeRange(0, [self.txtMessage.text length])];
    BOOL isContainLink = resultString.count > 0;

    if (isContainLink) {
        for (NSTextCheckingResult* result in  resultString) {
            CGRect linkPosition = [self frameOfTextRange:result.range inTextView:self.txtMessage];

            if(CGRectContainsPoint(linkPosition, tapLocation) == 1){
                if (result.resultType == NSTextCheckingTypePhoneNumber) {
                    NSString *phoneNumber = [@"telprompt://" stringByAppendingString:result.phoneNumber];
                    [[UIApplication sharedApplication] openURL:[NSURL URLWithString:phoneNumber]];
                }
                else if (result.resultType == NSTextCheckingTypeLink) {
                    [[UIApplication sharedApplication] openURL:result.URL];
                }
            }
        }
    }
}

 - (CGRect)frameOfTextRange:(NSRange)range inTextView:(UITextView *)textView
{
    UITextPosition *beginning = textView.beginningOfDocument;
    UITextPosition *start = [textView positionFromPosition:beginning offset:range.location];
    UITextPosition *end = [textView positionFromPosition:start offset:range.length];
    UITextRange *textRange = [textView textRangeFromPosition:start toPosition:end];
    CGRect firstRect = [textView firstRectForRange:textRange];
    CGRect newRect = [textView convertRect:firstRect fromView:textView.textInputView];
    return newRect;
}
1
Tony TRAN

Avec Swift 5 et iOS 12, vous pouvez créer une sous-classe de UITextView et le remplacer par point(inside:with:) avec une implémentation TextKit dans l'ordre pour ne faire que quelques NSAttributedStrings en tappable.


Le code suivant montre comment créer un UITextView qui réagit uniquement en tapant sur le NSAttributedStrings souligné:

InteractiveUnderlinedTextView.Swift

import UIKit

class InteractiveUnderlinedTextView: UITextView {

    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        configure()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        configure()
    }

    func configure() {
        isScrollEnabled = false
        isEditable = false
        isSelectable = false
        isUserInteractionEnabled = true
    }

    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        let superBool = super.point(inside: point, with: event)

        let characterIndex = layoutManager.characterIndex(for: point, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
        guard characterIndex < textStorage.length else { return false }
        let attributes = textStorage.attributes(at: characterIndex, effectiveRange: nil)

        return superBool && attributes[NSAttributedString.Key.underlineStyle] != nil
    }

}

ViewController.Swift

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let linkTextView = InteractiveUnderlinedTextView()
        linkTextView.backgroundColor = .orange

        let mutableAttributedString = NSMutableAttributedString(string: "Some text\n\n")
        let attributes = [NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue]
        let underlinedAttributedString = NSAttributedString(string: "Some other text", attributes: attributes)
        mutableAttributedString.append(underlinedAttributedString)
        linkTextView.attributedText = mutableAttributedString

        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(underlinedTextTapped))
        linkTextView.addGestureRecognizer(tapGesture)

        view.addSubview(linkTextView)
        linkTextView.translatesAutoresizingMaskIntoConstraints = false
        linkTextView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        linkTextView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
        linkTextView.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor).isActive = true

    }

    @objc func underlinedTextTapped(_ sender: UITapGestureRecognizer) {
        print("Hello")
    }

}
1
Imanou Petit