Search code examples
swifttextviewswiftuiuiviewrepresentableline-spacing

TextView UIViewRepresentable resets UndoManager when lineSpacing added via attributedText value change


I am using a TextView UIViewRepresentable as outlined here https://www.appcoda.com/swiftui-textview-uiviewrepresentable/.

It’s working as expected, apart from one issue concerning line spacing. SwiftUI’s lineSpacing modifier seems to have no effect on it. So, I have worked around it by adding the following to the UIViewRepresentable’s func updateUIView(_ uiView: UITextView, context: Context) :

let style = NSMutableParagraphStyle()
style.lineSpacing = 4
let attributes = [NSAttributedString.Key.paragraphStyle : style]
uiView.attributedText = NSAttributedString(string: self.text, attributes:attributes)

This was as advised by Adjusting the line spacing of UITextView.

So, the full function looks like:

func updateUIView(_ uiView: UITextView, context: Context) {
    uiView.text = text
    
    // line spacing
    let style = NSMutableParagraphStyle()
    style.lineSpacing = 4
    let attributes = [NSAttributedString.Key.paragraphStyle : style]
    uiView.attributedText = NSAttributedString(string: self.text, attributes:attributes)
}

This does the job, however it results in the UndoManager resetting. That is, as soon as I make any change, the UndoManager doesn’t believe there is anything that can be undone (or redone). From my searches, it appears this is a general side-effect of changing the value of attributedText. I’m wondering if there’s a workaround available, whether a tweak to my approach or an altogether different way of achieving lineSpacing without resetting the UndoManager state.

UPDATE: attempted Asperi’s recommendation but with mixed results.

This is full code of TextView and corresponding Coordinator:

import SwiftUI
 
struct TextView: UIViewRepresentable {
  
// MARK: Bindings
@Binding var text: String
@Binding var textStyle: UIFont.TextStyle

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

    textView.backgroundColor = UIColor.clear
    textView.delegate = context.coordinator
    textView.autocapitalizationType = .sentences
    textView.isSelectable = true
    textView.isUserInteractionEnabled = true
    textView.adjustsFontForContentSizeCategory = true
    
    return textView
}

func makeCoordinator() -> Coordinator {
    Coordinator($text)
}

func updateUIView(_ uiView: UITextView, context: Context) {

    let storage = uiView.textStorage
     storage.beginEditing()

     // line spacing
     let style = NSMutableParagraphStyle()
     style.lineSpacing = 4
     let attributes = [NSAttributedString.Key.paragraphStyle : style]
     storage.replaceCharacters(in: NSRange(location: 0, length: storage.length),
         with: NSAttributedString(string: self.text, attributes:attributes))
     storage.endEditing()
}

// MARK: -

// MARK: Internal classes

class Coordinator: NSObject, UITextViewDelegate {
    
    // MARK: Local
    var text: Binding<String>
    
    // MARK: -

    init(_ text: Binding<String>) {
        self.text = text
    }
 
    // MARK: -

    // MARK: Functions
    func textViewDidChange(_ textView: UITextView) {
        self.text.wrappedValue = textView.text
    }
}
}

If I keep self.text.wrappedValue = textView.text in textViewDidChange (which was taken from the AppCoda tutorial linked at the top) then the recommendation doesn’t work. However, if I remove it, it appears to work but there are other issues whereby the text automatically resets to the original state at the start of the session whenever the view (I think) is refreshed – for example, if I try switching to another app, I could see the text reset before doing so, or when I open a piece of UI that, for example, reduces the opacity of the TextView.


Solution

  • I may have stumbled across the correct approach thanks to Ivan’s answer in https://stackoverflow.com/a/44414510/698971.

    Inside func makeUIView(context: Context) -> UITextView for the TextView UIViewRepresentable I needed to add:

        let spacing = NSMutableParagraphStyle()
        spacing.lineSpacing = 4
        let attr = [NSAttributedString.Key.paragraphStyle : spacing]
        textView.typingAttributes = attr