objective c - que - Índice de caracteres en el punto de contacto para UILabel
lista de rasgos de caracter (5)
Aquí tienes mi implementación para el mismo problema. He necesitado marcar #hashtags
y @usernames
con reacción en los @usernames
.
No drawTextInRect:(CGRect)rect
porque el método predeterminado funciona a la perfección.
También he encontrado la siguiente implementación agradable https://github.com/Krelborn/KILabel . Usé algunas ideas de esta muestra también.
@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
Para una UILabel
, me gustaría averiguar qué índice de caracteres se encuentra en un punto específico recibido de un evento táctil. Me gustaría resolver este problema para iOS 7 utilizando el Kit de texto.
Dado que UILabel no proporciona acceso a su NSLayoutManager
, creé mi propio basado en la configuración de UILabel
esta manera:
- (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);
}
}
}
El código anterior está en una subclase de UILabel
con un UITapGestureRecognizer
configurado para llamar a textTapped:
( Gist ).
El índice de caracteres resultante tiene sentido (aumenta cuando se toca de izquierda a derecha), pero no es correcto (el último carácter se alcanza aproximadamente a la mitad del ancho de la etiqueta). Parece que tal vez el tamaño de fuente o el tamaño del contenedor de texto no está configurado correctamente, pero no puede encontrar el problema.
Realmente me gustaría mantener mi clase como una subclase de UILabel
lugar de utilizar UITextView
. ¿Alguien ha resuelto este problema para UILabel
?
Actualización: gasté un ticket de DTS en esta pregunta y el ingeniero de Apple recomendó anular el UILabel
de drawTextInRect:
con una implementación que usa mi propio administrador de diseño, similar a este fragmento de código:
- (void)drawTextInRect:(CGRect)rect
{
[yourLayoutManager drawGlyphsForGlyphRange:NSMakeRange(0, yourTextStorage.length) atPoint:CGPointMake(0, 0)];
}
Creo que sería mucho trabajo mantener mi propio administrador de diseño sincronizado con la configuración de la etiqueta, así que probablemente iré con UITextView
pesar de mi preferencia por UILabel
.
Actualización 2: Decidí usar UITextView
después de todo. El propósito de todo esto fue detectar los toques en los enlaces incrustados en el texto. Intenté usar NSLinkAttributeName
, pero esta configuración no NSLinkAttributeName
la devolución de llamada de delegado al tocar un enlace rápidamente. En su lugar, debe presionar el enlace durante un período de tiempo determinado, muy molesto. Así que creé CCHLinkTextView que no tiene este problema.
He implementado lo mismo en swift 3. A continuación, se encuentra el código completo para encontrar el Índice de caracteres en el punto de contacto de UILabel, puede ayudar a otros que están trabajando en swift y están buscando la solución:
//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)
Jugué con la solución de Alexey Ishkov. ¡Finalmente tengo una solución! Use este fragmento de código en su selector UITapGestureRecognizer:
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];
Espero que esto ayude a algunas personas por ahí!
Recibí el mismo error que usted, el índice aumentó de manera rápida, por lo que no fue preciso al final. La causa de este problema fue que self.attributedText
no contenía información de fuente completa para toda la cadena.
Cuando UILabel renderiza, utiliza la fuente especificada en self.font
y la aplica a la zona de todos los atributos. Este no es el caso cuando se asigna el TextText al TextStorage. Por lo tanto necesitas hacer esto tú mismo:
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))
Espero que esto ayude :)
Swift 4, sintetizado a partir de muchas fuentes, incluyendo buenas respuestas aquí. Mi contribución es el manejo correcto de inserciones, alineación y etiquetas multilínea. (la mayoría de las implementaciones tratan un toque en espacios en blanco al final como un toque en el carácter final de la línea)
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)
}
}
}