I am trying to implement the NSTextViewDelegate function textViewDidChangeSelection(_ notification: Notification). My text view's delegate is the Coordinator of my NSViewRepresentable. I've found that this delegate function never fires, but any other delegate function that I implement, as long as it doesn't take a Notification as an argument, does fire (e.g., textView(:willChangeSelectionFromCharacterRange:toCharacterRange:), fires and is called on the delegate exactly when it should be).
For context, I've verified all of the below:
textView.isSelectable = true
textView.isEditable = true
textView.delegate === my coordinator
I can call textViewDidChangeSelection(:) directly on the delegate without issue.
I can select and edit text without issues. I.e., the selections are being set correctly. But the delegate method is never called when they are.
I am able to add the intended delegate as an observer for the selector textViewDidChangeSelection via NotificationCenter. If I do this, the function executes when it should, but fires for every text view in my view hierarchy, which can number in the hundreds. I'm using an NSLayoutManager, so I figure this should only fire once. I've added a check within my code:
func textViewDidChangeSelection(_ notification: Notification) {
guard let textView = notification.object as? NSTextView,
textView === layoutManager.firstTextView else { return }
// Any code I want to execute...
}
But the above guard check lets through every notification, so, no matter what, my closure executes hundreds of times if I have hundreds of text views, all of them being sent by textView === layoutManager.firstTextView, but once for each and every text view managed by that layoutManager.
Does anyone know why this method isn't ever called on the delegate, while seemingly all other delegate methods are? I could go the NotificationCenter route, but I'd love to know why this won't execute as a delegate method when documentation says that it should, and I don't want to have to implement a counter to make sure my code only executes once per selection update. And for more reasons than that, implementing via delegate method is preferable to using notifications for my use case.
Thanks for any help!
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
I've posted a couple times now about major issues I'm having with NSLayoutManager and have written to Apple for code-level support, but no one at Apple has responded to me in more than two weeks. So I'm turning to the community again for any help whatsoever.
I'm fairly certain it's a real bug in TextKit. If I'm right about that, I'd love for anyone at Apple to take an interest. And better yet, if I'm wrong (and I hope I am), I'd be incredibly grateful to anyone who can point out where my mistake lies! I've been stuck with this bug for weeks on end.
The crux of the issue is that I'm getting what seemed to be totally incompatible results from back to back calls to textContainer(forGlyphAt:effectiveRange:) and lineFragmentRect(forGlyphAt:effectiveRange:withoutAdditionalLayout:)... I'd lay out my text into a fairly tall container of standard page width and then query the layout manager for the text container and line fragment rect for a particular glyph (a glyph that happens to fall after many newlines). Impossibly, the layout manager would report that that glyph was in said very tall container, but that the maxY of its lineFragmentRect was only at 14 points (my NSTextView's isFlipped is true, so that's 14 points measuring from the top down).
After investigating, it appears that what is happening under the hood is NSLayoutManager is for some reason laying out text back into the first container in my series of containers, rather than overflowing it into the next container(s) and/or giving me a nil result for textContainer(forGlyphAt:...)
I've created a totally stripped down version of my project that recreates this issue reliably and I'm hoping literally anyone at Apple will respond to me. In order to recreate the bug, I've had to build a very specific set of preview data - namely some NSTextStorage content and a unique set of NSTextViews / NSTextContainers.
Because of the unique and particular setup required to recreate this bug, the code is too much to paste here (my preview data definition is a little unwieldy but the code that actually processes/parses it is not).
I can share the project if anyone is able and willing to look into this with me. It seems I'm not able to share a .zip of the project folder here but am happy to email or share a dropbox link.
TLDR: NSLayoutManager's textContainer(forGlyphAt:effectiveRange:) and lineFragmentRect(forGlyphRange:effectiveRange:) are returning inconsistent results.
Context: I'm developing a word processing app that paginates from an NSTextStorage using NSLayoutManager. My app uses a text attribute (.columnType) to paginate sub-ranges of the text at a time, ensuring that each columnRange gets a container (or series of containers across page breaks) to fit. This is to support both multi-column and standard full-page-width content.
After any user edit, I update pagination data in my Paginator model class. I calcuate frames/sizes for the views/containers, along with what superview they belong to (page). The UI updates accordingly.
In order to determine whether the columnRange has overflowed from a container due to a page break OR whether the range of text hasn't overflowed its container and is actually using less space than available and should be sized down, I call both:
layoutManager.textContainer(forGlyphAt: lastGlyphOfColumn, effectiveRange: &actualGlyphRangeInContainer)`
// and
`layoutManager.lineFragmentRect(forGlyphAt: lastGlyphOfColumn, effectiveRange: nil)
Apple Documentation notes that both these calls force glyph generation and layout. As I'm in early development, I have not set non-contiguous layout. So these should be causing full layout, assuring accurate return values.
Or so I'd hoped.
This does work fine in many cases. I edit. Pagination works. But then I'll encounter UI-breaking inconsistent returns from these two calls. By inconsistent, I mean that the second call returns a line fragment rect that is in the container coordinates of A DIFFERENT container than the container returned by the first call. To be specific, the line fragment rect seems to be in the coordinates of the container that comes next in layoutManager.textContainers.
Example Code:
if !layoutManager.textContainers.indices.contains(i) {
containerToUse = createTextContainer(with: availableSize)
layoutManager.addTextContainer(containerToUse)
} else {
// We have a container already but it may be
// the wrong size.
containerToUse = layoutManager.textContainers[i]
if containerToUse.size.width != availableSize.width {
// Mandatory that we resize if we don't have
// a matching width. Height resizing is not
// mandatory and requires a layout check below.
containerToUse.size = availableSize
}
}
let glyphRange = layoutManager.glyphRange(forCharacterRange: remainingColumnRange, actualCharacterRange: nil)
let lastGlyphOfColumn = NSMaxRange(glyphRange) - 1
var containerForLastGlyphOfColumn = layoutManager.textContainer(forGlyphAt: lastGlyphOfColumn, effectiveRange: &actualGlyphRangeInContainer)
if containerForLastGlyphOfColumn != containerToUse
&& containerToUse.size.height < availableSize.height {
// If we are here, we overflowed the container,
// BUT the container we overflowed didn't use
// the maximum remaining page space (this
// means it was a pre-existing container that
// needs to be sized up and checked once more).
// NOTE RE: THE BUG:
// at this point, prints show...
// containerToUse.size.height
// =628
// availableSize.height
// =648
containerToUse.size = availableSize
containerForLastGlyphOfColumn = layoutManager.textContainer(forGlyphAt: lastGlyphOfColumn, effectiveRange: &actualGlyphRangeInContainer)
}
// We now check again, knowing that the container we
// are testing flow into is the max size it can be.
if containerForLastGlyphOfColumn != containerToUse {
// If we are here, we have overflowed the
// container, so containerToUse size SHOULD be
// final/accurate, since it is fully used.
actualCharRangeInContainer = layoutManager.characterRange(forGlyphRange: actualGlyphRangeInContainer, actualGlyphRange: nil)
// Start of overflow range is the first character
// in the container that was overflowed into.
let overflowLoc = actualCharRangeInContainer.location
remainingColumnRange = NSRange(location: overflowLoc, length: remainingColumnRange.length - overflowLoc)
// Update page count as we have broken to a new page
currentPage += 1
} else {
// If we are here, we have NOT overflowed
// from the container. BUT...
// THE BUG:
// ***** HERE IS THE BUG! *****
lineFragmentRectForLastChar = layoutManager.lineFragmentRect(forGlyphAt: lastGlyphOfColumn, effectiveRange: nil)
let usedHeight = lineFragmentRectForLastChar.maxY
// BUG: ^The lines of code above return a
// fragment rect that is in the coordinates
// of the WRONG text container. Prints show:
// usedHeight
// =14
// usedHeight shouldn't be just 14 if this is
// the SAME container that, when it was 628
// high, resulted in text overflowing.
// Therefore, the line fragment here seems
// to be in the coordinates of the ENSUING
// container that we overflowed INTO, but
// that shouldn't be possible, since we're in
// a closure for which we know:
//
// containerForLastGlyphOfColumn == containerToUse
//
// If the last glyph container is the container
// we just had to size UP, why does the final
// glyph line fragment rect have a maxY of 14!?
// Including ensuing code below only for context.
if usedHeight < containerToUse.size.height {
// Adjust container size down to usedRect
containerToUse.size = CGSize(width: containerToUse.size.width, height: usedHeight)
} else if usedHeight == availableSize.height {
// We didn't force break to a new page BUT
// we've used exactly the height of our page
// to layout this column range, so need to
// break to a new page for any ensuing text
// columns.
currentPage += 1
} else if usedHeight > containerToUse.size.height {
// We should have caught this earlier. Text
// has overflowed, but this should've been
// caught when we checked
// containerForLastGlyphOfColumn !=
// containerToUse.
//
// Note: this error has never thrown.
throw PaginationError.unknownError("Oops.")
}
}
Per my comments in the code block above, I don't understand why the very same text container that just overflowed and so had to be sized up from 628 to 648 in order to try to fit a glyph would now report that same glyph as both being IN that same container and having a line fragment rect with a maxY of just 14. A glyph couldn't fit in a container when it was 628 high, but if I size it up to 648, it only needs 14?
There's something very weird going on here. Working with NSLayoutManager is a bit of a nightmare given the unclear documentation.
Any help or insight here would be massively, massively appreciated.
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.