I'm trying to implement pagination using NSLayoutManager
and multiple text containers. Creating NSTextView
/UITextView
instances works as expected, and the text flows from text view to another.
However, I'd like to force a page break, ie. determine myself where to break onto the next container. I'm working with parsed content, so I can't insert any additional ASCII control characters into the text.
I tried subclassing NSLayoutManager
and overriding textContainer(glyphIndex:, effectiveRange:)
, and here's a very brute-force example:
override func textContainer(forGlyphAt glyphIndex: Int, effectiveRange effectiveGlyphRange: NSRangePointer?) -> NSTextContainer? {
if glyphIndex > 100 {
return self.textContainers[1]
} else {
return super.textContainer(forGlyphAt: glyphIndex, effectiveRange: effectiveGlyphRange)
}
}
I'd expect this to move any glyphs after 100th index onto the second container, but the results are weird:
I suppose I'd have to subclass NSTextContainer
and tell the layout manager that it's already full of text. It has a method called lineFragmentRect(forProposedRect:at:writingDirection:remaining:)
but the documentation is very sparse, and I can't find any working examples.
Existing documentation around displaying text is very outdated, so any ideas or hints are welcome. I'm very confused about this, but still hopeful there is a simple way of telling the layout manager where to cut off content in each container.
One possible solution
NSTextContainer.exclusionPaths
could be used to rule out the rest of the possible space in containers.
let glyphIndex = layoutManager.glyphIndexForCharacter(at: 100)
var rect = layoutManager.lineFragmentRect(forGlyphAt: glyphIndex, effectiveRange: nil)
// Get the line range and used rect
var lineRange = NSMakeRange(NSNotFound, 0)
var usedRect = layoutManager.lineFragmentUsedRect(forGlyphAt: gi, effectiveRange: NSRangePointer(&lineRange))
// Calculate the remainder of the line
let remainder = NSRange(location: glyphIndex, length: NSMaxRange(lineRange) - glyphIndex)
var rectCount:Int = 0
var breakRects = layoutManager.rectArray(forGlyphRange: remainder, withinSelectedGlyphRange: remainder, in: textContainer1, rectCount: UnsafeMutablePointer(&rectCount))
// Create the rect for the remainder of the line
var lineRect = breakRect!.pointee
lineRect.size.width = textContainer1.size.width - lineRect.origin.x
// Then create a rect to cover up the rest
var coverRest = NSMakeRect(0, lineRect.origin.y + lineRect.height, textContainer1.size.width, CGFloat.greatestFiniteMagnitude)
// Add exclusion paths
textContainer1.exclusionPaths = [NSBezierPath(rect: lineRect), NSBezierPath(rect: coverRest)]
This results in expected behavior:
This requires a ton of calculations, and text containers with exclusion paths are noticeably slower. The app could potentially have hundreds of text views, which makes this quite inefficient.