Индекс символов в точке касания для UILabel

Для UILabelЯ хотел бы узнать, какой индекс символа находится в определенной точке, полученной от события касания. Я хотел бы решить эту проблему для iOS 7 с помощью Text Kit.

Поскольку UILabel не предоставляет доступ к своим NSLayoutManagerЯ создал свой собственный на основе UILabelВот такая конфигурация:

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

Код выше находится в UILabel подкласс с UITapGestureRecognizer настроен на вызов textTapped: ( Гист)

Результирующий индекс символа имеет смысл (увеличивается при нажатии слева направо), но не является правильным (последний символ достигается примерно на половине ширины метки). Похоже, что размер шрифта или размер контейнера текста не настроен должным образом, но не может найти проблему.

Я бы очень хотел, чтобы мой класс был подклассом UILabel Вместо того, чтобы использовать UITextView, Кто-нибудь решил эту проблему для UILabel?

Обновление: я потратил билет DTS на этот вопрос, и инженер Apple рекомендовал переопределить UILabel"s drawTextInRect: с реализацией, которая использует мой собственный менеджер макета, похожий на этот фрагмент кода:

- (void)drawTextInRect:(CGRect)rect 
{
    [yourLayoutManager drawGlyphsForGlyphRange:NSMakeRange(0, yourTextStorage.length) atPoint:CGPointMake(0, 0)];
}

Я думаю, что было бы много работы, чтобы синхронизировать мой собственный менеджер компоновки с настройками ярлыка, поэтому я, вероятно, пойду с UITextView несмотря на мое предпочтение UILabel,

Обновление 2: я решил использовать UITextView в конце концов. Целью всего этого было обнаружение нажатий на ссылки, встроенные в текст. Я пытался использовать NSLinkAttributeName, но эта настройка не запускает обратный вызов делегата при быстром нажатии на ссылку. Вместо этого вы должны нажать на ссылку в течение определенного периода времени - очень раздражает. Поэтому я создал CCHLinkTextView, у которого нет этой проблемы.

9 ответов

Я поиграл с решением Алексея Ишкова. Наконец-то я получил решение! Используйте этот фрагмент кода в селекторе 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];

Надеюсь, что это поможет некоторым людям там!

Я получил ту же ошибку, что и вы, индекс увеличился до скорости, поэтому в конце он не был точным. Причиной этой проблемы было то, что self.attributedTextне содержит полную информацию о шрифте для всей строки.

Когда рендеринг UILabel использует шрифт, указанный в self.font и применяет его ко всей приписанной строке. Это не тот случай, когда присваивается атрибут атрибута textStorage. Поэтому вам нужно сделать это самостоятельно:

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

Надеюсь это поможет:)

Swift 4, синтезированный из многих источников, включая хорошие ответы здесь. Мой вклад - правильная обработка вставки, выравнивания и многострочных меток. (большинство реализаций обрабатывают касание в конце пробела как нажатие на последний символ в строке)

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

Вот вы моя реализация для той же проблемы. Мне нужно было отметить #hashtags а также @usernames с реакцией на краны.

Я не отменяю drawTextInRect:(CGRect)rect потому что метод по умолчанию работает идеально.

Также я нашел следующую хорошую реализацию https://github.com/Krelborn/KILabel. Я тоже использовал некоторые идеи из этого примера.

@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

Я использую это в контексте UIViewRepresentable в SwiftUI и пытаюсь добавить на него ссылки. Ни один из кодов, которые я нашел в этих ответах, не был совершенно правильным (особенно для многострочного), и он настолько точен (и настолько чист), насколько я мог его получить:

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

Вау, это было ужасно отлаживать. Все уже предоставленные ответы были близки и могут работать, вплоть до применения пользовательского шрифта. Все развалилось после того, как я применил собственный шрифт.

Линии, которые заставляли меня работать, устанавливали

      layoutManager.usesFontLeading = false

и добавил дополнительную высоту к размеру текстового контейнера

      textContainer.size = CGSize(
    width: labelSize.width,
    height: labelSize.height + 10000
)

Полный код приведен ниже. Да, это очень похоже на все остальные, но все равно это здесь.

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

В процессе отладки я создал несколько полезных элементов для отображения ограничивающих рамок представления и каждого из символов. Они представлены ниже.

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

Я реализовал то же самое в swift 3. Ниже приведен полный код для поиска индекса символов в точке прикосновения для UILabel, он может помочь другим, кто работает над swift и ищет решение:

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

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

}

меня устраивает.

В Swift 5 создайте класс для интерактивной метки и назначьте его любому uiLabel, который вы хотите сделать интерактивным URL-адресом. Он будет работать с многострочным кодом. Он определит, является ли подстрока в метке URL-адресом, и сделает ее кликабельной.

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

Использование : Создайте UILabel в контроллере представления и назначьте его как LinkUILabel.

        @IBOutlet weak var detailLbl: LinkUILabel!
  detailLbl.text = text
Другие вопросы по тегам