Как получить позицию последнего символа в UILabel?

Мне нужно сделать что-то типа такого:

Пример двух UILabel Где "Left long text" и "Right text" - это 2 отдельных UILabel и между ними прерывистая линия

Для этого мне нужно узнать как получить позицию последнего символа, но это моё предположение)

Пожалуйста, подскажите, как получить позицию последнего символа в UILabel или может быть есть другой вариант как это реализовать?


Ответы (1 шт):

Автор решения: schmidt9

Ниже представлен метод получения границ для всех строк лейблов и использование границ последней строки из левого и правого лейбла. Границы строк определяются путем перебора координат всех символов в строках - если координаты двух символов различаются по оси Y, значит они на разных строках. Затем мы концертируем эти границы в координаты экрана и рисуем пунктир

Ограничения: лейблы должны иметь одинаковый размер шрифта и одинаковое форматирование текста, в тестовом проекте они выровнены с друг другом по нижней границе

Границы строк визуально, черные линии это полная высота строк, красные линии это границы по базовым линиям строк (которые как раз нужны для рисования пунктира)

введите сюда описание изображения

Конечный результат

введите сюда описание изображения

Тестовый проект: https://github.com/schmidt9/TwoLabelsWithLineBetween

//
//  GlyphPositionLabel.swift
//  TwoLabelsWithLineBetween
//

import UIKit

struct TextLineGeometry {
    var fullRect: CGRect // used for debug here
    var baselineRect: CGRect
}

class GlyphPositionLabel: UILabel {
    
    var debug = false
    
    /// See https://stackoverflow.com/a/77427472/3004003
    var linesGeometry: [TextLineGeometry] {
        guard let text, let attributedText, let font, !text.isEmpty else {
            return []
        }
        
        let textContainer = NSTextContainer(size: bounds.size)
        textContainer.lineFragmentPadding  = 0;
        textContainer.maximumNumberOfLines = self.numberOfLines;
        textContainer.lineBreakMode = self.lineBreakMode;

        let textStorage = NSTextStorage(attributedString: attributedText)
        let layoutManager = NSLayoutManager()
        textStorage.addLayoutManager(layoutManager)
        layoutManager.addTextContainer(textContainer)
        
        let baseline = font.ascender
        
        var geometries = [TextLineGeometry]()
        var index = text.startIndex
        var currentRect: CGRect?
        
        while index != text.endIndex {
            let range = index..<text.index(after: index)
            // ignore white spaces
            if text[range].trimmingCharacters(in: .whitespaces).isEmpty {
                index = text.index(after: index)
                continue
            }

            let rect = layoutManager.boundingRect(forGlyphRange: NSRange(range, in: text), in: textContainer)
            index = text.index(after: index)
            
            if currentRect == nil {
                currentRect = rect
            } else if abs(currentRect!.minY - rect.minY) < 1 {
                // glyph on the same line, resize line rect
                currentRect!.size = CGSize(width: rect.maxX, height: currentRect!.height)
            } else {
                let geometry = TextLineGeometry(
                    fullRect: currentRect!,
                    baselineRect: CGRect(origin: currentRect!.origin, size: CGSize(width: currentRect!.width, height: baseline))
                )
                geometries.append(geometry)
                currentRect = rect
            }
        }
        
        let geometry = TextLineGeometry(
            fullRect: currentRect!,
            baselineRect: CGRect(origin: currentRect!.origin, size: CGSize(width: currentRect!.width, height: baseline))
        )
        
        geometries.append(geometry)
        
        return geometries
    }

    override func draw(_ rect: CGRect) {
        super.draw(rect)
        
        if !debug {
            return
        }
        
        linesGeometry.forEach {
            UIColor.black.setStroke()
            let fullPath = UIBezierPath(rect: $0.fullRect)
            fullPath.stroke()
            
            UIColor.red.setStroke()
            let baseLinePath = UIBezierPath(rect: $0.baselineRect)
            baseLinePath.stroke()
        }
    }

}
//
//  ViewController.swift
//  TwoLabelsWithLineBetween
//

import UIKit

class ViewController: UIViewController {

    @IBOutlet var leftLabel: GlyphPositionLabel!
    @IBOutlet var rightLabel: GlyphPositionLabel!
    
    let lineLayerName = "lineLayerName"
    let linePadding: CGFloat = 4
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        drawLine()
    }

    func drawLine() {
        leftLabel.debug = false
        rightLabel.debug = false
        
        guard
            let leftLastLineGeometry = leftLabel.linesGeometry.last,
            let rightLastLineGeometry = rightLabel.linesGeometry.last else {
            return
        }
        
        let leftBaselineRect = leftLabel.convert(leftLastLineGeometry.baselineRect, to: view)
        let leftBaselineBottomRightPoint = CGPoint(x: leftBaselineRect.maxX + linePadding, y: leftBaselineRect.maxY)
        
        let rightBaselineRect = rightLabel.convert(rightLastLineGeometry.baselineRect, to: view)
        let rightBaselineBottomLeftPoint = CGPoint(x: rightBaselineRect.minX - linePadding, y: rightBaselineRect.maxY)
        
        for layer in view.layer.sublayers ?? [] {
            if layer.name == lineLayerName {
                layer.removeFromSuperlayer()
                break
            }
        }
        
        let lineLayer = CAShapeLayer()
        lineLayer.lineDashPattern = [4, 2]
        lineLayer.strokeColor = UIColor.gray.cgColor
        lineLayer.lineWidth = 1
        lineLayer.name = lineLayerName
        view.layer.addSublayer(lineLayer)
        
        let linePath = UIBezierPath()
        linePath.move(to: leftBaselineBottomRightPoint)
        linePath.addLine(to: rightBaselineBottomLeftPoint)
        lineLayer.path = linePath.cgPath
    }

}
→ Ссылка