创建可点击的“链接”;在一个UILabel的NSAttributedString中?

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

284243 次浏览

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

如果你在UITextView上启用了数据检测器,并且你的文本包含url、电话号码等,它们将以链接的形式出现。

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

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

    NSString *string = name;
NSError *error = NULL;
NSDataDetector *detector =
[NSDataDetector dataDetectorWithTypes:(NSTextCheckingTypes)NSTextCheckingTypeLink | NSTextCheckingTypePhoneNumber
error:&error];
NSArray *matches = [detector matchesInString:string
options:0
range:NSMakeRange(0, [string length])];
for (NSTextCheckingResult *match in matches)
{
if (([match resultType] == NSTextCheckingTypePhoneNumber))
{
NSString *phoneNumber = [match phoneNumber];
NSLog(@" Phone Number is :%@",phoneNumber);
label.enabledTextCheckingTypes = NSTextCheckingTypePhoneNumber;
}


if(([match resultType] == NSTextCheckingTypeLink))
{
NSURL *email = [match URL];
NSLog(@"Email is  :%@",email);
label.enabledTextCheckingTypes = NSTextCheckingTypeLink;
}


if (([match resultType] == NSTextCheckingTypeLink))
{
NSURL *url = [match URL];
NSLog(@"URL is  :%@",url);
label.enabledTextCheckingTypes = NSTextCheckingTypeLink;
}
}


label.text =name;
}
下面是超链接UILabel的示例代码: 来源:http://sickprogrammersarea.blogspot.in/2014/03/adding-links-to-uilabel.html < / p >
#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];
}

一般来说,如果我们想在UILabel显示的文本中有一个可点击的链接,我们需要解决两个独立的任务:

  1. 更改部分文本的外观,使其看起来像链接
  2. 检测和处理链接上的触摸(打开URL是一个特殊情况)

第一个很简单。从ios6开始,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显示包含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/"]];
}
}

(我的回答建立在@NAlexN的优秀的答案上。我不会在这里重复他对每一步的详细解释。)

我发现这是最方便和直接的添加支持可点击的UILabel文本作为类别UITapGestureRecognizer。(你不来使用UITextView的数据检测器,正如一些答案所建议的那样。)

添加以下方法到你的UITapGestureRecognizer类别:

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


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


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


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


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

示例代码

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


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


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

手势回调

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


}

我创建了名为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}];

我很难处理这件事…带有链接的UILabel在带属性的文本上…这只是一个头痛,所以我最终使用ZSWTappableLabel

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

正如我在这篇文章中提到的, 下面是我专门为UILabel FRHyperLabel中的链接创建的一个轻量级库

为了达到这样的效果:

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 portal 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];
我强烈建议使用自动检测文本中的url并将其转换为链接的库。 试一试:< / p >

两者都得到了麻省理工学院的许可。

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

然而,如果你需要自定义检测,也就是说,如果你想在用户点击某个特定单词后调用任何自定义方法,你需要使用带有NSLinkAttributeName属性的NSAttributedStrings,该属性将指向一个自定义URL方案(而不是默认使用http URL方案)。雷·温德里奇在这里报道

引用上述链接中的代码:

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


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


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

要检测这些链接点击,实现这个:

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

注:确保你的UITextViewselectable

标签 # Swift2.0

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

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

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


}

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

我扩展了@NAlexN原来的详细解决方案,与@zekel优秀的扩展UITapGestureRecognizer,并在斯威夫特提供。

延长UITapGestureRecognizer

extension UITapGestureRecognizer {


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


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


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


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


return NSLocationInRange(indexOfCharacter, targetRange)
}


}

使用

设置UIGestureRecognizer将动作发送到tapLabel:,你可以检测目标范围是否在myLabel中被点击。

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

重要提示:UILabel换行模式必须设置为按word/char换行。以某种方式,NSTextContainer将假设文本是单行,只有当换行模式为其他模式时。

以下是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之外的其他事情,你可以做任何自定义操作。

作为UILabel上的一个类别的dropin解决方案(这假设你的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

下面是一个插入的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的混乱)来完成,但如果你像我一样,你更喜欢避免不必要的(第三方)子类,只是为了给现有的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仓库在这里。 改编自Kai Burghardt的SO帖子在这里.

我扩展了@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)
}


}

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

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


}

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

这里是一个Swift实现,它是尽可能少的,还包括触摸反馈。警告:

  1. 你必须在NSAttributedStrings中设置字体
  2. 你只能使用NSAttributedStrings!
  3. 你必须确保你的链接不能换行(使用不间断的空格:"\u{a0}")
  4. 设置文本后,不能更改lineBreakMode或numberolines
  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
}
}
}
}

这个通用方法也可以!

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

我遵循这个版本,

斯威夫特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
}

在这里是我基于@Luca Davanzo的回答的答案,覆盖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)
}
}
}}

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

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

    } < / p >

然后在你的视图控制器中,你创建了一个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)")
}
}
}

开始了!

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

- (BOOL)didTapAttributedTextInLabel:(UILabel *)label inRange:(NSRange)targetRange{
NSLayoutManager *layoutManager = [NSLayoutManager new];
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeZero];
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:label.attributedText];


[layoutManager addTextContainer:textContainer];
[textStorage addLayoutManager:layoutManager];


textContainer.lineFragmentPadding = 0.0;
textContainer.lineBreakMode = label.lineBreakMode;
textContainer.maximumNumberOfLines = label.numberOfLines;
CGSize labelSize = label.bounds.size;
textContainer.size = labelSize;


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);
NSUInteger indexOfCharacter =[layoutManager characterIndexForPoint:locationOfTouchInTextContainer inTextContainer:textContainer fractionOfDistanceBetweenInsertionPoints:nil];


return NSLocationInRange(indexOfCharacter, targetRange);
}

修改了@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


编码来识别水龙头

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

这是沙玛林。基于Kedar的回答的iOS c#实现。

MyClickableTextViewWithCustomUrlScheme实现与ShouldInteractWithUrl覆盖:

// Inspired from https://stackoverflow.com/a/44112932/15186
internal class MyClickableTextViewWithCustomUrlScheme : UITextView, IUITextViewDelegate
{
public MyClickableTextViewWithCustomUrlScheme()
{
Initialize();
}


public MyClickableTextViewWithCustomUrlScheme(Foundation.NSCoder coder) : base(coder)
{
Initialize();
}


public MyClickableTextViewWithCustomUrlScheme(Foundation.NSObjectFlag t) : base(t)
{
Initialize();
}


public MyClickableTextViewWithCustomUrlScheme(IntPtr handle) : base(handle)
{
Initialize();
}


public MyClickableTextViewWithCustomUrlScheme(CoreGraphics.CGRect frame) : base(frame)
{
Initialize();
}


public MyClickableTextViewWithCustomUrlScheme(CoreGraphics.CGRect frame, NSTextContainer textContainer) : base(frame, textContainer)
{
Initialize();
}


void Initialize()
{
Delegate = this;
}


[Export("textView:shouldInteractWithURL:inRange:")]
public new bool ShouldInteractWithUrl(UITextView textView, NSUrl URL, NSRange characterRange)
{
if (URL.Scheme.CompareTo(@"username") == 0)
{
// Launch the Activity
return false;
}
// The system will handle the URL
return base.ShouldInteractWithUrl(textView, URL, characterRange);
}
}

在c#中转换的objective-C代码变成:

MyClickableTextViewWithCustomUrlScheme uiHabitTile = new MyClickableTextViewWithCustomUrlScheme();
uiHabitTile.Selectable = true;
uiHabitTile.ScrollEnabled = false;
uiHabitTile.Editable = false;


// https://stackoverflow.com/a/34014655/15186
string wholeTitle = @"This is an example by marcelofabri";


NSMutableAttributedString attributedString = new NSMutableAttributedString(wholeTitle);
attributedString.AddAttribute(UIStringAttributeKey.Link,
new NSString("username://marcelofabri"),
attributedString.Value.RangeOfString(@"marcelofabri")
);
NSMutableDictionary<NSString, NSObject> linkAttributes = new NSMutableDictionary<NSString, NSObject>();
linkAttributes[UIStringAttributeKey.ForegroundColor] = UIColor.Green;
linkAttributes[UIStringAttributeKey.UnderlineColor] = UIColor.LightGray;
linkAttributes[UIStringAttributeKey.UnderlineStyle] = new NSNumber((short)NSUnderlineStyle.PatternSolid);


uiHabitTile.AttributedText = attributedString;

确保将Editable = false和Selectable = true设置为能够单击链接。

同样,ScrollEnabled = true允许textview正确地调整其高度大小。

是的,这是可能的,尽管一开始很困惑。我将进一步向您展示如何甚至可以单击文本中的任何区域。

使用这个方法,你可以有一个UI标签,如下所示:

  • 多行友好
  • Autoshrink友好
  • 可点击友好型(是的,甚至是单个角色)
  • 斯威夫特5

步骤1:

使UILabel具有'截短的尾巴'的换行属性,并设置最小字体比例

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

minimumFontSize/defaultFontSize = fontscale

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

enter image description here

步骤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.
}
}

它是这样使用的(只需要用你实际的标签名替换<Label>):

<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或你认为合适的标签中(只需再次将<Label>替换为你的标签名:

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

下面是我的tapLabel函数的一个简化示例(只需将<Label>替换为您的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”以添加换行符和任何您想要的文本,并测试此以在下一行中找到字符串,它仍然会找到它并正确地检测点击!

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

有些答案并不如我所料。这是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,推荐使用基达Paranjape。基于卡尔·诺沃西的回答,我最终提出了一个简单的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自动打开。

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

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

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

  • https://stackoverflow.com/a/34238382/2439941 < a href = " https://stackoverflow.com/a/34238382/2439941 " > < / >
  • https://stackoverflow.com/a/47358270/2439941 < a href = " https://stackoverflow.com/a/47358270/2439941 " > < / >

后改变:

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

:

// 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的属性有关。上面的代码改进在整个字符串上应用了label.font,它有一个规则的权重。

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

// 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,这是预期的结果。

斯威夫特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
}
}

从iOS 15开始,SwiftUI内置支持Markdown标记语言,因此Markdown中的文本:

Text("You can [click this link](https://www.example.com) to visit the website.")

enter image description here

或者作为一个使用SwiftUI的最简单的例子,你可以这样做:

        HStack() {
Text("Open the")
.foregroundColor(.black)
Link(destination: URL(string: "https://www.example.com/TOS.html")!) {
Text("link")
.foregroundColor(.blue)
.underline()
}
Text("in browser")
.foregroundColor(.black)
}

enter image description here