我如何让持久数据在我的提醒应用程序中工作

How would I get persistent data working in my reminder app

我有一个提醒应用程序,我正在尝试在其中实现持久数据,但每当我关闭该应用程序时,都不会保存任何数据。我知道如何让它与普通 MVC 一起工作,但我想让它与我拥有的视图模型一起工作。

我想我知道需要更改什么才能解决问题,但我不确定如何找到解决方案。我很确定在 NavigationView 下的 ReminderApp 文件中它说 HomeViewModel(reminds: store.reminds) 我认为 store.reminds 部分需要在开头与 $ 绑定但是当我尝试这样做是行不通的,而是说 HomeViewModel 提醒 属性 期望提醒而不是绑定。

ReminderStore 将提醒加载并保存到包含提醒的文件中,HomeViewModel 包含提醒数组并在用户添加新提醒时将提醒附加到数组。

如果有人知道如何让这个工作,那就太好了,因为我一直坚持这个。我的最小可重现示例代码如下。

提醒应用程序

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
                }
            }
        }
    }
}
}

主视图

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: {})
}
}

提醒商店

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()
}
}

提醒

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")
]
}

ReminderStore.save 没有及时调用。

当它调用时,它还没有 have/get 提醒数据。

这是我要确保完成的第一件事。之后您可能会 运行 陷入其他问题,但我个人会首先关注它。

你在这里苦苦挣扎的原因是因为你试图拥有多个 Source of truth

documentation on dataflow in SwiftUI

您应该将代码从 HomeViewModel 移动到您的 ReminderStore 并将 static 函数更改为实例函数。这将使您的逻辑保持在一个地方。

您可以将 ReminderStore 作为 @EnvironmentObject

传递给 HomeView

这会将您的代码简化为:

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() }
                }
            }
        }
    }
}

我建议解决的一个问题: 以 Swift 已经采用的东西命名类型是个坏主意。您应该将 Data 结构重命名为不同的名称。