İOS'ta bir UITextView'da atıfta bulunulan metin üzerindeki dokunmaları algılama


122

Bir UITextViewgösteren bir NSAttributedString. Bu dize, dokunulabilir hale getirmek istediğim kelimeleri içerir, öyle ki onlara dokunulduğunda bir eylem gerçekleştirebilmem için geri çağrılırım. Bunun UITextViewbir URL'ye dokunmaları algılayıp temsilcimi geri arayabileceğini anlıyorum , ancak bunlar URL değil.

Bana öyle geliyor ki iOS 7 ve TextKit'in gücü ile bu artık mümkün olmalı, ancak herhangi bir örnek bulamıyorum ve nereden başlayacağımı bilmiyorum.

Artık dizede özel öznitelikler oluşturmanın mümkün olduğunu anlıyorum (bunu henüz yapmamış olsam da) ve belki de bunlar sihirli sözcüklerden birine dokunulup dokunulmadığını tespit etmede yararlı olacak? Her halükarda, o dokunuşu nasıl engelleyeceğimi ve dokunmanın hangi kelimeyle gerçekleştiğini tespit etmeyi hala bilmiyorum.

İOS 6 uyumluluğunun gerekli olmadığını unutmayın .

Yanıtlar:


118

Sadece başkalarına biraz daha yardım etmek istedim. Shmidt'in cevabını takiben, orijinal sorumda sorduğum şeyi tam olarak yapmak mümkün.

1) Tıklanabilir kelimelere uygulanan özel özelliklerle bir ilişkilendirilmiş dize oluşturun. Örneğin.

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

2) Bu dizeyi görüntülemek için bir UITextView oluşturun ve buna bir UITapGestureRecognizer ekleyin. Ardından musluğu tutun:

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

    }
}

Nasıl olduğunu bildiğinde çok kolay!


Bunu IOS 6'da nasıl çözersiniz? Lütfen bu soruya bir bakar mısınız? stackoverflow.com/questions/19837522/…
Steaphann

Aslında characterIndexForPoint: inTextContainer: fractionOfDistanceBetweenInsertionPoints iOS 6'da mevcut, bu yüzden çalışması gerektiğini düşünüyorum. Bilmemize izin ver! Bir örnek için bu projeye bakın: github.com/laevandus/NSTextFieldHyperlinks/blob/master/…
tarmes

Belgeler, yalnızca IOS 7 veya sonraki sürümlerde mevcut olduğunu söylüyor :)
Steaphann

1
Evet üzgünüm. Kendimi Mac OS ile karıştırıyordum! Bu yalnızca iOS7'dir.
tarmes

Eğer değil seçilebilir UITextView var zaman, işin görünmüyor
Paul Brewczynski

64

Swift ile atfedilen metinde dokunmaları algılama

Bazen yeni başlayanlar için bir şeyleri nasıl ayarlayacağını bilmek biraz zor (zaten benim içindi), bu yüzden bu örnek biraz daha dolgun.

UITextViewProjenize bir ekleyin .

Çıkış

Bağlayın UITextViewüzere ViewControlleradında bir çıkış ile textView.

Özel özellik

Bir Uzantı yaparak özel bir nitelik oluşturacağız .

Not: Bu adım teknik olarak isteğe bağlıdır, ancak bunu yapmazsanız, gibi standart bir öznitelik kullanmak için sonraki bölümde kodu düzenlemeniz gerekecektir NSAttributedString.Key.foregroundColor. Özel bir öznitelik kullanmanın avantajı, ilişkilendirilmiş metin aralığında hangi değerleri depolamak istediğinizi tanımlayabilmenizdir.

Dosya> Yeni> Dosya ...> iOS> Kaynak> Swift Dosyası ile yeni bir swift dosyası ekleyin . Ona ne istersen diyebilirsin. Benim NSAttributedStringKey + CustomAttribute.swift'i arıyorum .

Aşağıdaki kodu yapıştırın:

import Foundation

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

kod

ViewController.swift'teki kodu aşağıdaki ile değiştirin. Not edin 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)")
            }

        }
    }
}

görüntü açıklamasını buraya girin

Şimdi "Swift" in "w" ye dokunursanız, aşağıdaki sonucu almalısınız:

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

notlar

  • Burada özel bir öznitelik kullandım, ancak NSAttributedString.Key.foregroundColorbir değeri olan (metin rengi) aynı derecede kolay olabilirdi UIColor.green.
  • Eskiden metin görünümü düzenlenebilir veya seçilebilir olamazdı, ancak Swift 4.2 için güncellenmiş cevabımda, bunların seçilip seçilmediğine bakılmaksızın iyi çalışıyor gibi görünüyor.

İlerideki çalışma

Bu cevap, bu soruya verilen diğer birkaç cevaba dayanıyordu. Bunların yanında ayrıca bakınız


myTextView.textStorageyerine kullanınmyTextView.attributedText.string
fatihyildizhan

İOS 9'da dokunarak dokunma hareketini algılama, art arda dokunmalar için çalışmaz. Bununla ilgili herhangi bir güncelleme var mı?
Dheeraj Jami

1
@WaqasMahmood, bu konu için yeni bir soru başlattım . Ona yıldız ekleyebilir ve daha sonra yanıtlar için tekrar kontrol edebilirsiniz. Daha fazla ilgili ayrıntı varsa, bu soruyu düzenlemekten veya yorum eklemekten çekinmeyin.
Suragch

1
@dejix Sorunu, TextView’imin sonuna her seferinde başka bir "" boş dize ekleyerek çözüyorum. Bu şekilde son sözünüzden sonra algılama durur. Umarım yardımcı olur
PoolHallJunkie

1
Birden çok dokunuşla mükemmel çalışıyor, bunu kanıtlamak için kısa bir rutin uyguladım: if characterIndex <12 {textView.textColor = UIColor.magenta} else {textView.textColor = UIColor.blue} Gerçekten açık ve basit bir kod
Jeremy Andrews

32

Bu, @tarmes cevabından oluşan biraz değiştirilmiş bir versiyondur. valueDeğişkenin herhangi bir şey döndürmesini sağlayamadım, ancak nullaşağıdaki ince ayar yapmadan. Ayrıca, sonuçta ortaya çıkan eylemi belirlemek için döndürülen tam öznitelik sözlüğüne ihtiyacım vardı. Bunu yorumlara koyardım ama bunu yapacak bir temsilciye sahip görünmüyorum. Protokolü ihlal ettiysem şimdiden özür dilerim.

textView.textStorageBunun yerine özel ince ayar kullanmaktır textView.attributedText. Hâlâ öğrenen bir iOS programcısı olarak bunun neden olduğundan pek emin değilim, ama belki başka biri bizi aydınlatabilir.

Musluk işleme yönteminde özel değişiklik:

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

Görünüm denetleyicimde tam kod

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

    }
}

TextView.attributedText ile aynı sorunu yaşadım! TextView.textStorage ipucu için TEŞEKKÜR EDERİZ!
Kai Burghardt

İOS 9'da dokunarak dokunma hareketini algılama, art arda dokunmalar için çalışmaz.
Dheeraj Jami

25

İOS 7 ile özel bağlantı oluşturmak ve istediğiniz şeyi muslukta yapmak çok daha kolay hale geldi. Ray Wenderlich'te çok iyi bir örnek var


Bu, kapsayıcı görünümlerine göre dizi konumlarını hesaplamaya çalışmaktan çok daha temiz bir çözümdür.
Chris C

2
Sorun, textView'ın seçilebilir olması gerektiğidir ve bu davranışı istemiyorum.
Thomás Calmon

ThomásC @. UITextViewIB aracılığıyla algılamak için ayarladığım halde bağlantılarımın neden algılamadığını gösteren işaretçi için +1 . (Ben de seçilemez hale getirmiştim)
seçilemez

13

WWDC 2013 örneği :

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

Teşekkür ederim! WWDC videosunu da izleyeceğim.
2013

@Suragch "Gelişmiş Metin Düzenleri ve Metin Kitiyle Efektler".
Shmidt

10

NSLinkAttributeName ile bunu oldukça basit bir şekilde çözebildim

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
  }

}

Sen URL vurdu ve başka bir URL ile olduğunu kontrol etmelidir if URL.scheme == "cs"ve return truedış ifyüzden açıklamada UITextView, normal işleyebilir https://aday olan bağlantıları
Daniel Fırtına

Bunu yaptım ve iPhone 6 ve 6+ üzerinde oldukça iyi çalıştı ancak iPhone 5'te hiç çalışmadı. Yukarıdaki Suragch çözümüne gittim, bu sadece çalışıyor. İPhone 5'in neden bununla bir sorunu olacağını asla bulamadım, mantıklı gelmedi.
n13

9

Swift 3 ile ilişkilendirilmiş metin üzerindeki eylemleri algılamak için eksiksiz örnek

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
}

Ve sonra shouldInteractWith URLUITextViewDelegate delegate metodu ile eylemi yakalayabilirsiniz, bu yüzden temsilciyi doğru ayarladığınızdan emin olun.

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
    }

Bilge gibi, ihtiyacınıza göre herhangi bir eylemi gerçekleştirebilirsiniz.

Alkış !!


Teşekkürler! Günümü kurtardın!
Dmih


4

Swift 5 ve iOS 12 ile, yalnızca bir kısmını dokunulabilir hale getirmek için bazı TextKit uygulamalarının bir alt sınıfını oluşturabilir UITextViewve point(inside:with:)bazı TextKit uygulamalarıyla geçersiz kılabilirsiniz NSAttributedStrings.


Aşağıdaki kod, UITextViewyalnızca NSAttributedStringiçindeki altı çizili ' e dokunmalara tepki veren bir' nin nasıl oluşturulacağını gösterir :

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

}

Merhaba, Bunu birden çok özelliğe uydurmanın bir yolu var mı?
David Lintin

1

Bu, kısa bağlantı, metin görünümünde çoklu bağlantı ile sorunsuz çalışabilir. İOS 6,7,8 ile sorunsuz çalışır.

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

İOS 9'da dokunarak dokunma hareketini algılama, art arda dokunmalar için çalışmaz.
Dheeraj Jami

1

Swift için bu uzantıyı kullanın:

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

UITapGestureRecognizerAşağıdaki seçiciyle metin görünümünüze ekleyin :

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
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.