Phát hiện các lần nhấn trên văn bản được gán trong UITextView trong iOS


122

Tôi có một UITextViewhiển thị một NSAttributedString. Chuỗi này chứa các từ mà tôi muốn làm cho có thể chạm được, sao cho khi chúng được nhấn, tôi sẽ được gọi lại để có thể thực hiện một hành động. Tôi nhận thấy rằng điều đó UITextViewcó thể phát hiện các lần nhấn trên một URL và gọi lại người đại diện của tôi, nhưng đây không phải là URL.

Đối với tôi, dường như với iOS 7 và sức mạnh của TextKit, điều này bây giờ có thể thực hiện được, tuy nhiên tôi không thể tìm thấy bất kỳ ví dụ nào và tôi không chắc nên bắt đầu từ đâu.

Tôi hiểu rằng bây giờ có thể tạo các thuộc tính tùy chỉnh trong chuỗi (mặc dù tôi chưa làm điều này) và có lẽ những thuộc tính này sẽ hữu ích để phát hiện xem một trong những từ ma thuật đã được khai thác hay chưa? Trong mọi trường hợp, tôi vẫn không biết làm thế nào để chặn lần nhấn đó và phát hiện lần nhấn đã xảy ra từ nào.

Lưu ý rằng khả năng tương thích với iOS 6 là không bắt buộc.

Câu trả lời:


118

Tôi chỉ muốn giúp đỡ người khác nhiều hơn một chút. Sau câu trả lời của Shmidt, bạn có thể thực hiện chính xác như những gì tôi đã hỏi trong câu hỏi ban đầu của mình.

1) Tạo một chuỗi phân bổ với các thuộc tính tùy chỉnh được áp dụng cho các từ có thể nhấp. ví dụ.

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

2) Tạo một UITextView để hiển thị chuỗi đó và thêm một UITapGestureRecognizer vào đó. Sau đó xử lý vòi:

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

    }
}

Thật dễ dàng khi bạn biết cách!


Bạn sẽ giải quyết vấn đề này như thế nào trong IOS 6? Bạn có thể vui lòng xem qua câu hỏi này? stackoverflow.com/questions/19837522/…
Steaphann,

Trên thực tế characterIndexForPoint: inTextContainer: fractionOfDistanceBetweenInsertionPoints có sẵn trên iOS 6, vì vậy tôi nghĩ nó sẽ hoạt động. Hãy cho chúng tôi biết! Xem dự án này để biết ví dụ: github.com/laevandus/NSTextFieldHyperlinks/blob/master/…
tarmes

Tài liệu cho biết nó chỉ có sẵn trong IOS 7 trở lên :)
Steaphann

1
Vâng xin lôi. Tôi đã nhầm lẫn với Mac OS! Đây chỉ là iOS7.
tarmes

Nó dường như không làm việc, khi bạn có UITextView không-thể lựa chọn
Paul Brewczynski

64

Phát hiện các lần nhấn trên văn bản được gán bằng Swift

Đôi khi đối với những người mới bắt đầu thì hơi khó để biết cách thiết lập mọi thứ (dù sao thì đối với tôi cũng vậy), vì vậy ví dụ này đầy đủ hơn một chút.

Thêm một UITextViewvào dự án của bạn.

Cửa hàng

Kết nối UITextViewđến ViewControllervới một lối thoát tên textView.

Thuộc tính tùy chỉnh

Chúng tôi sẽ tạo một thuộc tính tùy chỉnh bằng cách tạo một Tiện ích mở rộng .

Lưu ý: Về mặt kỹ thuật, bước này là tùy chọn, nhưng nếu không thực hiện, bạn sẽ cần chỉnh sửa mã trong phần tiếp theo để sử dụng thuộc tính tiêu chuẩn như NSAttributedString.Key.foregroundColor. Lợi thế của việc sử dụng thuộc tính tùy chỉnh là bạn có thể xác định những giá trị nào bạn muốn lưu trữ trong phạm vi văn bản được phân bổ.

Thêm một tệp nhanh mới với Tệp> Mới> Tệp ...> iOS> Nguồn> Tệp Swift . Bạn có thể gọi nó là gì bạn muốn. Tôi đang gọi NSAttributedStringKey + CustomAttribute.swift của tôi .

Dán mã sau:

import Foundation

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

Thay thế mã trong ViewController.swift bằng mã sau. Lưu ý 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)")
            }

        }
    }
}

nhập mô tả hình ảnh ở đây

Bây giờ nếu bạn nhấn vào "w" của "Swift", bạn sẽ nhận được kết quả sau:

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

Ghi chú

  • Ở đây tôi đã sử dụng một thuộc tính tùy chỉnh, nhưng nó có thể dễ dàng như vậy NSAttributedString.Key.foregroundColor(màu văn bản) có giá trị là UIColor.green.
  • Trước đây, chế độ xem văn bản không thể chỉnh sửa hoặc có thể chọn được, nhưng trong câu trả lời cập nhật của tôi cho Swift 4.2, nó dường như hoạt động tốt cho dù chúng có được chọn hay không.

Học cao hơn

Câu trả lời này dựa trên một số câu trả lời khác cho câu hỏi này. Bên cạnh những điều này, hãy xem thêm


sử dụng myTextView.textStoragethay vì myTextView.attributedText.string
fatihyildizhan

Cử chỉ phát hiện chạm qua chạm trong iOS 9 không hoạt động cho các lần nhấn liên tiếp. Bất kỳ cập nhật về điều đó?
Dheeraj Jami

1
@WaqasMahmood, tôi đã bắt đầu một câu hỏi mới cho vấn đề này. Bạn có thể gắn dấu sao nó và kiểm tra lại sau để biết bất kỳ câu trả lời nào. Vui lòng chỉnh sửa câu hỏi đó hoặc thêm nhận xét nếu có thêm bất kỳ chi tiết thích hợp nào.
Suragch vào

1
@dejix Tôi giải quyết sự cố bằng cách thêm mỗi lần một chuỗi trống "" khác vào cuối TextView của mình. Bằng cách đó, việc phát hiện dừng lại sau từ cuối cùng của bạn Hy vọng nó sẽ hữu ích
PoolHallJunkie

1
Hoạt động hoàn hảo với nhiều lần nhấn, tôi chỉ cần thực hiện một quy trình ngắn để chứng minh điều này: if characterIndex <12 {textView.textColor = UIColor.magenta} else {textView.textColor = UIColor.blue} Mã thực sự rõ ràng và đơn giản
Jeremy Andrews

32

Đây là phiên bản được sửa đổi một chút, dựa trên câu trả lời @tarmes. Tôi không thể nhận được valuebiến để trả về bất kỳ thứ gì mà nullkhông có tinh chỉnh bên dưới. Ngoài ra, tôi cần từ điển thuộc tính đầy đủ được trả về để xác định hành động kết quả. Tôi đã đưa điều này vào các bình luận nhưng dường như không có người đại diện để làm như vậy. Xin lỗi trước nếu tôi đã vi phạm giao thức.

Tinh chỉnh cụ thể là sử dụng textView.textStoragethay vì textView.attributedText. Là một lập trình viên iOS vẫn đang học hỏi, tôi thực sự không chắc tại sao lại như vậy, nhưng có lẽ ai đó có thể khai sáng cho chúng tôi.

Sửa đổi cụ thể trong phương pháp xử lý vòi:

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

Mã đầy đủ trong bộ điều khiển chế độ xem của tôi

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

    }
}

Gặp vấn đề tương tự với textView.attributedText! CẢM ƠN BẠN về gợi ý textView.textStorage!
Kai Burghardt

Cử chỉ phát hiện chạm qua chạm trong iOS 9 không hoạt động cho các lần nhấn liên tiếp.
Dheeraj Jami

25

Tạo liên kết tùy chỉnh và làm những gì bạn muốn khi chạm đã trở nên dễ dàng hơn nhiều với iOS 7. Có một ví dụ rất hay tại Ray Wenderlich


Đây là một giải pháp gọn gàng hơn nhiều so với việc cố gắng tính toán các vị trí chuỗi liên quan đến chế độ xem vùng chứa của chúng.
Chris C

2
Vấn đề là textView cần phải được chọn và tôi không muốn hành vi này.
Thomás Calmon

@ ThomásC. +1 cho con trỏ về lý do tại sao của tôi UITextViewkhông phát hiện liên kết ngay cả khi tôi đã đặt nó để phát hiện chúng qua IB. (Tôi cũng đã làm cho nó không được chọn)
Kedar Paranjape

13

Ví dụ về 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:
}

Cảm ơn bạn! Tôi cũng sẽ xem video WWDC.
tarmes

@Suragch "Bố cục và hiệu ứng văn bản nâng cao với Bộ công cụ văn bản".
Shmidt

10

Tôi đã có thể giải quyết vấn đề này khá đơn giản với 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
  }

}

Bạn nên kiểm tra xem URL của bạn đã được khai thác và không khác với URL if URL.scheme == "cs"return truebên ngoài iftuyên bố như vậy UITextViewcó thể xử lý bình thường https://liên kết được khai thác
Daniel bão

Tôi đã làm điều đó và nó hoạt động khá tốt trên iPhone 6 và 6+ nhưng hoàn toàn không hoạt động trên iPhone 5. Đã sử dụng giải pháp Suragch ở trên, giải pháp này chỉ hoạt động. Không bao giờ tìm ra lý do tại sao iPhone 5 lại gặp vấn đề với điều này, không có ý nghĩa gì.
n13, 21/12/16

9

Ví dụ hoàn chỉnh để phát hiện các hành động trên văn bản được gán với 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
}

Và sau đó bạn có thể bắt hành động với shouldInteractWith URLphương thức ủy quyền UITextViewDelegate. Vì vậy, hãy đảm bảo rằng bạn đã đặt ủy quyền đúng cách.

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
    }

Giống như khôn ngoan, bạn có thể thực hiện bất kỳ hành động nào theo yêu cầu của bạn.

Chúc mừng !!


Cảm ơn! Bạn tiết kiệm ngày của tôi!
Dmih


4

Với Swift 5 và iOS 12, bạn có thể tạo một lớp con UITextViewvà ghi đè point(inside:with:)bằng một số triển khai TextKit để chỉ một số NSAttributedStringstrong đó có thể sử dụng được.


Đoạn mã sau đây cho biết cách tạo một UITextViewchỉ phản ứng với các lần nhấn vào các dấu gạch chân NSAttributedStringtrong đó:

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

}

Xin chào, Có cách nào để làm cho điều này phù hợp với nhiều thuộc tính thay vì chỉ một thuộc tính không?
David Lintin

1

Cái này có thể hoạt động tốt với liên kết ngắn, đa liên kết trong một chế độ xem văn bản. Nó hoạt động tốt với 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;
}

Cử chỉ phát hiện chạm qua chạm trong iOS 9 không hoạt động cho các lần nhấn liên tiếp.
Dheeraj Jami

1

Sử dụng phần mở rộng này cho 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)
    }
}

Thêm vào UITapGestureRecognizerchế độ xem văn bản của bạn với bộ chọn sau:

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
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.