iOS의 UITextView에서 속성 텍스트에 대한 탭 감지


122

나는이 UITextView을 표시하는가 NSAttributedString. 이 문자열에는 탭할 수있게 만들고 싶은 단어가 포함되어 있으므로 탭하면 다시 전화를 걸어 작업을 수행 할 수 있습니다. 나는 그것을 깨닫는다UITextViewURL에 대한 탭을 감지하고 대리인에게 전화를 걸 수 있지만 URL이 아닙니다.

iOS 7과 TextKit의 힘으로 이제 가능할 것 같지만 예제를 찾을 수 없으며 어디서 시작해야할지 모르겠습니다.

이제 문자열에 사용자 지정 속성을 만들 수 있다는 것을 알고 있으며 (아직 수행하지는 않았지만) 마법 단어 중 하나가 탭되었는지 감지하는 데 유용 할 것입니다. 어쨌든 나는 여전히 그 탭을 가로 채고 어떤 단어에서 탭이 발생했는지 감지하는 방법을 모릅니다.

iOS 6 호환성은 필요 하지 않습니다.

답변:


118

나는 단지 다른 사람들을 조금 더 돕고 싶었습니다. Shmidt의 답변에 이어 원래 질문에서 요청한대로 정확하게 수행 할 수 있습니다.

1) 클릭 가능한 단어에 적용되는 사용자 정의 속성으로 속성 문자열을 만듭니다. 예.

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

2) 해당 문자열을 표시 할 UITextView를 만들고 여기에 UITapGestureRecognizer를 추가합니다. 그런 다음 탭 처리 :

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

    }
}

방법을 알면 아주 쉽습니다!


IOS 6에서이 문제를 어떻게 해결할 수 있습니까? 이 질문 좀 봐주 시겠어요? stackoverflow.com/questions/19837522/…
Steaphann 2013

실제로 characterIndexForPoint : inTextContainer : fractionOfDistanceBetweenInsertionPoints는 iOS 6에서 사용할 수 있으므로 작동해야한다고 생각합니다. 알려주세요! 예제를 보려면이 프로젝트를 참조하십시오. github.com/laevandus/NSTextFieldHyperlinks/blob/master/…
tarmes

문서에 따르면 IOS 7 이상에서만 사용할 수 있습니다. :)
Steaphann

1
맞아 미안해. Mac OS와 혼동이 생겼습니다! 이것은 iOS7 전용입니다.
tarmes

그렇지 선택할 수 UITextView이 때, 작동하지 않는 것
폴 Brewczynski

64

Swift로 속성 텍스트에 대한 탭 감지

때때로 초보자에게는 설정을하는 방법을 알기가 조금 어렵습니다 (어쨌든 저를위한 것이 었습니다). 그래서이 예제는 조금 더 꽉 차 있습니다.

UITextView프로젝트에를 추가 하십시오.

콘센트

연결 UITextView받는 사람을 ViewController라는 콘센트 textView.

맞춤 속성

Extension 을 만들어 사용자 지정 속성을 만들 것 입니다.

참고 : 이 단계는 기술적으로 선택 사항이지만 그렇게하지 않으면 다음 부분에서 코드를 편집하여와 같은 표준 속성을 사용해야합니다 NSAttributedString.Key.foregroundColor. 사용자 지정 특성 사용의 장점은 특성이있는 텍스트 범위에 저장할 값을 정의 할 수 있다는 것입니다.

다음을 사용하여 새 신속한 파일 추가 File> New> File ...> iOS> Source> Swift File . 원하는대로 부를 수 있습니다. 내 NSAttributedStringKey + CustomAttribute.swift를 호출하고 있습니다. 있습니다.

다음 코드를 붙여 넣으십시오.

import Foundation

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

암호

ViewController.swift의 코드를 다음으로 바꿉니다. 를 참고 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)")
            }

        }
    }
}

여기에 이미지 설명 입력

이제 "Swift"의 "w"를 탭하면 다음 결과가 표시됩니다.

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

노트

  • 여기에서는 사용자 지정 속성을 사용했지만 NSAttributedString.Key.foregroundColor값이 UIColor.green.
  • 이전에는 텍스트보기를 편집하거나 선택할 수 없었지만 Swift 4.2에 대한 업데이트 된 답변에서는 선택 여부에 관계없이 잘 작동하는 것 같습니다.

추가 연구

이 답변은이 질문에 대한 몇 가지 다른 답변을 기반으로합니다. 이 외에도 참조


사용 myTextView.textStorage대신에 myTextView.attributedText.string
fatihyildizhan

iOS 9에서 탭 제스처를 통한 탭 감지는 연속 탭에서 작동하지 않습니다. 그것에 대한 업데이트가 있습니까?
Dheeraj Jami 2015 년

1
@WaqasMahmood, 이 문제에 대한 새로운 질문 을 시작했습니다 . 별표를 표시하고 나중에 다시 확인하여 답변을 확인할 수 있습니다. 관련 세부 사항이 더 있으면 해당 질문을 편집하거나 의견을 추가하십시오.
Suragch 2015

1
@dejix 내 TextView 끝에 ""빈 문자열을 추가하여 문제를 해결했습니다. 이렇게하면 마지막 단어 후에 감지가 중지됩니다. 도움이
되었으면합니다

1
여러 번의 탭으로 완벽하게 작동합니다.이를 증명하기 위해 짧은 루틴을 추가했습니다. if characterIndex <12 {textView.textColor = UIColor.magenta} else {textView.textColor = UIColor.blue} 정말 명확하고 간단한 코드
Jeremy Andrews

32

이것은 @tarmes 답변을 기반으로 약간 수정 된 버전입니다. 아래의 조정 없이는 value아무것도 반환 할 변수를 얻을 수 없습니다 null. 또한 결과 작업을 결정하기 위해 반환 된 전체 속성 사전이 필요했습니다. 나는 이것을 코멘트에 넣었을 것이지만 그렇게 할 담당자가없는 것 같습니다. 프로토콜을 위반 한 경우 미리 사과드립니다.

특정 조정은 textView.textStorage대신 사용하는 것입니다.textView.attributedText . 아직 iOS 프로그래머를 배우고있는 저는 이것이 왜 그런지 잘 모르겠지만 아마도 다른 누군가가 우리를 깨달을 수있을 것입니다.

탭 핸들링 방법의 특정 수정 :

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

내 뷰 컨트롤러의 전체 코드

- (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와 동일한 문제가있었습니다! textView.textStorage 힌트에 감사드립니다!
Kai Burghardt 2014 년

iOS 9에서 탭 제스처를 통한 탭 감지는 연속 탭에서 작동하지 않습니다.
Dheeraj Jami 2015 년

25

iOS 7에서는 사용자 지정 링크를 만들고 원하는 작업을 수행하는 것이 훨씬 쉬워졌습니다. Ray Wenderlich 에는 아주 좋은 예가 있습니다.


이것은 컨테이너 뷰를 기준으로 문자열 위치를 계산하는 것보다 훨씬 더 깨끗한 솔루션입니다.
Chris C

2
문제는 textView를 선택할 수 있어야하고이 동작을 원하지 않는다는 것입니다.
Thomás Calmon 2015

@ ThomásC. UITextViewIB를 통해 링크를 감지하도록 설정 했는데도 링크를 감지하지 못한 이유에 대한 포인터 +1 . (또한 선택
불가능

13

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

감사합니다! WWDC 영상도 볼게요.
tarmes

@Suragch "텍스트 키트로 고급 텍스트 레이아웃 및 효과".
Shmidt 2015-08-26

10

NSLinkAttributeName으로 아주 간단하게 해결할 수있었습니다.

스위프트 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
  }

}

당신은 당신의 URL은 도청이 아니라 다른 URL을 가진 것을 확인해야 if URL.scheme == "cs"return true의 외부 if(가) 그래서 문 UITextView정상적인 처리 할 수있는 https://탭하는 링크
다니엘 스톰을

저는 그렇게했고 iPhone 6 및 6+에서는 상당히 잘 작동했지만 iPhone 5에서는 전혀 작동하지 않았습니다. 위의 Suragch 솔루션을 사용했습니다. 왜 iPhone 5가 이것에 문제가 있는지 알지 못했습니다.
n13 dec.

9

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
}

그런 다음 shouldInteractWith URLUITextViewDelegate delegate method로 액션을 잡을 수 있으므로 delegate를 올바르게 설정했는지 확인하십시오.

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
    }

마찬가지로 요구 사항에 따라 모든 작업을 수행 할 수 있습니다.

건배!!


감사! 당신은 내 하루를 구합니다!
Dmih


4

Swift 5 및 iOS 12를 사용하면 일부만 탭 가능하게 만들기 위해 일부 TextKit 구현 의 하위 클래스를 UITextView만들고 재정 의 할 수 있습니다 .point(inside:with:)NSAttributedStrings


다음 코드는 UITextView밑줄이 그어진 NSAttributedStrings 탭에만 반응하는를 만드는 방법을 보여줍니다 .

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

}

안녕하세요, 이것이 하나가 아닌 여러 속성을 준수하도록 만드는 방법이 있습니까?
David Lintin

1

이것은 textview에서 짧은 링크, 다중 링크로 정상적으로 작동 할 수 있습니다. 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;
}

iOS 9에서 탭 제스처를 통한 탭 감지는 연속 탭에서 작동하지 않습니다.
Dheeraj Jami

1

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

UITapGestureRecognizer다음 선택기를 사용하여 텍스트보기에 추가하십시오 .

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
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.