Updating the contents of an array from a different view

Viewed 86

I'm writing a macOS app in Swiftui, for Big Sur and newer. It's a three pane navigationview app, where the left most pane has the list of options (All Notes in this case), the middle pane is a list of the actual items (title and date), and the last one is a TextEditor where the user adds text.

Each pane is a view that calls the the next view via a NavigationLink. Here's the basic code for that.

struct NoteItem: Codable, Hashable, Identifiable {
    let id: Int
    var text: String
    var date = Date()
    var dateText: String {
        dateFormatter.dateFormat = "EEEE, MMM d yyyy, h:mm a"
        return dateFormatter.string(from: date)
    }
    var tags: [String] = []
}


struct ContentView: View {
    @State var selection: Set<Int> = [0]
    var body: some View {
        NavigationView {
            
            List(selection: self.$selection) {
                NavigationLink(destination: AllNotes()) {
                    Label("All Notes", systemImage: "doc.plaintext")
                }
                .tag(0)
            }
            .listStyle(SidebarListStyle())
            .frame(minWidth: 100, idealWidth: 150, maxWidth: 200, maxHeight: .infinity)
            
            Text("Select a note...")
                .frame(maxWidth: .infinity, maxHeight: .infinity)
        }
    }
}

struct AllNotes: View {

    @State var items: [NoteItem] = {
        guard let data = UserDefaults.standard.data(forKey: "notes") else { return [] }
        if let json = try? JSONDecoder().decode([NoteItem].self, from: data) {
            return json
        }
        return []
    }()
    
    @State var noteText: String = ""

    var body: some View {
       NavigationView {
         List(items) { item in
                NavigationLink(destination: NoteView()) {
                    VStack(alignment: .leading) {
                        Text(item.text.components(separatedBy: NSCharacterSet.newlines).first!)
                        Text(item.dateText).font(.body).fontWeight(.light)
                    }
                    .padding(.vertical, 8)
                }
            }
            .listStyle(InsetListStyle())

            Text("Select a note...")
                .frame(maxWidth: .infinity, maxHeight: .infinity)
       }
    }
    .navigationTitle("A title")
    .toolbar {
        ToolbarItem(placement: .navigation) {
                Button(action: {
                    NewNote()
                }) {
                    Image(systemName: "square.and.pencil")
                }
         }
    }

}

struct NoteView: View {
    @State var text: String = ""
    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                TextEditor(text: $text).padding().font(.body)
                    .onChange(of: text, perform: { value in
                            print("Value of text modified to = \(text)")
                        })
                Spacer()
            }
            Spacer()
        }
        .padding()
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.white)
    }
}

When I create a new note, how can I save the text the user added on the TextEditor in NoteView in the array loaded in AllNotes so I could save the new text? Ideally there is a SaveNote() function that would happen on TextEditor .onChange. But again, given that the array lives in AllNotes, how can I update it from other views?

Thanks for the help. Newbie here!

1 Answers

use EnvironmentObject in App

import SwiftUI

@main
struct NotesApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(DataModel())
        }
    }
}

now DataModel is a class conforming to ObservableObject

import SwiftUI

final class DataModel: ObservableObject {
    @AppStorage("notes") public var notes: [NoteItem] = []
}

any data related stuff should be done in DataModel not in View, plus you can access it and update it from anywhere, declare it like this in your ContentView or any child View

NoteView

import SwiftUI

struct NoteView: View {
    
    @EnvironmentObject private var data: DataModel
    var note: NoteItem
    
    @State var text: String = ""
    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                TextEditor(text: $text).padding().font(.body)
                    .onChange(of: text, perform: { value in
                        guard let index =     data.notes.firstIndex(of: note) else { return }
                        data.notes[index].text = value
                    })
                Spacer()
            }
            Spacer()
        }
        .padding()
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.white)
        .onAppear() {
            print(data.notes.count)
        }
    }
}

AppStorage is the better way to use UserDefaults but AppStorage does not work with custom Objects yet (I think it does for iOS 15), so you need to add this extension to make it work.

import SwiftUI

struct NoteItem: Codable, Hashable, Identifiable {
    let id: UUID
    var text: String
    var date = Date()
    var dateText: String {
        let df = DateFormatter()
        df.dateFormat = "EEEE, MMM d yyyy, h:mm a"
        return df.string(from: date)
    }
    var tags: [String] = []
}

extension Array: RawRepresentable where Element: Codable {
    public init?(rawValue: String) {
        guard let data = rawValue.data(using: .utf8),
              let result = try? JSONDecoder().decode([Element].self, from: data)
        else {
            return nil
        }
        self = result
    }
    
    public var rawValue: String {
        guard let data = try? JSONEncoder().encode(self),
              let result = String(data: data, encoding: .utf8)
        else {
            return "[]"
        }
        return result
    }
}

Now I changed AllNotes view to work with new changes

struct AllNotes: View {
    
    @EnvironmentObject private var data: DataModel
    
    @State var noteText: String = ""
    
    var body: some View {
        NavigationView {
            List(data.notes) { note in
                NavigationLink(destination: NoteView(note: note)) {
                    VStack(alignment: .leading) {
                        Text(note.text.components(separatedBy: NSCharacterSet.newlines).first!)
                        Text(note.dateText).font(.body).fontWeight(.light)
                    }
                    .padding(.vertical, 8)
                }
            }
            .listStyle(InsetListStyle())
            
            Text("Select a note...")
                .frame(maxWidth: .infinity, maxHeight: .infinity)
        }
        .navigationTitle("A title")
        .toolbar {
            ToolbarItem(placement: .navigation) {
                Button(action: {
                    data.notes.append(NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []))
                }) {
                    Image(systemName: "square.and.pencil")
                }
            }
        }
    }
}
Related