The approach presented by hassan313 is similar to one I'm experimenting with using the code below. Here's a brief overview of what the code does:
Calls ensureLayout(_:) but with an empty text range containing only the target location, i.e., the end of the document. This is near instant.
Determines the frame of the target text line fragment.
Scrolls the NSScrollView to make the frame of the target text line fragment visible.
Recursively performs the same steps from step 1 until the frame of the target text line fragment remains unchanged, at which point we're no longer getting an estimated frame.
The code can be generalized to scroll to any location in the document, but for the purpose of this example, it scrolls to the end of the document only.
While this works, it feels hacky to keep scrolling until the frame of the text line fragment stabilizes. I'm thinking there must be a better approach, and I would appreciate if someone from Apple can shed some light on how this is done in NSTextView.
override func moveToEndOfDocument(_ sender: Any?) {
moveToEndOfDocument(previousTextLineFragmentFrame: .null)
}
private func moveToEndOfDocument(previousTextLineFragmentFrame: CGRect) {
let targetLocation = textLayoutManager.documentRange.endLocation
let beforeTargetLocation = textLayoutManager.location(targetLocation, offsetBy: -1)!
let textRange = NSTextRange(location: targetLocation, end: targetLocation)!
textLayoutManager.ensureLayout(for: textRange)
textLayoutManager.textViewportLayoutController.layoutViewport()
guard let textLayoutFragment = textLayoutManager.textLayoutFragment(for: beforeTargetLocation) else {
return
}
guard let textLineFragment = textLayoutFragment.textLineFragment(for: targetLocation, isUpstreamAffinity: true) else {
return
}
let textLayoutFragmentFrame = textLayoutFragment.layoutFragmentFrame
let textLineFragmentFrame = textLineFragment.typographicBounds.offsetBy(dx: 0, dy: textLayoutFragmentFrame.minY)
guard textLineFragmentFrame != previousTextLineFragmentFrame else {
// Scrolling did not affect the layout of the text line fragment, so we do not need to scroll again.
return
}
scrollToVisible(textLineFragmentFrame)
moveToEndOfDocument(previousTextLineFragmentFrame: textLineFragmentFrame)
}