Set Initial View Offset for UITextView in SwiftUI

428 views Asked by At

I am stumped on where to inject an initial content offset for a TextView created as a representable in SwiftUI. Here is the TextView code, followed by the ContentView that uses it. My objective is to be able to reposition a long text view back to where it left off when the view was hidden or closed.

import SwiftUI
import UIKit

struct ZTextView: UIViewRepresentable
{
    
    @Binding var text: String
    
    func makeUIView(context: Context) -> UITextView
    {
        let textView = UITextView()
        textView.font = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.largeTitle)
        
        textView.autocapitalizationType = .sentences
        textView.isSelectable = true
        textView.isUserInteractionEnabled = true
        
        textView.delegate = context.coordinator
        
        return textView
    }
    
    func updateUIView(_ uiView: UITextView, context: Context)
    {
        uiView.text = text
    }
    
    // COORDINATOR
    
    func makeCoordinator() -> Coordinator
    {
        Coordinator($text)
    }
    
    class Coordinator: NSObject, UITextViewDelegate
    {
        var text: Binding<String>
        
        init(_ text: Binding<String>)
        {
            self.text = text
        }
        
        func textViewDidChange(_ textView: UITextView)
        {
            self.text.wrappedValue = textView.text
            print("change")
            getOffset(textView)
        }
        
        func scrollViewDidScroll(_ scrollView: UIScrollView)
        {
            print("scroll")
        }
        
        func getOffset(_ textView: UITextView)
        {
            let topLeft = CGPoint(x: textView.bounds.minX, y: textView.bounds.minY)
            let bottomRight = CGPoint(x: textView.bounds.maxX, y: textView.bounds.maxY)
            guard let topLeftTextPosition = textView.closestPosition(to: topLeft),
                  let bottomRightTextPosition = textView.closestPosition(to: bottomRight)
            else {
                return
            }
            let charOffset = textView.offset(from: textView.beginningOfDocument, to: topLeftTextPosition)
            let length = textView.offset(from: topLeftTextPosition, to: bottomRightTextPosition)
            
            print("Offset: \(charOffset), Length: \(length)")
        }
        
        
    }
    
    func setOffset(textview: UITextView, offset: Int)
    {
        let range = NSRange(location: offset, length: 1)
        textview.scrollRangeToVisible(range)
    }
}

The ContentView code.

import SwiftUI

struct ContentView: View
{
    @State var text = "Mary\nhad\na\nlittle\nlamb\nwho's\nfleece\nwas\nwhite\nas\nsnow.\n\nEverywhere\nthat\nMary\nwent\nthe\nlamb\nwas\nsure\nto\ngo.\n"
    var body: some View
    {
        ZTextView(text: $text)
            .padding()
    }
}

I have been trying all sorts of possibilities but clearly do not understand SwiftUI well enough to make this happen. I've got a similar app working using just UIKit, so I know it can be done, just now how or where.

It seems that if I can figure out where to place a call to the setOffset method I have shown, I should be all set. Any suggestions? Calling this with an offset of 46 on an iPod Touch simulator should scroll so the word "as" is at the top of the screen when the ZTextView is first shown.

1

There are 1 answers

0
Phantom59 On

Still need help with this? I ran into your question looking for help in something very similar. I eventually figured out something that worked at least for my specific need.

You need to create a binding to an offset property in your ZTextView class. The binding is necessary so that updateUIView() is called whenever the value of offset changes thus updating your ZTextView. The update could come from your ContentView in which case the offset may be an @State property or from an Observable object that Publishes an offset value (my case).

So, to ZTextView, add the binding and modify updateUIView() as shown below:

  @Binding var offset: CGPoint

  func updateUIView(_ uiView: UITextView, context: Context) {
      uiView.text = text
      uiView.contentOffset = offset
  }

Is this what you were looking for?