1

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!

Aleph
  • 465
  • 2
  • 12

1 Answers1

0

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")
                }
            }
        }
    }
}
vdotup
  • 347
  • 4
  • 12
  • Thank you. I'm trying to use `data` instead of `items` in `AllNotes` but I'm having trouble understanding how `data` can be used. – Aleph Aug 16 '21 at 10:47
  • If I use it instead of `items` (which were loaded from `UserDefaults`), then I can't access the elements on the array. And I get an error: The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions – Aleph Aug 16 '21 at 10:50
  • Oh, I see. I use it as `data.items`. That works, thank you! – Aleph Aug 16 '21 at 10:57
  • Although I'm still stuck trying to access `data.items.text` so I can update it in `.onChange`. I get the `Value of type '[NoteItem]' has no member 'text'` error. – Aleph Aug 16 '21 at 11:07
  • data.items is array of of NoteItem, you need a way to store selected note index in AllItems either send it as Binding to NoteView, or store it in DataModel, if you show full code I can help you in details. – vdotup Aug 16 '21 at 11:13
  • Thank you @vdotup Look at the code in `NoteView` above, the one you posted. Now substitute the `print` with `data.items.text = text`. I get the error: Value of type '[NoteItem]' has no member 'text' – Aleph Aug 16 '21 at 11:45
  • Essentially, when the user types, need to update that note's text and save it. – Aleph Aug 16 '21 at 11:51
  • 1
    I edited the answer, I hope it is clear now. – vdotup Aug 16 '21 at 12:16
  • Fantastic, @vdotup thank you! Now I need to figure out how to save any text added after the new note is created. – Aleph Aug 16 '21 at 13:13
  • Trying to add `UserDefaults.standard.set(data, forKey: "notes")` but can't figure out where... – Aleph Aug 16 '21 at 13:14
  • No, it doesn't matter what I try, I can't get this to save the data from the texteditor... – Aleph Aug 16 '21 at 15:31
  • 1
    don't use UserDefaults look at my answer i used AppStorage, please use that – vdotup Aug 16 '21 at 15:41
  • Awesome, that worked. Now to figure out how to do a `.sorted(by:)` on the AppStorage – Aleph Aug 16 '21 at 16:07