许多应用程序都有文本,文本中是圆角矩形的web超链接,当我点击它们时,UIWebView就会打开。让我困惑的是,他们经常有自定义链接,例如,如果单词以#开头,它也是可点击的,应用程序通过打开另一个视图来响应。我该怎么做呢?是否可以用UILabel或者我需要UITextView或者其他什么?


当前回答

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

我找到了一种方法来检测你从互联网上找到的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
        }

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

first you create an extension of the 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(_:))))

    }

然后你实现了手柄tap:

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

开始了!

我希望这个解决方案能帮助你,就像它帮助我一样。

其他回答

对于完全自定义的链接,你需要使用UIWebView -你可以拦截调用,这样当链接被按下时,你可以转到应用程序的其他部分。

我创建了一个名为ResponsiveLabel的UILabel子类,它是基于ios7中引入的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}];

我们使用来自zekel的UITapGestureRecognizer类别的方便解决方案。 它使用了NSTextContainer,就像这个问题的许多答案一样。

但是,这将返回错误的字符索引。显然是因为NSTextContainer缺少关于字体样式的信息,正如这些其他帖子所指出的:

https://stackoverflow.com/a/34238382/2439941 https://stackoverflow.com/a/47358270/2439941

后改变:

NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:label.attributedText];

To:

// Apply the font of the label to the attributed text: 
NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithAttributedString:label.attributedText];
NSMutableParagraphStyle *paragraphStyle = NSMutableParagraphStyle.new;
paragraphStyle.alignment = self.label.textAlignment;
[attributedText addAttributes:@{NSFontAttributeName: label.font, NSParagraphStyleAttributeName: paragraphStyle} 
                        range:NSMakeRange(0, label.attributedText.string.length)];

// Init with attributed text from label:
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedText];

结果明显更好,点击区域现在正确地从目标字符串的第一个字符开始。但最后一个字符仍然返回NO。我们期望这与我们的目标字符串具有将字体权重设置为UIFontWeightSemibold的属性有关。而上面的代码改进应用标签。整个字符串上的字体,它有一个规则的权重。

为了解决这个问题,我们进一步改进了上面的代码片段,通过遍历所有属性范围,以支持文本中的多种字体样式:

// According to https://stackoverflow.com/a/47358270/2439941 it's required to apply the paragraph style and font of the UILabel.
// However, the attributed string might contain font formatting as well, e.g. to emphasize a word in a different font style.
// Therefor copy all attributes:
NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithAttributedString:label.attributedText];
[label.attributedText enumerateAttributesInRange:NSMakeRange(0, label.attributedText.length)
                                         options:0
                                      usingBlock:^(NSDictionary<NSAttributedStringKey,id> * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) {
    // Add each attribute:
    [attributedText addAttributes:attrs
                            range:range];
    
    // In case the attributes of this range do NOT contain a font specifier, apply the font from the UILabel:
    if (![attrs objectForKey:NSFontAttributeName]) {
        [attributedText addAttributes:@{ NSFontAttributeName : label.font }
                                range:range];
    }
}];

// Init the storage with the font attributed text:
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedText];

现在,该方法为半粗体字符串范围内的每个字符返回YES,这是预期的结果。

有些答案并不如我所料。这是Swift解决方案,也支持textAlignment和多行。不需要子类化,只是这个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")
        }
    }
}

就像在前面的回答中报告的那样,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 })
    }