In summation: I have a nasty bug where my layout manager is laying out text visually overlapping on top of other text, i.e., into a container that it should have left in the rear view as it continues to lay out into ensuing containers. Details below...
I'm coding a word processing app with some custom pagination that involves multiple pages, within which there can be multiple NSTextView
/NSTextContainer
pairs that represent single column or dual column runs of text.
I generate pagination data by using a measuring NSLayoutManager
. This process ensures that no containers overlap, and that they are sized correctly for their associated ranges of text (i.e., non-overlapping, continuous ranges from a single NSTextStorage
).
I determine frame sizes by a series of checks, most importantly, by finding the last glyph in a column. Prior to the code below, remainingColumnRange
represents the remaining range of my textStorage that is of a consistent column type (i.e., single, left column, or right column). My measuring passes consist of my measuringLayoutManager
laying out text into its textContainers, the final of which is an extra overflowContainer
(i.e., == measuringLayoutManager.textContainers.last!
) which I only use to find the last glyph in the second to last container (measuringContainer
, which is thus == measuringLayoutManager.textContainers[count - 2]
)
let glyphRangeOfLastColumnChar = measuringLayoutManager.glyphRange(forCharacterRange: remainingColumnRange, actualCharacterRange: nil)
let lastGlyphIndex = NSMaxRange(glyphRangeOfLastColumnChar) - 1
measuringLayoutManager.ensureLayout(for: measuringContainer) // Not sure if this is necessary, but I've added it to insure I'm getting accurate measurements.
if measuringLayoutManager.textContainer(forGlyphAt: lastGlyphOfColumnIndex, effectiveRange: &actualGlyphRangeInContainer) == overflowContainer {
actualCharRangeInContainer = measuringLayoutManager.characterRange(forGlyphRange: actualGlyphRangeInContainer, actualGlyphRange: nil)
let overflowLoc = actualCharRangeInContainer.location
remainingColumnRange = NSRange(location: overflowLoc, length: remainingColumnRange.length - overflowLoc)
currentPage += 1
} else {
lineFragmentRectForLastChar = measuringLayoutManager.lineFragmentRect(forGlyphAt: lastGlyphIndex, effectiveRange: nil)
// Resize measuring container if needed.
let usedHeight = lineFragmentRectForLastChar.maxY
if usedHeight < measuringContainer.size.height {
measuringContainer.size = CGSize(width: measuringContainer.size.width, height: usedHeight)
} else if usedHeight == measuringContainer.size.height {
currentPage += 1 // we perfectly filled the page
} else {
// This would be an error case, because all cases should have been handled prior to arriving here. I throw an error. I have never fallen through here.
throw MyClass.anError
}
}
// I use the above data to create a PageLayoutItem, which is a struct that has frame data (CGRect/x,y,w,h), a containerIndex (Int), pageNumber (Int), textRange (NSRange), columnType (custom enum).
// After this I remove the overflowContainer, and continue to iterate through. This is inefficient but I'm simplifying my code to identify the root issue.
I don't explicitly use these containers when done with my pagination process. Rather, I use the PageLayoutItems I have created to generate/resize/remove textContainers/textViews for the UI as needed. My UI-interfacing/generating NSLayoutManager
, which is of course assigned to the same NSTextStorage
as the measuring layout manager, then iterates through my paginator model class' pageLayoutItems array to generate/resize/remove.
I have verified my pagination data. None of my frames overlap. They are sized exactly the same as they should be per my measurement passes. The number of containers/views needed is correct.
But here's the issue:
My views render the text that SHOULD appear in my final textContainer/textView as visually overlapping the text in my second to last textContainer/textView. I see a garble of text.
When I iterate through my UI textContainers, I get this debug print:
TextContainer 0 glyphRange: {0, 172}
TextContainer 1 glyphRange: {172, 55}
TextContainer 2 glyphRange: {227, 100} // this is wrong, final 31 chars should be in container 3
TextContainer 3 glyphRange: {327, 0} // empty range here, odd
I have tried setting textContainers for glyph ranges explicitly, via:
// Variable names just for clarity here
layoutManager.setTextContainer(correctTextView.textContainer!, forGlyphRange: correctGlyphRangeForThisContainer)
Debug prints show that I'm setting the right ranges there. But they don't retain.
I have tried resizing my final text container to be much larger in case that was the issue. No dice. My final range of text/glyphs still lays out in the wrong container and overlaps the other content laid out there.
Any help here?? I've scoured the forums and have been dealing with this bug for two weeks straight with no hope in sight.
Thank you.
I decided to keep trying at it - as my code base was a little too much to simplify in order to share it.
I couldn't solve it no matter what I did at a granular level.
BUT, when I simply removed my measuringLayoutManager
and had all code use a single layoutManager
, the problems went away.
Something in the text system really did not like it when I had more than one layout manager laying out the same text storage, even though I ensured that that second layout manager used a separate set of text containers, and even though those text containers were not tied to any views, nor ever passed as objects/references. Merely having a second layout manager assigned to the text storage, doing layout passes into its own containers in order to calculate sizes seemed to be the source of the problem.
I simplified and am using a single layout manager now.
My calculated sizes are the same as they were when calculated by my second measuringLayoutManager
. So there was nothing wrong with the calculations there. But I no longer get overlapping text and bizarre behavior.
I greatly appreciate your attempts to assist me with this.
I have another bug now working with NSLayoutManager which I've made a post about here (it's currently being reviewed prior to posting but should post soon).
If you see my other post and have any insight with that issue, I'd be very thankful. From my limited experience, NSLayoutManager is pretty finicky and pretty vaguely-documented. This seems to be an agreed upon point of view in the developer community from what I've found online.
Unfortunately, TextKit2 is not yet capable of doing the kind of multi-column, multi-page pagination I need. Until it is, I'll have to figure out how to work with NSLayoutManager reliably.