The following is verbatim of a feedback report (FB19809442) I submitted, shared here as someone else might be interested to see it (I hate the fact that we can't see each other's feedbacks).
On iOS 16, TextKit 2 calls NSTextLayoutFragment
's draw(at:in:)
method once for the first paragraph, but for every other paragraph, it calls it continuously on every scroll step in the UITextView. (The first paragraph is not cached; its draw is called again when it is about to be displayed again, but then it is again called only once per its lifecycle.)
On iOS 17, the behavior is similar; the draw method gets called once for the 1st and 2nd paragraph, and for every other paragraph it again gets called continuously as a user scrolls a UITextView.
On iOS 18 (and iOS 26 beta 4), TextKit 2 calls the layout fragment's draw(at:in:)
on every scroll step in the UITextView, for all paragraphs. This results in terrible performance.
TextKit 2 is promised to bring many performance benefits by utilizing the viewport - a new concept that represents the visible area of a text view, along with a small overscroll. However, having the draw method being constantly called almost negates all the performance benefits that viewport brings. Imagine what could happen if someone needs to add just a bit of logic to that draw method. FPS drops significantly and UX is terribly degraded.
I tried optimizing this by only rendering those text line fragments which are in the viewport, by using NSTextViewportLayoutController.viewportBounds
and converting NSTextLineFragment.typographicBounds
to the viewport-relative coordinate space (i.e. the coordinate space of the UITextView
itself). However, this patch only works on iOS 18 where the draw method is called too many times, as the viewport changes. (I may have some other problems in my implementation, but I gave up on improving those, as this can't work reliably on all OS versions since the underlying framework isn't calling the method consistently.)
Is this expected? What are our options for improving performance in these areas?
The behavior should be that only the fragments that fall in the view port + estimated overdraw region are re-drawn, and that should be true for all system versions.
Thanks for creating the demo project for me. I've tried your project with my Xcode 26 Beta 6 + iOS 26 Beta 7 + iPhone 16 Plus, and don't see that the system re-draws all the paragraph. Here is what I do:
a. Use the following print
instead to better observe the fragments that are re-drawn:
print("draw called in \(rangeInElement)")
b. Add more paragraphs to textContents
so the content is longer.
With that, here is what I see:
- Launch the app. Xcode shows the following log, indicating that only the first fragment is drawn.
draw called in 0...1531
(Happens one time)
- Scroll a bit. Xcode shows the following log, indicating that only the first and second fragments are drawn:
draw called in 0...1531
draw called in 1532...3237
... (Repeats many times.)
- Scroll down to the bottom, clear the log, and then scroll a bit. Xcode shows the following log, indicating that only the last and second last fragments are drawn:
draw called in 11362...12656
draw called in 12657...13951
... (Repeats many times.)
So it doesn't seem that the system redraws all the paragraphs in the above cases.
The log repeats a lot of times in step 2 and 3, meaning the relevant fragments are drawn a lot of times, but that's part of the drawing process of updating the whole view port.
Best,
——
Ziqiao Chen
Worldwide Developer Relations.