SwiftUI TextEditor - save the state after completion of editing
Asked Answered
D

1

6

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!

Dick answered 30/1, 2021 at 9:40 Comment(0)
N
13

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
    }
}
Noontime answered 30/1, 2021 at 14:24 Comment(6)
thank you for the suggestions! It helps a lot!! I should have been clearer, for instance, I am looking at the Apple-made Native Notes app on iPhone, whenever you type something in a note, it is saved immediately. Do you know what that solution is? is that similar to your second solution?Dick
A bit of context, sorry, it is a child view navigation link, how to automatically save the changes when users click the button to go back. It may simplify the scenarios.Dick
Your welcome! Yep, I think that would simplify the scenario. Saving inside .onDisappear works fine for me?Noontime
I have never worked on something like that, but it looks like Apple Notes may use a similar solution to my second one.Noontime
Intuitively, onDisappear is the way to go, right? I tried to add a modifier like this below. But it popped up a strange error saying is is null so unwrap failed when I clicked it. It is weird since the id cannot be null as long as it is selected, and in the parent navigation view id has been passed to this view and it is null. swift onDisappear(perform: { UserData.NoteList[label!]![id!].title = title }) How should I deal with the environment object in the onDisappear modifier? Can you advise? thanks!Dick
I figured the autosaving has something to do with the publisher and onReceive, it is mentioned in the Stanford cs 193p lesson 9 - data flow.Dick

© 2022 - 2024 — McMap. All rights reserved.