许多应用程序都有文本,文本中是圆角矩形的web超链接,当我点击它们时,UIWebView就会打开。让我困惑的是,他们经常有自定义链接,例如,如果单词以#开头,它也是可点击的,应用程序通过打开另一个视图来响应。我该怎么做呢?是否可以用UILabel或者我需要UITextView或者其他什么?
当前回答
有些答案并不如我所料。这是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支持OS3.0中的数据检测器,而UILabel不支持。
如果你在UITextView上启用了数据检测器,并且你的文本包含url、电话号码等,它们将以链接的形式出现。
我们使用来自zekel的UITapGestureRecognizer类别的方便解决方案。 它使用了NSTextContainer,就像这个问题的许多答案一样。
但是,这将返回错误的字符索引。显然是因为NSTextContainer缺少关于字体样式的信息,正如这些其他帖子所指出的:
https://stackoverflow.com/a/34238382/2439941 https://stackoverflow.com/a/47358270/2439941
后改变:
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:label.attributedText];
To:
// Apply the font of the label to the attributed text:
NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithAttributedString:label.attributedText];
NSMutableParagraphStyle *paragraphStyle = NSMutableParagraphStyle.new;
paragraphStyle.alignment = self.label.textAlignment;
[attributedText addAttributes:@{NSFontAttributeName: label.font, NSParagraphStyleAttributeName: paragraphStyle}
range:NSMakeRange(0, label.attributedText.string.length)];
// Init with attributed text from label:
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedText];
结果明显更好,点击区域现在正确地从目标字符串的第一个字符开始。但最后一个字符仍然返回NO。我们期望这与我们的目标字符串具有将字体权重设置为UIFontWeightSemibold的属性有关。而上面的代码改进应用标签。整个字符串上的字体,它有一个规则的权重。
为了解决这个问题,我们进一步改进了上面的代码片段,通过遍历所有属性范围,以支持文本中的多种字体样式:
// According to https://stackoverflow.com/a/47358270/2439941 it's required to apply the paragraph style and font of the UILabel.
// However, the attributed string might contain font formatting as well, e.g. to emphasize a word in a different font style.
// Therefor copy all attributes:
NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithAttributedString:label.attributedText];
[label.attributedText enumerateAttributesInRange:NSMakeRange(0, label.attributedText.length)
options:0
usingBlock:^(NSDictionary<NSAttributedStringKey,id> * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) {
// Add each attribute:
[attributedText addAttributes:attrs
range:range];
// In case the attributes of this range do NOT contain a font specifier, apply the font from the UILabel:
if (![attrs objectForKey:NSFontAttributeName]) {
[attributedText addAttributes:@{ NSFontAttributeName : label.font }
range:range];
}
}];
// Init the storage with the font attributed text:
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedText];
现在,该方法为半粗体字符串范围内的每个字符返回YES,这是预期的结果。
我正在扩展@NAlexN原始的详细解决方案,与@zekel优秀的UITapGestureRecognizer扩展,并在Swift中提供。
Extending UITapGestureRecognizer
extension UITapGestureRecognizer {
func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool {
// Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: CGSize.zero)
let textStorage = NSTextStorage(attributedString: label.attributedText!)
// Configure layoutManager and textStorage
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
// Configure textContainer
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = label.lineBreakMode
textContainer.maximumNumberOfLines = label.numberOfLines
let labelSize = label.bounds.size
textContainer.size = labelSize
// Find the tapped character location and compare it to the specified range
let locationOfTouchInLabel = self.location(in: label)
let textBoundingBox = layoutManager.usedRect(for: textContainer)
let textContainerOffset = CGPoint(
x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y
)
let locationOfTouchInTextContainer = CGPoint(
x: locationOfTouchInLabel.x - textContainerOffset.x,
y: locationOfTouchInLabel.y - textContainerOffset.y
)
let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
return NSLocationInRange(indexOfCharacter, targetRange)
}
}
使用
设置UIGestureRecognizer发送动作到tapLabel:,你可以检测目标范围是否在myLabel上被点击。
@IBAction func tapLabel(gesture: UITapGestureRecognizer) {
if gesture.didTapAttributedTextInLabel(myLabel, inRange: targetRange1) {
print("Tapped targetRange1")
} else if gesture.didTapAttributedTextInLabel(myLabel, inRange: targetRange2) {
print("Tapped targetRange2")
} else {
print("Tapped none")
}
}
重要提示:UILabel换行模式必须设置为按word/char换行。以某种方式,只有当换行模式为其他模式时,NSTextContainer才会假定文本为单行。
这是沙玛林。基于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正确地调整其高度大小。
标签# 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)
}
}
}
}
}
推荐文章
- 如何删除默认的导航栏空间在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