skip to Main Content

I have a reminder app that I am trying to implement persistent data in but whenever I close the app no data is saved. I know how to make it work with a normal MVC but I would like to get it working with the view model that I have.

I think I know what needs to change to fix the problem but I am not sure how to get to the solution. I am pretty sure that in the ReminderApp file under the NavigationView where it says HomeViewModel(reminds: store.reminds) I think that the store.reminds part needs to be binded to with a $ at the beginning but when I try doing that it doesn’t work and instead says that HomeViewModel reminds property expects Reminder instead of Binding.

ReminderStore loads and saves the reminders to a file with the reminders and HomeViewModel contains the reminders array and appends a reminder to the array when a user adds a new reminder.

If anyone knows how to get this working that would be great since I have been stuck on this. My minimal reproducable example code is below.

RemindersApp

import SwiftUI

@main
struct RemindersApp: App {
@StateObject private var store = ReminderStore()

var body: some Scene {
    WindowGroup {
        NavigationView {
            HomeView(homeVM: HomeViewModel(reminds: store.reminds)) {
                ReminderStore.save(reminds: store.reminds) { result in
                    if case .failure(let error) = result {
                        fatalError(error.localizedDescription)
                    }
                }
            }
            .navigationBarHidden(true)
        }
        .onAppear {
            ReminderStore.load { result in
                switch result {
                case .failure(let error):
                    fatalError(error.localizedDescription)
                case .success(let reminds):
                    store.reminds = reminds
                }
            }
        }
    }
}
}

HomeView

import SwiftUI

struct HomeView: View {
@StateObject var homeVM: HomeViewModel
@Environment(.scenePhase) private var scenePhase
@State var addView = false
let saveAction: ()->Void

var body: some View {
    VStack {
        List {
            ForEach($homeVM.reminds) { $remind in
                Text(remind.title)
            }
        }
    }
    .safeAreaInset(edge: .top) {
        HStack {
            Text("Reminders")
                .font(.title)
                .padding()
            
            Spacer()
            
            Button(action: {
                addView.toggle()
            }) {
                Image(systemName: "plus")
                    .padding()
                    .font(.title2)
            }
            .sheet(isPresented: $addView) {
                NavigationView {
                    VStack {
                        Form {
                            TextField("Title", text: $homeVM.newRemindData.title)
                        }
                    }
                        .toolbar {
                            ToolbarItem(placement: .cancellationAction) {
                                Button("Dismiss") {
                                    homeVM.newRemindData = Reminder.Data()
                                    addView.toggle()
                                }
                            }
                            ToolbarItem(placement: .principal) {
                                Text("New Reminder")
                                    .font(.title3)
                            }
                            ToolbarItem(placement: .confirmationAction) {
                                Button("Add") {
                                    homeVM.addRemindData(remindData: homeVM.newRemindData)
                                    addView.toggle()
                                }
                            }
                        }
                }
            }
            .onChange(of: scenePhase) { phase in
                if phase == .inactive { saveAction() }
            }
        }
    }
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
    HomeView(homeVM: HomeViewModel(reminds: Reminder.sampleReminders), saveAction: {})
}
}

ReminderStore

import Foundation
import SwiftUI

class ReminderStore: ObservableObject {
@Published var reminds: [Reminder] = []

private static func fileURL() throws -> URL {
    try FileManager.default.url(for: .documentDirectory,
                                in: .userDomainMask,
                                appropriateFor: nil,
                                create: false)
    .appendingPathComponent("reminds.data")
}

static func load(completion: @escaping (Result<[Reminder], Error>) -> Void) {
    DispatchQueue.global(qos: .background).async {
        do {
            let fileURL = try fileURL()
            guard let file = try? FileHandle(forReadingFrom: fileURL) else {
                DispatchQueue.main.async {
                    completion(.success([]))
                }
                return
            }
            let reminds = try JSONDecoder().decode([Reminder].self, from: file.availableData)
            DispatchQueue.main.async {
                completion(.success(reminds))
            }
        } catch {
            DispatchQueue.main.async {
                completion(.failure(error))
            }
        }
    }
}

static func save(reminds: [Reminder], completion: @escaping (Result<Int, Error>) -> Void) {
    do {
        let data = try JSONEncoder().encode(reminds)
        let outfile = try fileURL()
        try data.write(to: outfile)
        DispatchQueue.main.async {
            completion(.success(reminds.count))
        }
    } catch {
        DispatchQueue.main.async {
            completion(.failure(error))
        }
    }
}
}

HomeViewModel

import Foundation

class HomeViewModel: ObservableObject {
@Published var reminds: [Reminder]
@Published var newRemindData = Reminder.Data()

init(reminds: [Reminder]) {
    self.reminds = reminds
}

func addRemindData(remindData: Reminder.Data) {
    let newRemind = Reminder(data: remindData)
    reminds.append(newRemind)
    newRemindData = Reminder.Data()
}
}

Reminder

import Foundation

struct Reminder: Identifiable, Codable {
var title: String
let id: UUID

init(title: String, id: UUID = UUID()) {
    self.title = title
    self.id = id
}
}

extension Reminder {
struct Data {
    var title: String = ""
    var id: UUID = UUID()
        
}

var data: Data {
    Data(title: title)
}

mutating func update(from data: Data) {
    title = data.title
}

init(data: Data) {
    title = data.title
    id = UUID()
}
}

extension Reminder {
static var sampleReminders = [
    Reminder(title: "Reminder1"),
    Reminder(title: "Reminder2"),
    Reminder(title: "Reminder3")
]
}

2

Answers


  1. ReminderStore.save isn’t invoking in time.

    By the time it invokes it doesn’t have/get the reminder data.

    That’s the first thing I would make sure gets done. You may end up running into other issues afterward, but I would personally focus on that first.

    Login or Signup to reply.
  2. The reason you are struggeling here is because you try to have multiple Source of truth.

    documentation on dataflow in SwiftUI

    You should move the code from HomeViewModel to your ReminderStore and change the static functions to instance functions. This would keep your logic in one place.

    You can pass your ReminderStore to your HomeView as an @EnvironmentObject

    This would simplify your code to:

    class ReminderStore: ObservableObject {
        @Published var reminds: [Reminder] = []
        @Published var newRemindData = Reminder.Data()
        
        private func fileURL() throws -> URL {
            try FileManager.default.url(for: .documentDirectory,
                                        in: .userDomainMask,
                                        appropriateFor: nil,
                                        create: false)
            .appendingPathComponent("reminds.data")
        }
        
        func load() {
            DispatchQueue.global(qos: .background).async {
                do {
                    let fileURL = try self.fileURL()
                    guard let file = try? FileHandle(forReadingFrom: fileURL) else {
                        return
                    }
                    let reminds = try JSONDecoder().decode([Reminder].self, from: file.availableData)
                    DispatchQueue.main.async {
                        self.reminds = reminds
                    }
                } catch {
                    DispatchQueue.main.async {
                        fatalError(error.localizedDescription)
                    }
                }
            }
        }
        
        func save() {
            do {
                let data = try JSONEncoder().encode(reminds)
                let outfile = try fileURL()
                try data.write(to: outfile)
                
            } catch {
                fatalError(error.localizedDescription)
            }
        }
        
        func addRemindData() {
            let newRemind = Reminder(data: newRemindData)
            reminds.append(newRemind)
            newRemindData = Reminder.Data()
        }
    }
    
    struct RemindersApp: App {
        @StateObject private var store = ReminderStore()
        
        var body: some Scene {
            WindowGroup {
                NavigationView {
                    HomeView() {
                        store.save()
                    }
                    .navigationBarHidden(true)
                    .environmentObject(store)
                }
                .onAppear {
                    store.load()
                }
            }
        }
    }
    
    struct HomeView: View {
        @Environment(.scenePhase) private var scenePhase
        @EnvironmentObject private var store: ReminderStore
        @State var addView = false
        let saveAction: ()->Void
        
        var body: some View {
            VStack {
                List {
                    ForEach(store.reminds) { remind in
                        Text(remind.title)
                    }
                }
            }
            .safeAreaInset(edge: .top) {
                HStack {
                    Text("Reminders")
                        .font(.title)
                        .padding()
                    
                    Spacer()
                    
                    Button(action: {
                        addView.toggle()
                    }) {
                        Image(systemName: "plus")
                            .padding()
                            .font(.title2)
                    }
                    .sheet(isPresented: $addView) {
                        NavigationView {
                            VStack {
                                Form {
                                    TextField("Title", text: $store.newRemindData.title)
                                }
                            }
                            .toolbar {
                                ToolbarItem(placement: .cancellationAction) {
                                    Button("Dismiss") {
                                        store.newRemindData = Reminder.Data()
                                        addView.toggle()
                                    }
                                }
                                ToolbarItem(placement: .principal) {
                                    Text("New Reminder")
                                        .font(.title3)
                                }
                                ToolbarItem(placement: .confirmationAction) {
                                    Button("Add") {
                                        store.addRemindData()
                                        addView.toggle()
                                    }
                                }
                            }
                        }
                    }
                    .onChange(of: scenePhase) { phase in
                        if phase == .inactive { saveAction() }
                    }
                }
            }
        }
    }
    

    An issue I would recommend solving:
    Naming a type after something that´s allready taken by Swift is a bad idea. You should rename your Data struct to something different.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search