Search code examples
macosswiftuinstextview

Binding with "RichTextField" (i.e. NSTextView in NSViewRepresentable) - Value resets after redraw, LostFocus, etc


I'm struggling with a custom made RichTextField, i.e. a NSTextView in a NSViewRepresentable, which has the below code (programmed with the help of How to use an NSAttributedString with a ScrollView in SwiftUI?):

Setting the attributedString in the code works and I can change the formatting, but as soon as the application loses the focus, the RichTextField resets to the last value set programmatically:

Formatting resets

Furthermore, when using the RichTextField in a List, the application goes into a loop.

RichTextField

import Foundation
import SwiftUI

struct RichTextField: NSViewRepresentable {
    typealias NSViewType = NSTextView
    
    @Binding var attributedString: NSMutableAttributedString 
    var isEditable: Bool
    
    func makeNSView(context: Context) -> NSTextView {
        let textView = NSTextView(frame: .zero)

        textView.textStorage?.setAttributedString(self.attributedString)
        textView.isEditable = isEditable

        textView.translatesAutoresizingMaskIntoConstraints = false 
        textView.autoresizingMask = [.width, .height] 

        return textView
    }

    func updateNSView(_ nsView: NSTextView, context: Context) {
        nsView.textStorage?.setAttributedString(self.attributedString)
    }

}

View

import SwiftUI

struct EditWindow: View {
    
    @ObservedObject var model: EditEntryViewModel
    
 var body: some View {

        VStack (alignment: .leading, spacing: 20) {
            
            RichTextField(attributedString: self.$model.answer1, isEditable: true)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
            Spacer()
            
        }.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
        
    }
    
}

ViewModel

import Foundation
import SwiftUI

class EditEntryViewModel: ObservableObject {
    
    init(entryID: Int32) {
        
        let entryToUse = db.getEntry(id: entryID)
        
        id = entryToUse!.id
        answer1 = entryToUse!.answer1.getNSMutableAttributedStringFromHTML() // Converts HTML from the DB to a NSMutableAttributedString
        
    }
    
    @Published var id: Int32
    @Published var answer1: NSMutableAttributedString = NSMutableAttributedString() {
        didSet {
            print("NEW answer1: " + answer1.string)
        }
    }
    
}

I wonder if there is a way to bind the attributedString to the ViewModel?

Thanks a lot for the help.


Solution

  • Not sure if it's the correct way to do it, but I got the binding to work with the following code:

    import Foundation
    import SwiftUI
    
    
    struct RichTextField: NSViewRepresentable {
        typealias NSViewType = NSTextView
        
        @Binding var attributedString: NSAttributedString  
        var isEditable: Bool
        
        func makeCoordinator() -> Coordinator {
            Coordinator(self)
        }
    
        func makeNSView(context: Context) -> NSTextView {
            let textView = NSTextView(frame: .zero)
            
            textView.textStorage?.setAttributedString(self.attributedString)
            textView.isEditable = isEditable
            textView.delegate = context.coordinator
            
            textView.translatesAutoresizingMaskIntoConstraints = false 
            textView.autoresizingMask = [.width, .height] 
    
            return textView
        }
    
        func updateNSView(_ nsView: NSTextView, context: Context) {
            nsView.textStorage!.setAttributedString(self.attributedString)
        }
    
        // Source: https://medium.com/fantageek/use-xib-de9d8a295757
        class Coordinator: NSObject, NSTextViewDelegate {
            let parent: RichTextField
            
            init(_ RichTextField: RichTextField) {
                self.parent = RichTextField
            }
    
            func textDidChange(_ notification: Notification) {
    
                guard let textView = notification.object as? NSTextView else { return }
                self.parent.attributedString = textView.attributedString()
    
            }
    
        }
        
    }
    

    (I still get the "cycle detected" error when using the RichTextField in a List (non-editable) though..)