Search code examples
swiftuiorientationuiviewrepresentable

Force refresh of UIViewRepresentable on orientation change


I have a custom UITextView as an UIViewRepresentable in my SwiftUI app. Using TextKit2, I determine regions where I do drawing.

Everything works fine, except when I rotate the device (Simulator or Preview), the drawing is not updated.

Before rotation:

enter image description here

After rotation:

enter image description here

What I did find, is that when I tap the orientation twice, the redraw does occur:

enter image description here

What am I missing?

Here is an MRE:

import SwiftUI

struct ContentView: View {
    var body: some View {
        TextViewRepresentable()
    }
}

#Preview {
    ContentView()
}

struct TextViewRepresentable: UIViewRepresentable {
    let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"

    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()

        let tc = NSTextContainer(size: textView.textContainer.size)
        tc.heightTracksTextView = true
        tc.widthTracksTextView = true

        let lm = NSTextLayoutManager()
        lm.textContainer = tc

        let ts = NSTextContentStorage()
        ts.addTextLayoutManager(lm)

        let myTextView = MyTextView(frame: textView.frame, textContainer: tc)

        myTextView.font = UIFont.monospacedSystemFont(ofSize: 44, weight: .regular)

        return myTextView
    }

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

class MyTextView: UITextView {
    override func draw(_ rect: CGRect) {
        super.draw(rect)

        debugPrint("draw")
        
        guard let context = UIGraphicsGetCurrentContext() else { return }

        context.setFillColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)

        for rect in rects(for: NSMakeRange(3, 12)) {
            context.fill(rect)
        }
    }

    func rects(for range: NSRange) -> [CGRect] {
        var rects: [CGRect] = []

        if let textLayoutManager = textLayoutManager, let contentManager = textLayoutManager.textContentManager {
            guard let start = contentManager.location(contentManager.documentRange.location, offsetBy: range.location),
                  let end = contentManager.location(start, offsetBy: range.length),
                  let textRange = NSTextRange(location: start, end: end)
            else {
                return []
            }
            textLayoutManager.enumerateTextSegments(in: textRange, type: .standard, options: .rangeNotRequired) { _, rect, _, _ in

                var adjustedRect = rect
                adjustedRect.size.height -= CGFloat(4.0)
                adjustedRect = CGRectOffset(rect, 0, CGFloat(8.0))

                rects.append(adjustedRect)

                return true
            }
        }

        return rects
    }
}

Solution

  • It seems like you just want draw to be called again whenever the frame of the MyTextView changes. You can just set its contentMode to .redraw:

    myTextView.contentMode = .redraw