CTFramesetterSuggestFrameSizeWithConstraints иногда возвращает неправильный размер?

В приведенном ниже коде CTFramesetterSuggestFrameSizeWithConstraints иногда возвращает CGSize с высотой, которая недостаточно велика, чтобы вместить весь текст, который передается в нее. Я посмотрел на этот ответ. Но в моем случае ширина текстового поля должна быть постоянной. Есть ли другой / лучший способ определить правильную высоту для моей приписанной строки? Спасибо!

CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attributedString);
CGSize tmpSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0,0), NULL, CGSizeMake(self.view.bounds.size.width, CGFLOAT_MAX), NULL); 
CGSize textBoxSize = CGSizeMake((int)tmpSize.width + 1, (int)tmpSize.height + 1);

7 ответов

Решение

CTFramesetterSuggestFrameSizeWithConstraints() сломано. Я подал ошибку на это некоторое время назад. Ваша альтернатива заключается в использовании CTFramesetterCreateFrame() с пути, который достаточно высок. Затем вы можете измерить прямоугольник CTFrame, который вы получите. Обратите внимание, что вы не можете использовать CGFLOAT_MAX для высоты, так как CoreText использует перевернутую систему координат от iPhone и разместит его текст в верхней части окна. Это означает, что если вы используете CGFLOAT_MAXу вас не будет достаточно точности, чтобы на самом деле сказать высоту коробки. Я рекомендую использовать что-то вроде 10000 в качестве вашего роста, так как это в 10 раз выше, чем сам экран, и все же дает достаточную точность для получающегося прямоугольника. Если вам нужно выложить еще более высокий текст, вы можете сделать это несколько раз для каждого раздела текста (вы можете запросить у CTFrameRef диапазон в исходной строке, который он мог выложить).

CTFramesetterSuggestFrameSizeWithConstraints работает правильно. Причина, по которой вы получаете слишком низкую высоту, заключается в том, что в стиле абзаца по умолчанию добавлена ​​начальная строка для прикрепленных строк. Если вы не присоединяете стиль абзаца к строке, то CoreText возвращает высоту, необходимую для визуализации текста, но без пробелов между строками. Это заняло у меня целую вечность, чтобы понять. Ничто в документации не объясняет это. Я только что заметил, что мои высоты были короткими на величину, равную (количество строк х ожидаемое ведение). Чтобы получить ожидаемый результат высоты, вы можете использовать код, подобный следующему:

NSString  *text = @"This\nis\nsome\nmulti-line\nsample\ntext."
UIFont    *uiFont = [UIFont fontWithName:@"Helvetica" size:17.0];
CTFontRef ctFont = CTFontCreateWithName((CFStringRef) uiFont.fontName, uiFont.pointSize, NULL);

//  When you create an attributed string the default paragraph style has a leading 
//  of 0.0. Create a paragraph style that will set the line adjustment equal to
//  the leading value of the font.
CGFloat leading = uiFont.lineHeight - uiFont.ascender + uiFont.descender;
CTParagraphStyleSetting paragraphSettings[1] = { kCTParagraphStyleSpecifierLineSpacingAdjustment, sizeof (CGFloat), &leading };

CTParagraphStyleRef  paragraphStyle = CTParagraphStyleCreate(paragraphSettings, 1);
CFRange textRange = CFRangeMake(0, text.length);

//  Create an empty mutable string big enough to hold our test
CFMutableAttributedStringRef string = CFAttributedStringCreateMutable(kCFAllocatorDefault, text.length);

//  Inject our text into it
CFAttributedStringReplaceString(string, CFRangeMake(0, 0), (CFStringRef) text);

//  Apply our font and line spacing attributes over the span
CFAttributedStringSetAttribute(string, textRange, kCTFontAttributeName, ctFont);
CFAttributedStringSetAttribute(string, textRange, kCTParagraphStyleAttributeName, paragraphStyle);

CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(string);
CFRange fitRange;

CGSize frameSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, textRange, NULL, bounds, &fitRange);

CFRelease(framesetter);
CFRelease(string);

Спасибо Крису ДеСальво за отличный ответ! Наконец-то заканчиваются 16 часов отладки. У меня были небольшие проблемы с выяснением синтаксиса Swift 3. Так что поделитесь версией Swift 3 настройки стиля абзаца.

let leading = uiFont.lineHeight - uiFont.ascender + uiFont.descender
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = leading      
mutableAttributedString.addAttribute(NSParagraphStyleAttributeName, value: paragraphStyle, range: textRange)

Работая над этой проблемой и следуя множеству разных ответов с нескольких плакатов, я реализовал для себя решение огромной проблемы правильного размера текста. CTFramesetterSuggestFrameSizeWithConstraints не работает должным образом, поэтому нам нужно использовать CTFramesetterCreateFrame а затем измерьте размер этого кадра (это UIFontextension) это быстро 100%

Ссылки

CTFramesetterSuggestFrameSizeWithConstraints иногда возвращает неправильный размер?

Как получить реальную высоту текста, нарисованного на CTFrame

Использование CFArrayGetValueAtIndex в Swift с UnsafePointer (AUPreset)

func sizeOfString (string: String, constrainedToWidth width: Double) -> CGSize {
        let attributes = [NSAttributedString.Key.font:self]
        let attString = NSAttributedString(string: string,attributes: attributes)
        let framesetter = CTFramesetterCreateWithAttributedString(attString)

        let frame = CTFramesetterCreateFrame(framesetter,CFRange(location: 0,length: 0),CGPath.init(rect: CGRect(x: 0, y: 0, width: width, height: 10000), transform: nil),nil)

        return UIFont.measure(frame: frame)
    }

Затем мы измеряем наш CTFrame

static func measure(frame:CTFrame) ->CGSize {

        let lines = CTFrameGetLines(frame)
        let numOflines = CFArrayGetCount(lines)
        var maxWidth : Double = 0

        for index in 0..<numOflines {
            let line : CTLine = unsafeBitCast(CFArrayGetValueAtIndex(lines, index), to: CTLine.self)
            var ascent : CGFloat = 0
            var descent : CGFloat = 0
            var leading : CGFloat = 0
            let width = CTLineGetTypographicBounds(line, &ascent, &descent, &leading)

            if(width > maxWidth) {
                maxWidth = width
            }
        }

        var ascent : CGFloat = 0
        var descent : CGFloat = 0
        var leading : CGFloat = 0


        CTLineGetTypographicBounds(unsafeBitCast(CFArrayGetValueAtIndex(lines, 0), to: CTLine.self), &ascent, &descent, &leading)
        let firstLineHeight = ascent + descent + leading

        CTLineGetTypographicBounds(unsafeBitCast(CFArrayGetValueAtIndex(lines, numOflines - 1), to: CTLine.self), &ascent, &descent, &leading)
        let lastLineHeight = ascent + descent + leading

        var firstLineOrigin : CGPoint = CGPoint(x: 0, y: 0)
        CTFrameGetLineOrigins(frame, CFRangeMake(0, 1), &firstLineOrigin);

        var lastLineOrigin : CGPoint = CGPoint(x: 0, y: 0)
        CTFrameGetLineOrigins(frame, CFRangeMake(numOflines - 1, 1), &lastLineOrigin);

        let textHeight = abs(firstLineOrigin.y - lastLineOrigin.y) + firstLineHeight + lastLineHeight

        return CGSize(width: maxWidth, height: Double(textHeight))
    }

Это сводило меня с ума, и ни одно из приведенных выше решений не помогло мне рассчитать макет длинных строк на нескольких страницах и вернуть значения диапазона с полностью усеченными строками. После прочтения примечаний к API параметр в CTFramesetterSuggestFrameSizeWithConstraints для 'stringRange' выглядит следующим образом:

'Диапазон строк, к которому будет применяться размер кадра. Диапазон строк — это диапазон строк, который использовался для создания фреймсеттера. Если для длины диапазона установлено значение 0, программа установки фреймов будет продолжать добавлять строки до тех пор, пока не закончится текст или пространство.

После установки диапазона строк на «CFRangeMake(currentLocation, 0)» вместо «CFRangeMake(currentLocation, string.length)» все работает отлично.

После нескольких недель попыток всего, любых возможных комбинаций, я сделал прорыв и нашел то, что работает. Эта проблема кажется более заметной в macOS, чем в iOS, но по-прежнему возникает в обоих случаях.

Что сработало для меня, так это использоватьвместоNSTextField(в macOS) илиUILabel(на iOS).

И используя boundingRect(with:options:context:) вместоCTFramesetterSuggestFrameSizeWithConstraints. Несмотря на то, что теоретически последний должен быть более низким уровнем, чем первый, и я предполагал, что он будет более точным, переломным моментом оказывается NSString.DrawingOptions.usesDeviceMetrics.

Предлагаемый размер рамы подходит как шарм.

Пример:

      let attributedString = NSAttributedString(string: "my string")
let maxWidth = CGFloat(300)
let size = attributedString.boundingRect(
                with: .init(width: maxWidth,
                            height: .greatestFiniteMagnitude),
                options: [
                    .usesFontLeading,
                    .usesLineFragmentOrigin,
                    .usesDeviceMetrics])

let textLayer = CATextLayer()
textLayer.frame = .init(origin: .zero, size: size)
textLayer.contentsScale = 2 // for retina
textLayer.isWrapped = true // for multiple lines
textLayer.string = attributedString

Затем вы можете добавитьCATextLayerлюбомуNSView/UIView.

macOS

      let view = NSView()
view.wantsLayer = true
view.layer?.addSublayer(textLayer)

iOS

      let view = UIView()
view.layer.addSublayer(textLayer)

Я, наконец, выясняю.. Когда CTFramesetterSuggestFrameSizeWithConstraints возвращает CGSize для рисования текста, размер считается недостаточно большим (иногда) для всего текста, поэтому последняя строка не рисуется. Я просто добавляю 1 к size.height для возврата, это кажется сейчас.

Что-то вроде этого:

SuggesSize = CGSizeMake(SuggesSize.width, предложил Size.height + 1);

Другие вопросы по тегам