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.
TextKit
RSS for tagManage text storage and perform custom layout of text-based content in your app's views using TextKit.
Posts under TextKit tag
33 Posts
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
tl;dr: UITextView does not auto layout when isScrollEnabled = false
I have a screen with multiple UITextViews on it, contained within a ScrollView. For each textview, I calculate the height needed to display the entire content in SwiftUI and set it using the .frame(width:height:) modifier.
The UITextView will respect the size passed in and layout within the container, but since UITextView is embedded within a UIScrollView, when a user attempts to scroll on the page, often they will scroll within a UITextView block rather than the page. They currently need to scroll along the margins outside of the textview to get the proper behavior.
Since I am already calculating the height to display the text, I don't want the UITextView to scroll. However, when I set isScrollEnabled = false, the text view displays in a single long line that gets truncated. I have tried
Setting various frame/size attributes but that seems to have zero affect on the layout.
Embedding the textView within a UIView, and then sizing the container, but then the textView does not display at all.
Setting a fixed size textContainer in the layoutManager but did not work.
There's a lot of code so I can't copy/paste it all, but generally, it looks like
struct SwiftUITextEditor: View {
@State var text: AttributedString = ""
var body: some View {
ZStack {
MyTextViewRepresentable(text: $text)
}
.dynamicallySized(from: $text)
}
}
struct MyTextViewRepresentable: UIViewRepresentable {
@Binding var text: AttributedString
let textView = UITextView(usingTextLayoutManager: true)
func makeUIView(context: Context) -> UITextView {
textView.attributedText = text
textView.isScrollEnabled = false
}
...
}
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.
I'm trying to update my app to use TextKit 2. The one thing that I'm still not sure about is how I can get the selection frame. My app uses it to auto-scroll the text to keep the cursor at the same height when the text wraps onto a new line or a newline is manually inserted. Currently I'm using NSLayoutManager.layoutManager!.boundingRect(forGlyphRange:in:).
The code below almost works. When editing the text or changing the selection, the current selection frame is printed out. My expectation is that the selection frame after a text or selection change should be equal to the selection frame before the next text change. I've noticed that this is not always true when the text has a NSParagraphStyle with spacing > 0. As long as I type at the end of the text, everything's fine, but if I insert some lines, then move the selection somewhere into the middle of the text and insert another newline, the frame printed after manually moving the selection is different than the frame before the newline is inserted. It seems that the offset between the two frames is exactly the same as the paragraph style's spacing. Instead when moving the selection with the arrow key the printed frames are correct.
I've filed FB17104954.
class ViewController: NSViewController, NSTextViewDelegate {
private var textView: NSTextView!
override func loadView() {
let scrollView = NSScrollView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
textView = NSTextView(frame: scrollView.frame)
textView.autoresizingMask = [.width, .height]
textView.delegate = self
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 40
textView.typingAttributes = [.foregroundColor: NSColor.labelColor, .paragraphStyle: paragraphStyle]
scrollView.documentView = textView
scrollView.hasVerticalScroller = true
view = scrollView
}
func textView(_ textView: NSTextView, shouldChangeTextIn affectedCharRange: NSRange, replacementString: String?) -> Bool {
print("before", selectionFrame.maxY, selectionFrame)
return true
}
func textDidChange(_ notification: Notification) {
print("after ", selectionFrame.maxY, selectionFrame)
}
func textViewDidChangeSelection(_ notification: Notification) {
print("select", selectionFrame.maxY, selectionFrame)
}
var selectionFrame: CGRect {
guard let selection = textView.textLayoutManager!.textSelections.first?.textRanges.first else {
return .null
}
var frame = CGRect.null
textView.textLayoutManager!.ensureLayout(for: selection)
textView.textLayoutManager!.enumerateTextSegments(in: selection, type: .selection, options: [.rangeNotRequired]) { _, rect, _, _ in
frame = rect
return false
}
return frame
}
}
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()
}
}
}
demo code :
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
// Flip the coordinate system
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
NSDictionary *attrs = @{NSFontAttributeName: [UIFont systemFontOfSize:20],
NSForegroundColorAttributeName: [UIColor blueColor],
NSUnderlineStyleAttributeName: @(NSUnderlineStyleThick),
};
// Make an attributed string
NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:@"Hello CoreText!" attributes:attrs];
CFAttributedStringRef attributedStringRef = (__bridge CFAttributedStringRef)attributedString;
// Simple CoreText with CTFrameDraw
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attributedStringRef);
CGPathRef path = CGPathCreateWithRect(self.bounds,NULL);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter,CFRangeMake(0, 0),path,NULL);
//CTFrameDraw(frame, context);
// You can comment the line 'CTFrameDraw' and use the following lines
// draw with CTLineDraw
CFArrayRef lines = CTFrameGetLines(frame);
CGPoint lineOrigins[CFArrayGetCount(lines)];
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigins);
for (int i = 0; i < CFArrayGetCount(lines); i++) {
CTLineRef line = CFArrayGetValueAtIndex(lines, i);
CGContextSetTextPosition(context, lineOrigins[i].x, lineOrigins[i].y);
// CTLineDraw(line, context);
// You can comment the line 'CTLineDraw' and use the following lines
// draw with CTRunDraw
// use CTRunDraw will lost some attributes like NSUnderlineStyleAttributeName,
// so you need draw it by yourself
CFArrayRef runs = CTLineGetGlyphRuns(line);
for (int j = 0; j < CFArrayGetCount(runs); j++) {
CTRunRef run = CFArrayGetValueAtIndex(runs, j);
CTRunDraw(run, context, CFRangeMake(0, 0));
}
}
}
this code will use CTRunDraw to draw the content , and the underline will draw and show normally in iOS17 & Xcode 15 , But when you build it with XCode16 & iOS18 beta . the underline will be missing .
Hello, community,
I'm using an HTML editor in a .NET MAUI application running on macOS, and I'm encountering some unexpected behavior during text editing:
Double-click text selection disappears after approximately one second.
Styles randomly revert or are applied to the wrong text unexpectedly.
It appears to be related to macOS spell checking. When using editable elements (, or with contenteditable), the system enables spell checking by default.
During this, MAUI attempts to communicate with a system process:
com.apple.TextInput.rdt, which is not running, leading to repeated errors like:
Error Domain=NSCocoaErrorDomain Code=4099
"The connection to service named com.apple.TextInput.rdt was invalidated: failed at lookup with error 3 - No such process."
Question:
What is com.apple.TextInput.rdt, and why might it not be running?
Thank you for any help!
The main function of this code is that when I click on the link within the TextView, the TextView will respond to the event, while when clicking on other places, the TextView will not respond to the event.
I want to know how to use TextKit2 to achieve the same functionality as the following code?
class MyTextView: UITextView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
var location = point
location.x -= textContainerInset.left
location.y -= textContainerInset.top
let characterIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
if characterIndex < textStorage.length, characterIndex >= 0 {
if let _ = textStorage.attribute(.link, at: characterIndex, effectiveRange: nil) {
return self
}
}
return nil
}
}
I haven't found a method similar to that in Textkit1 within Textkit2. I'm looking forward to everyone's guidance.
With TextKit 1, I was able to “tag” characters with attribute string keys that flagged them to be invisible, then I would use NSLayoutManager’s layoutManager(_:shouldGenerateGlyphs:properties:characterIndexes:font:forGlyphRange:) to strip these characters out, preventing change to the underlying storage.
In TextKit 2, I don’t see an opportunity to do this. The best point I think to intercept would be NSTextLayoutFragment, but without being able to see what’s happening, I don’t know if it’s possible to alter the content used to generate the line fragments.
My end goal is to be able to hide characters for a Markdown editor, so maybe I’m thinking about this wrong? Any advice would be welcome.
Based on this TextKit 2 demo project I thought that I could implement syntax highlighting by parsing syntax block tokens (e.g. comments like <!-- --> or /* */) in processEditing and storing their locations, and then actually applying the rendering with NSTextContentStorageDelegate in textContentStorage(_:textParagraphWith:) by checking the location of each paragraph against the store of syntax tokens.
This sort of works except that the rendering is only updated for paragraphs which are changed.
Is there a way to trigger NSTextContentStorage to re-fetch paragraphs in a given range? Or is this a totally misguided approach to the problem?
I have an attributedString with 100 NSTextAttachments(contains image of 400kb). When i scroll the textview, it is lagging, When i did the same in textkit 1, it is butter smooth. It can be because of how textkit 1 & 2 layout the elements.
let attachment = NSTextAttachment()
attachment.image = UIImage(named: "image2")
let attachmentString = NSAttributedString(attachment: attachment)
let mutableAttributedString = NSMutableAttributedString(attributedString: textView.attributedText)
for _ in 0...100 {
mutableAttributedString.append(NSAttributedString(string: "\n"))
mutableAttributedString.append(attachmentString)
}
textView.attributedText = mutableAttributedString
How to handle images in textkit 2 so that it feels smooth while scrolling textview?
(NOTE: In sum, this is destructive of user data.)
The client is a professor of Classics in constant need of properly-rendered glyphs that represent legitimate code points. As an example, the correct spelling might be:
εὔτρητος
It is spelled and rendered as intended. A file by this name will be correctly spelled by ls in the Terminal. Note that two diacritics are applied to the second letter, an upsilon (ὔ)
However, the Finder displays that file as
ἐύτρητος
and iterating the string reveals that the accents are improperly distributed over the two. This would never be correct.
This handicaps digital-humanities researchers from college to postdoctoral work.
A Character by Character iteration demonstrates the mangling.:
intended (εὔτρητος)
displayed (ἐύτρητος)
3B5 (ε) 1F10 (ἐ)
GREEK SMALL LETTER EPSILON,
GREEK SMALL LETTER EPSILON WITH PSILI
1F54 (ὔ) 3CD (ύ)
GREEK SMALL LETTER UPSILON WITH PSILI AND OXIA
GREEK SMALL LETTER UPSILON WITH TONOS
3C4 (τ) 3C4 (τ)
(back in sync)
3C1 (ρ) 3C1 (ρ)
3B7 (η) 3B7 (η)
3C4 (τ) 3C4 (τ)
3BF (ο) 3BF (ο)
3C2 (ς) 3C2 (ς)
I don't want to muddy the waters by guessing where and how the mistake is made, just see for yourself.
I have a UITextField with UITextContentType equal to oneTimeCode.
It works as expected if the message is in English and the keyword "OTP" exists.
It doesn't work if the message is in Greek and the keyword "OTP" is translated also in greek.
Is the OTP keyword really needed? Is there any alternative? Which are the keywords for any case? Are these keywords only in English?
Thanks in advance!