Erkennen von Abgriffen auf zugeordneten Text in einer UITextView unter iOS

122

Ich habe eine, UITextViewdie eine anzeigt NSAttributedString. Diese Zeichenfolge enthält Wörter, die ich tippbar machen möchte, sodass ich beim Tippen zurückgerufen werde, damit ich eine Aktion ausführen kann. Mir ist klar, dass UITextViewdas Abhören einer URL erkannt und mein Delegat zurückgerufen werden kann, aber dies sind keine URLs.

Es scheint mir, dass dies mit iOS 7 und der Leistungsfähigkeit von TextKit jetzt möglich sein sollte, ich kann jedoch keine Beispiele finden und bin mir nicht sicher, wo ich anfangen soll.

Ich verstehe, dass es jetzt möglich ist, benutzerdefinierte Attribute in der Zeichenfolge zu erstellen (obwohl ich dies noch nicht getan habe), und vielleicht sind diese nützlich, um festzustellen, ob eines der magischen Wörter getippt wurde? Auf jeden Fall weiß ich immer noch nicht, wie ich diesen Tipp abfangen und erkennen soll, bei welchem ​​Wort der Tipp aufgetreten ist.

Beachten Sie, dass keine iOS 6-Kompatibilität erforderlich ist.

Tarmes
quelle

Antworten:

118

Ich wollte nur ein bisschen mehr anderen helfen. Nach Shmidts Antwort ist es möglich, genau das zu tun, was ich in meiner ursprünglichen Frage gestellt hatte.

1) Erstellen Sie eine zugeordnete Zeichenfolge mit benutzerdefinierten Attributen, die auf die anklickbaren Wörter angewendet werden. z.B.

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

2) Erstellen Sie eine UITextView, um diese Zeichenfolge anzuzeigen, und fügen Sie ihr einen UITapGestureRecognizer hinzu. Dann handhaben Sie den Wasserhahn:

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

    }
}

So einfach, wenn Sie wissen wie!

Tarmes
quelle
Wie würden Sie dies in IOS 6 lösen? Können Sie sich bitte diese Frage ansehen? stackoverflow.com/questions/19837522/…
Steaphann
Eigentlich ist charakterIndexForPoint: inTextContainer: FractionOfDistanceBetweenInsertionPoints unter iOS 6 verfügbar, daher denke ich, dass es funktionieren sollte. Lass uns wissen! In diesem Projekt finden Sie ein Beispiel: github.com/laevandus/NSTextFieldHyperlinks/blob/master/…
Tarmes
Dokumentation sagt, dass es nur in IOS 7 oder höher verfügbar ist :)
Steaphann
1
Ja Entschuldigung. Ich habe mich mit Mac OS verwechselt! Dies ist nur iOS7.
Tarmes
Es scheint nicht zu funktionieren, wenn Sie UITextView
Paul Brewczynski
64

Erkennen von Abgriffen auf zugeordneten Text mit Swift

Manchmal ist es für Anfänger etwas schwierig zu wissen, wie man Dinge einrichtet (es war sowieso für mich), daher ist dieses Beispiel etwas ausführlicher.

Fügen Sie UITextViewIhrem Projekt ein hinzu.

Auslauf

Schließen Sie das UITextViewan die ViewControllermit einem benannten Auslass textView.

Benutzerdefiniertes Attribut

Wir werden ein benutzerdefiniertes Attribut erstellen, indem wir eine Erweiterung erstellen .

Hinweis: Dieser Schritt ist technisch optional. Wenn Sie ihn jedoch nicht ausführen, müssen Sie den Code im nächsten Teil bearbeiten, um ein Standardattribut wie zu verwenden NSAttributedString.Key.foregroundColor. Der Vorteil der Verwendung eines benutzerdefinierten Attributs besteht darin, dass Sie definieren können, welche Werte im zugeordneten Textbereich gespeichert werden sollen.

Fügen Sie eine neue schnelle Datei mit Datei> Neu> Datei ...> iOS> Quelle> Schnelle Datei hinzu . Sie können es so nennen, wie Sie wollen. Ich rufe meinen NSAttributedStringKey + CustomAttribute.swift auf .

Fügen Sie den folgenden Code ein:

import Foundation

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

Code

Ersetzen Sie den Code in ViewController.swift durch den folgenden. Beachten Sie die 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)")
            }

        }
    }
}

Geben Sie hier die Bildbeschreibung ein

Wenn Sie nun auf das "w" von "Swift" tippen, sollten Sie das folgende Ergebnis erhalten:

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

Anmerkungen

  • Hier habe ich ein benutzerdefiniertes Attribut verwendet, aber es hätte genauso gut NSAttributedString.Key.foregroundColor(Textfarbe) sein können, das einen Wert von hat UIColor.green.
  • Früher konnte die Textansicht nicht bearbeitet oder ausgewählt werden, aber in meiner aktualisierten Antwort für Swift 4.2 scheint sie einwandfrei zu funktionieren, unabhängig davon, ob diese ausgewählt sind oder nicht.

Weitere Studie

Diese Antwort basierte auf mehreren anderen Antworten auf diese Frage. Daneben siehe auch

Suragch
quelle
Verwenden Sie myTextView.textStorageanstelle von myTextView.attributedText.string
fatihyildizhan
Das Erkennen von Tippen durch Tippen in iOS 9 funktioniert nicht für aufeinanderfolgende Tippen. Irgendwelche Updates dazu?
Dheeraj Jami
1
@WaqasMahmood, ich habe eine neue Frage für dieses Problem gestartet . Sie können es markieren und später erneut nach Antworten suchen. Fühlen Sie sich frei, diese Frage zu bearbeiten oder Kommentare hinzuzufügen, wenn es weitere relevante Details gibt.
Suragch
1
@dejix Ich behebe das Problem, indem ich jedes Mal eine weitere leere Zeichenfolge am Ende meiner Textansicht hinzufüge. Auf diese Weise stoppt die Erkennung nach Ihrem letzten Wort. Hoffe es hilft
PoolHallJunkie
1
Funktioniert perfekt mit mehreren Taps. Ich habe nur eine kurze Routine eingefügt, um dies zu beweisen: Wenn characterIndex <12 {textView.textColor = UIColor.magenta} else {textView.textColor = UIColor.blue} Wirklich klarer und einfacher Code
Jeremy Andrews
32

Dies ist eine leicht modifizierte Version, die auf der Antwort von @tarmes aufbaut. Ich konnte die valueVariable nicht dazu bringen , etwas zurückzugeben, außer nullohne die Optimierung unten. Außerdem musste das vollständige Attributwörterbuch zurückgegeben werden, um die resultierende Aktion zu bestimmen. Ich hätte dies in die Kommentare aufgenommen, habe aber anscheinend nicht den Repräsentanten, der dies tut. Entschuldigung im Voraus, wenn ich gegen das Protokoll verstoßen habe.

Spezifische Optimierung ist textView.textStorageanstelle von zu verwenden textView.attributedText. Als noch lernender iOS-Programmierer bin ich mir nicht sicher, warum das so ist, aber vielleicht kann uns jemand anderes aufklären.

Spezifische Änderung der Tap-Handhabungsmethode:

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

Vollständiger Code in meinem View Controller

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

    }
}
natenash203
quelle
Hatte das gleiche Problem mit dem textView.attributedText! DANKE für den textView.textStorage-Hinweis!
Kai Burghardt
Das Erkennen von Tippen durch Tippen in iOS 9 funktioniert nicht für aufeinanderfolgende Tippen.
Dheeraj Jami
25

Mit iOS 7 ist es viel einfacher geworden, benutzerdefinierte Links zu erstellen und das zu tun, was Sie möchten. Bei Ray Wenderlich gibt es ein sehr gutes Beispiel

Aditya Mathur
quelle
Dies ist eine viel sauberere Lösung als der Versuch, Zeichenfolgenpositionen relativ zu ihrer Containeransicht zu berechnen.
Chris C
2
Das Problem ist, dass textView auswählbar sein muss und ich dieses Verhalten nicht möchte.
Thomás Calmon
@ ThomásC. +1 für den Zeiger, warum ich UITextViewLinks nicht erkannt habe, obwohl ich sie so eingestellt hatte, dass sie über IB erkannt werden. (Ich hatte es auch nicht
auswählbar
13

WWDC 2013 Beispiel :

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:
}
Shmidt
quelle
Danke dir! Ich werde mir auch das WWDC-Video ansehen.
Tarmes
@Suragch "Erweiterte Textlayouts und Effekte mit Text Kit".
Shmidt
10

Ich konnte dies ziemlich einfach mit NSLinkAttributeName lösen

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
  }

}
Jase Whatson
quelle
Sie sollten überprüfen, ob Ihre URL getippt wurde und nicht eine andere URL mit if URL.scheme == "cs"und return trueaußerhalb der ifAnweisung, damit die UITextViewnormalen https://Links, die getippt werden, verarbeitet werden können
Daniel Storm
Ich habe das gemacht und es hat einigermaßen gut auf iPhone 6 und 6+ funktioniert, aber auf iPhone 5 überhaupt nicht. Ging mit der obigen Suragch-Lösung, die einfach funktioniert. Nie herausgefunden, warum iPhone 5 ein Problem damit haben würde, ergab keinen Sinn.
n13
9

Vollständiges Beispiel zum Erkennen von Aktionen für zugeordneten Text mit 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
}

Anschließend können Sie die Aktion mit der shouldInteractWith URLUITextViewDelegate-Delegatmethode abfangen. Stellen Sie daher sicher, dass Sie den Delegaten ordnungsgemäß festgelegt haben.

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
    }

Ebenso können Sie jede Aktion entsprechend Ihren Anforderungen ausführen.

Prost!!

Akila Wasala
quelle
Vielen Dank! Du rettest meinen Tag!
Dmih
4

Mit Swift 5 und iOS 12 können Sie eine Unterklasse von TextKit-Implementierungen erstellen UITextViewund diese überschreiben point(inside:with:), um nur einige NSAttributedStringsdavon abrufbar zu machen .


Der folgende Code zeigt, wie Sie eine erstellen UITextView, die nur auf das Tippen auf unterstrichene NSAttributedStrings reagiert :

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

}
Imanou Petit
quelle
Hallo, gibt es eine Möglichkeit, dies an mehrere Attribute anzupassen, anstatt nur an eines?
David Lintin
1

Dieser funktioniert möglicherweise in Ordnung mit einem kurzen Link, einem Multilink in einer Textansicht. Es funktioniert gut mit 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;
}
Tony TRAN
quelle
Das Erkennen von Tippen durch Tippen in iOS 9 funktioniert nicht für aufeinanderfolgende Tippen.
Dheeraj Jami
1

Verwenden Sie diese Erweiterung für 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)
    }
}

Fügen Sie UITapGestureRecognizerIhrer Textansicht mit der folgenden Auswahl hinzu:

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
}
Mol0ko
quelle