1

Here's example code block

let attrString = NSAttributedString(string: "very long string goes here...") // very long string
var currentTextPos = 400 // current text position. ex. 400

let framesetter = CTFramesetterCreateWithAttributedString(attrString as CFAttributedString)
let path = CGPath(rect: UIScreen.main.bounds, transform: nil)
let ctFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(currentTextPos, 0), path, nil)

// draw frame with CTFrameDraw() function later

As an example, imagine we have very long attributed string and we want to draw a frame starting from 400 index.

To draw the next frame, we can just calculate how many characters fits in current frame and draw it. Here's a sample of how to achieve it:

// Get the length of string which fits in current CTFrame object
let frameLength = CTFrameGetVisibleStringRange(ctFrame).length // ex. frameLength == 95
// Append this to our currentTextPos variable
currentTextPos += frameLength // 400 + 95 = 495 . This is our next frame starting position
// Create another frame starting from 495 position
let nextFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(currentTextPos, 0), path, nil)

In this case we are able to get the next CTFrame object. This is easy to achieve because CTFramesetterCreateFrame will fill the whole frame with a lines until it reaches the end of frame or until the end of the string. So it's very easy to get the next CTFrame starting position.

The main question is how to get previous CTFrame object? Or previous CTFrame start position?

Imagine that we have only current text position and nothing else. It's also easy to calculate each frame starting positions individually from the beginning of the string till the end and cache it somewhere, but this question became really tricky if we are able to change attributed string font size or smth similar which will change the whole CTFrame's. After changing font size the CTFrame object should be drawn from the same currentTextPosition that it was before fontsize changing, in another words, changing font size should not affect current frame start position.

Igor Prusyazhnyuk
  • 133
  • 2
  • 14
  • 29

1 Answers1

0

The CTFramesetter does not manage list of CTFrame, and the frames themselves are immutable.

The frames management are on our responsibility, so if there are any changes in text source, ie. attributed string, we have to re-generate all frames from start, because this is the only way how framesetter progresses - there is no reverse-progressing mode. Fortunately this process is proved fast and can be done in background thread.

In the case it is just needed to have a frame (eg. offscreen drawing, etc.) for text range previous to current position (if, eg., from current position is drawn on screen), that frame can be created using like the following extension

extension CTFramesetter {
    func prevFrame(position: Int) -> CTFrame {

        let size = CTFramesetterSuggestFrameSizeWithConstraints(self,
            CFRange(location: 0, length: position), nil,
            CGSize(width: UIScreen.main.bounds.width, height: CGFloat.greatestFiniteMagnitude), nil)

        let path = CGPath(rect: CGRect(origin: .zero, size: size), transform: nil)
        return CTFramesetterCreateFrame(self, CFRangeMake(0, position), path, nil)
    }
}

of course it is not generic, because layout paths can differ, and attributes can differ, but for simplified conditions as provided in topic started question it can be used.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690