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


当前回答

标签# Swift2.0

我从- excellent - @NAlexN的回答中获得灵感,我决定自己写一个UILabel的包装器。 我还尝试了TTTAttributedLabel,但我不能使它工作。

希望你能欣赏这段代码,欢迎任何建议!

import Foundation

@objc protocol TappableLabelDelegate {
    optional func tappableLabel(tabbableLabel: TappableLabel, didTapUrl: NSURL, atRange: NSRange)
}

/// Represent a label with attributed text inside.
/// We can add a correspondence between a range of the attributed string an a link (URL)
/// By default, link will be open on the external browser @see 'openLinkOnExternalBrowser'

class TappableLabel: UILabel {

    // MARK: - Public properties -

    var links: NSMutableDictionary = [:]
    var openLinkOnExternalBrowser = true
    var delegate: TappableLabelDelegate?

    // MARK: - Constructors -

    override func awakeFromNib() {
        super.awakeFromNib()
        self.enableInteraction()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.enableInteraction()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    private func enableInteraction() {
        self.userInteractionEnabled = true
        self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: Selector("didTapOnLabel:")))
    }

    // MARK: - Public methods -

    /**
    Add correspondence between a range and a link.

    - parameter url:   url.
    - parameter range: range on which couple url.
    */
    func addLink(url url: String, atRange range: NSRange) {
        self.links[url] = range
    }

    // MARK: - Public properties -

    /**
    Action rised on user interaction on label.

    - parameter tapGesture: gesture.
    */
    func didTapOnLabel(tapGesture: UITapGestureRecognizer) {
        let labelSize = self.bounds.size;

        let layoutManager = NSLayoutManager()
        let textContainer = NSTextContainer(size: CGSizeZero)
        let textStorage = NSTextStorage(attributedString: self.attributedText!)

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

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

        // find the tapped character location and compare it to the specified range
        let locationOfTouchInLabel = tapGesture.locationInView(self)

        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,
            locationOfTouchInLabel.y - textContainerOffset.y)
        let indexOfCharacter = layoutManager.characterIndexForPoint(locationOfTouchInTextContainer,
            inTextContainer:textContainer,
            fractionOfDistanceBetweenInsertionPoints: nil)

        for (url, value) in self.links {
            if let range = value as? NSRange {
                if NSLocationInRange(indexOfCharacter, range) {
                    let url = NSURL(string: url as! String)!
                    if self.openLinkOnExternalBrowser {
                        UIApplication.sharedApplication().openURL(url)
                    }
                    self.delegate?.tappableLabel?(self, didTapUrl: url, atRange: range)
                }
            }
        }
    }

}

其他回答

斯威夫特5.2

我在之前的回答中发现了多行文本标签的几个问题,所以我给出了我最终的工作解决方案。

它解决了多行和文本对齐的问题。

    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 textAligmentOffset = aligmentOffset(for: label)
        let textContainerOffset = CGPoint(x: (labelSize.width - textBoundingBox.size.width) * textAligmentOffset - textBoundingBox.origin.x,
                                          y: (labelSize.height - textBoundingBox.size.height) * textAligmentOffset - textBoundingBox.origin.y)
        let locationOfTouchInTextContainer = CGPoint(x: (locationOfTouchInLabel.x - textContainerOffset.x),
                                                     y: 0 )
        // Adjust for multiple lines of text
        let lineModifier = Int(floor(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)
        return NSLocationInRange(adjustedRange, targetRange)
    }
    
    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
        }
    }

我们使用来自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,这是预期的结果。

我正在扩展@NAlexN原始的详细解决方案,与@zekel优秀的UITapGestureRecognizer扩展,并在Swift中提供。

Extending 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换行模式必须设置为按word/char换行。以某种方式,只有当换行模式为其他模式时,NSTextContainer才会假定文本为单行。

用下面的.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

基于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;
    }
}