Question: Active UITextView moving in UIScrollView

I have a large scrolling canvas that is 2000 pixels wide, with an unlimited length, inside of which is a single large UIView.
You can interact with the UIView to add objects (lines, shapes, text boxes, images, pdfs etc).
When you add a text box, I create a subclass of UITextView that is configured to auto-grow with text - limited to 600px wide and unlimited length (you can force it wider). Key point - UITextViews never scroll internally.
The problem I am having is moving the UIscrollview (i.e. the big canvas) to keep the cursor of the active text box visible. When you are on a limited width display (such as an iPhone), you should be able to click on a text box and start typing. The scroll view should move around to keep your current cursor (text entry point) on the screen, even when the UITextView is very wide.
This is not too hard - I have a UITextViewDelegate with a "DidChangeSelection:" function. When the selection moves and the delegate method gets called - I find the current cursor (SelectedTextRange), convert that into a rect (GetFirstRectForRange), then convert from UITextView coordinates to the UIView coordinates (ConvertRectToView), then request the UIScrollView being that rect into view (ScrollRectToVisible).
What is weird is that even though my rect is correct - the UITextView is always aligning so the far right hand edge is visible. So if the UITextView is 1000px wide, and the visible part of the UIScrollView is only 500px wide, then only ½ of the UITextView can be seen. No matter where the cursor is, the UIScrollView scrolls so that the right hand side of the UITextView is always visible.
The UITextView seems to be trying to make itself visible when it is the active control, but it seems to always attempt to "ScrollToRect" to its full width, and if the  UITextView is wider than the visible area, the UIScrollView always seems to show the right hand side. I know this because I can turn off my delegate, and the UITextView does scroll into view, but only the right hand side.
Is this standard behaviour? Ideally I want to turn off whatever mechanism is automatically scrolling the active UITextView into view so that my correct ScrollToRect call will be used instead.
Thanks for any advice you can give.
Cheers.


Answer:
Couple options to try...
First, subclass UIScrollView:
class MyScrollView: UIScrollView {
    override func scrollRectToVisible(_ rect: CGRect, animated: Bool) {
        // don't do anything
    }
}

This will prevent the built-in "auto-scroll" when the text view is active.
Second option:

Embed the text view in a "container scroll view"
disable scrolling of that scroll view
constrain the text view to that scroll view's .frameLayoutGuide, so the text view controls the size of the container scroll view
add that container scroll view as a subview of your "content" view

Now, when the text view is "active," its "container" scroll view will receive the auto-scroll messages instead of the "main" scroll view.
In either case, of course, your code will be responsible for managing any horizontal / vertical scrolling needed to keep the insertion point visible.

Edit
Quick example for Second Option:
class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let tv = UITextView()
        let tvsv = UIScrollView()
        let cv = UIView()
        let sv = UIScrollView()
        
        [tv, tvsv, cv, sv].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
        }
        tvsv.addSubview(tv)
        cv.addSubview(tvsv)
        sv.addSubview(cv)
        view.addSubview(sv)
        
        let g = view.safeAreaLayoutGuide
        let cg = sv.contentLayoutGuide
        let fg = sv.frameLayoutGuide
        
        let tsfg = tvsv.frameLayoutGuide
        
        NSLayoutConstraint.activate([
            
            tv.topAnchor.constraint(equalTo: tsfg.topAnchor, constant: 0.0),
            tv.leadingAnchor.constraint(equalTo: tsfg.leadingAnchor, constant: 0.0),
            tv.trailingAnchor.constraint(equalTo: tsfg.trailingAnchor, constant: 0.0),
            tv.bottomAnchor.constraint(lessThanOrEqualTo: tsfg.bottomAnchor, constant: 0.0),
            
            tv.widthAnchor.constraint(equalToConstant: 600.0),

            tvsv.topAnchor.constraint(equalTo: cv.topAnchor, constant: 20.0),
            tvsv.leadingAnchor.constraint(equalTo: cv.leadingAnchor, constant: 20.0),
            tvsv.bottomAnchor.constraint(lessThanOrEqualTo: cv.bottomAnchor, constant: -20.0),
            
            tv.widthAnchor.constraint(equalToConstant: 600.0),
            

            cv.topAnchor.constraint(equalTo: cg.topAnchor, constant: 20.0),
            cv.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 20.0),
            cv.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: -20.0),
            cv.bottomAnchor.constraint(equalTo: cg.bottomAnchor, constant: -20.0),
            
            cv.widthAnchor.constraint(equalToConstant: 1000.0),
            
            sv.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            sv.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            sv.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            sv.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
            
        ])
        
        tv.isScrollEnabled = false
        tvsv.isScrollEnabled = false

        // so we can see the framing
        tv.backgroundColor = .yellow
        cv.backgroundColor = .systemBlue
        sv.backgroundColor = .systemRed
        view.backgroundColor = .systemYellow
        
    }
    
}

Note: I have not implemented any code for manually handling the scroll view's content offset, which would be needed to keep the insertion point visible. However, this will demonstrate that the "auto-scroll" is no longer in effect.