Mendeteksi ketukan pada teks yang diatribusikan dalam UITextView di iOS


122

Saya memiliki UITextViewyang menampilkan NSAttributedString. String ini berisi kata-kata yang ingin saya buat dapat disadap, sehingga ketika diketuk, saya dipanggil kembali sehingga saya dapat melakukan tindakan. Saya menyadari bahwa UITextViewdapat mendeteksi ketukan pada URL dan memanggil kembali delegasi saya, tetapi ini bukan URL.

Menurut saya, dengan iOS 7 dan kekuatan TextKit sekarang ini seharusnya dapat dilakukan, namun saya tidak dapat menemukan contoh apa pun dan saya tidak yakin harus mulai dari mana.

Saya mengerti bahwa sekarang mungkin untuk membuat atribut khusus dalam string (meskipun saya belum melakukannya), dan mungkin ini akan berguna untuk mendeteksi jika salah satu kata ajaib telah diketuk? Bagaimanapun, saya masih tidak tahu cara mencegat ketukan itu dan mendeteksi kata mana ketukan itu terjadi.

Perhatikan bahwa kompatibilitas iOS 6 tidak diperlukan.

Jawaban:


118

Saya hanya ingin lebih membantu orang lain. Mengikuti dari tanggapan Shmidt, dimungkinkan untuk melakukan persis seperti yang saya tanyakan dalam pertanyaan awal saya.

1) Buat string yang dikaitkan dengan atribut khusus yang diterapkan ke kata-kata yang dapat diklik. misalnya.

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

2) Buat UITextView untuk menampilkan string itu, dan tambahkan UITapGestureRecognizer ke dalamnya. Lalu tangani keran:

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

    }
}

Sangat mudah bila Anda tahu caranya!


Bagaimana Anda mengatasi ini di IOS 6? Bisakah Anda melihat pertanyaan ini? stackoverflow.com/questions/19837522/…
Steaphann

Sebenarnya characterIndexForPoint: inTextContainer: fractionOfDistanceBetweenInsertionPoints tersedia di iOS 6, jadi menurut saya seharusnya berfungsi. Beritahu kami! Lihat proyek ini untuk contoh: github.com/laevandus/NSTextFieldHyperlinks/blob/master/…
tarmes

Dokumentasi mengatakan itu hanya tersedia di IOS 7 atau lebih baru :)
Steaphann

1
Ya maaf. Saya mulai bingung dengan Mac OS! Ini hanya untuk iOS7.
tarmes

Tampaknya tidak berhasil, jika Anda memiliki UITextView yang tidak dapat dipilih
Paul Brewczynski

64

Mendeteksi ketukan pada teks yang diatribusikan dengan Swift

Kadang-kadang untuk pemula agak sulit untuk mengetahui bagaimana mengatur sesuatu (itu untuk saya sih), jadi contoh ini sedikit lebih lengkap.

Tambahkan UITextViewke proyek Anda.

Toko

Hubungkan UITextViewke ViewControllerdengan outlet bernamatextView .

Atribut khusus

Kami akan membuat atribut khusus dengan membuat Ekstensi .

Catatan: Langkah ini secara teknis opsional, tetapi jika Anda tidak melakukannya, Anda perlu mengedit kode di bagian selanjutnya untuk menggunakan atribut standar seperti NSAttributedString.Key.foregroundColor. Keuntungan menggunakan atribut khusus adalah Anda dapat menentukan nilai apa yang ingin Anda simpan dalam rentang teks yang diatribusikan.

Tambahkan file swift baru dengan File> New> File ...> iOS> Source> Swift File . Anda bisa menyebutnya apa yang Anda inginkan. Saya memanggil milik saya NSAttributedStringKey + CustomAttribute.swift .

Tempel kode berikut:

import Foundation

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

Kode

Ganti kode di ViewController.swift dengan yang berikut ini. Perhatikan 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)")
            }

        }
    }
}

masukkan deskripsi gambar di sini

Sekarang jika Anda mengetuk "w" dari "Swift", Anda akan mendapatkan hasil sebagai berikut:

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

Catatan

  • Di sini saya menggunakan atribut khusus, tetapi bisa juga dengan mudah NSAttributedString.Key.foregroundColor(warna teks) yang memiliki nilai UIColor.green.
  • Sebelumnya tampilan teks tidak dapat diedit atau dipilih, tetapi dalam jawaban saya yang diperbarui untuk Swift 4.2 tampaknya berfungsi dengan baik tidak peduli apakah ini dipilih atau tidak.

Pelajaran lanjutan

Jawaban ini didasarkan pada beberapa jawaban lain atas pertanyaan ini. Selain ini, lihat juga


gunakan myTextView.textStoragesebagai pengganti myTextView.attributedText.string
fatihyildizhan

Mendeteksi ketuk melalui gerakan ketuk di iOS 9 tidak berfungsi untuk ketukan berturut-turut. Ada pembaruan tentang itu?
Dheeraj Jami

1
@WaqasMahmood, saya memulai pertanyaan baru untuk masalah ini. Anda dapat membintangi dan memeriksa lagi nanti untuk mendapatkan jawaban. Jangan ragu untuk mengedit pertanyaan itu atau menambahkan komentar jika ada detail yang lebih relevan.
Suragch

1
@dejix Saya menyelesaikan masalah dengan menambahkan "" setiap kali string kosong ke akhir TextView saya. Dengan begitu, pendeteksian berhenti setelah kata terakhir Anda. Semoga membantu
PoolHallJunkie

1
Bekerja sempurna dengan beberapa ketukan, saya hanya memasukkan rutinitas singkat untuk membuktikan ini: if characterIndex <12 {textView.textColor = UIColor.magenta} else {textView.textColor = UIColor.blue} Kode yang sangat jelas & sederhana
Jeremy Andrews

32

Ini adalah versi yang sedikit dimodifikasi, dibangun dari jawaban @tarmes. Saya tidak bisa mendapatkan valuevariabel untuk mengembalikan apa pun kecualinull tanpa perubahan di bawah ini. Juga, saya membutuhkan kamus atribut lengkap dikembalikan untuk menentukan tindakan yang dihasilkan. Saya akan meletakkan ini di komentar tetapi tampaknya tidak memiliki perwakilan untuk melakukannya. Mohon maaf sebelumnya jika saya telah melanggar protokol.

Tweak khusus digunakan textView.textStoragesebagai pengganti textView.attributedText. Sebagai programmer iOS yang masih belajar, saya tidak begitu yakin mengapa ini terjadi, tapi mungkin orang lain dapat mencerahkan kita.

Modifikasi khusus dalam metode penanganan keran:

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

Kode lengkap di pengontrol tampilan saya

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

    }
}

Punya masalah yang sama dengan textView.attributedText! TERIMA KASIH untuk petunjuk textView.textStorage!
Kai Burghardt

Mendeteksi ketuk melalui gerakan ketuk di iOS 9 tidak berfungsi untuk ketukan berturut-turut.
Dheeraj Jami

25

Membuat tautan khusus dan melakukan apa yang Anda inginkan dengan cepat menjadi lebih mudah dengan iOS 7. Ada contoh yang sangat bagus di Ray Wenderlich


Ini adalah solusi yang jauh lebih bersih daripada mencoba menghitung posisi string relatif terhadap tampilan penampungnya.
Chris C

2
Masalahnya adalah textView perlu dipilih, dan saya tidak menginginkan perilaku ini.
Thomás Calmon

@ ThomáC. +1 untuk penunjuk tentang mengapa UITextViewtautan saya tidak mendeteksi bahkan ketika saya telah menyetelnya untuk mendeteksinya melalui IB. (Saya juga membuatnya tidak dapat dipilih)
Kedar Paranjape

13

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

Terima kasih! Saya akan menonton video WWDC juga.
tarmes

@Suragch "Tata Letak dan Efek Teks Tingkat Lanjut dengan Kit Teks".
Shmidt

10

Saya bisa menyelesaikan ini cukup sederhana dengan NSLinkAttributeName

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

}

Anda harus memeriksa apakah URL Anda diketuk dan bukan URL lain dengan if URL.scheme == "cs"dan di return trueluar ifpernyataan sehingga UITextViewdapat menangani https://tautan normal yang disadap
Daniel Storm

Saya melakukan itu dan itu bekerja dengan cukup baik di iPhone 6 dan 6+ tetapi tidak berfungsi sama sekali di iPhone 5. Pergi dengan solusi Suragch di atas, yang hanya berfungsi. Tidak pernah tahu kenapa iPhone 5 akan bermasalah dengan ini, tidak masuk akal.
n13

9

Contoh lengkap untuk mendeteksi tindakan pada teks yang diatribusikan dengan 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
}

Dan kemudian Anda bisa menangkap tindakan dengan shouldInteractWith URLmetode delegasi UITextViewDelegate. Jadi pastikan Anda telah mengatur delegasi dengan benar.

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
    }

Seperti bijak, Anda dapat melakukan tindakan apa pun sesuai dengan kebutuhan Anda.

Bersulang!!


Terima kasih! Anda menyelamatkan hari saya!
Dmih


4

Dengan Swift 5 dan iOS 12, Anda dapat membuat subkelas UITextViewdan mengganti point(inside:with:)dengan beberapa implementasi TextKit agar hanya beberapa NSAttributedStringsdi dalamnya yang dapat di-tap.


Kode berikut menunjukkan cara membuat UITextViewyang hanya bereaksi terhadap ketukan pada NSAttributedStrings yang digarisbawahi di dalamnya:

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

}

Hai, Apakah ada cara untuk membuat ini sesuai dengan beberapa atribut, bukan hanya satu?
David Lintin

1

Yang ini mungkin berfungsi dengan baik dengan tautan pendek, multilink dalam tampilan teks. Ini bekerja dengan baik dengan 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;
}

Mendeteksi ketuk melalui gerakan ketuk di iOS 9 tidak berfungsi untuk ketukan berturut-turut.
Dheeraj Jami

1

Gunakan ekstensi ini untuk 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)
    }
}

Tambahkan UITapGestureRecognizerke tampilan teks Anda dengan pemilih berikut:

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
}
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.