It seems that NSTextView
has an issue with deleting text and setting any attribute at the same time, when it also has a textContainerInset
.
With the code below, after 1 second, the empty line in the text view is automatically deleted and the first line is colored red. The top part of the last line remains visible at its old position. Selecting the whole text and then deselecting it again makes the issue disappear.
Is there a workaround?
I've created FB16897003.
class ViewController: NSViewController {
@IBOutlet var textView: NSTextView!
override func viewDidAppear() {
textView.textContainerInset = CGSize(width: 0, height: 8)
let _ = textView.layoutManager
textView.textStorage!.setAttributedString(NSAttributedString(string: "1\n\n2\n3\n4"))
textView.textStorage!.addAttribute(.foregroundColor, value: NSColor.labelColor, range: NSRange(location: 0, length: textView.textStorage!.length))
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [self] in
textView.selectedRange = NSRange(location: 3, length: 0)
textView.deleteBackward(nil)
textView.textStorage!.beginEditing()
textView.textStorage!.addAttribute(.foregroundColor, value: NSColor.red, range: NSRange(location: 0, length: 2))
textView.textStorage!.endEditing()
}
}
}
You are using TextKit1 (by accessing textView.layoutManager
). If you can switch to TextKit2, as shown in the following code, the issue will disappear.
guard let textContextManager = textView.textLayoutManager?.textContentManager else {
print("`textView.textLayoutManager?.textContentManager` is nil. You are still on TextKit1?")
return
}
textView.textContainerInset = CGSize(width: 0, height: 8)
//let _ = textView.layoutManager
textContextManager.performEditingTransaction {
textView.textStorage!.setAttributedString(NSAttributedString(string: "1\n\n2\n3\n4"))
textView.textStorage!.addAttribute(.foregroundColor, value: NSColor.labelColor, range: NSRange(location: 0, length: textView.textStorage!.length))
textView.textStorage!.addAttribute(.paragraphStyle, value: paragraphStyle(indent: 100), range: NSRange(location: 0, length: 2))
textView.textStorage!.addAttribute(.paragraphStyle, value: paragraphStyle(indent: 100), range: NSRange(location: 1, length: 2))
textView.textStorage!.addAttribute(.paragraphStyle, value: paragraphStyle(indent: 100), range: NSRange(location: 3, length: 2))
textView.textStorage!.addAttribute(.paragraphStyle, value: paragraphStyle(indent: 100), range: NSRange(location: 5, length: 2))
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [self] in
textContextManager.performEditingTransaction {
textView.selectedRange = NSRange(location: 3, length: 0)
textView.deleteBackward(nil)
//textView.textStorage!.beginEditing()
textView.textStorage!.addAttribute(.paragraphStyle, value: paragraphStyle(indent: 100), range: NSRange(location: 0, length: 2))
textView.textStorage!.addAttribute(.paragraphStyle, value: paragraphStyle(indent: 100), range: NSRange(location: 2, length: 2))
textView.textStorage!.addAttribute(.paragraphStyle, value: paragraphStyle(indent: 100), range: NSRange(location: 4, length: 2))
textView.textStorage!.addAttribute(.paragraphStyle, value: paragraphStyle(indent: 100), range: NSRange(location: 6, length: 1))
//textView.textStorage!.endEditing()
}
}
If you need to stick with TextKit1, in which case I'd be curious of why, commenting out the following line seems to work around the issue:
//textView.textContainerInset = CGSize(width: 0, height: 8)
If the intent of using a custom textContainerInset
is to move the text down 8 points, you might be able to achieve it by moving textView
down.
Best,
——
Ziqiao Chen
Worldwide Developer Relations.