许多应用程序都有文本,文本中是圆角矩形的web超链接,当我点击它们时,UIWebView就会打开。让我困惑的是,他们经常有自定义链接,例如,如果单词以#开头,它也是可点击的,应用程序通过打开另一个视图来响应。我该怎么做呢?是否可以用UILabel或者我需要UITextView或者其他什么?
当前回答
一般来说,如果我们想在UILabel显示的文本中有一个可点击的链接,我们需要解决两个独立的任务:
更改部分文本的外观,使其看起来像链接 检测和处理链接上的触摸(打开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显示带有链接的String
现在我们应该检测这个链接上的触摸。其思想是捕获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/"]];
}
}
其他回答
这是一个Objective-C类别,它支持现有UILabel中的可点击链接。attributedText字符串,利用现有的NSLinkAttributeName属性。
@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!
我已经试着让它尽可能的最小化侵入性通过使用现有的NSLinkAttributeName属性在NSAttributedString中已经可用。所以很简单:
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! :-)
基本上,它通过添加一个UIGestureRecognizer到你的UILabel来工作。最难的工作是在gestureRecognizerShouldBegin:中完成的,它会重新布局attributedText字符串,以找出被点击的字符。如果这个字符是NSLinkAttributeName的一部分,那么手势识别器将随后触发,检索相应的URL(从NSLinkAttributeName值),并打开每个通常的[UIApplication。sharedApplication openURL:url进程。
注意:通过在gestureRecognizerShouldBegin:中执行所有这些操作,如果你没有碰巧点击标签中的链接,事件就会被传递。因此,例如,你的UITableViewCell将捕获点击链接,但其他行为正常(选择单元格,取消选择,滚动,…)。
我把它放在了GitHub仓库里。 改编自Kai Burghardt的SO帖子。
标签# 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)
}
}
}
}
}
这里是一个Swift实现,它是尽可能少的,还包括触摸反馈。警告:
你必须在NSAttributedStrings中设置字体 你只能使用NSAttributedStrings! 你必须确保你的链接不能自动换行(使用不间断的空格:"\u{a0}") 设置文本后,不能更改lineBreakMode或numberolines 通过添加带有.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
}
}
}
}
UITextView支持OS3.0中的数据检测器,而UILabel不支持。
如果你在UITextView上启用了数据检测器,并且你的文本包含url、电话号码等,它们将以链接的形式出现。
我找到了另一个解决方案:
我找到了一种方法来检测你从互联网上找到的html文本链接,你把它转换成nsattributeString:
func htmlAttributedString(fontSize: CGFloat = 17.0) -> NSAttributedString? {
let fontName = UIFont.systemFont(ofSize: fontSize).fontName
let string = self.appending(String(format: "<style>body{font-family: '%@'; font-size:%fpx;}</style>", fontName, fontSize))
guard let data = string.data(using: String.Encoding.utf16, allowLossyConversion: false) else { return nil }
guard let html = try? NSMutableAttributedString (
data: data,
options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html],
documentAttributes: nil) else { return nil }
return html
}
我的方法允许您检测超链接,而无需指定它们。
first you create an extension of the tapgesturerecognizer : extension UITapGestureRecognizer { func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool { guard let attrString = label.attributedText else { return false } let layoutManager = NSLayoutManager() let textContainer = NSTextContainer(size: .zero) let textStorage = NSTextStorage(attributedString: attrString) layoutManager.addTextContainer(textContainer) textStorage.addLayoutManager(layoutManager) textContainer.lineFragmentPadding = 0 textContainer.lineBreakMode = label.lineBreakMode textContainer.maximumNumberOfLines = label.numberOfLines let labelSize = label.bounds.size textContainer.size = labelSize let locationOfTouchInLabel = self.location(in: label) let textBoundingBox = layoutManager.usedRect(for: textContainer) let textContainerOffset = CGPoint(x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x, y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y) let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x, y: locationOfTouchInLabel.y - textContainerOffset.y) let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil) return NSLocationInRange(indexOfCharacter, targetRange) } }
然后在你的视图控制器中,你创建了一个url和范围列表来存储属性文本包含的所有链接和范围:
var listurl : [String] = []
var listURLRange : [NSRange] = []
找到URL和URLRange,你可以使用:
fun findLinksAndRange(attributeString : NSAttributeString){
notification.enumerateAttribute(NSAttributedStringKey.link , in: NSMakeRange(0, notification.length), options: [.longestEffectiveRangeNotRequired]) { value, range, isStop in
if let value = value {
print("\(value) found at \(range.location)")
let stringValue = "\(value)"
listurl.append(stringValue)
listURLRange.append(range)
}
}
westlandNotifcationLabel.addGestureRecognizer(UITapGestureRecognizer(target : self, action: #selector(handleTapOnLabel(_:))))
}
然后你实现了手柄tap:
@objc func handleTapOnLabel(_ recognizer: UITapGestureRecognizer) {
for index in 0..<listURLRange.count{
if recognizer.didTapAttributedTextInLabel(label: westlandNotifcationLabel, inRange: listURLRange[index]) {
goToWebsite(url : listurl[index])
}
}
}
func goToWebsite(url : String){
if let websiteUrl = URL(string: url){
if #available(iOS 10, *) {
UIApplication.shared.open(websiteUrl, options: [:],
completionHandler: {
(success) in
print("Open \(websiteUrl): \(success)")
})
} else {
let success = UIApplication.shared.openURL(websiteUrl)
print("Open \(websiteUrl): \(success)")
}
}
}
开始了!
我希望这个解决方案能帮助你,就像它帮助我一样。
推荐文章
- 如何删除默认的导航栏空间在SwiftUI导航视图
- 如何在iOS中使用Swift编程segue
- Swift -整数转换为小时/分钟/秒
- Swift:声明一个空字典
- 我如何用javascript编程点击链接?
- 在成功提交我的应用程序后,“太多符号文件”
- 首先添加一个UIView,甚至是导航栏
- 我如何改变UIButton标题颜色?
- 在Swift中如何调用GCD主线程上的参数方法?
- NSLayoutConstraints是可动画的吗?
- iOS -构建失败,CocoaPods无法找到头文件
- CFNetwork SSLHandshake iOS 9失败
- 请求失败:不可接受的内容类型:文本/html使用AFNetworking 2.0
- 缺少推荐的图标文件-该包不包含iPhone / iPod Touch的应用程序图标,像素为“120x120”,png格式
- 以编程方式创建segue