【问题标题】:Create tap-able "links" in the NSAttributedString of a UILabel?在 UILabel 的 NSAttributedString 中创建可点击的“链接”?
【发布时间】:2010-11-18 10:02:12
【问题描述】:

我已经搜索了几个小时,但我失败了。我可能什至不知道我应该寻找什么。

许多应用程序都有文本,并且在此文本中是圆角矩形的 Web 超链接。当我单击它们时,UIWebView 打开。令我困惑的是,它们通常具有自定义链接,例如,如果单词以 # 开头,它也是可点击的,并且应用程序通过打开另一个视图来响应。我怎样才能做到这一点?是否可以使用UILabel 或者我需要UITextView 或其他?

【问题讨论】:

标签: ios hyperlink uilabel nsattributedstring uitapgesturerecognizer


【解决方案1】:

一般来说,如果我们希望 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 显示 String 与 link

现在我们应该检测到此链接上的触摸。这个想法是捕捉 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/"]];
    }
}

【讨论】:

  • 我将如何在cellForRowAtIndexPath 中组织这个?我在cellForRowAtIndexPath 中创建和配置实例,并在其中托管handleTapOnLabel 函数。但是在cell.textLabel.addGestureRecognizer(UITapGestureRecognizer(target: cell, action: "handleTapOnLabel:")),我收到了unrecognized selector
  • 此解决方案假定标签的textAlignment 属性设置为NSTextAlignmentCenter。如果您使用的是非居中文本,则需要在上述代码中调整 textContainerOffset 的计算。
  • @AndreyM。在计算textContainerOffsetx 值时,使用常数0.5。这将计算NSTextAlignmentCent‌er 的正确位置。要左对齐、自然对齐或两端对齐,请使用值0.0。要右对齐,请使用1.0
  • 它也适用于我,但仅适用于单行标签。如果 Label 包含超过 1 行,则此方法无法正常工作。谁能告诉他用多行执行相同的任务
  • 我要添加到现有解决方案的两个关键点: 1. 确保属性文本包含文本对齐属性。使用属性NSParagraphStyleAttributeName 和标签的文本对齐属性添加它。 2. 确保NSTextStorage 具有使用NSFontAttributeName 和标签的字体属性设置的字体属性。
【解决方案2】:

我正在扩展@NAlexN原来的详细解决方案,用@zekel很好的扩展UITapGestureRecognizer,并在Swift中提供。

扩展 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 会假设文本是单行的,除非换行模式不是这样。

【讨论】:

  • @rodrigo-ruiz 我在下面添加了一个多行示例
  • @Koen 它确实适用于多个链接。请参阅targetRange1targetRange2 示例的用法。
  • 对于仍然存在多行问题或不正确范围问题的任何人,请将您的 UILabel 设置为 Attributed,然后允许 自动换行,并设置属性标签的文本到NSMutableAttributedString(attributedString: text),其中'text'是NSAttributedString
  • @Mofe-hendyEjegi 我仍然遇到多行文本问题。我正在使用带有 uilabel 宽度约束的自动布局。这有关系吗?
  • 任何人都可以通过多行标签来解决这个问题?我发现我只能在第一行获得正确的字符索引
【解决方案3】:

老问题,但如果有人可以使用UITextView 而不是UILabel,那么这很容易。标准网址、电话号码等将被自动检测(并可点击)。

但是,如果您需要自定义检测,也就是说,如果您希望能够在用户单击特定单词后调用任何自定义方法,则需要使用 NSAttributedStringsNSLinkAttributeName 属性,该属性将指向到自定义 URL 方案(而不是默认使用 http url 方案)。 Ray Wenderlich has it covered here

引用上述链接中的代码:

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
}

PS:确保您的UITextViewselectable

【讨论】:

  • 这应该被接受。我花了很多时间试图让@NAlexN 工作的代码,然后在 5 分钟内用 UITextView 实现它。
  • 这个问题是如果你想让它对不同的链接通用,你必须检查什么是 URL 以采取适当的行动
  • Make sure your UITextView is selectable :这节省了我的时间
  • 我添加了实现这种方法的简单 UITextView 子类stackoverflow.com/a/65980444/286361
【解决方案4】:

如果您没有为其设置任何图像,则 UIButtonTypeCustom 是一个可点击的标签。

【讨论】:

  • 仅当整个文本可点击且只有一个链接时。
【解决方案5】:

(我的答案基于@NAlexN 的excellent answer。我不会在这里重复他对每个步骤的详细解释。)

我发现将可点击的 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);

}

【讨论】:

  • 刚刚完成这项工作 - 但我在使用 linkText.location 时遇到问题 - 我的 NSAttributedString 没有这个属性?
  • @MattBolt 哎呀,这是个错误。那应该是链接文本的起始索引,在这个例子中它应该是plainText.length
  • CGPoint locationOfTouchInLabel = [self locationInView:label] 发生错误;
  • @zekel 非常感谢你提供这个解决方案。但是您能解释一下“将以下方法添加到您的 UITapGestureRecognizer 类别”的确切含义吗?不知道我应该在这里做什么。
  • @eivindml 您可以使用类别将方法添加到现有类,这对于处理您未编写的类很有用,例如UITapGestureRecognizer。这是some info 添加类别。
【解决方案6】:

将@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()
    }
}

【讨论】:

  • 不适合我。 didTapAttributedTextInLabel 需要 NSRange 作为参数,但 rangeTerms 返回不同的东西。在 Swift 4 中,handleTapOnLabel 函数也应该用@objc 标记。
【解决方案7】:

UITextView 在 OS3.0 中支持数据检测器,而UILabel 不支持。

如果您在UITextView 上启用数据检测器并且您的文本包含 URL、电话号码等。它们将显示为链接。

【讨论】:

【解决方案8】:

最简单可靠的方法是使用 UITextView 作为Kedar Paranjape 推荐。基于answer of Karl Nosworthy我终于想出了一个简单的UITextView子类:

class LinkTextView: UITextView, UITextViewDelegate {
    
    typealias Links = [String: String]
    
    typealias OnLinkTap = (URL) -> Bool
    
    var onLinkTap: OnLinkTap?
    
    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        isEditable = false
        isSelectable = true
        isScrollEnabled = false //to have own size and behave like a label
        delegate = self
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
    
    func addLinks(_ links: Links) {
        guard attributedText.length > 0  else {
            return
        }
        let mText = NSMutableAttributedString(attributedString: attributedText)
        
        for (linkText, urlString) in links {
            if linkText.count > 0 {
                let linkRange = mText.mutableString.range(of: linkText)
                mText.addAttribute(.link, value: urlString, range: linkRange)
            }
        }
        attributedText = mText
    }
    
    func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
        return onLinkTap?(URL) ?? true
    }
    
    // to disable text selection
    func textViewDidChangeSelection(_ textView: UITextView) {
        textView.selectedTextRange = nil
    }
}

用法很简单:

    let linkTextView = LinkTextView()
    let tu = "Terms of Use"
    let pp = "Privacy Policy"
    linkTextView.text = "Please read the Some Company \(tu) and \(pp)"
    linkTextView.addLinks([
        tu: "https://some.com/tu",
        pp: "https://some.com/pp"
    ])
    linkTextView.onLinkTap = { url in
        print("url: \(url)")
        return true
    }

请注意 isScrollEnabled 默认为 false,因为在大多数情况下,我们需要具有自己大小且无需滚动的小标签式视图。如果您想要一个可滚动的文本视图,只需将其设置为 true。

另请注意,UITextView 与 UILabel 不同,具有默认文本填充。要删除它并使布局与 UILabel 中的相同,只需添加:linkTextView.textContainerInset = .zero

不需要实现onLinkTap 闭包,没有它,URL 会被 UIApplication 自动打开。

由于文本选择在大多数情况下是不可取的,但它无法关闭,它在委托方法中被关闭 (Thanks to Carson Vo)

【讨论】:

  • 这做得很好,经过测试,效果很好。谢谢??
【解决方案9】:

正如我在this post 中提到的, 这是我专门为 UILabel FRHyperLabel 中的链接创建的轻量级库。

要达到这样的效果:

Lorem ipsum dolor sit amet,consectetur adipiscing elit。 Pellentesque quis blandit eros,坐在 amet vehicula justo。 Nam at urna neque。 Maecenas ac sem eu sem porta dictum nec vel tellus。

使用代码:

//Step 1: Define a normal attributed string for non-link texts
NSString *string = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque quis blandit eros, sit amet vehicula justo. Nam at urna neque. Maecenas ac sem eu sem porta dictum nec vel tellus.";
NSDictionary *attributes = @{NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline]};

label.attributedText = [[NSAttributedString alloc]initWithString:string attributes:attributes];


//Step 2: Define a selection handler block
void(^handler)(FRHyperLabel *label, NSString *substring) = ^(FRHyperLabel *label, NSString *substring){
    NSLog(@"Selected: %@", substring);
};


//Step 3: Add link substrings
[label setLinksForSubstrings:@[@"Lorem", @"Pellentesque", @"blandit", @"Maecenas"] withLinkHandler:handler];

【讨论】:

  • 如果标签文本是动态的来自 API 并且您不知道文本长度,那么如何建立链接。
  • 在 Swift 4 上也能正常工作。
  • 您还在更新 FRHyperLabel 吗?如果我的属性字符串包含使用 kCTRubyAnnotationAttributeName 创建的 ruby​​ 文本,我无法让超链接工作
【解决方案10】:

有些答案没有像预期的那样对我有用。这是还支持textAlignment 和多行的Swift 解决方案。不需要子类化,只需这个UITapGestureRecognizer 扩展:

import UIKit


extension UITapGestureRecognizer {
    
    func didTapAttributedString(_ string: String, in label: UILabel) -> Bool {
        
        guard let text = label.text else {
            
            return false
        }
        
        let range = (text as NSString).range(of: string)
        return self.didTapAttributedText(label: label, inRange: range)
    }
    
    private func didTapAttributedText(label: UILabel, inRange targetRange: NSRange) -> Bool {
        
        guard let attributedText = label.attributedText else {
            
            assertionFailure("attributedText must be set")
            return false
        }
        
        let textContainer = createTextContainer(for: label)
        
        let layoutManager = NSLayoutManager()
        layoutManager.addTextContainer(textContainer)
        
        let textStorage = NSTextStorage(attributedString: attributedText)
        if let font = label.font {
            
            textStorage.addAttribute(NSAttributedString.Key.font, value: font, range: NSMakeRange(0, attributedText.length))
        }
        textStorage.addLayoutManager(layoutManager)
        
        let locationOfTouchInLabel = location(in: label)
        let textBoundingBox = layoutManager.usedRect(for: textContainer)
        let alignmentOffset = aligmentOffset(for: label)
        
        let xOffset = ((label.bounds.size.width - textBoundingBox.size.width) * alignmentOffset) - textBoundingBox.origin.x
        let yOffset = ((label.bounds.size.height - textBoundingBox.size.height) * alignmentOffset) - textBoundingBox.origin.y
        let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - xOffset, y: locationOfTouchInLabel.y - yOffset)
        
        let characterTapped = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
        
        let lineTapped = Int(ceil(locationOfTouchInLabel.y / label.font.lineHeight)) - 1
        let rightMostPointInLineTapped = CGPoint(x: label.bounds.size.width, y: label.font.lineHeight * CGFloat(lineTapped))
        let charsInLineTapped = layoutManager.characterIndex(for: rightMostPointInLineTapped, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
        
        return characterTapped < charsInLineTapped ? targetRange.contains(characterTapped) : false
    }
    
    private func createTextContainer(for label: UILabel) -> NSTextContainer {
        
        let textContainer = NSTextContainer(size: label.bounds.size)
        textContainer.lineFragmentPadding = 0.0
        textContainer.lineBreakMode = label.lineBreakMode
        textContainer.maximumNumberOfLines = label.numberOfLines
        return textContainer
    }
    
    private func aligmentOffset(for label: UILabel) -> CGFloat {
        
        switch label.textAlignment {
            
        case .left, .natural, .justified:
            
            return 0.0
        case .center:
            
            return 0.5
        case .right:
            
            return 1.0
            
            @unknown default:
            
            return 0.0
        }
    }
}

用法:

class ViewController: UIViewController {
    
    @IBOutlet var label : UILabel!
    
    let selectableString1 = "consectetur"
    let selectableString2 = "cupidatat"
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let text = "Lorem ipsum dolor sit amet, \(selectableString1) adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat \(selectableString2) non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
        label.attributedText = NSMutableAttributedString(attributedString: NSAttributedString(string: text))
        
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(labelTapped))
        label.addGestureRecognizer(tapGesture)
        label.isUserInteractionEnabled = true
    }
    
    @objc func labelTapped(gesture: UITapGestureRecognizer) {
        
        if gesture.didTapAttributedString(selectableString1, in: label) {
            
            print("\(selectableString1) tapped")
        } else if gesture.didTapAttributedString(selectableString2, in: label) {
            
            print("\(selectableString2) tapped")
        } else {
            
            print("Text tapped")
        }
    }
}

【讨论】:

  • 伟大的补充,这一定是在顶部!
  • 不错的答案。只是一个小错字:aligmentOffset -> alignmentOffset :)
【解决方案11】:

在 Swift 3 中工作,在此处粘贴整个代码

    //****Make sure the textview 'Selectable' = checked, and 'Editable = Unchecked'

import UIKit

class ViewController: UIViewController, UITextViewDelegate {

    @IBOutlet var theNewTextView: UITextView!
    override func viewDidLoad() {
        super.viewDidLoad()

        //****textview = Selectable = checked, and Editable = Unchecked

        theNewTextView.delegate = self

        let theString = NSMutableAttributedString(string: "Agree to Terms")
        let theRange = theString.mutableString.range(of: "Terms")

        theString.addAttribute(NSLinkAttributeName, value: "ContactUs://", range: theRange)

        let theAttribute = [NSForegroundColorAttributeName: UIColor.blue, NSUnderlineStyleAttributeName: NSUnderlineStyle.styleSingle.rawValue] as [String : Any]

        theNewTextView.linkTextAttributes = theAttribute

     theNewTextView.attributedText = theString             

theString.setAttributes(theAttribute, range: theRange)

    }

    func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {

        if (URL.scheme?.hasPrefix("ContactUs://"))! {

            return false //interaction not allowed
        }

        //*** Set storyboard id same as VC name
        self.navigationController!.pushViewController((self.storyboard?.instantiateViewController(withIdentifier: "TheLastViewController"))! as UIViewController, animated: true)

        return true
    }

}

【讨论】:

  • 这是新的 API,只允许 Swift 10 及以上版本:(
  • @t4nhpt 你的意思是 iOS 10 ;-)
【解决方案12】:

我创建了名为ResponsiveLabel 的UILabel 子类,它基于iOS 7 中引入的textkit API。它使用NAlexN 建议的相同方法。它提供了指定要在文本中搜索的模式的灵活性。可以指定应用于这些模式的样式以及在点击这些模式时要执行的操作。

//Detects email in text

 NSString *emailRegexString = @"[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,4}";
 NSError *error;
 NSRegularExpression *regex = [[NSRegularExpression alloc]initWithPattern:emailRegexString options:0 error:&error];
 PatternDescriptor *descriptor = [[PatternDescriptor alloc]initWithRegex:regex withSearchType:PatternSearchTypeAll withPatternAttributes:@{NSForegroundColorAttributeName:[UIColor redColor]}];
 [self.customLabel enablePatternDetection:descriptor];

如果你想让一个字符串可以点击,你可以这样做。此代码将属性应用于字符串“text”的每次出现。

PatternTapResponder tapResponder = ^(NSString *string) {
    NSLog(@"tapped = %@",string);
};

[self.customLabel enableStringDetection:@"text" withAttributes:@{NSForegroundColorAttributeName:[UIColor redColor],
                                                                 RLTapResponderAttributeName: tapResponder}];

【讨论】:

  • ResponsiveLabel 似乎是很好的组件,但由于某种原因,我无法为可点击文本设置颜色,也无法设置可点击字符串数组。
  • @MatrosovAlexander 目前,ResponsiveLabel 没有采用字符串数组并使它们可点击的方法。您可以在 github 上创建问题,我会尽快实现。
  • 是的,这不是问题,但有这种方法可以接受数组。
【解决方案13】:

这是 NAlexN 答案的快速版本。

class TapabbleLabel: UILabel {

let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: CGSize.zero)
var textStorage = NSTextStorage() {
    didSet {
        textStorage.addLayoutManager(layoutManager)
    }
}

var onCharacterTapped: ((label: UILabel, characterIndex: Int) -> Void)?

let tapGesture = UITapGestureRecognizer()

override var attributedText: NSAttributedString? {
    didSet {
        if let attributedText = attributedText {
            textStorage = NSTextStorage(attributedString: attributedText)
        } else {
            textStorage = NSTextStorage()
        }
    }
}

override var lineBreakMode: NSLineBreakMode {
    didSet {
        textContainer.lineBreakMode = lineBreakMode
    }
}

override var numberOfLines: Int {
    didSet {
        textContainer.maximumNumberOfLines = numberOfLines
    }
}

/**
 Creates a new view with the passed coder.

 :param: aDecoder The a decoder

 :returns: the created new view.
 */
required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    setUp()
}

/**
 Creates a new view with the passed frame.

 :param: frame The frame

 :returns: the created new view.
 */
override init(frame: CGRect) {
    super.init(frame: frame)
    setUp()
}

/**
 Sets up the view.
 */
func setUp() {
    userInteractionEnabled = true
    layoutManager.addTextContainer(textContainer)
    textContainer.lineFragmentPadding = 0
    textContainer.lineBreakMode = lineBreakMode
    textContainer.maximumNumberOfLines = numberOfLines
    tapGesture.addTarget(self, action: #selector(TapabbleLabel.labelTapped(_:)))
    addGestureRecognizer(tapGesture)
}

override func layoutSubviews() {
    super.layoutSubviews()
    textContainer.size = bounds.size
}

func labelTapped(gesture: UITapGestureRecognizer) {
    guard gesture.state == .Ended else {
        return
    }

    let locationOfTouch = gesture.locationInView(gesture.view)
    let textBoundingBox = layoutManager.usedRectForTextContainer(textContainer)
    let textContainerOffset = CGPoint(x: (bounds.width - textBoundingBox.width) / 2 - textBoundingBox.minX,
                                      y: (bounds.height - textBoundingBox.height) / 2 - textBoundingBox.minY)        
    let locationOfTouchInTextContainer = CGPoint(x: locationOfTouch.x - textContainerOffset.x,
                                                 y: locationOfTouch.y - textContainerOffset.y)
    let indexOfCharacter = layoutManager.characterIndexForPoint(locationOfTouchInTextContainer,
                                                                inTextContainer: textContainer,
                                                                fractionOfDistanceBetweenInsertionPoints: nil)

    onCharacterTapped?(label: self, characterIndex: indexOfCharacter)
}
}

然后您可以在 viewDidLoad 方法中创建该类的实例,如下所示:

let label = TapabbleLabel()
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-[view]-|",
                                               options: [], metrics: nil, views: ["view" : label]))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-[view]-|",
                                               options: [], metrics: nil, views: ["view" : label]))

let attributedString = NSMutableAttributedString(string: "String with a link", attributes: nil)
let linkRange = NSMakeRange(14, 4); // for the word "link" in the string above

let linkAttributes: [String : AnyObject] = [
    NSForegroundColorAttributeName : UIColor.blueColor(), NSUnderlineStyleAttributeName : NSUnderlineStyle.StyleSingle.rawValue,
    NSLinkAttributeName: "http://www.apple.com"]
attributedString.setAttributes(linkAttributes, range:linkRange)

label.attributedText = attributedString

label.onCharacterTapped = { label, characterIndex in
    if let attribute = label.attributedText?.attribute(NSLinkAttributeName, atIndex: characterIndex, effectiveRange: nil) as? String,
        let url = NSURL(string: attribute) {
        UIApplication.sharedApplication().openURL(url)
    }
}

当一个字符被点击时最好有一个自定义属性。现在,它是 NSLinkAttributeName,但可以是任何值,您可以使用该值来做其他事情,而不是打开一个 url,您可以执行任何自定义操作。

【讨论】:

  • 这太棒了!我用 LongPressRecognizer 替换了 TapGestureRecognizer,它破坏了 tableview 滚动。关于如何防止gestureRecognizer破坏tableview滚动的任何建议?谢谢!!!
  • 你可以同时使用 shouldRecognizedeveloper.apple.com/documentation/uikit/…
【解决方案14】:

以下是超链接 UILabel 的示例代码: 来源:http://sickprogrammersarea.blogspot.in/2014/03/adding-links-to-uilabel.html

#import "ViewController.h"
#import "TTTAttributedLabel.h"

@interface ViewController ()
@end

@implementation ViewController
{
    UITextField *loc;
    TTTAttributedLabel *data;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    UILabel *lbl = [[UILabel alloc] initWithFrame:CGRectMake(5, 20, 80, 25) ];
    [lbl setText:@"Text:"];
    [lbl setFont:[UIFont fontWithName:@"Verdana" size:16]];
    [lbl setTextColor:[UIColor grayColor]];
    loc=[[UITextField alloc] initWithFrame:CGRectMake(4, 20, 300, 30)];
    //loc.backgroundColor = [UIColor grayColor];
    loc.borderStyle=UITextBorderStyleRoundedRect;
    loc.clearButtonMode=UITextFieldViewModeWhileEditing;
    //[loc setText:@"Enter Location"];
    loc.clearsOnInsertion = YES;
    loc.leftView=lbl;
    loc.leftViewMode=UITextFieldViewModeAlways;
    [loc setDelegate:self];
    [self.view addSubview:loc];
    [loc setRightViewMode:UITextFieldViewModeAlways];
    CGRect frameimg = CGRectMake(110, 70, 70,30);
    UIButton *srchButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    srchButton.frame=frameimg;
    [srchButton setTitle:@"Go" forState:UIControlStateNormal];
    [srchButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
    srchButton.backgroundColor=[UIColor clearColor];
    [srchButton addTarget:self action:@selector(go:) forControlEvents:UIControlEventTouchDown];
    [self.view addSubview:srchButton];
    data = [[TTTAttributedLabel alloc] initWithFrame:CGRectMake(5, 120,self.view.frame.size.width,200) ];
    [data setFont:[UIFont fontWithName:@"Verdana" size:16]];
    [data setTextColor:[UIColor blackColor]];
    data.numberOfLines=0;
    data.delegate = self;
    data.enabledTextCheckingTypes=NSTextCheckingTypeLink|NSTextCheckingTypePhoneNumber;
    [self.view addSubview:data];
}
- (void)attributedLabel:(TTTAttributedLabel *)label didSelectLinkWithURL:(NSURL *)url
{
    NSString *val=[[NSString alloc]initWithFormat:@"%@",url];
    if ([[url scheme] hasPrefix:@"mailto"]) {
              NSLog(@" mail URL Selected : %@",url);
        MFMailComposeViewController *comp=[[MFMailComposeViewController alloc]init];
        [comp setMailComposeDelegate:self];
        if([MFMailComposeViewController canSendMail])
        {
            NSString *recp=[[val substringToIndex:[val length]] substringFromIndex:7];
            NSLog(@"Recept : %@",recp);
            [comp setToRecipients:[NSArray arrayWithObjects:recp, nil]];
            [comp setSubject:@"From my app"];
            [comp setMessageBody:@"Hello bro" isHTML:NO];
            [comp setModalTransitionStyle:UIModalTransitionStyleCrossDissolve];
            [self presentViewController:comp animated:YES completion:nil];
        }
    }
    else{
        [[UIApplication sharedApplication] openURL:[NSURL URLWithString:val]];
    }
}
-(void)mailComposeController:(MFMailComposeViewController *)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error{
    if(error)
    {
        UIAlertView *alrt=[[UIAlertView alloc]initWithTitle:@"Erorr" message:@"Some error occureed" delegate:nil cancelButtonTitle:@"" otherButtonTitles:nil, nil];
        [alrt show];
        [self dismissViewControllerAnimated:YES completion:nil];
    }
    else{
        [self dismissViewControllerAnimated:YES completion:nil];
    }
}

- (void)attributedLabel:(TTTAttributedLabel *)label didSelectLinkWithPhoneNumber:(NSString *)phoneNumber
{
    NSLog(@"Phone Number Selected : %@",phoneNumber);
    UIDevice *device = [UIDevice currentDevice];
    if ([[device model] isEqualToString:@"iPhone"] ) {
        [[UIApplication sharedApplication] openURL:[NSURL URLWithString:[NSString stringWithFormat:@"tel:%@",phoneNumber]]];
    } else {
        UIAlertView *Notpermitted=[[UIAlertView alloc] initWithTitle:@"Alert" message:@"Your device doesn't support this feature." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
        [Notpermitted show];
    }
}
-(void)go:(id)sender
{
    [data setText:loc.text];
}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"Reached");
    [loc resignFirstResponder];
}

【讨论】:

    【解决方案15】:

    我很难处理这个问题... UILabel 在属性文本上带有链接...这只是一个令人头疼的问题,所以我最终使用了ZSWTappableLabel

    【讨论】:

    • 谢谢。它真的适用于我的情况。它将检测电子邮件 ID、电话号码和链接。
    【解决方案16】:

    这是一个尽可能少的 Swift 实现,还包括触摸反馈。注意事项:

    1. 您必须在 NSAttributedStrings 中设置字体
    2. 您只能使用 NSAttributedStrings!
    3. 您必须确保您的链接不能换行(使用不间断空格:"\u{a0}"
    4. 设置文本后无法更改 lineBreakMode 或 numberOfLines
    5. 您可以通过使用.link 键添加属性来创建链接

    .

    public class LinkLabel: UILabel {
        private var storage: NSTextStorage?
        private let textContainer = NSTextContainer()
        private let layoutManager = NSLayoutManager()
        private var selectedBackgroundView = UIView()
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            textContainer.lineFragmentPadding = 0
            layoutManager.addTextContainer(textContainer)
            textContainer.layoutManager = layoutManager
            isUserInteractionEnabled = true
            selectedBackgroundView.isHidden = true
            selectedBackgroundView.backgroundColor = UIColor(white: 0, alpha: 0.3333)
            selectedBackgroundView.layer.cornerRadius = 4
            addSubview(selectedBackgroundView)
        }
    
        public required convenience init(coder: NSCoder) {
            self.init(frame: .zero)
        }
    
        public override func layoutSubviews() {
            super.layoutSubviews()
            textContainer.size = frame.size
        }
    
        public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            super.touchesBegan(touches, with: event)
            setLink(for: touches)
        }
    
        public override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
            super.touchesMoved(touches, with: event)
            setLink(for: touches)
        }
    
        private func setLink(for touches: Set<UITouch>) {
            if let pt = touches.first?.location(in: self), let (characterRange, _) = link(at: pt) {
                let glyphRange = layoutManager.glyphRange(forCharacterRange: characterRange, actualCharacterRange: nil)
                selectedBackgroundView.frame = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer).insetBy(dx: -3, dy: -3)
                selectedBackgroundView.isHidden = false
            } else {
                selectedBackgroundView.isHidden = true
            }
        }
    
        public override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
            super.touchesCancelled(touches, with: event)
            selectedBackgroundView.isHidden = true
        }
    
        public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
            super.touchesEnded(touches, with: event)
            selectedBackgroundView.isHidden = true
    
            if let pt = touches.first?.location(in: self), let (_, url) = link(at: pt) {
                UIApplication.shared.open(url)
            }
        }
    
        private func link(at point: CGPoint) -> (NSRange, URL)? {
            let touchedGlyph = layoutManager.glyphIndex(for: point, in: textContainer)
            let touchedChar = layoutManager.characterIndexForGlyph(at: touchedGlyph)
            var range = NSRange()
            let attrs = attributedText!.attributes(at: touchedChar, effectiveRange: &range)
            if let urlstr = attrs[.link] as? String {
                return (range, URL(string: urlstr)!)
            } else {
                return nil
            }
        }
    
        public override var attributedText: NSAttributedString? {
            didSet {
                textContainer.maximumNumberOfLines = numberOfLines
                textContainer.lineBreakMode = lineBreakMode
                if let txt = attributedText {
                    storage = NSTextStorage(attributedString: txt)
                    storage!.addLayoutManager(layoutManager)
                    layoutManager.textStorage = storage
                    textContainer.size = frame.size
                }
            }
        }
    }
    

    【讨论】:

    • 我看到mxcl,我试一试,效果很好。如果您想设置链接外观的样式,请改用NSAttributedString.Key.attachment
    【解决方案17】:

    我关注这个版本,

    斯威夫特 4:

    import Foundation
    
    class AELinkedClickableUILabel: UILabel {
    
        typealias YourCompletion = () -> Void
    
        var linkedRange: NSRange!
        var completion: YourCompletion?
    
        @objc func linkClicked(sender: UITapGestureRecognizer){
    
            if let completionBlock = completion {
    
                let textView = UITextView(frame: self.frame)
                textView.text = self.text
                textView.attributedText = self.attributedText
                let index = textView.layoutManager.characterIndex(for: sender.location(in: self),
                                                                  in: textView.textContainer,
                                                                  fractionOfDistanceBetweenInsertionPoints: nil)
    
                if linkedRange.lowerBound <= index && linkedRange.upperBound >= index {
    
                    completionBlock()
                }
            }
        }
    
    /**
     *  This method will be used to set an attributed text specifying the linked text with a
     *  handler when the link is clicked
     */
        public func setLinkedTextWithHandler(text:String, link: String, handler: @escaping ()->()) -> Bool {
    
            let attributextText = NSMutableAttributedString(string: text)
            let foundRange = attributextText.mutableString.range(of: link)
    
            if foundRange.location != NSNotFound {
                self.linkedRange = foundRange
                self.completion = handler
                attributextText.addAttribute(NSAttributedStringKey.link, value: text, range: foundRange)
                self.isUserInteractionEnabled = true
                self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(linkClicked(sender:))))
                return true
            }
            return false
        }
    }
    

    调用示例:

    button.setLinkedTextWithHandler(text: "This website (stackoverflow.com) is awesome", link: "stackoverflow.com") 
    {
        // show popup or open to link
    }
    

    【讨论】:

      【解决方案18】:

      我找到了另一个解决方案:

      我找到了一种方法来检测您从互联网上找到的 html 文本中的链接,您可以使用以下方法将其转换为 nsattributeString:

      func htmlAttributedString(fontSize: CGFloat = 17.0) -> NSAttributedString? {
                  let fontName = UIFont.systemFont(ofSize: fontSize).fontName
                  let string = self.appending(String(format: "<style>body{font-family: '%@'; font-size:%fpx;}</style>", fontName, fontSize))
                  guard let data = string.data(using: String.Encoding.utf16, allowLossyConversion: false) else { return nil }
      
                  guard let html = try? NSMutableAttributedString (
                      data: data,
                      options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html],
                      documentAttributes: nil) else { return nil }
                  return html
              }
      

      我的方法允许您检测超链接而无需指定它们。

      • 首先你创建一个 Tapgesturerecognizer 的扩展:

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

        }

      然后在您的视图控制器中,您创建了一个 url 和范围列表来存储所有链接和属性文本包含的范围:

      var listurl : [String] = []
          var listURLRange : [NSRange] = []
      

      找到你可以使用的 URL 和 URLRange :

          fun findLinksAndRange(attributeString : NSAttributeString){
              notification.enumerateAttribute(NSAttributedStringKey.link , in: NSMakeRange(0, notification.length), options: [.longestEffectiveRangeNotRequired]) { value, range, isStop in
                          if let value = value {
                              print("\(value) found at \(range.location)")
                              let stringValue = "\(value)"
                              listurl.append(stringValue)
                              listURLRange.append(range)
                          }
                      }
      
                  westlandNotifcationLabel.addGestureRecognizer(UITapGestureRecognizer(target : self, action: #selector(handleTapOnLabel(_:))))
      
          }
      

      然后你实现手柄水龙头:

      @objc func handleTapOnLabel(_ recognizer: UITapGestureRecognizer) {
              for index in 0..<listURLRange.count{
                  if recognizer.didTapAttributedTextInLabel(label: westlandNotifcationLabel, inRange: listURLRange[index]) {
                      goToWebsite(url : listurl[index])
                  }
              }
          }
      
          func goToWebsite(url : String){
              if let websiteUrl = URL(string: url){
                  if #available(iOS 10, *) {
                      UIApplication.shared.open(websiteUrl, options: [:],
                                                completionHandler: {
                                                  (success) in
                                                  print("Open \(websiteUrl): \(success)")
                      })
                  } else {
                      let success = UIApplication.shared.openURL(websiteUrl)
                      print("Open \(websiteUrl): \(success)")
                  }
              }
          }
      

      我们开始吧!

      我希望这个解决方案能帮助你喜欢它帮助我。

      【讨论】:

        【解决方案19】:

        就像之前的回答中报道的那样,UITextView 能够处理链接上的触摸。这可以通过将文本的其他部分用作链接来轻松扩展。 AttributedTextView 库是一个 UITextView 子类,可以很容易地处理这些。欲了解更多信息,请参阅:https://github.com/evermeer/AttributedTextView

        您可以使文本的任何部分像这样交互(其中 textView1 是 UITextView IBOutlet):

        textView1.attributer =
            "1. ".red
            .append("This is the first test. ").green
            .append("Click on ").black
            .append("evict.nl").makeInteract { _ in
                UIApplication.shared.open(URL(string: "http://evict.nl")!, options: [:], completionHandler: { completed in })
            }.underline
            .append(" for testing links. ").black
            .append("Next test").underline.makeInteract { _ in
                print("NEXT")
            }
            .all.font(UIFont(name: "SourceSansPro-Regular", size: 16))
            .setLinkColor(UIColor.purple) 
        

        对于处理主题标签和提及,您可以使用如下代码:

        textView1.attributer = "@test: What #hashtags do we have in @evermeer #AtributedTextView library"
            .matchHashtags.underline
            .matchMentions
            .makeInteract { link in
                UIApplication.shared.open(URL(string: "https://twitter.com\(link.replacingOccurrences(of: "@", with: ""))")!, options: [:], completionHandler: { completed in })
            }
        

        【讨论】:

          【解决方案20】:

          我正在扩展 @samwize 的答案以处理多行 UILabel 并举例说明如何使用 UIButton

          extension UITapGestureRecognizer {
          
              func didTapAttributedTextInButton(button: UIButton, inRange targetRange: NSRange) -> Bool {
                  guard let label = button.titleLabel else { return false }
                  return didTapAttributedTextInLabel(label, inRange: targetRange)
              }
          
              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.locationInView(label)
                  let textBoundingBox = layoutManager.usedRectForTextContainer(textContainer)
                  let textContainerOffset = CGPointMake((labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
                                                        (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y);
                  let locationOfTouchInTextContainer = CGPointMake((locationOfTouchInLabel.x - textContainerOffset.x),
                                                                   0 );
                  // Adjust for multiple lines of text
                  let lineModifier = Int(ceil(locationOfTouchInLabel.y / label.font.lineHeight)) - 1
                  let rightMostFirstLinePoint = CGPointMake(labelSize.width, 0)
                  let charsPerLine = layoutManager.characterIndexForPoint(rightMostFirstLinePoint, inTextContainer: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
          
                  let indexOfCharacter = layoutManager.characterIndexForPoint(locationOfTouchInTextContainer, inTextContainer: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
                  let adjustedRange = indexOfCharacter + (lineModifier * charsPerLine)
          
                  return NSLocationInRange(adjustedRange, targetRange)
              }
          
          }
          

          【讨论】:

          • 我尝试了您的多行 UILabel 解决方案,但它并不适合我。触摸总是在我的 UILabel 的最后一行注册。
          • @ChristianSchober 你有自定义字体或行高吗?
          • 不是真的,我们使用字体 HelveticaNeue 和标准高度
          • 当换行符不在标签右边缘时不起作用
          • 我有默认字体,但行间距不起作用,有什么想法吗?
          【解决方案21】:

          对于完全自定义的链接,您需要使用 UIWebView - 您可以拦截调用,以便在按下链接时转到应用的其他部分。

          【讨论】:

          • UIWebViews 在分配时并没有那么快,所以如果你能侥幸逃脱,使用 UILabel 或 UITextField 库(如 FancyLabel 或 TTTAttributedLabel)会更好。如果您需要在 tableview 单元格等中包含可点击链接,这一点尤其相关。
          【解决方案22】:

          根据 Charles Gamble 的回答,这是我使用的(我删除了一些让我感到困惑并给我错误索引的行):

          - (BOOL)didTapAttributedTextInLabel:(UILabel *)label inRange:(NSRange)targetRange TapGesture:(UIGestureRecognizer*) gesture{
              NSParameterAssert(label != nil);
          
              // create instances of NSLayoutManager, NSTextContainer and NSTextStorage
              NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
              NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:label.attributedText];
          
              // configure layoutManager and textStorage
              [textStorage addLayoutManager:layoutManager];
          
              // configure textContainer for the label
              NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(label.frame.size.width, label.frame.size.height)];
          
              textContainer.lineFragmentPadding = 0.0;
              textContainer.lineBreakMode = label.lineBreakMode;
              textContainer.maximumNumberOfLines = label.numberOfLines;
          
              // find the tapped character location and compare it to the specified range
              CGPoint locationOfTouchInLabel = [gesture locationInView:label];
              [layoutManager addTextContainer:textContainer]; //(move here, not sure it that matter that calling this line after textContainer is set
          
              NSInteger indexOfCharacter = [layoutManager characterIndexForPoint:locationOfTouchInLabel
                                                                     inTextContainer:textContainer
                                            fractionOfDistanceBetweenInsertionPoints:nil];
              if (NSLocationInRange(indexOfCharacter, targetRange)) {
                  return YES;
              } else {
                  return NO;
              }
          }
          

          【讨论】:

            【解决方案23】:

            这是一个插入式 Objective-C 类别,它利用现有的 NSLinkAttributeName 属性启用现有 UILabel.attributedText 字符串中的可点击链接。

            @interface UILabel (GSBClickableLinks) <UIGestureRecognizerDelegate>
            @property BOOL enableLinks;
            @end
            
            #import <objc/runtime.h>
            static const void *INDEX;
            static const void *TAP;
            
            @implementation UILabel (GSBClickableLinks)
            
            - (void)setEnableLinks:(BOOL)enableLinks
            {
                UITapGestureRecognizer *tap = objc_getAssociatedObject(self, &TAP); // retreive tap
                if (enableLinks && !tap) { // add a gestureRegonzier to the UILabel to detect taps
                    tap = [UITapGestureRecognizer.alloc initWithTarget:self action:@selector(openLink)];
                    tap.delegate = self;
                    [self addGestureRecognizer:tap];
                    objc_setAssociatedObject(self, &TAP, tap, OBJC_ASSOCIATION_RETAIN_NONATOMIC); // save tap
                }
                self.userInteractionEnabled = enableLinks; // note - when false UILAbel wont receive taps, hence disable links
            }
            
            - (BOOL)enableLinks
            {
                return (BOOL)objc_getAssociatedObject(self, &TAP); // ie tap != nil
            }
            
            // First check whether user tapped on a link within the attributedText of the label.
            // If so, then the our label's gestureRecogizer will subsequently fire, and open the corresponding NSLinkAttributeName.
            // If not, then the tap will get passed along, eg to the enclosing UITableViewCell...
            // Note: save which character in the attributedText was clicked so that we dont have to redo everything again in openLink.
            - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
            {
                if (gestureRecognizer != objc_getAssociatedObject(self, &TAP)) return YES; // dont block other gestures (eg swipe)
            
                // Re-layout the attributedText to find out what was tapped
                NSTextContainer *textContainer = [NSTextContainer.alloc initWithSize:self.frame.size];
                textContainer.lineFragmentPadding = 0;
                textContainer.maximumNumberOfLines = self.numberOfLines;
                textContainer.lineBreakMode = self.lineBreakMode;
                NSLayoutManager *layoutManager = NSLayoutManager.new;
                [layoutManager addTextContainer:textContainer];
                NSTextStorage *textStorage = [NSTextStorage.alloc initWithAttributedString:self.attributedText];
                [textStorage addLayoutManager:layoutManager];
            
                NSUInteger index = [layoutManager characterIndexForPoint:[gestureRecognizer locationInView:self]
                                                         inTextContainer:textContainer
                                fractionOfDistanceBetweenInsertionPoints:NULL];
                objc_setAssociatedObject(self, &INDEX, @(index), OBJC_ASSOCIATION_RETAIN_NONATOMIC); // save index
            
                return (BOOL)[self.attributedText attribute:NSLinkAttributeName atIndex:index effectiveRange:NULL]; // tapped on part of a link?
            }
            
            - (void)openLink
            {
                NSUInteger index = [objc_getAssociatedObject(self, &INDEX) unsignedIntegerValue]; // retrieve index
                NSURL *url = [self.attributedText attribute:NSLinkAttributeName atIndex:index effectiveRange:NULL];
                if (url && [UIApplication.sharedApplication canOpenURL:url]) [UIApplication.sharedApplication openURL:url];
            }
            
            @end 
            

            通过 UILabel 子类(即没有 objc_getAssociatedObject 乱七八糟),这会更干净一些,但如果你像我一样,你宁愿避免为了给现有的添加一些额外的功能而创建不必要的(第 3 方)子类UIKit 类。此外,它的美妙之处在于它向任何现有的 UILabel 添加了可点击链接,例如现有的 UITableViewCells!

            我已尝试通过使用 NSAttributedString 中已有的现有 NSLinkAttributeName 属性内容,使其侵入性尽可能小。所以它很简单:

            NSURL *myURL = [NSURL URLWithString:@"http://www.google.com"];
            NSMutableAttributedString *myString = [NSMutableAttributedString.alloc initWithString:@"This string has a clickable link: "];
            [myString appendAttributedString:[NSAttributedString.alloc initWithString:@"click here" attributes:@{NSLinkAttributeName:myURL}]];
            ...
            myLabel.attributedText = myString;
            myLabel.enableLinks = YES; // yes, that's all! :-)
            

            基本上,它通过在您的 UILabel 中添加 UIGestureRecognizer 来工作。艰苦的工作是在gestureRecognizerShouldBegin: 中完成的,它重新布置了attributedText 字符串以找出点击了哪个字符。如果此字符是 NSLinkAttributeName 的一部分,则gestureRecognizer 将随后触发,检索相应的 URL(从 NSLinkAttributeName 值),并按照通常的[UIApplication.sharedApplication openURL:url] 过程打开链接。

            注意 - 通过在gestureRecognizerShouldBegin: 中执行所有这些操作,如果您没有碰巧点击标签中的链接,则该事件将被传递。因此,例如,您的 UITableViewCell 将捕获链接上的点击,但在其他方面表现正常(选择单元格、取消选择、滚动......)。

            我已将其放入 GitHub 存储库 here。 改编自 Kai Burghardt 的 SO 帖子 here

            【讨论】:

              【解决方案24】:

              是的,这是可能的,尽管一开始很难弄清楚。我将更进一步,向您展示如何点击文本中的任何区域。

              使用此方法,您可以拥有 UI 标签:

              • 多行友好
              • 自动收缩友好
              • 可点击友好(是的,甚至是单个字符)
              • 斯威夫特 5

              第 1 步:

              使 UILabel 具有“Truncate Tail”的换行符属性并设置最小字体比例

              如果您不熟悉字体比例,请记住以下规则:

              minimumFontSize/defaultFontSize = fontscale

              在我的例子中,我希望 7.2 成为最小字体大小,而我的起始字体大小是 36。因此,7.2 / 36 = 0.2

              第 2 步:

              如果您不关心标签的可点击性,而只是想要一个有效的多行标签,那么您就完成了!

              但是,如果您希望 标签可点击,请继续阅读...

              添加我创建的以下扩展

              extension UILabel {
              
                  func setOptimalFontSize(maxFontSize:CGFloat,text:String){
                      let width = self.bounds.size.width
              
                      var font_size:CGFloat = maxFontSize //Set the maximum font size.
                      var stringSize = NSString(string: text).size(withAttributes: [.font : self.font.withSize(font_size)])
                      while(stringSize.width > width){
                          font_size = font_size - 1
                          stringSize = NSString(string: text).size(withAttributes: [.font : self.font.withSize(font_size)])
                      }
              
                      self.font = self.font.withSize(font_size)//Forcefully change font to match what it would be graphically.
                  }
              }
              

              这样使用(只需将&lt;Label&gt; 替换为您的实际标签名称):

              <Label>.setOptimalFontSize(maxFontSize: 36.0, text: formula)
              

              这个扩展是必需的,因为自动收缩在标签自动收缩后不会改变标签的'font'属性,所以你必须通过与使用 . size(withAttributes) 函数,它模拟特定字体的大小。

              这是必要的,因为检测标签点击位置的解决方案需要知道确切的字体大小

              第 3 步:

              添加以下扩展:

              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 mutableAttribString = NSMutableAttributedString(attributedString: label.attributedText!)
                      mutableAttribString.addAttributes([NSAttributedString.Key.font: label.font!], range: NSRange(location: 0, length: label.attributedText!.length))
              
                      let paragraphStyle = NSMutableParagraphStyle()
                      paragraphStyle.lineSpacing = 6
                      paragraphStyle.lineBreakMode = .byTruncatingTail
                      paragraphStyle.alignment = .center
                      mutableAttribString.addAttributes([.paragraphStyle: paragraphStyle], range: NSMakeRange(0, mutableAttribString.string.count))
              
                      let textStorage = NSTextStorage(attributedString: mutableAttribString)
              
                      // Configure textContainer
                      textContainer.lineFragmentPadding = 0.0
                      textContainer.lineBreakMode = label.lineBreakMode
                      textContainer.maximumNumberOfLines = label.numberOfLines
              
                      // Configure layoutManager and textStorage
                      layoutManager.addTextContainer(textContainer)
              
                      textStorage.addLayoutManager(layoutManager)
              
                      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 = CGPointMake((labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
                                                            //(labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y);
                      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 = CGPointMake(locationOfTouchInLabel.x - textContainerOffset.x,
                                                                      // locationOfTouchInLabel.y - textContainerOffset.y);
                      let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x, y: locationOfTouchInLabel.y - textContainerOffset.y)
              
                      let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
                      print("IndexOfCharacter=",indexOfCharacter)
              
                      print("TargetRange=",targetRange)
                      return NSLocationInRange(indexOfCharacter, targetRange)
                  }
              
              }
              

              您需要针对特定​​的多行情况修改此扩展。在我的例子中,你会注意到我使用了段落样式。

              let paragraphStyle = NSMutableParagraphStyle()
                      paragraphStyle.lineSpacing = 6
                      paragraphStyle.lineBreakMode = .byTruncatingTail
                      paragraphStyle.alignment = .center
                      mutableAttribString.addAttributes([.paragraphStyle: paragraphStyle], range: NSMakeRange(0, mutableAttribString.string.count))
              

              确保在扩展程序中将此更改为您实际使用的行距,以便一切计算正确。

              第 4 步:

              将gestureRecognizer添加到viewDidLoad中的标签或您认为合适的位置(只需再次将&lt;Label&gt;替换为您的标签名称:

              <Label>.addGestureRecognizer(UITapGestureRecognizer(target:self, action: #selector(tapLabel(gesture:))))
              

              这是我的 tapLabel 函数的简化示例(只需将 &lt;Label&gt; 替换为您的 UILabel 名称):

              @IBAction func tapLabel(gesture: UITapGestureRecognizer) {
                      guard let text = <Label>.attributedText?.string else {
                          return
                      }
              
                      let click_range = text.range(of: "(α/β)")
              
                      if gesture.didTapAttributedTextInLabel(label: <Label>, inRange: NSRange(click_range!, in: text)) {
                         print("Tapped a/b")
                      }else {
                         print("Tapped none")
                      }
                  }
              

              在我的示例中只是一个注释,我的字符串是BED = N * d * [ RBE + ( d / (α/β) ) ],所以在这种情况下我只是得到了α/β 的范围。您可以在字符串中添加“\n”以添加换行符和您想要的任何文本,然后测试它以在下一行找到一个字符串,它仍然会找到它并正确检测到点击!

              就是这样!你完成了。享受多行 可点击标签。

              【讨论】:

                【解决方案25】:

                使用以下 .h 和 .m 文件创建类。在.m文件中有如下函数

                 - (void)linkAtPoint:(CGPoint)location
                

                在这个函数中,我们将检查我们需要对其进行操作的子字符串的范围。使用你自己的逻辑来设置你的范围。

                以下是子类的用法

                TaggedLabel *label = [[TaggedLabel alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
                [self.view addSubview:label];
                label.numberOfLines = 0;
                NSMutableAttributedString *attributtedString = [[NSMutableAttributedString alloc] initWithString : @"My name is @jjpp" attributes : @{ NSFontAttributeName : [UIFont systemFontOfSize:10],}];                                                                                                                                                                              
                //Do not forget to add the font attribute.. else it wont work.. it is very important
                [attributtedString addAttribute:NSForegroundColorAttributeName
                                        value:[UIColor redColor]
                                        range:NSMakeRange(11, 5)];//you can give this range inside the .m function mentioned above
                

                下面是.h文件

                #import <UIKit/UIKit.h>
                
                @interface TaggedLabel : UILabel<NSLayoutManagerDelegate>
                
                @property(nonatomic, strong)NSLayoutManager *layoutManager;
                @property(nonatomic, strong)NSTextContainer *textContainer;
                @property(nonatomic, strong)NSTextStorage *textStorage;
                @property(nonatomic, strong)NSArray *tagsArray;
                @property(readwrite, copy) tagTapped nameTagTapped;
                
                @end   
                

                下面是.m文件

                #import "TaggedLabel.h"
                @implementation TaggedLabel
                
                - (id)initWithFrame:(CGRect)frame
                {
                 self = [super initWithFrame:frame];
                 if (self)
                 {
                  self.userInteractionEnabled = YES;
                 }
                return self;
                }
                
                - (id)initWithCoder:(NSCoder *)aDecoder
                {
                 self = [super initWithCoder:aDecoder];
                if (self)
                {
                 self.userInteractionEnabled = YES;
                }
                return self;
                }
                
                - (void)setupTextSystem
                {
                 _layoutManager = [[NSLayoutManager alloc] init];
                 _textContainer = [[NSTextContainer alloc] initWithSize:CGSizeZero];
                 _textStorage = [[NSTextStorage alloc] initWithAttributedString:self.attributedText];
                 // Configure layoutManager and textStorage
                 [_layoutManager addTextContainer:_textContainer];
                 [_textStorage addLayoutManager:_layoutManager];
                 // Configure textContainer
                 _textContainer.lineFragmentPadding = 0.0;
                 _textContainer.lineBreakMode = NSLineBreakByWordWrapping;
                 _textContainer.maximumNumberOfLines = 0;
                 self.userInteractionEnabled = YES;
                 self.textContainer.size = self.bounds.size;
                }
                
                - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
                {
                 if (!_layoutManager)
                 {
                  [self setupTextSystem];
                 }
                 // Get the info for the touched link if there is one
                 CGPoint touchLocation = [[touches anyObject] locationInView:self];
                 [self linkAtPoint:touchLocation];
                }
                
                - (void)linkAtPoint:(CGPoint)location
                {
                 // Do nothing if we have no text
                 if (_textStorage.string.length == 0)
                 {
                  return;
                 }
                 // Work out the offset of the text in the view
                 CGPoint textOffset = [self calcGlyphsPositionInView];
                 // Get the touch location and use text offset to convert to text cotainer coords
                 location.x -= textOffset.x;
                 location.y -= textOffset.y;
                 NSUInteger touchedChar = [_layoutManager glyphIndexForPoint:location inTextContainer:_textContainer];
                 // If the touch is in white space after the last glyph on the line we don't
                 // count it as a hit on the text
                 NSRange lineRange;
                 CGRect lineRect = [_layoutManager lineFragmentUsedRectForGlyphAtIndex:touchedChar effectiveRange:&lineRange];
                 if (CGRectContainsPoint(lineRect, location) == NO)
                 {
                  return;
                 }
                 // Find the word that was touched and call the detection block
                    NSRange range = NSMakeRange(11, 5);//for this example i'm hardcoding the range here. In a real scenario it should be iterated through an array for checking all the ranges
                    if ((touchedChar >= range.location) && touchedChar < (range.location + range.length))
                    {
                     NSLog(@"range-->>%@",self.tagsArray[i][@"range"]);
                    }
                }
                
                - (CGPoint)calcGlyphsPositionInView
                {
                 CGPoint textOffset = CGPointZero;
                 CGRect textBounds = [_layoutManager usedRectForTextContainer:_textContainer];
                 textBounds.size.width = ceil(textBounds.size.width);
                 textBounds.size.height = ceil(textBounds.size.height);
                
                 if (textBounds.size.height < self.bounds.size.height)
                 {
                  CGFloat paddingHeight = (self.bounds.size.height - textBounds.size.height) / 2.0;
                  textOffset.y = paddingHeight;
                 }
                
                 if (textBounds.size.width < self.bounds.size.width)
                 {
                  CGFloat paddingHeight = (self.bounds.size.width - textBounds.size.width) / 2.0;
                  textOffset.x = paddingHeight;
                 }
                 return textOffset;
                 }
                
                @end
                

                【讨论】:

                  【解决方案26】:

                  我强烈建议使用自动检测文本中的 URL 并将其转换为链接的库。 试试:

                  两者都在 MIT 许可下。

                  【讨论】:

                  • 你在重复以前的答案。
                  【解决方案27】:

                  作为UILabel 上的一个类别的插入式解决方案(假设您的UILabel 使用带有一些NSLinkAttributeName 属性的属性字符串):

                  @implementation UILabel (Support)
                  
                  - (BOOL)openTappedLinkAtLocation:(CGPoint)location {
                    CGSize labelSize = self.bounds.size;
                  
                    NSTextContainer* textContainer = [[NSTextContainer alloc] initWithSize:CGSizeZero];
                    textContainer.lineFragmentPadding = 0.0;
                    textContainer.lineBreakMode = self.lineBreakMode;
                    textContainer.maximumNumberOfLines = self.numberOfLines;
                    textContainer.size = labelSize;
                  
                    NSLayoutManager* layoutManager = [[NSLayoutManager alloc] init];
                    [layoutManager addTextContainer:textContainer];
                  
                    NSTextStorage* textStorage = [[NSTextStorage alloc] initWithAttributedString:self.attributedText];
                    [textStorage addAttribute:NSFontAttributeName value:self.font range:NSMakeRange(0, textStorage.length)];
                    [textStorage addLayoutManager:layoutManager];
                  
                    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(location.x - textContainerOffset.x, location.y - textContainerOffset.y);
                    NSInteger indexOfCharacter = [layoutManager characterIndexForPoint:locationOfTouchInTextContainer inTextContainer:textContainer fractionOfDistanceBetweenInsertionPoints:nullptr];
                    if (indexOfCharacter >= 0) {
                      NSURL* url = [textStorage attribute:NSLinkAttributeName atIndex:indexOfCharacter effectiveRange:nullptr];
                      if (url) {
                        [[UIApplication sharedApplication] openURL:url];
                        return YES;
                      }
                    }
                    return NO;
                  }
                  
                  @end
                  

                  【讨论】:

                    【解决方案28】:

                    这个通用方法也有效!

                    func didTapAttributedTextInLabel(gesture: UITapGestureRecognizer, inRange targetRange: NSRange) -> Bool {
                    
                            let layoutManager = NSLayoutManager()
                            let textContainer = NSTextContainer(size: CGSize.zero)
                            guard let strAttributedText = self.attributedText else {
                                return false
                            }
                    
                            let textStorage = NSTextStorage(attributedString: strAttributedText)
                    
                            // Configure layoutManager and textStorage
                            layoutManager.addTextContainer(textContainer)
                            textStorage.addLayoutManager(layoutManager)
                    
                            // Configure textContainer
                            textContainer.lineFragmentPadding = Constants.lineFragmentPadding
                            textContainer.lineBreakMode = self.lineBreakMode
                            textContainer.maximumNumberOfLines = self.numberOfLines
                            let labelSize = self.bounds.size
                            textContainer.size = CGSize(width: labelSize.width, height: CGFloat.greatestFiniteMagnitude)
                    
                            // Find the tapped character location and compare it to the specified range
                            let locationOfTouchInLabel = gesture.location(in: self)
                    
                            let xCordLocationOfTouchInTextContainer = locationOfTouchInLabel.x
                            let yCordLocationOfTouchInTextContainer = locationOfTouchInLabel.y
                            let locOfTouch = CGPoint(x: xCordLocationOfTouchInTextContainer ,
                                                     y: yCordLocationOfTouchInTextContainer)
                    
                            let indexOfCharacter = layoutManager.characterIndex(for: locOfTouch, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
                    
                            guard let strLabel = text else {
                                return false
                            }
                    
                            let charCountOfLabel = strLabel.count
                    
                            if indexOfCharacter < (charCountOfLabel - 1) {
                                return NSLocationInRange(indexOfCharacter, targetRange)
                            } else {
                                return false
                            }
                        }
                    

                    你可以调用该方法

                    let text = yourLabel.text
                    let termsRange = (text as NSString).range(of: fullString)
                    if yourLabel.didTapAttributedTextInLabel(gesture: UITapGestureRecognizer, inRange: termsRange) {
                                showCorrespondingViewController()
                            }
                    

                    【讨论】:

                    • 在您使用代码的示例中,UITapGestureRecognizer 来自哪里?它是一个出口吗?您设置的属性?
                    【解决方案29】:

                    Here 是我基于@Luca Davanzo 的answer 的答案,覆盖touchesBegan 事件而不是点击手势:

                    import UIKit
                    
                    public protocol TapableLabelDelegate: NSObjectProtocol {
                       func tapableLabel(_ label: TapableLabel, didTapUrl url: String, atRange range: NSRange)
                    }
                    
                    public class TapableLabel: UILabel {
                    
                    private var links: [String: NSRange] = [:]
                    private(set) var layoutManager = NSLayoutManager()
                    private(set) var textContainer = NSTextContainer(size: CGSize.zero)
                    private(set) var textStorage = NSTextStorage() {
                        didSet {
                            textStorage.addLayoutManager(layoutManager)
                        }
                    }
                    
                    public weak var delegate: TapableLabelDelegate?
                    
                    public override var attributedText: NSAttributedString? {
                        didSet {
                            if let attributedText = attributedText {
                                textStorage = NSTextStorage(attributedString: attributedText)
                            } else {
                                textStorage = NSTextStorage()
                                links = [:]
                            }
                        }
                    }
                    
                    public override var lineBreakMode: NSLineBreakMode {
                        didSet {
                            textContainer.lineBreakMode = lineBreakMode
                        }
                    }
                    
                    public override var numberOfLines: Int {
                        didSet {
                            textContainer.maximumNumberOfLines = numberOfLines
                        }
                    }
                    
                    
                    public override init(frame: CGRect) {
                        super.init(frame: frame)
                        setup()
                    }
                    
                    public required init?(coder aDecoder: NSCoder) {
                        super.init(coder: aDecoder)
                        setup()
                    }
                    
                    public override func layoutSubviews() {
                        super.layoutSubviews()
                        textContainer.size = bounds.size
                    }
                    
                    
                    /// addLinks
                    ///
                    /// - Parameters:
                    ///   - text: text of link
                    ///   - url: link url string
                    public func addLink(_ text: String, withURL url: String) {
                        guard let theText = attributedText?.string as? NSString else {
                            return
                        }
                    
                        let range = theText.range(of: text)
                    
                        guard range.location !=  NSNotFound else {
                            return
                        }
                    
                        links[url] = range
                    }
                    
                    private func setup() {
                        isUserInteractionEnabled = true
                        layoutManager.addTextContainer(textContainer)
                        textContainer.lineFragmentPadding = 0
                        textContainer.lineBreakMode = lineBreakMode
                        textContainer.maximumNumberOfLines  = numberOfLines
                    }
                    
                    public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
                        guard let locationOfTouch = touches.first?.location(in: self) else {
                            return
                        }
                    
                        textContainer.size = bounds.size
                        let indexOfCharacter = layoutManager.glyphIndex(for: locationOfTouch, in: textContainer)
                    
                        for (urlString, range) in links {
                            if NSLocationInRange(indexOfCharacter, range), let url = URL(string: urlString) {
                                delegate?.tapableLabel(self, didTapUrl: urlString, atRange: range)
                            }
                        }
                    }}
                    

                    【讨论】:

                      【解决方案30】:

                      修改 @timbroder 代码以正确处理 swift4.2 的多行

                      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: 0 );
                              // Adjust for multiple lines of text
                              let lineModifier = Int(ceil(locationOfTouchInLabel.y / label.font.lineHeight)) - 1
                              let rightMostFirstLinePoint = CGPoint(x: labelSize.width, y: 0)
                              let charsPerLine = layoutManager.characterIndex(for: rightMostFirstLinePoint, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
                      
                              let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
                              let adjustedRange = indexOfCharacter + (lineModifier * charsPerLine)
                              var newTargetRange = targetRange
                              if lineModifier > 0 {
                                  newTargetRange.location = targetRange.location+(lineModifier*Int(ceil(locationOfTouchInLabel.y)))
                              }
                              return NSLocationInRange(adjustedRange, newTargetRange)
                          }
                      }
                      

                      UILabel 代码

                      let tapAction = UITapGestureRecognizer(target: self, action: #selector(self.tapLabel(gesture:)))
                      
                      let quote = "For full details please see our privacy policy and cookie policy."
                      let attributedString = NSMutableAttributedString(string: quote)
                      
                      let string1: String = "privacy policy", string2: String = "cookie policy"
                      
                      // privacy policy
                      let rangeString1 = quote.range(of: string1)!
                      let indexString1: Int = quote.distance(from: quote.startIndex, to: rangeString1.lowerBound)
                      attributedString.addAttributes(
                                  [.font: <UIfont>,
                                   .foregroundColor: <UI Color>,
                                   .underlineStyle: 0, .underlineColor:UIColor.clear
                              ], range: NSRange(location: indexString1, length: string1.count));
                      
                      // cookie policy
                      let rangeString2 = quote.range(of: string2)!
                      let indexString2: Int = quote.distance(from: quote.startIndex, to: rangeString2.lowerBound )
                      
                      attributedString.addAttributes(
                                  [.font: <UIfont>,
                                   .foregroundColor: <UI Color>,
                                   .underlineStyle: 0, .underlineColor:UIColor.clear
                              ], range: NSRange(location: indexString2, length: string2.count));
                      
                      let label = UILabel()
                      label.frame = CGRect(x: 20, y: 200, width: 375, height: 100)
                      label.isUserInteractionEnabled = true
                      label.addGestureRecognizer(tapAction)
                      label.attributedText = attributedString
                      
                      

                      识别Tap的代码

                       @objc
                        func tapLabel(gesture: UITapGestureRecognizer) {
                           if gesture.didTapAttributedTextInLabel(label: <UILabel>, inRange: termsLabelRange {
                                  print("Terms of service")
                           } else if gesture.didTapAttributedTextInLabel(label:<UILabel> inRange: privacyPolicyLabelRange) {
                                  print("Privacy policy")
                           } else {
                                  print("Tapped none")
                           }
                          }
                      

                      【讨论】:

                        猜你喜欢
                        • 2014-03-04
                        • 2014-03-04
                        • 2016-07-31
                        • 2013-11-20
                        • 1970-01-01
                        • 1970-01-01
                        • 2020-06-28
                        相关资源
                        最近更新 更多