Wykrywanie dotknięć przypisanego tekstu w UITextView w iOS

122

Mam, UITextViewktóry wyświetla NSAttributedString. Ten ciąg zawiera słowa, które chciałbym uczynić możliwymi do kliknięcia, tak że po ich dotknięciu jestem wywoływany z powrotem, aby móc wykonać akcję. Zdaję sobie sprawę, że UITextViewmoże wykryć dotknięcia adresu URL i oddzwonić do mojego pełnomocnika, ale to nie są adresy URL.

Wydaje mi się, że z iOS 7 i mocą TextKit powinno to być teraz możliwe, jednak nie mogę znaleźć żadnych przykładów i nie jestem pewien, od czego zacząć.

Rozumiem, że jest teraz możliwe tworzenie niestandardowych atrybutów w ciągu znaków (chociaż jeszcze tego nie zrobiłem) i być może przydadzą się do wykrycia, czy któreś z magicznych słów zostało dotknięte? W każdym razie nadal nie wiem, jak przechwycić to dotknięcie i wykryć, na którym słowie nastąpiło dotknięcie.

Pamiętaj, że zgodność z iOS 6 nie jest wymagana.

tarmes
źródło

Odpowiedzi:

118

Chciałem tylko trochę bardziej pomóc innym. Kontynuując odpowiedź Shmidta, można zrobić dokładnie to, o co zadałem w moim pierwotnym pytaniu.

1) Utwórz ciąg znaków z przypisanymi atrybutami i zastosuj niestandardowe atrybuty do klikalnych słów. na przykład.

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

2) Utwórz UITextView, aby wyświetlić ten ciąg, i dodaj do niego UITapGestureRecognizer. Następnie obsłuż kran:

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

    }
}

Takie proste, kiedy wiesz, jak to zrobić!

tarmes
źródło
Jak rozwiązałbyś to w IOS 6? Czy możesz spojrzeć na to pytanie? stackoverflow.com/questions/19837522/…
Steaphann
Właściwie characterIndexForPoint: inTextContainer: fractionOfDistanceBetweenInsertionPoints jest dostępny na iOS 6, więc myślę, że powinien działać. Powiadom nas! Zobacz ten projekt jako przykład: github.com/laevandus/NSTextFieldHyperlinks/blob/master/…
tarmes
Dokumentacja mówi, że jest dostępna tylko w
systemie
1
Tak, przepraszam. Mylę się z Mac OS! To jest tylko iOS7.
tarmes
Wygląda na to, że nie działa, gdy nie można wybrać UITextView
Paul Brewczyński
64

Wykrywanie dotknięć przypisanego tekstu za pomocą Swift

Czasami początkującym jest trochę trudno wiedzieć, jak to zrobić (i tak było dla mnie), więc ten przykład jest trochę pełniejszy.

Dodaj UITextViewdo swojego projektu.

Wylot

Podłącz UITextViewdo ViewControllerz gniazdem o nazwietextView .

Atrybut niestandardowy

Zamierzamy utworzyć atrybut niestandardowy, tworząc rozszerzenie .

Uwaga: ten krok jest technicznie opcjonalny, ale jeśli tego nie zrobisz, będziesz musiał edytować kod w następnej części, aby użyć standardowego atrybutu, takiego jak NSAttributedString.Key.foregroundColor. Zaletą używania atrybutu niestandardowego jest to, że możesz określić, jakie wartości chcesz przechowywać w przypisanym zakresie tekstowym.

Dodaj nowy plik Swift za pomocą polecenia Plik> Nowy> Plik ...> iOS> Źródło> Swift Plik . Możesz to nazwać, jak chcesz. Nazywam mój NSAttributedStringKey + CustomAttribute.swift .

Wklej następujący kod:

import Foundation

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

Kod

Zastąp kod w ViewController.swift następującym. Zwróć uwagę na 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)")
            }

        }
    }
}

wprowadź opis obrazu tutaj

Jeśli teraz dotkniesz „w” opcji „Swift”, powinieneś otrzymać następujący wynik:

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

Uwagi

  • Tutaj użyłem atrybutu niestandardowego, ale równie łatwo mógł NSAttributedString.Key.foregroundColor(kolor tekstu) mieć wartość UIColor.green.
  • Wcześniej widoku tekstu nie można było edytować ani wybierać, ale w mojej zaktualizowanej odpowiedzi dla Swift 4.2 wydaje się, że działa dobrze, niezależnie od tego, czy są one zaznaczone, czy nie.

Dalsze badanie

Ta odpowiedź została oparta na kilku innych odpowiedziach na to pytanie. Oprócz tego zobacz także

Suragch
źródło
użyj myTextView.textStoragezamiast myTextView.attributedText.string
fatihyildizhan
Wykrywanie dotknięcia gestem w iOS 9 nie działa w przypadku kolejnych dotknięć. Jakieś aktualizacje na ten temat?
Dheeraj Jami
1
@WaqasMahmood, zacząłem nowe pytanie w tej sprawie. Możesz oznaczyć gwiazdką i później sprawdzić odpowiedzi. Możesz edytować to pytanie lub dodawać komentarze, jeśli są jakieś bardziej istotne szczegóły.
Suragch
1
@dejix Rozwiązuję ten problem dodając za każdym razem kolejny "" pusty ciąg na końcu mojego TextView. W ten sposób wykrywanie kończy się po Twoim ostatnim słowie. Mam nadzieję, że to pomoże
PoolHallJunkie
1
Działa idealnie z wieloma dotknięciami, po prostu zastosowałem krótką procedurę, aby to udowodnić: if characterIndex <12 {textView.textColor = UIColor.magenta} else {textView.textColor = UIColor.blue} Naprawdę jasny i prosty kod
Jeremy Andrews
32

To jest nieco zmodyfikowana wersja, oparta na odpowiedzi @tarmes. Nie mogłem zmusić valuezmiennej do zwracania czegokolwiek, ale nullbez poniższych poprawek. Potrzebowałem również zwróconego pełnego słownika atrybutów, aby określić wynikową akcję. Umieściłbym to w komentarzach, ale nie wydaje się, żebym miał tego przedstawiciela. Z góry przepraszam, jeśli naruszyłem protokół.

Specyficzną modyfikacją jest użycie textView.textStoragezamiast textView.attributedText. Jako wciąż uczący się programista iOS nie jestem do końca pewien, dlaczego tak jest, ale być może ktoś inny może nas oświecić.

Specyficzna modyfikacja sposobu obsługi kranu:

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

Pełny kod w moim kontrolerze widoku

- (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
źródło
Miałem ten sam problem z textView.attributedText! DZIĘKUJEMY za wskazówkę textView.textStorage!
Kai Burghardt
Wykrywanie dotknięcia gestem w iOS 9 nie działa w przypadku kolejnych dotknięć.
Dheeraj Jami
25

Tworzenie niestandardowego łącza i robienie tego, co chcesz za pomocą kranu, stało się znacznie łatwiejsze dzięki iOS 7. Bardzo dobry przykład jest w Ray Wenderlich

Aditya Mathur
źródło
Jest to znacznie czystsze rozwiązanie niż próba obliczenia pozycji ciągów względem ich widoku kontenera.
Chris C
2
Problem polega na tym, że textView musi być wybieralny i nie chcę tego zachowania.
Thomás Calmon
@ ThomásC. +1 dla wskaźnika, dlaczego mój UITextViewnie wykrywa linków, nawet jeśli ustawiłem go na wykrywanie ich przez IB. (Zrobiłem również, że nie można go wybrać)
Kedar Paranjape
13

Przykład 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:
}
Shmidt
źródło
Dziękuję Ci! Obejrzę też wideo WWDC.
tarmes
@Suragch „Zaawansowane układy i efekty tekstu z zestawem tekstowym”.
Shmidt
10

Udało mi się rozwiązać ten problem po prostu za pomocą NSLinkAttributeName

Szybki 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
źródło
Powinieneś sprawdzić, czy Twój adres URL został dotknięty, a nie inny adres URL z instrukcją if URL.scheme == "cs"i return truepoza nią if, aby UITextViewmógł obsłużyć zwykłe klikane https://linki
Daniel Storm
Zrobiłem to i działało całkiem dobrze na iPhonie 6 i 6+, ale nie działało wcale na iPhonie 5. Poszedłem z powyższym rozwiązaniem Suragch, które po prostu działa. Nigdy nie dowiedziałem się, dlaczego iPhone 5 miałby z tym problem, bez sensu.
n13,
9

Kompletny przykład wykrywania działań na przypisanym tekście za pomocą 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
}

Następnie możesz złapać akcję za pomocą shouldInteractWith URLmetody delegata UITextViewDelegate, więc upewnij się, że ustawiłeś delegata poprawnie.

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
    }

Podobnie jak mądrze, możesz wykonać dowolną akcję zgodnie ze swoimi wymaganiami.

Twoje zdrowie!!

Akila Wasala
źródło
Dzięki! Ratujesz mój dzień!
Dmih
4

Dzięki Swift 5 i iOS 12 możesz utworzyć podklasę UITextViewi zastąpić point(inside:with:)ją niektórymi implementacjami TextKit, aby tylko niektóre NSAttributedStringsz nich były dostępne.


Poniższy kod pokazuje, jak utworzyć element, UITextViewktóry reaguje tylko na dotknięcia podkreślonych NSAttributedStringw nim s:

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
źródło
Cześć, czy istnieje sposób, aby to było zgodne z wieloma atrybutami, a nie tylko z jednym?
David Lintin
1

Ten może działać dobrze z krótkim linkiem, łączem wielokrotnym w widoku tekstowym. Działa OK z 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
źródło
Wykrywanie dotknięcia gestem w iOS 9 nie działa w przypadku kolejnych dotknięć.
Dheeraj Jami
1

Użyj tego rozszerzenia dla 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)
    }
}

Dodaj UITapGestureRecognizerdo widoku tekstu za pomocą następującego selektora:

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
źródło