Update: When I set a breakpoint and step through the code slowly, all measurements are made correctly. There seems to be something happening asynchronously. Apple documentation for NSLayoutManager says that both lineFragmentRect(forGlyphAt:effectiveRange:) and textContainer(forGlyphAt:effectiveRange:) force glyph generation and layout, but it seems like maybe those generation/layout passes aren't completing synchronously in runtime if I'm getting different results from those calls when stepping through as opposed to when I don't have any breakpoints set.
Post
Replies
Boosts
Views
Activity
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.
@Mr. Jefferson , did this ever get solved for you? Did you find your own solution or find one elsewhere?
@Frameworks Engineer, do you have a follow-up to OP's reply to your comment?
I ask because I'm having the exact same issue. I'm coding a word processing app with custom behavior that requires running checks before deleting, and then changing attributes around/near the deletion point after deleting. (I have some other custom behaviors that are affected by this same issue but for simplicity will just refer to the deletion one.)
I want to be able to use default NSTextView behaviors that already come with "free" undo/redo (e.g., typing, deleteBackward(:), insertNewline(:), but then want to modify attributes for a certain range of the textStorage afterward. I register my attribute modifications with the undoManager. Like OP, I can get undo/redo to work flawlessly but only when using only NSTextView default behavior OR when using only my modifications/undos to the NSTextStorage. The second I start mixing both, things get ugly. My code looks like this (note: this is for macOS/AppKit but I have the exact same issue so wherever my error lies, it seems cross-platform in scope):
//...
let oldTypingAttributes = textView.typingAttributes
let oldSelectedRange = textView.selectedRange
let selectedRangePostDelete: NSRange
if selectedRange.length == 0 {
// If selection is zero length, then deletion will result in the caret moving one position left
selectedRangePostDelete = NSMakeRange((oldSelectedRange.location - 1), 0)
} else {
// Otherwise, deletion will result in the caret being positioned at the start of the originally-selected range
selectedRangePostDelete = NSMakeRange(oldSelectedRange.location, 0)
}
undoManager?.beginUndoGrouping()
textView.deleteBackward(nil)
// Note: I have also tried setting selectedRangePostDelete HERE - after deleteBackward - by calling textView.selectedRange, but I opted to move it earlier to be certain that my issue wasn't caused by run loop timing issues i.e., to be sure I wasn't querying textView.selectedRange before it had been updated post-deletion... all this said, the issue occurs either way
let changeAttributesRange = NSMakeRange(selectedRangePostDelete.location, aLength)
let oldAttributedString = textStorage.attributedSubstring(from: changeAttributesRange)
textStorage.beginEditing()
textStorage.addAttributes(newAttributes, range: changeAttributesRange)
textStorage.endEditing()
textView.selectedRange = selectedRangePostDelete
let undoHandler: (MyCustomViewRepresentable.Coordinator) -> Void = { [oldAttributedString = oldAttributedString, changeAttributesRange = changeAttributesRange, oldTypingAttributes = oldTypingAttributes, changeAttributesRange = changeAttributesRange] target in
textStorage.beginEditing()
target.textStorage.replaceCharacters(in: changeAttributesRange, with: oldAttributedString)
textStorage.endEditing()
// I have tried setting selection range here in the undo handler to either selectedRangePostDelete or oldSelectedRange. I have also tried setting typingAttributes to oldTypingAttributes. Neither seems to have an effect on the issue I'm seeing.
} // end undoHandler declaration
undoManager?.registerUndo(withTarget: self, handler: undoHandler)
undoManager?.setActionName("Typing")
undoManager?.endUndoGrouping()
//...
With this code, the undo/redo stack seems to get corrupted. Behavior will appear as expected for a few cycles of undo/redo but ultimately starts to produce unwanted behavior after a few cycles.
My use case precludes me from relying only on NSTextView behavior. So I need to modify the underlying textStorage at times. I realize that I could simply make ALL my changes by modifying textStorage only and prevent/override any default NSTextView modifications to the text, but I really don't want to have to reinvent the wheel if I don't have to. Completely coding my own implementation that modifies textStorage directly for all possible keyDown events that would edit text would be a bit absurd and seemingly unnecessary. I'd love to use NSTextView's standard behavior whenever it's applicable, which is 90% of the time in my case.
So I'm desperate to figure out how I can I make NSTextView standard behavior and custom NSTextStorage modification play nice together and fluidly support undo/redo. Any solution has escaped me thus far. I felt relieved to find someone struggling with the same issue!
For further context, before calling the above block of code, I am overriding my NSTextView's doCommand(by selector: Selector) and then running
...
if selector == #selector(deleteBackward(_:)) {
coordinator.handleDelete(for: self)
return
}
...
Where handleDelete(for:) calls the big block of code above, and where coordinator is the coordinator for my custom NSViewRepresentable class, which builds a view to provide to SwiftUI. The NSViewRepresentable view hierarchy contains the NSTextView in question.
Note: if you're wondering why in my undo handler closure I call replaceCharacters(in: range, with: oldAttributedSubstring) rather than something like addAttributes(oldAttributes)... it's because at the time of the original addAttributes call, string attributes may differ at various ranges/locations within the range of textStorage in question. To fully restore the textStorage to its original state, I have to completely replace the original attributedSubstring for that range).