1

I am working on a SwiftUI app that uses SwiftData for data persistence.

I've implemented a ContentViewModel that handles fetching, creating, updating, and deleting operations on SwiftData entities.

The issue I'm facing is that while updates to entities are reflected immediately in the UI, create and delete operations do not update the UI until the app is restarted, even though the underlying data changes are successful and persisted.

Below is the simplified code for my ViewModel and View:

ContentViewModel:

import Combine
import Foundation
import SwiftData
import SwiftUI

@Observable
class ContentViewModel {
    var modelContext: ModelContext
    var items: [Item] = []
    var showingSheet: Bool = false
    var name: String = ""
    var content: String = ""
    var editingItem: Item?

    init(modelContext: ModelContext) {
        self.modelContext = modelContext
        self.items = fetchData()
    }

    func fetchData() -> [Item] {
        do {
            var fetchDescriptor = FetchDescriptor<Item>()
            fetchDescriptor.sortBy = [SortDescriptor<Item>(\.dateAdded, order: .forward)]
            let items = try modelContext.fetch(fetchDescriptor)
            return items
        }
        catch {
            fatalError(error.localizedDescription)
        }
    }

    func newItem() {
        editingItem = nil
        showingSheet = true
    }

    func startEditing(_ item: Item) {
        editingItem = item
        name = item.name
        content = item.content
        showingSheet = true
    }

    func clearFields() {
        name = ""
        content = ""
        editingItem = nil
    }

    func addNewItem() {
        let newItem = Item(name: name, content: content)
        insert(newItem)
        showingSheet = false
    }

    func insert(_ item: Item) {
        modelContext.insert(item)
        save()
    }

    func update(_ item: Item, newName: String, newContent: String) {
        item.name = newName
        item.content = newContent
        item.dateAdded = Date()

        save()
        showingSheet = false
    }

    func delete(_ item: Item) {
        modelContext.delete(item)
        save()
    }

    func save() {
        do {
            try modelContext.save()
        }
        catch {
            print(error.localizedDescription)
        }
    }
}

ContentView:

import SwiftData
import SwiftUI

struct ContentView: View {
    @State var viewModel: ContentViewModel

    init(modelContext: ModelContext) {
        let viewModel = ContentViewModel(modelContext: modelContext)
        _viewModel = State(initialValue: viewModel)
    }

    var body: some View {
        NavigationStack {
            List {
                ForEach(viewModel.items) { item in
                    VStack(alignment: .leading) {
                        HStack(alignment: .top) {
                            Text(item.name)
                                .bold()

                            Spacer()

                            Text(item.dateAdded.formatted(date: .numeric, time: .shortened))
                                .font(.caption)
                                .foregroundStyle(.gray)

                            Button(action: {
                                viewModel.startEditing(item)
                            }) {
                                Image(systemName: "pencil")
                                    .foregroundColor(.blue)
                            }

                            Button(action: {
                                viewModel.delete(item)
                            }) {
                                Image(systemName: "trash")
                                    .foregroundColor(.red)
                            }
                        }

                        Text(item.content)
                            .font(.subheadline)
                    }
                }
            }
            .navigationTitle("To Do List")
            .toolbar {
                ToolbarItem(placement: .automatic) {
                    Button {
                        viewModel.newItem()
                    } label: {
                        Image(systemName: "plus")
                    }
                }
            }
            .sheet(isPresented: $viewModel.showingSheet, onDismiss: {
                viewModel.clearFields()
            }) {
                Form {
                    Section {
                        TextField("Add the title...", text: $viewModel.name)
                    } header: {
                        Text("Title")
                    }

                    Section {
                        TextEditor(text: $viewModel.content)
                    } header: {
                        Text("Content / Notes")
                    }

                    Section {
                        if viewModel.editingItem != nil {
                            Button("Update Item") {
                                if let editingItem = viewModel.editingItem {
                                    viewModel.update(editingItem, newName: viewModel.name, newContent: viewModel.content)
                                }
                            }
                            .disabled(viewModel.name.isEmpty || viewModel.content.isEmpty)
                        }
                        else {
                            Button("Add Item") {
                                viewModel.addNewItem()
                            }
                            .disabled(viewModel.name.isEmpty || viewModel.content.isEmpty)
                        }
                    }
                }
            }
        }
    }
}

I ensure that the list of items is published and that I fetch the items again after each create/delete operation.

However, the UI does not refresh to show these changes immediately after they occur.

Interestingly, update operations do refresh the UI immediately.

Question: What could be causing the UI to not update immediately after create/delete operations on SwiftData entities in a SwiftUI app, and how can I ensure that these changes are reflected in the UI without needing to restart the app?

Thanks.

4
  • 2
    Use @Query instead of a view model for fetching Item objects. Or manually call fetchData() inside the view model whenever something changes Commented Feb 4, 2024 at 8:52
  • thanks for answering, but I'm testing mvvm for my project so I cannot use @Query for now. Commented Feb 4, 2024 at 8:55
  • 2
    Then you need to do it yourself, right now there is nothing in your view model that reacts to changes and updates the items property which is your responsibility when using MVVM. Commented Feb 4, 2024 at 8:56
  • thanks for answer! I understood responsibility when using MVVM with SwiftData. it was helpful :) Commented Feb 4, 2024 at 8:58

2 Answers 2

0

In SwiftUI the View struct is your view model already so you shouldn't have a class just use struct ContentView: View { for your view model data. SwiftUI uses this struct to generate the view layer, ie the V, for you automatically.

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks for your response! There's still a lot of discussion about SwiftUI and MVVM, but for me it's the inability to leverage logic that's also utilized in other views, and for more complex logic it seems complicated to mix it up with views. Do you have any advice for me?
Shared logic can go in a state struct or an environment key struct. Make sure the funcs return something you can store in state if you use classes you'll run into the kind of consistency bugs you experienced.
0

Seems like you're not calling fetchData() after inserting or deleting an item.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.