In order to create a UITextView like that of the Messages app whose height grows to fits its contents (number of lines), I subclassed UITextView and customized the intrinsicContentSize like so:
override var intrinsicContentSize: CGSize {
var size = super.intrinsicContentSize
if size.height == UIView.noIntrinsicMetric {
layoutManager.glyphRange(for: textContainer)
size.height = layoutManager.usedRect(for: textContainer).height + textContainerInset.top + textContainerInset.bottom
}
return size
}
As noted at WWDC, accessing layoutManager will force TextKit 1, we should instead use textLayoutManager. How can this code be migrated to support TextKit 2?
How did we do? We’d love to know your thoughts on this year’s conference. Take the survey here
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
55 Posts
Sort by:
Post
Replies
Boosts
Views
Activity
I have a UITextView that contains paragraphs with text bullet lists (via NSTextList). I also implement NSTextContentStorageDelegate.textContentStorage(_:, textParagraphWith:) in order to apply some custom attributes to the text without affecting the underlying attributed text. My implementation returns a new NSParagraph that modifies the foreground color of the text. I based this on the example in the WWDC 21 session "Meet Text Kit 2".
UITextView stops rendering the bullets when I implement the delegate function and return a custom paragraph. Why?
func textContentStorage(_ textContentStorage: NSTextContentStorage, textParagraphWith range: NSRange) -> NSTextParagraph? {
guard let originalText = textContentStorage.textStorage?.attributedSubstring(from: range) else { return nil }
let updatedText = NSMutableAttributedString(attributedString: originalText)
updatedText.addAttribute(.foregroundColor, value: UIColor.green, range: NSRange(location: 0, length: updatedText.length))
let paragraph = NSTextParagraph(attributedString: updatedText)
// Verify that the text still contains NSTextList
if let paragraphStyle = paragraph.attributedString.attribute(.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle {
assert(!paragraphStyle.textLists.isEmpty)
} else {
assertionFailure("Paragraph has lost its text lists")
}
return paragraph
}
Hi, I've got an app that displays markdown in UITextView / NSTextView.
I would like it to behave like Notes app does, that is if user types the line start modifier, e.g:
"# "
or
"> "
I would like the keyboard to show a capitalized letters.
I've tried looking into overriding insertText - and it breaks the predictive text (can not insert white space).
I've tried implementing UITextInputTokenizer but no luck either.
Like I said, I see the Notes app does it perfectly, so maybe I'm missing something obvious or there is a way to do it without interrupting the auto-correction and predictive text.
Ideally same thing can be applied to the NSTextView as the app support both platforms.
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!
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.
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
}
...
}
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
}
}
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.
We have crash reports as shown below that we haven't yet been able to repro and could use some help deubgging.
My guess is that the app is giving a label or text view an attributed string with an invalid attribute range, but attributed strings are used in many places throughout the app, and I don't know an efficient way to track this down.
I'm posting the stack trace here in hopes that someone more familiar with the internals of the system frameworks mentioned will be able to provide a clue to help narrow where I should look.
Fatal Exception: NSRangeException
NSMutableRLEArray objectAtIndex:effectiveRange:: Out of bounds
0 CoreFoundation 0x2d5fc __exceptionPreprocess
1 libobjc.A.dylib 0x31244 objc_exception_throw
2 Foundation 0x47130 blockForLocation
3 UIFoundation 0x2589c -[NSTextLineFragment _defaultRenderingAttributesAtCharacterIndex:effectiveRange:]
4 UIFoundation 0x25778 __53-[NSTextLineFragment initWithAttributedString:range:]_block_invoke
5 CoreText 0x58964 TLine::DrawGlyphsWithAttributeOverrides(TLineDrawContext const&, __CFDictionary const* (long, CFRange*) block_pointer, TDecoratorObserver*) const
6 CoreText 0x58400 CTLineDrawWithAttributeOverrides
7 UIFoundation 0x25320 _NSCoreTypesetterRenderLine
8 UIFoundation 0x24b10 -[NSTextLineFragment drawAtPoint:graphicsContext:]
9 UIFoundation 0x3e634 -[NSTextLineFragment drawAtPoint:inContext:]
10 UIFoundation 0x3e450 -[NSTextLayoutFragment drawAtPoint:inContext:]
11 UIKitCore 0x3e3098 __38-[_UITextLayoutFragmentView drawRect:]_block_invoke
12 UIKitCore 0x3e31cc _UITextCanvasDrawWithFadedEdgesInContext
13 UIKitCore 0x3e3040 -[_UITextLayoutFragmentView drawRect:]
14 UIKitCore 0xd7a98 -[UIView(CALayerDelegate) drawLayer:inContext:]
15 QuartzCore 0x109340 CABackingStoreUpdate_
16 QuartzCore 0x109224 invocation function for block in CA::Layer::display_()
17 QuartzCore 0x917f0 -[CALayer _display]
18 QuartzCore 0x90130 CA::Layer::layout_and_display_if_needed(CA::Transaction*)
19 QuartzCore 0xe50c4 CA::Context::commit_transaction(CA::Transaction*, double, double*)
20 QuartzCore 0x5bd8c CA::Transaction::commit()
21 UIKitCore 0x9f3f0 _UIApplicationFlushCATransaction
22 UIKitCore 0x9c89c __setupUpdateSequence_block_invoke_2
23 UIKitCore 0x9c710 _UIUpdateSequenceRun
24 UIKitCore 0x9f040 schedulerStepScheduledMainSection
25 UIKitCore 0x9cc5c runloopSourceCallback
26 CoreFoundation 0x73f4c __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
27 CoreFoundation 0x73ee0 __CFRunLoopDoSource0
28 CoreFoundation 0x76b40 __CFRunLoopDoSources0
29 CoreFoundation 0x75d3c __CFRunLoopRun
30 CoreFoundation 0xc8284 CFRunLoopRunSpecific
31 GraphicsServices 0x14c0 GSEventRunModal
32 UIKitCore 0x3ee674 -[UIApplication _run]
33 UIKitCore 0x14e88 UIApplicationMain
also filed as FB16905066
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()
}
}
}
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.
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 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!
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.
In my app, User can select word in the UITextView, then I want to insert a content under the selected words(the comment words shouldn't be selected).like:
I found TextKit2 only support edit NSTextParagraph position, or I missed some features in NSTextLayoutManager?
I try to override the NSTextLayoutFragment and update the draw(at point: CGPoint, in context: CGContext) but I found, user still can select the origin content at the origin position, even if the layer of linefragment layer on the corrent position not the origin position.
I have discovered an odd issue with NSTextView, NSAdaptiveImageGlyph, and NSLayoutManager.
(Forgive the Objective-C... I'm old-school.)
I can easily display an attributed string containing text and Genmoji NSAdaptiveImageGlyphs with something very basic like this:
textView = [[NSTextView alloc] initWithFrame:NSMakeRect(100.0, 100.0, 500.0, 100.0)];
[textView.textStorage setAttributedString:sampleText];
[self addSubview:textView];
//NSLayoutManager *layoutManager = textView.layoutManager;
I can even insert or paste new Genmoji into there as well.
However, if I uncomment out that last line to retrieve the layoutManager of the text view it breaks the rendering of Genmoji, even if I do nothing with the layoutManager! Just getting it causes the NSTextView to skip past Genmoji glyphs when spacing out glyphs and when rendering.
I thought perhaps getting the layoutManager caused an internal cache to break so I tried ensureLayoutForTextContainer but that didn't help.
Interestingly if I paste a new Genmoji into the NSTextView it doesn't display either. Yet if I select all, copy, and paste into TextEdit I see all the Genmoji just fine so they are there, just invisible and zero size. Using the arrow keys I have to skip past the invisible Genmoji.
But if I comment out that line again I see all Genmoji with no issues.
I do need to use a layoutManager to measure things like text bounds and heights. Elsewhere in the code, my existing drawing routines would like to draw the resulting attributed string not with drawAtPoint (which works fine) but with drawGlyphsForGlyphRange but that doesn't work either as it uses NSLayoutManager.
Is there a trick to getting NSAdaptiveImageGlyphs working with NSLayoutManager?
Thanks for any assistance!
Hi
I am drawing TextKit2 managed NSAttributedStrings into a NSBitmapImageRep successfully, enumerating the Text Layout Fragments is giving me bogus background drawing
This is the core drawing code, its pretty simple: I manage the flipped property myself since NSTextLayoutManager assumes a flipped coordinate.
if let context = NSGraphicsContext(bitmapImageRep: self.textImageRep!)
{
NSGraphicsContext.current = context
let rect = NSRect(origin: .zero, size: self.outputSize)
NSColor.clear.set()
rect.fill()
// Flip the context
context.cgContext.saveGState()
context.cgContext.translateBy(x: 0, y: self.outputSize.height)
context.cgContext.scaleBy(x: 1.0, y: -1.0)
let textOrigin = CGPoint(x: 0.0, y: 0.0 )
let titleRect = CGRect(origin: textOrigin, size: self.themeTextContainer.size)
NSColor.orange.withAlphaComponent(1).set()
titleRect.fill()
self.layoutManager.enumerateTextLayoutFragments(from: nil, using: { textLayoutFragment in
// Get the fragment's rendering bounds
let fragmentBounds = textLayoutFragment.layoutFragmentFrame
print("fragmentBounds: \(fragmentBounds)")
// Render the fragment into the context
textLayoutFragment.draw(at: fragmentBounds.origin, in: context.cgContext)
return true
})
context.cgContext.restoreGState()
}
NSGraphicsContext.restoreGraphicsState()
I have a mutable string which has various paragraph styles which I add to the layout manager / text storage like so
let titleParagraphStyle = NSMutableParagraphStyle()
titleParagraphStyle.alignment = .center
titleParagraphStyle.lineBreakMode = .byWordWrapping
titleParagraphStyle.lineBreakStrategy = .standard
var range = NSMakeRange(0, self.attributedProgrammingBlockTitle.length)
self.attributedProgrammingBlockTitle.addAttribute(.foregroundColor, value: NSColor(red: 243.0/255.0, green: 97.0/255.0, blue: 97.0/255.0, alpha: 1.0), range:range)
self.attributedProgrammingBlockTitle.addAttribute(.backgroundColor, value: NSColor.cyan, range:range)
self.attributedProgrammingBlockTitle.addAttribute(.font, value: NSFont.systemFont(ofSize: 64), range:range)
self.attributedProgrammingBlockTitle.addAttribute(.paragraphStyle, value:titleParagraphStyle, range:range)
range = NSMakeRange(0, self.attributedThemeTitle.length)
self.attributedThemeTitle.addAttribute(.foregroundColor, value: NSColor.white, range:range )
self.attributedThemeTitle.addAttribute(.backgroundColor, value: NSColor.purple, range:range)
self.attributedThemeTitle.addAttribute(.font, value: NSFont.systemFont(ofSize: 48), range:range)
self.attributedThemeTitle.addAttribute(.paragraphStyle, value:NSParagraphStyle.default, range:range)
range = NSMakeRange(0, self.attributedText.length)
self.attributedText.addAttribute(.foregroundColor, value: NSColor.white, range:range )
self.attributedText.addAttribute(.backgroundColor, value: NSColor.yellow, range:range)
self.attributedText.addAttribute(.font, value: NSFont.systemFont(ofSize: 36), range:range)
self.attributedText.addAttribute(.paragraphStyle, value:NSParagraphStyle.default, range:range)
let allText = NSMutableAttributedString()
allText.append(self.attributedProgrammingBlockTitle)
allText.append(NSAttributedString(string: "\n\r"))
allText.append(self.attributedThemeTitle)
allText.append(NSAttributedString(string: "\n\r"))
allText.append(self.attributedText)
self.textStorage.textStorage?.beginEditing()
self.textStorage.textStorage?.setAttributedString(allText)
self.textStorage.textStorage?.endEditing()
self.layoutManager.ensureLayout(for: self.layoutManager.documentRange)
however, i get incorrect drawing for the background color font attributes. Its origin is zero, and not correctly aligned at all with the text.
How can I get correct rendering of backgrounds from TextKit2?
Here is an image of my output: