[ios] UILabel의 NSAttributedString에서 탭 가능한 “링크”를 만드시겠습니까?

몇 시간 동안 이것을 검색했지만 실패했습니다. 아마 내가 무엇을 찾아야할지 모르겠다.

많은 응용 프로그램에는 텍스트가 있으며이 텍스트에는 둥근 사각형의 웹 하이퍼 링크가 있습니다. 클릭하면 UIWebView열립니다. 예를 들어 단어가 #으로 시작하는 경우 # 클릭 할 수 있고 응용 프로그램이 다른보기를 열어 응답하는 경우가 종종 있습니다. 어떻게해야합니까? 가능 UILabel하거나 필요 UITextView하거나 다른 것입니까?



답변

일반적으로 UILabel에 의해 표시되는 텍스트로 클릭 가능한 링크를 가지려면 두 가지 독립적 인 작업을 해결해야합니다.

  1. 링크처럼 보이도록 텍스트 일부의 모양 변경
  2. 링크 터치 감지 및 처리 (URL 열기는 특별한 경우)

첫 번째는 쉽습니다. iOS 6부터 UILabel은 속성 문자열 표시를 지원합니다. NSMutableAttributedString 인스턴스를 생성하고 구성하기 만하면됩니다.

NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"String with a link" attributes:nil];
NSRange linkRange = NSMakeRange(14, 4); // for the word "link" in the string above

NSDictionary *linkAttributes = @{ NSForegroundColorAttributeName : [UIColor colorWithRed:0.05 green:0.4 blue:0.65 alpha:1.0],
                                  NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle) };
[attributedString setAttributes:linkAttributes range:linkRange];

// Assign attributedText to UILabel
label.attributedText = attributedString;

그게 다야! 위의 코드는 UILabel 링크 가있는 문자열 을 표시 하도록 합니다.

이제이 링크의 터치를 감지해야합니다. 아이디어는 UILabel 내의 모든 탭을 잡고 탭의 위치가 링크에 충분히 가까운 지 알아내는 것입니다. 터치를 잡기 위해 탭 제스처 인식기를 라벨에 추가 할 수 있습니다. 레이블에 대해 userInteraction을 활성화하십시오. 기본적으로 꺼져 있습니다.

label.userInteractionEnabled = YES;
[label addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapOnLabel:)]]; 

가장 정교한 것 : 탭의 다른 부분이 아닌 링크가 표시된 위치에 탭이 있는지 확인하십시오. 우리가 한 줄짜리 UILabel을 가지고 있다면,이 작업은 링크가 표시되는 영역 경계를 하드 코딩하여 비교적 쉽게 해결할 수 있지만,이 문제를보다 우아하고 일반적인 경우-링크 레이아웃에 대한 사전 지식이없는 여러 줄 UILabel을 해결해 보겠습니다.

접근 방식 중 하나는 iOS 7에 도입 된 Text Kit API의 기능을 사용하는 것입니다.

// Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeZero];
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString];

// Configure layoutManager and textStorage
[layoutManager addTextContainer:textContainer];
[textStorage addLayoutManager:layoutManager];

// Configure textContainer
textContainer.lineFragmentPadding = 0.0;
textContainer.lineBreakMode = label.lineBreakMode;
textContainer.maximumNumberOfLines = label.numberOfLines;

NSLayoutManager, NSTextContainer 및 NSTextStorage의 생성 및 구성된 인스턴스를 클래스의 속성 (대부분 UIViewController의 하위 항목)에 저장합니다. 다른 방법에서도 필요합니다.

이제 레이블이 프레임을 변경할 때마다 textContainer의 크기를 업데이트하십시오.

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];
    self.textContainer.size = self.label.bounds.size;
}

마지막으로 탭이 정확히 링크에 있는지 여부를 감지하십시오.

- (void)handleTapOnLabel:(UITapGestureRecognizer *)tapGesture
{
    CGPoint locationOfTouchInLabel = [tapGesture locationInView:tapGesture.view];
    CGSize labelSize = tapGesture.view.bounds.size;
    CGRect textBoundingBox = [self.layoutManager usedRectForTextContainer:self.textContainer];
    CGPoint textContainerOffset = CGPointMake((labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
                                              (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y);
    CGPoint locationOfTouchInTextContainer = CGPointMake(locationOfTouchInLabel.x - textContainerOffset.x,
                                                         locationOfTouchInLabel.y - textContainerOffset.y);
    NSInteger indexOfCharacter = [self.layoutManager characterIndexForPoint:locationOfTouchInTextContainer
                                                            inTextContainer:self.textContainer
                                   fractionOfDistanceBetweenInsertionPoints:nil];
    NSRange linkRange = NSMakeRange(14, 4); // it's better to save the range somewhere when it was originally used for marking link in attributed string
    if (NSLocationInRange(indexOfCharacter, linkRange)) {
        // Open an URL, or handle the tap on the link in any other way
        [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://stackoverflow.com/"]];
    }
}


답변

@zekel의 뛰어난 확장 기능 으로 @NAlexN 독창적 인 세부 솔루션을 확장 하고 Swift 에서 제공하고 있습니다.UITapGestureRecognizer .

UITapGestureRecognizer 확장

extension UITapGestureRecognizer {

    func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool {
        // Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
        let layoutManager = NSLayoutManager()
        let textContainer = NSTextContainer(size: CGSize.zero)
        let textStorage = NSTextStorage(attributedString: label.attributedText!)

        // Configure layoutManager and textStorage
        layoutManager.addTextContainer(textContainer)
        textStorage.addLayoutManager(layoutManager)

        // Configure textContainer
        textContainer.lineFragmentPadding = 0.0
        textContainer.lineBreakMode = label.lineBreakMode
        textContainer.maximumNumberOfLines = label.numberOfLines
        let labelSize = label.bounds.size
        textContainer.size = labelSize

        // Find the tapped character location and compare it to the specified range
        let locationOfTouchInLabel = self.location(in: label)
        let textBoundingBox = layoutManager.usedRect(for: textContainer)
        let textContainerOffset = CGPoint(
            x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
            y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y
        )
        let locationOfTouchInTextContainer = CGPoint(
            x: locationOfTouchInLabel.x - textContainerOffset.x,
            y: locationOfTouchInLabel.y - textContainerOffset.y
        )
        let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)

        return NSLocationInRange(indexOfCharacter, targetRange)
    }

}

용법

UIGestureRecognizer작업을 보내도록 설정 하고 tapLabel:에서 대상 범위를 탭하고 있는지 감지 할 수 있습니다 myLabel.

@IBAction func tapLabel(gesture: UITapGestureRecognizer) {
    if gesture.didTapAttributedTextInLabel(myLabel, inRange: targetRange1) {
        print("Tapped targetRange1")
    } else if gesture.didTapAttributedTextInLabel(myLabel, inRange: targetRange2) {
        print("Tapped targetRange2")
    } else {
        print("Tapped none")
    }
}

중요 : UILabel줄 바꿈 모드는 단어 / 문자로 줄 바꿈되도록 설정해야합니다. 어떻게 든 NSTextContainer줄 바꿈 모드가 아닌 경우에만 텍스트가 한 줄이라고 가정합니다.


답변

오래된 질문이지만 누군가가 UITextView대신 사용할 수 있다면UILabel 쉽습니다. 표준 URL, 전화 번호 등이 자동으로 감지되고 클릭 가능합니다.

그러나 사용자 정의 감지가 필요한 경우, 즉 사용자가 특정 단어를 클릭 한 후 사용자 정의 메소드를 호출하려면 사용자 정의 URL 스킴을 가리키는 속성 NSAttributedStrings과 함께 사용해야 NSLinkAttributeName합니다 ( http URL 체계를 기본적으로 사용). 레이 웬더 리치가 여기에서 다뤘습니다

위에서 언급 한 링크에서 코드 인용 :

NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"This is an example by @marcelofabri_"];
[attributedString addAttribute:NSLinkAttributeName
                     value:@"username://marcelofabri_"
                     range:[[attributedString string] rangeOfString:@"@marcelofabri_"]];

NSDictionary *linkAttributes = @{NSForegroundColorAttributeName: [UIColor greenColor],
                             NSUnderlineColorAttributeName: [UIColor lightGrayColor],
                             NSUnderlineStyleAttributeName: @(NSUnderlinePatternSolid)};

// assume that textView is a UITextView previously created (either by code or Interface Builder)
textView.linkTextAttributes = linkAttributes; // customizes the appearance of links
textView.attributedText = attributedString;
textView.delegate = self;

이러한 링크 클릭을 감지하려면 다음을 구현하십시오.

- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange {
    if ([[URL scheme] isEqualToString:@"username"]) {
        NSString *username = [URL host];
        // do something with this username
        // ...
        return NO;
    }
    return YES; // let the system open this URL
}

추신 : 물론 당신의 확인 UITextViewIS를 selectable.


답변

UIButtonTypeCustom은 이미지를 설정하지 않은 경우 클릭 가능한 레이블입니다.


답변

(내 답변은 @NAlexN의 탁월한 답변을 기반으로 합니다. 각 단계에 대한 자세한 설명은 여기에 중복되지 않습니다.)

탭 가능한 UILabel 텍스트에 대한 지원을 UITapGestureRecognizer의 범주로 추가하는 것이 가장 편리하고 간단하다는 것을 알았습니다. (당신은 가지고 있지 않습니다 일부 답변 제안으로, UITextView의 데이터 감지기를 사용합니다.)

UITapGestureRecognizer 범주에 다음 방법을 추가하십시오.

/**
 Returns YES if the tap gesture was within the specified range of the attributed text of the label.
 */
- (BOOL)didTapAttributedTextInLabel:(UILabel *)label inRange:(NSRange)targetRange {
    NSParameterAssert(label != nil);

    CGSize labelSize = label.bounds.size;
    // create instances of NSLayoutManager, NSTextContainer and NSTextStorage
    NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
    NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeZero];
    NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:label.attributedText];

    // configure layoutManager and textStorage
    [layoutManager addTextContainer:textContainer];
    [textStorage addLayoutManager:layoutManager];

    // configure textContainer for the label
    textContainer.lineFragmentPadding = 0.0;
    textContainer.lineBreakMode = label.lineBreakMode;
    textContainer.maximumNumberOfLines = label.numberOfLines;
    textContainer.size = labelSize;

    // find the tapped character location and compare it to the specified range
    CGPoint locationOfTouchInLabel = [self locationInView:label];
    CGRect textBoundingBox = [layoutManager usedRectForTextContainer:textContainer];
    CGPoint textContainerOffset = CGPointMake((labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
                                              (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y);
    CGPoint locationOfTouchInTextContainer = CGPointMake(locationOfTouchInLabel.x - textContainerOffset.x,
                                                         locationOfTouchInLabel.y - textContainerOffset.y);
    NSInteger indexOfCharacter = [layoutManager characterIndexForPoint:locationOfTouchInTextContainer
                                                            inTextContainer:textContainer
                                   fractionOfDistanceBetweenInsertionPoints:nil];
    if (NSLocationInRange(indexOfCharacter, targetRange)) {
        return YES;
    } else {
        return NO;
    }
}

예제 코드

// (in your view controller)    
// create your label, gesture recognizer, attributed text, and get the range of the "link" in your label
myLabel.userInteractionEnabled = YES;
[myLabel addGestureRecognizer:
   [[UITapGestureRecognizer alloc] initWithTarget:self
                                           action:@selector(handleTapOnLabel:)]];

// create your attributed text and keep an ivar of your "link" text range
NSAttributedString *plainText;
NSAttributedString *linkText;
plainText = [[NSMutableAttributedString alloc] initWithString:@"Add label links with UITapGestureRecognizer"
                                                   attributes:nil];
linkText = [[NSMutableAttributedString alloc] initWithString:@" Learn more..."
                                                  attributes:@{
                                                      NSForegroundColorAttributeName:[UIColor blueColor]
                                                  }];
NSMutableAttributedString *attrText = [[NSMutableAttributedString alloc] init];
[attrText appendAttributedString:plainText];
[attrText appendAttributedString:linkText];

// ivar -- keep track of the target range so you can compare in the callback
targetRange = NSMakeRange(plainText.length, linkText.length);

제스처 콜백

// handle the gesture recognizer callback and call the category method
- (void)handleTapOnLabel:(UITapGestureRecognizer *)tapGesture {
    BOOL didTapLink = [tapGesture didTapAttributedTextInLabel:myLabel
                                            inRange:targetRange];
    NSLog(@"didTapLink: %d", didTapLink);

}


답변

UITextViewOS3.0에서 데이터 탐지기를 지원하지만 UILabel그렇지 않습니다.

에서 데이터 탐지기를 사용하도록 설정하고 UITextView텍스트에 URL, 전화 번호 등이 포함되어 있으면 링크로 나타납니다.


답변

@samwize의 확장 기능을 Swift 4로 변환 :

extension UITapGestureRecognizer {
    func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool {
        guard let attrString = label.attributedText else {
            return false
        }

        let layoutManager = NSLayoutManager()
        let textContainer = NSTextContainer(size: .zero)
        let textStorage = NSTextStorage(attributedString: attrString)

        layoutManager.addTextContainer(textContainer)
        textStorage.addLayoutManager(layoutManager)

        textContainer.lineFragmentPadding = 0
        textContainer.lineBreakMode = label.lineBreakMode
        textContainer.maximumNumberOfLines = label.numberOfLines
        let labelSize = label.bounds.size
        textContainer.size = labelSize

        let locationOfTouchInLabel = self.location(in: label)
        let textBoundingBox = layoutManager.usedRect(for: textContainer)
        let textContainerOffset = CGPoint(x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x, y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y)
        let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x, y: locationOfTouchInLabel.y - textContainerOffset.y)
        let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
        return NSLocationInRange(indexOfCharacter, targetRange)
    }
}

인식기를 설정하려면 (텍스트와 내용을 채색 한 후에) :

lblTermsOfUse.isUserInteractionEnabled = true
lblTermsOfUse.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTapOnLabel(_:))))

그런 다음 제스처 인식기 :

@objc func handleTapOnLabel(_ recognizer: UITapGestureRecognizer) {
    guard let text = lblAgreeToTerms.attributedText?.string else {
        return
    }

    if let range = text.range(of: NSLocalizedString("_onboarding_terms", comment: "terms")),
        recognizer.didTapAttributedTextInLabel(label: lblAgreeToTerms, inRange: NSRange(range, in: text)) {
        goToTermsAndConditions()
    } else if let range = text.range(of: NSLocalizedString("_onboarding_privacy", comment: "privacy")),
        recognizer.didTapAttributedTextInLabel(label: lblAgreeToTerms, inRange: NSRange(range, in: text)) {
        goToPrivacyPolicy()
    }
}