SwiftUI TextEditor - save the state after completion of editing

Viewed 3518

Overview of the issue

I am building a note taking app and there is a note editing view where a few TextEditors are used to listen to users' input. Right now an environment object is passed to the this note editing view and I designed a save button to save the change. It works well except that users have to click the save button to update the model.

Expected behaviors

The text editor is expected to update the value of instances of the EnvironmentObject once the editing is done. Do not necessarily click the save button to save the changes.

below is the sample code of view

struct NoteDetails: View {

    @EnvironmentObject var UserData: Notes
    @Binding var selectedNote: SingleNote?
    @Binding var selectedFolder: String?
    @Binding var noteEditingMode: Bool

    @State var title:String = ""
    @State var updatedDate:Date = Date()
    @State var content: String = ""
    
    var id: Int?
    var label: String?
    
    @State private var wordCount: Int = 0

 var body: some View {

VStack(alignment: .leading) {
                TextEditor(text: $title)
                    .font(.title2)
                    .padding(.top)
                    .padding(.horizontal)
                    .frame(width: UIScreen.main.bounds.width, height: 50, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
                   
                    
                
                                        
                Text(updatedDate, style: .date)
                    .font(.subheadline)
                    .padding(.horizontal)
                
                Divider()
                
                TextEditor(text: $content)
                    .font(.body)
                    .lineSpacing(15)
                    .padding(.horizontal)
                    
                    .frame(maxHeight:.infinity)
                    

                Spacer()
                
            }
}
}

below is the sample code of edit func of the EnvironmentObject

UserData.editPost(label: label!, id: id!, data: SingleNote(updateDate: Date(), title: title, body: content))

Ways I have tried

  • I tried to add a onChange modifier to the TextEditor but it applies as soon as any change happens, which is not desired. I also tried a few other modifiers like onDisapper etc.
  • User data has @Published var NoteList: [String: [SingleNote]] and I tried to pass the $UserData.NoteList[label][id].title in to the TextEditor and it was not accepted either

Did not find sound solutions in the Internet so bring up this question here. Thanks for suggestions in advance!

1 Answers

I don't know exactly what you mean to save once the editing is done. Here are two possible approaches I found.

Note: In the following demos, the text with blue background displays the saved text.

1. Saving when user dismisses keyboard

Solution: Adding a tap gesture to let users dismiss the keyboard when tapped outside of the TextEditor. Call save() at the same time.

Code:

struct ContentView: View {
    
    @State private var text: String = ""
    @State private var savedText: String = ""
    
    
    var body: some View {
        VStack(spacing: 20) {
            Text(savedText)
                .frame(width: 300, height: 200, alignment: .topLeading)
                .background(Color.blue)
            
            TextEditor(text: $text)
                .frame(width: 300, height: 200)
                .border(Color.black, width: 1)
                .onTapGesture {}
        }
        .onTapGesture { hideKeyboardAndSave() }
    }
    
    
    private func hideKeyboardAndSave() {
        UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
        save()
    }
    
    
    private func save() {
        savedText = text
    }
}

2. Saving after no changes for x seconds

Solution: Using Combine with .debounce to publish and observe only after x seconds have passed with no further events.

I have set x to 3.

Code:

struct ContentView: View {
    
    @State private var text: String = ""
    @State private var savedText: String = ""
    
    let detector = PassthroughSubject<Void, Never>()
    let publisher: AnyPublisher<Void, Never>
    
    init() {
        publisher = detector
            .debounce(for: .seconds(3), scheduler: DispatchQueue.main)
            .eraseToAnyPublisher()
    }
    
    
    var body: some View {
        VStack(spacing: 20) {
            Text(savedText)
                .frame(width: 300, height: 200, alignment: .topLeading)
                .background(Color.blue)
            
            TextEditor(text: $text)
                .frame(width: 300, height: 200)
                .border(Color.black, width: 1)
                .onChange(of: text) { _ in detector.send() }
                .onReceive(publisher) { save() }
        }
    }
    
    
    private func save() {
        savedText = text
    }
}
Related