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:
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.
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..)