Rilevamento di tocchi sul testo attribuito in un UITextView in iOS


122

Ho un UITextViewche mostra un NSAttributedString. Questa stringa contiene parole che vorrei rendere toccabili, in modo tale che quando vengono toccate vengo richiamato in modo da poter eseguire un'azione. Mi rendo conto che UITextViewpuò rilevare i tocchi su un URL e richiamare il mio delegato, ma questi non sono URL.

Mi sembra che con iOS 7 e la potenza di TextKit questo dovrebbe essere ora possibile, tuttavia non riesco a trovare alcun esempio e non sono sicuro da dove iniziare.

Capisco che ora è possibile creare attributi personalizzati nella stringa (anche se non l'ho ancora fatto), e forse questi saranno utili per rilevare se una delle parole magiche è stata toccata? In ogni caso, non so ancora come intercettare quel tap e rilevare su quale parola è avvenuto il tap.

Nota che la compatibilità con iOS 6 non è richiesta.

Risposte:


118

Volevo solo aiutare gli altri un po 'di più. A seguito della risposta di Shmidt è possibile fare esattamente come avevo chiesto nella mia domanda originale.

1) Crea una stringa con attributi con attributi personalizzati applicati alle parole selezionabili. per esempio.

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

2) Crea un UITextView per visualizzare quella stringa e aggiungi un UITapGestureRecognizer ad esso. Quindi gestisci il rubinetto:

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

    }
}

Così facile quando sai come!


Come risolveresti questo in IOS 6? Puoi dare un'occhiata a questa domanda? stackoverflow.com/questions/19837522/...
Steaphann

In realtà characterIndexForPoint: inTextContainer: fractionOfDistanceBetweenInsertionPoints è disponibile su iOS 6, quindi penso che dovrebbe funzionare. Facci sapere! Guarda questo progetto per un esempio: github.com/laevandus/NSTextFieldHyperlinks/blob/master/…
tarmes

La documentazione dice che è disponibile solo in IOS 7 o
versioni

1
Sì scusa. Mi stavo confondendo con Mac OS! Questo è solo iOS7.
asfalto

Non sembra funzionare, quando hai UITextView non selezionabile
Paul Brewczynski

64

Rilevamento dei tocchi sul testo attribuito con Swift

A volte per i principianti è un po 'difficile sapere come sistemare le cose (lo era comunque per me), quindi questo esempio è un po' più completo.

Aggiungi una UITextViewal tuo progetto.

Presa

Collega il UITextViewa ViewControllercon una presa denominata textView.

Attributo personalizzato

Creeremo un attributo personalizzato creando un'estensione .

Nota: questo passaggio è tecnicamente facoltativo, ma se non lo fai dovrai modificare il codice nella parte successiva per utilizzare un attributo standard come NSAttributedString.Key.foregroundColor. Il vantaggio dell'utilizzo di un attributo personalizzato è che puoi definire i valori che desideri memorizzare nell'intervallo di testo attribuito.

Aggiungi un nuovo file swift con File> Nuovo> File ...> iOS> Sorgente> File Swift . Puoi chiamarlo come vuoi. Sto chiamando il mio NSAttributedStringKey + CustomAttribute.swift .

Incolla il codice seguente:

import Foundation

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

Codice

Sostituisci il codice in ViewController.swift con il seguente. Nota il 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)")
            }

        }
    }
}

inserisci qui la descrizione dell'immagine

Ora se tocchi la "w" di "Swift", dovresti ottenere il seguente risultato:

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

Appunti

  • Qui ho usato un attributo personalizzato, ma avrebbe potuto essere altrettanto facilmente NSAttributedString.Key.foregroundColor(colore del testo) che ha un valore di UIColor.green.
  • In precedenza la visualizzazione del testo non poteva essere modificabile o selezionabile, ma nella mia risposta aggiornata per Swift 4.2 sembra funzionare bene, indipendentemente dal fatto che siano selezionati o meno.

Ulteriore studio

Questa risposta era basata su molte altre risposte a questa domanda. Oltre a questi, vedi anche


utilizzare myTextView.textStorageinvece di myTextView.attributedText.string
fatihyildizhan

Il rilevamento del tocco tramite tocco in iOS 9 non funziona per i tocchi successivi. Eventuali aggiornamenti su questo?
Dheeraj Jami

1
@ WaqasMahmood, ho iniziato una nuova domanda per questo problema. Puoi aggiungerlo a Speciali e ricontrollare più tardi per eventuali risposte. Sentiti libero di modificare la domanda o aggiungere commenti se ci sono dettagli più pertinenti.
Suragch

1
@dejix risolvo il problema aggiungendo ogni volta un'altra "" stringa vuota alla fine del mio TextView. In questo modo il rilevamento si interrompe dopo la tua ultima parola. Spero che aiuti
PoolHallJunkie

1
Funziona perfettamente con più tocchi, ho appena inserito una breve routine per dimostrarlo: if characterIndex <12 {textView.textColor = UIColor.magenta} else {textView.textColor = UIColor.blue} Codice davvero chiaro e semplice
Jeremy Andrews

32

Questa è una versione leggermente modificata, basata sulla risposta di @tarmes. Non sono riuscito a ottenere che la valuevariabile restituisse nulla, ma nullsenza il tweak di seguito. Inoltre, avevo bisogno del dizionario completo degli attributi restituito per determinare l'azione risultante. Lo avrei messo nei commenti ma non sembra che abbia il rappresentante per farlo. Mi scuso in anticipo se ho violato il protocollo.

Il tweak specifico è usare al textView.textStorageposto di textView.attributedText. Come programmatore iOS che sta ancora imparando, non sono proprio sicuro del motivo per cui questo è, ma forse qualcun altro può illuminarci.

Modifica specifica nel metodo di manipolazione del rubinetto:

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

Codice completo nel mio controller di visualizzazione

- (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

    }
}

Ha avuto lo stesso problema con textView.attributedText! GRAZIE per il suggerimento textView.textStorage!
Kai Burghardt

Il rilevamento del tocco tramite tocco in iOS 9 non funziona per i tocchi successivi.
Dheeraj Jami

25

Creare un collegamento personalizzato e fare quello che vuoi al tocco è diventato molto più semplice con iOS 7. C'è un ottimo esempio in Ray Wenderlich


Questa è una soluzione molto più pulita rispetto al tentativo di calcolare le posizioni delle stringhe rispetto alla visualizzazione del contenitore.
Chris C

2
Il problema è che textView deve essere selezionabile e non voglio questo comportamento.
Thomás Calmon

@ Thomasc. +1 per il puntatore sul motivo per cui il mio UITextViewnon stava rilevando i collegamenti anche quando l'avevo impostato per rilevarli tramite IB. (Lo avevo anche reso non selezionabile)
Kedar Paranjape

13

Esempio WWDC 2013 :

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

Grazie! Guarderò anche il video del WWDC.
asfalto il

@ Suragch "Effetti e layout di testo avanzati con kit di testo".
Shmidt

10

Sono stato in grado di risolvere questo problema in modo abbastanza semplice con 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
  }

}

Dovresti controllare che il tuo URL sia stato toccato e non un altro URL con if URL.scheme == "cs"e return trueal di fuori ifdell'istruzione in modo che UITextViewpossa gestire i normali https://collegamenti che vengono toccati
Daniel Storm

L'ho fatto e ha funzionato abbastanza bene su iPhone 6 e 6+ ma non ha funzionato affatto su iPhone 5. Sono andato con la soluzione Suragch sopra, che funziona. Non ho mai scoperto perché iPhone 5 avrebbe avuto un problema con questo, non aveva senso.
n13

9

Esempio completo per rilevare azioni sul testo attribuito con Swift 3

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
}

Quindi puoi catturare l'azione con il shouldInteractWith URLmetodo delegato UITextViewDelegate, quindi assicurati di aver impostato correttamente il delegato.

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
    }

Allo stesso modo, puoi eseguire qualsiasi azione in base alle tue esigenze.

Saluti!!


Grazie! Mi salvi la giornata!
Dmih


4

Con Swift 5 e iOS 12, puoi creare una sottoclasse di UITextViewed eseguire l'override point(inside:with:)con alcune implementazioni di TextKit per renderne selezionabili solo alcune NSAttributedStrings.


Il codice seguente mostra come creare un UITextViewche reagisce solo ai tocchi sulle NSAttributedStrings sottolineate in esso:

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

}

Ciao, esiste un modo per renderlo conforme a più attributi anziché a uno solo?
David Lintin,

1

Questo potrebbe funzionare bene con il collegamento breve, multilink in una visualizzazione di testo. Funziona bene con 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;
}

Il rilevamento del tocco tramite tocco in iOS 9 non funziona per i tocchi successivi.
Dheeraj Jami

1

Usa questa estensione per 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)
    }
}

Aggiungi UITapGestureRecognizeralla visualizzazione del testo con il seguente selettore:

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
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.