Reputation: 331
For a UILabel
, I'd like to find out which character index is at specific point received from a touch event. I'd like to solve this problem for iOS 7 using Text Kit.
Since UILabel doesn't provide access to its NSLayoutManager
, I created my own based on UILabel
's configuration like this:
- (void)textTapped:(UITapGestureRecognizer *)recognizer
{
if (recognizer.state == UIGestureRecognizerStateEnded) {
CGPoint location = [recognizer locationInView:self];
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:self.attributedText];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[textStorage addLayoutManager:layoutManager];
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:self.bounds.size];
[layoutManager addTextContainer:textContainer];
textContainer.maximumNumberOfLines = self.numberOfLines;
textContainer.lineBreakMode = self.lineBreakMode;
NSUInteger characterIndex = [layoutManager characterIndexForPoint:location
inTextContainer:textContainer
fractionOfDistanceBetweenInsertionPoints:NULL];
if (characterIndex < textStorage.length) {
NSRange range = NSMakeRange(characterIndex, 1);
NSString *value = [self.text substringWithRange:range];
NSLog(@"%@, %zd, %zd", value, range.location, range.length);
}
}
}
The code above is in a UILabel
subclass with a UITapGestureRecognizer
configured to call textTapped:
(Gist).
The resulting character index makes sense (increases when tapping from left to right), but is not correct (the last character is reached at roughly half the width of the label). It looks like maybe the font size or text container size is not configured properly, but can't find the problem.
I'd really like to keep my class a subclass of UILabel
instead of using UITextView
. Has anyone solved this problem for UILabel
?
Update: I spent a DTS ticket on this question and the Apple engineer recommended to override UILabel
's drawTextInRect:
with an implementation that uses my own layout manager, similar to this code snippet:
- (void)drawTextInRect:(CGRect)rect
{
[yourLayoutManager drawGlyphsForGlyphRange:NSMakeRange(0, yourTextStorage.length) atPoint:CGPointMake(0, 0)];
}
I think it would be a lot of work to keep my own layout manager in sync with the label's settings, so I'll probably go with UITextView
despite my preference for UILabel
.
Update 2: I decided to use UITextView
after all. The purpose of all this was to detect taps on links embedded in the text. I tried to use NSLinkAttributeName
, but this setup didn't trigger the delegate callback when tapping a link quickly. Instead, you have to press the link for a certain amount of time – very annoying. So I created CCHLinkTextView that doesn't have this problem.
Upvotes: 26
Views: 19434
Reputation: 400
In Swift 5, create a Class for interactive label, and assign it to any uiLabel that you want to make it clickable URLs. It will work on multiline, It will find if substring in label is URL and will make it clickable.
import Foundation
import UIKit
@IBDesignable
class LinkUILabel: UILabel {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
}
override init(frame: CGRect) {
super.init(frame: frame)
}
override var text: String? {
didSet {
guard text != nil else { return }
self.addAttributedString()
}
}
// Find the URLs from a string with multiple urls and add attributes
private func addAttributedString() {
let labelStr = self.text ?? ""
guard labelStr != "" else { return }
let stringArray : [String] = labelStr.split(separator: " ").map { String($0) }
let attributedString = NSMutableAttributedString(string: labelStr)
for urlStr in stringArray where isValidUrl(urlStr: urlStr) {
self.isUserInteractionEnabled = true
self.isEnabled = true
let startIndices = labelStr.indices(of: urlStr).map { $0.utf16Offset(in: labelStr) }
for index in startIndices {
attributedString.addAttribute(.link, value: urlStr, range: NSRange(location: index, length: urlStr.count))
}
}
self.attributedText = attributedString
}
private func isValidUrl(urlStr: String) -> Bool {
if let url = NSURL(string: urlStr) {
return UIApplication.shared.canOpenURL(url as URL)
}
return false
}
// Triggered when the user lifts a finger.
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
// Configure NSTextContainer
let textContainer = NSTextContainer(size: bounds.size)
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = lineBreakMode
textContainer.maximumNumberOfLines = numberOfLines
// Configure NSLayoutManager and add the text container
let layoutManager = NSLayoutManager()
layoutManager.addTextContainer(textContainer)
guard let attributedText = attributedText else { return }
// Configure NSTextStorage and apply the layout manager
let textStorage = NSTextStorage(attributedString: attributedText)
textStorage.addAttribute(NSAttributedString.Key.font, value: font!, range: NSMakeRange(0, attributedText.length))
textStorage.addLayoutManager(layoutManager)
// get the tapped character location
let locationOfTouchInLabel = location
// account for text alignment and insets
let textBoundingBox = layoutManager.usedRect(for: textContainer)
let alignmentOffset: CGFloat = aligmentOffset(for: self)
let xOffset = ((bounds.size.width - textBoundingBox.size.width) * alignmentOffset) - textBoundingBox.origin.x
let yOffset = ((bounds.size.height - textBoundingBox.size.height) * alignmentOffset) - textBoundingBox.origin.y
let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - xOffset, y: locationOfTouchInLabel.y - yOffset)
// work out which character was tapped
let characterIndex = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
let attributeValue = self.attributedText?.attribute(.link, at: characterIndex, effectiveRange: nil)
if let value = attributeValue {
if let url = NSURL(string: value as! String) {
UIApplication.shared.open(url as URL)
return
}
}
}
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
}
}
}
Usage: Create a UILabel in view controller and assign as LinkUILabel
@IBOutlet weak var detailLbl: LinkUILabel!
detailLbl.text = text
Upvotes: 0
Reputation: 91
Wow, this was awful to debug. All the answered already provided were close, and can work, right up until you apply a custom font. Everything fell apart after i applied a custom font.
The lines that made it work for me was setting
layoutManager.usesFontLeading = false
and added extra height to the text container size
textContainer.size = CGSize(
width: labelSize.width,
height: labelSize.height + 10000
)
The full code is provided below. Yes this looks muchlike all the others, but here it is anyways.
// I'm inside a lambda here with weak self, so lets guard my required items.
guard let self, event.state == .ended, let text = self.attributedText else { return nil }
// Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: CGSize.zero)
let textStorage = NSTextStorage(attributedString: text)
// Configure layoutManager and textStorage
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
// Configure textContainer
layoutManager.usesFontLeading = false
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = self.lineBreakMode
textContainer.maximumNumberOfLines = self.numberOfLines
textContainer.size = CGSize(
width: self.bounds.size.width,
height: self.bounds.size.height + 10000
)
return layoutManager.characterIndex(for: event.location(in: self), in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
In the process of Debugging this I created some useful items for displaying the bounding boxes of the view, and each of the characters. Those are provided below.
public struct UILabelLayoutManagerInfo {
let layoutManager: NSLayoutManager
let textContainer: NSTextContainer
let textStorage: NSTextStorage
}
public class DebugUILabel: UILabel {
override public func draw(_ rect: CGRect) {
super.draw(rect)
if let ctx = UIGraphicsGetCurrentContext(), let info = makeLayoutManager() {
ctx.setStrokeColor(UIColor.red.cgColor)
ctx.setLineWidth(1)
for i in 0..<attributedText!.length {
ctx.addRect(info.layoutManager.boundingRect(forGlyphRange: NSRange(location: i, length: 1), in: info.textContainer))
ctx.strokePath()
}
ctx.setStrokeColor(UIColor.blue.cgColor)
ctx.setLineWidth(2)
ctx.addRect(info.layoutManager.usedRect(for: info.textContainer))
ctx.strokePath()
}
}
}
public extension UILabel {
func makeLayoutManager() -> UILabelLayoutManagerInfo? {
guard let text = self.attributedText else { return nil }
// Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: CGSize.zero)
let textStorage = NSTextStorage(attributedString: text)
// Configure layoutManager and textStorage
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
// Configure textContainer
layoutManager.usesFontLeading = false
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = self.lineBreakMode
textContainer.maximumNumberOfLines = self.numberOfLines
textContainer.size = CGSize(
width: self.bounds.size.width,
height: self.bounds.size.height + 10000
)
return UILabelLayoutManagerInfo(
layoutManager: layoutManager,
textContainer: textContainer,
textStorage: textStorage
)
}
}
Upvotes: 3
Reputation: 576
I'm using this in the context of a UIViewRepresentable in SwiftUI, and trying to add links to it. None of the code I found in these answers was quite right (especially for multi-line), and this is as precise (and as clean) as I could get it:
// set up the text engine
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: .zero)
let textStorage = NSTextStorage(attributedString: attrString)
// copy over properties from the label
// assuming left aligned text, might need further adjustments for other alignments
textContainer.lineFragmentPadding = 0
textContainer.lineBreakMode = label.lineBreakMode
textContainer.maximumNumberOfLines = label.numberOfLines
let labelSize = label.bounds.size
textContainer.size = labelSize
// hook up the text engine
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
// adjust for the layout manager's geometry (not sure exactly how this works but it's required)
let locationOfTouchInLabel = tap.location(in: label)
let textBoundingBox = layoutManager.usedRect(for: textContainer)
let textContainerOffset = CGPoint(
x: labelSize.width/2 - textBoundingBox.midX,
y: labelSize.height/2 - textBoundingBox.midY
)
let locationOfTouchInTextContainer = CGPoint(
x: locationOfTouchInLabel.x - textContainerOffset.x,
y: locationOfTouchInLabel.y - textContainerOffset.y
)
// actually perform the check to get the index, accounting for multiple lines
let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
// get the attributes at the index
let attributes = attrString.attributes(at: indexOfCharacter, effectiveRange: nil)
// use `.attachment` instead of `.link` so you can bring your own styling
if let url = attributes[.attachment] as? URL {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
Upvotes: 3
Reputation: 21
Swift 5
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)
return NSLocationInRange(adjustedRange, targetRange)
}
}
it works for me.
Upvotes: 1
Reputation: 1630
i got the same error as you, the index increased way to fast so it wasn't accurate at the end. The cause of this issue was that self.attributedText
did not contain full font information for the whole string.
When UILabel renders it uses the font specified in self.font
and applies it to the whole attributedString. This is not the case when assigning the attributedText to the textStorage. Therefore you need to do this yourself:
NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithAttributedString:self.attributedText];
[attributedText addAttributes:@{NSFontAttributeName: self.font} range:NSMakeRange(0, self.attributedText.string.length];
Swift 4
let attributedText = NSMutableAttributedString(attributedString: self.attributedText!)
attributedText.addAttributes([.font: self.font], range: NSMakeRange(0, attributedText.string.count))
Hope this helps :)
Upvotes: 23
Reputation: 2789
Swift 4, synthesized from many sources including good answers here. My contribution is correct handling of inset, alignment, and multi-line labels. (most implementations treat a tap on trailing whitespace as a tap on the final character in the line)
class TappableLabel: UILabel {
var onCharacterTapped: ((_ label: UILabel, _ characterIndex: Int) -> Void)?
func makeTappable() {
let tapGesture = UITapGestureRecognizer()
tapGesture.addTarget(self, action: #selector(labelTapped))
tapGesture.isEnabled = true
self.addGestureRecognizer(tapGesture)
self.isUserInteractionEnabled = true
}
@objc func labelTapped(gesture: UITapGestureRecognizer) {
// only detect taps in attributed text
guard let attributedText = attributedText, gesture.state == .ended else {
return
}
// Configure NSTextContainer
let textContainer = NSTextContainer(size: bounds.size)
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = lineBreakMode
textContainer.maximumNumberOfLines = numberOfLines
// Configure NSLayoutManager and add the text container
let layoutManager = NSLayoutManager()
layoutManager.addTextContainer(textContainer)
// Configure NSTextStorage and apply the layout manager
let textStorage = NSTextStorage(attributedString: attributedText)
textStorage.addAttribute(NSAttributedStringKey.font, value: font, range: NSMakeRange(0, attributedText.length))
textStorage.addLayoutManager(layoutManager)
// get the tapped character location
let locationOfTouchInLabel = gesture.location(in: gesture.view)
// account for text alignment and insets
let textBoundingBox = layoutManager.usedRect(for: textContainer)
var alignmentOffset: CGFloat!
switch textAlignment {
case .left, .natural, .justified:
alignmentOffset = 0.0
case .center:
alignmentOffset = 0.5
case .right:
alignmentOffset = 1.0
}
let xOffset = ((bounds.size.width - textBoundingBox.size.width) * alignmentOffset) - textBoundingBox.origin.x
let yOffset = ((bounds.size.height - textBoundingBox.size.height) * alignmentOffset) - textBoundingBox.origin.y
let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - xOffset, y: locationOfTouchInLabel.y - yOffset)
// figure out which character was tapped
let characterTapped = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
// figure out how many characters are in the string up to and including the line tapped
let lineTapped = Int(ceil(locationOfTouchInLabel.y / font.lineHeight)) - 1
let rightMostPointInLineTapped = CGPoint(x: bounds.size.width, y: font.lineHeight * CGFloat(lineTapped))
let charsInLineTapped = layoutManager.characterIndex(for: rightMostPointInLineTapped, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
// ignore taps past the end of the current line
if characterTapped < charsInLineTapped {
onCharacterTapped?(self, characterTapped)
}
}
}
Upvotes: 19
Reputation: 289
I have implemented the same on swift 3. Below is the complete code to find Character index at touch point for UILabel, it can help others who are working on swift and looking for the solution :
//here myLabel is the object of UILabel
//added this from @warly's answer
//set font of attributedText
let attributedText = NSMutableAttributedString(attributedString: myLabel!.attributedText!)
attributedText.addAttributes([NSFontAttributeName: myLabel!.font], range: NSMakeRange(0, (myLabel!.attributedText?.string.characters.count)!))
// Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: CGSize(width: (myLabel?.frame.width)!, height: (myLabel?.frame.height)!+100))
let textStorage = NSTextStorage(attributedString: attributedText)
// Configure layoutManager and textStorage
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
// Configure textContainer
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = myLabel!.lineBreakMode
textContainer.maximumNumberOfLines = myLabel!.numberOfLines
let labelSize = myLabel!.bounds.size
textContainer.size = labelSize
// get the index of character where user tapped
let indexOfCharacter = layoutManager.characterIndex(for: tapLocation, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
Upvotes: 1
Reputation: 509
Here you are my implementation for the same problem. I have needed to mark #hashtags
and @usernames
with reaction on the taps.
I do not override drawTextInRect:(CGRect)rect
because default method works perfect.
Also I have found the following nice implementation https://github.com/Krelborn/KILabel. I used some ideas from this sample too.
@protocol EmbeddedLabelDelegate <NSObject>
- (void)embeddedLabelDidGetTap:(EmbeddedLabel *)embeddedLabel;
- (void)embeddedLabel:(EmbeddedLabel *)embeddedLabel didGetTapOnHashText:(NSString *)hashStr;
- (void)embeddedLabel:(EmbeddedLabel *)embeddedLabel didGetTapOnUserText:(NSString *)userNameStr;
@end
@interface EmbeddedLabel : UILabel
@property (nonatomic, weak) id<EmbeddedLabelDelegate> delegate;
- (void)setText:(NSString *)text;
@end
#define kEmbeddedLabelHashtagStyle @"hashtagStyle"
#define kEmbeddedLabelUsernameStyle @"usernameStyle"
typedef enum {
kEmbeddedLabelStateNormal = 0,
kEmbeddedLabelStateHashtag,
kEmbeddedLabelStateUsename
} EmbeddedLabelState;
@interface EmbeddedLabel ()
@property (nonatomic, strong) NSLayoutManager *layoutManager;
@property (nonatomic, strong) NSTextStorage *textStorage;
@property (nonatomic, weak) NSTextContainer *textContainer;
@end
@implementation EmbeddedLabel
- (void)dealloc
{
_delegate = nil;
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
[self setupTextSystem];
}
return self;
}
- (void)awakeFromNib
{
[super awakeFromNib];
[self setupTextSystem];
}
- (void)setupTextSystem
{
self.userInteractionEnabled = YES;
self.numberOfLines = 0;
self.lineBreakMode = NSLineBreakByWordWrapping;
self.layoutManager = [NSLayoutManager new];
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:self.bounds.size];
textContainer.lineFragmentPadding = 0;
textContainer.maximumNumberOfLines = self.numberOfLines;
textContainer.lineBreakMode = self.lineBreakMode;
textContainer.layoutManager = self.layoutManager;
[self.layoutManager addTextContainer:textContainer];
self.textStorage = [NSTextStorage new];
[self.textStorage addLayoutManager:self.layoutManager];
}
- (void)setFrame:(CGRect)frame
{
[super setFrame:frame];
self.textContainer.size = self.bounds.size;
}
- (void)setBounds:(CGRect)bounds
{
[super setBounds:bounds];
self.textContainer.size = self.bounds.size;
}
- (void)layoutSubviews
{
[super layoutSubviews];
self.textContainer.size = self.bounds.size;
}
- (void)setText:(NSString *)text
{
[super setText:nil];
self.attributedText = [self attributedTextWithText:text];
self.textStorage.attributedString = self.attributedText;
[self.gestureRecognizers enumerateObjectsUsingBlock:^(UIGestureRecognizer *recognizer, NSUInteger idx, BOOL *stop) {
if ([recognizer isKindOfClass:[UITapGestureRecognizer class]]) [self removeGestureRecognizer:recognizer];
}];
[self addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(embeddedTextClicked:)]];
}
- (NSMutableAttributedString *)attributedTextWithText:(NSString *)text
{
NSMutableParagraphStyle *style = [NSMutableParagraphStyle new];
style.alignment = self.textAlignment;
style.lineBreakMode = self.lineBreakMode;
NSDictionary *hashStyle = @{ NSFontAttributeName : [UIFont boldSystemFontOfSize:[self.font pointSize]],
NSForegroundColorAttributeName : (self.highlightedTextColor ?: (self.textColor ?: [UIColor darkTextColor])),
NSParagraphStyleAttributeName : style,
kEmbeddedLabelHashtagStyle : @(YES) };
NSDictionary *nameStyle = @{ NSFontAttributeName : [UIFont boldSystemFontOfSize:[self.font pointSize]],
NSForegroundColorAttributeName : (self.highlightedTextColor ?: (self.textColor ?: [UIColor darkTextColor])),
NSParagraphStyleAttributeName : style,
kEmbeddedLabelUsernameStyle : @(YES) };
NSDictionary *normalStyle = @{ NSFontAttributeName : self.font,
NSForegroundColorAttributeName : (self.textColor ?: [UIColor darkTextColor]),
NSParagraphStyleAttributeName : style };
NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:@"" attributes:normalStyle];
NSCharacterSet *charSet = [NSCharacterSet characterSetWithCharactersInString:kWhiteSpaceCharacterSet];
NSMutableString *token = [NSMutableString string];
NSInteger length = text.length;
EmbeddedLabelState state = kEmbeddedLabelStateNormal;
for (NSInteger index = 0; index < length; index++)
{
unichar sign = [text characterAtIndex:index];
if ([charSet characterIsMember:sign] && state)
{
[attributedText appendAttributedString:[[NSAttributedString alloc] initWithString:token attributes:state == kEmbeddedLabelStateHashtag ? hashStyle : nameStyle]];
state = kEmbeddedLabelStateNormal;
[token setString:[NSString stringWithCharacters:&sign length:1]];
}
else if (sign == '#' || sign == '@')
{
[attributedText appendAttributedString:[[NSAttributedString alloc] initWithString:token attributes:normalStyle]];
state = sign == '#' ? kEmbeddedLabelStateHashtag : kEmbeddedLabelStateUsename;
[token setString:[NSString stringWithCharacters:&sign length:1]];
}
else
{
[token appendString:[NSString stringWithCharacters:&sign length:1]];
}
}
[attributedText appendAttributedString:[[NSAttributedString alloc] initWithString:token attributes:state ? (state == kEmbeddedLabelStateHashtag ? hashStyle : nameStyle) : normalStyle]];
return attributedText;
}
- (void)embeddedTextClicked:(UIGestureRecognizer *)recognizer
{
if (recognizer.state == UIGestureRecognizerStateEnded)
{
CGPoint location = [recognizer locationInView:self];
NSUInteger characterIndex = [self.layoutManager characterIndexForPoint:location
inTextContainer:self.textContainer
fractionOfDistanceBetweenInsertionPoints:NULL];
if (characterIndex < self.textStorage.length)
{
NSRange range;
NSDictionary *attributes = [self.textStorage attributesAtIndex:characterIndex effectiveRange:&range];
if ([attributes objectForKey:kEmbeddedLabelHashtagStyle])
{
NSString *value = [self.attributedText.string substringWithRange:range];
[self.delegate embeddedLabel:self didGetTapOnHashText:[value stringByReplacingOccurrencesOfString:@"#" withString:@""]];
}
else if ([attributes objectForKey:kEmbeddedLabelUsernameStyle])
{
NSString *value = [self.attributedText.string substringWithRange:range];
[self.delegate embeddedLabel:self didGetTapOnUserText:[value stringByReplacingOccurrencesOfString:@"@" withString:@""]];
}
else
{
[self.delegate embeddedLabelDidGetTap:self];
}
}
else
{
[self.delegate embeddedLabelDidGetTap:self];
}
}
}
@end
Upvotes: 7
Reputation: 1503
I played around with the solution of Alexey Ishkov. Finally i got a solution! Use this code snippet in your UITapGestureRecognizer selector:
UILabel *textLabel = (UILabel *)recognizer.view;
CGPoint tapLocation = [recognizer locationInView:textLabel];
// init text storage
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:textLabel.attributedText];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[textStorage addLayoutManager:layoutManager];
// init text container
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(textLabel.frame.size.width, textLabel.frame.size.height+100) ];
textContainer.lineFragmentPadding = 0;
textContainer.maximumNumberOfLines = textLabel.numberOfLines;
textContainer.lineBreakMode = textLabel.lineBreakMode;
[layoutManager addTextContainer:textContainer];
NSUInteger characterIndex = [layoutManager characterIndexForPoint:tapLocation
inTextContainer:textContainer
fractionOfDistanceBetweenInsertionPoints:NULL];
Hope this will help some people out there!
Upvotes: 44