I want to rename an item in a ForEach list. When i try to load the EditListView for a selected list the entire app crashes.
This is a SwiftUI macOS app and the items are saved using CoreData.
The crash happens as soon as you click on "Edit List" for any of the list items.
It doesn't crash if i remove this view model var listVM: MyListViewModel from the EditListViewModel.
Here's the EditListView
struct EditListView: View {
let name: String
#Binding var isVisible: Bool
var list: MyListViewModel
#ObservedObject var editListVM: EditListViewModel
init(name: String,list: MyListViewModel, isVisible: Binding<Bool> ) {
self.list = list
editListVM = EditListViewModel(listVM: list)
_isVisible = isVisible
self.name = name
}
var body: some View {
VStack {
Text(name)
Button(action: {
editListItemVM.update()
}) {
Text("Update List Name")
}
Button(action: {
self.isVisible = false
}) {
Text("Cancel")
}
}......
EditListViewModel
class EditListViewModel: ObservableObject {
var listVM: MyListViewModel
#Published var name: String = ""
init(listVM: MyListViewModel) {
self.listVM = listVM
name = listVM.name
}
func update(){
....}
}
MyListViewModel
struct MyListViewModel: Identifiable {
private let myList: MyList
init(myList: MyList) {
self.myList = myList
}
var id: NSManagedObjectID {
myList.objectID
}
var name: String {
myList.name ?? ""
}
}
MyList Model
#objc(MyList)
public class MyList: NSManagedObject, BaseModel {
static var all: NSFetchRequest<MyList> {
let request: NSFetchRequest<MyList> = MyList.fetchRequest()
request.sortDescriptors = []
return request
}
}
extension MyList {
#nonobjc public class func fetchRequest() -> NSFetchRequest<MyList> {
return NSFetchRequest<MyList>(entityName: "MyList")
}
#NSManaged public var name: String?
}
extension MyList : Identifiable {
}
Here's the Main View
struct MyListsView: View {
#StateObject var vm: MyListsViewModel
#State private var showPopover: Bool = false
init(vm: MyListsViewModel) {
_vm = StateObject(wrappedValue: vm)
}
List {
Text("My Lists")
ForEach(vm.myLists) { myList in
NavigationLink {
MyListItemsHeaderView(name: myList.name)
.sheet(isPresented: $showPopover) {
EditListView(name: myList.name, list: MyListViewModel(myList: MyList()), isVisible: $showPopover)
}
}
}.contextMenu {
Button {
showPopover = true
// Show the EditListView
} label: {
Label("Edit List", systemImage: "pen.circle")
}......
First get rid of your view model objects we don't use those in SwiftUI. We use the View struct and the property wrappers like #FetchRequest make the struct behave like an object. It looks like this:
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
NavigationView {
List {
ForEach(items) { item in
ItemView(item: item)
I recommend looking at Xcode's app template with core data checked.
Then for editing you can use .sheet(item: like this:
struct ItemEditor: View {
#ObservedObject var item: Item // this is the scratch pad item
#Environment(\.managedObjectContext) private var context
#Environment(\.dismiss) private var dismiss // causes body to run
let onSave: () -> Void
#State var errorMessage: String?
var body: some View {
NavigationView {
Form {
Text(item.timestamp!, formatter: itemFormatter)
if let errorMessage = errorMessage {
Text(errorMessage)
}
Button("Update Time") {
item.timestamp = Date()
}
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
// first save the scratch pad context then call the handler which will save the view context.
do {
try context.save()
errorMessage = nil
onSave()
} catch {
let nsError = error as NSError
errorMessage = "Unresolved error \(nsError), \(nsError.userInfo)"
}
}
}
}
}
}
}
struct ItemEditorConfig: Identifiable {
let id = UUID()
let context: NSManagedObjectContext
let item: Item
init(viewContext: NSManagedObjectContext, objectID: NSManagedObjectID) {
// create the scratch pad context
context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
context.parent = viewContext
// load the item into the scratch pad
item = context.object(with: objectID) as! Item
}
}
struct EditItemButton: View {
let itemObjectID: NSManagedObjectID
#Environment(\.managedObjectContext) private var viewContext
#State var itemEditorConfig: ItemEditorConfig?
var body: some View {
Button(action: edit) {
Text("Edit")
}
.sheet(item: $itemEditorConfig, onDismiss: didDismiss) { config in
ItemEditor(item: config.item) {
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
itemEditorConfig = nil // dismiss the sheet
}
.environment(\.managedObjectContext, config.context)
}
}
func edit() {
itemEditorConfig = ItemEditorConfig(viewContext: viewContext, objectID: itemObjectID)
}
func didDismiss() {
// Handle the dismissing action.
}
}
struct ItemView: View {
#ObservedObject var item: Item
var body: some View {
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditItemButton(itemObjectID: item.objectID)
}
}
}
}
the params for EditListView in the main view were incorrect.
Fixed it with the following params:
.sheet(isPresented: $showPopover) {
EditListView(name: myList.name, list: myList, isVisible: $showPopover)
}
Related
The point of this app is to use core data to permanently add types of fruit to a list. I have two views: ContentView and SecondScreen. SecondScreen is a pop-up sheet. When I input a fruit and press 'save' in SecondScreen, I want to immediately update the list in ContentView to reflect the type of fruit that has just been added to core data as well as the other fruits which have previously been added to core data. My problem is that when I hit the 'save' button in SecondScreen, the new fruit is not immediately added to the list in ContentView. Instead, I have to restart the app to see the new fruit in the list.
Here is the class for my core data:
class CoreDataViewModel: ObservableObject {
let container: NSPersistentContainer
#Published var savedEntities: [FruitEntity] = []
init() {
container = NSPersistentContainer(name: "FruitsContainer")
container.loadPersistentStores { (description, error) in
if let error = error {
print("Error with coreData. \(error)")
}
}
fetchFruits()
}
func fetchFruits() {
let request = NSFetchRequest<FruitEntity>(entityName: "FruitEntity")
do {
savedEntities = try container.viewContext.fetch(request)
} catch let error {
print("Error fetching. \(error)")
}
}
func addFruit(text: String) {
let newFruit = FruitEntity(context: container.viewContext)
newFruit.name = text
saveData()
}
func saveData() {
do {
try container.viewContext.save()
fetchFruits()
} catch let error {
print("Error saving. \(error)")
}
}
}
Here is my ContentView struct:
struct ContentView: View {
//sheet variable
#State var showSheet: Bool = false
#StateObject var vm = CoreDataViewModel()
#State var refresh: Bool
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button(action: {
showSheet.toggle()
}, label: {
Text("Add Fruit")
})
List {
ForEach(vm.savedEntities) { entity in
Text(entity.name ?? "NO NAME")
}
}
}
.navigationTitle("Fruits")
.sheet(isPresented: $showSheet, content: {
SecondScreen(refresh: $refresh)
})
}
}
}
Here is my SecondScreen struct:
struct SecondScreen: View {
#Binding var refresh: Bool
#Environment(\.presentationMode) var presentationMode
#StateObject var vm = CoreDataViewModel()
#State var textFieldText: String = ""
var body: some View {
TextField("Add fruit here...", text: $textFieldText)
.font(.headline)
.padding(.horizontal)
Button(action: {
guard !textFieldText.isEmpty else { return }
vm.addFruit(text: textFieldText)
textFieldText = ""
presentationMode.wrappedValue.dismiss()
refresh.toggle()
}, label: {
Text("Save")
})
}
}
To try and solve this issue, I've created a #State boolean variable called 'refresh' in ContentView and bound it with the 'refresh' variable in SecondScreen. This variable is toggled when the user hits the 'save' button on SecondScreen, and I was thinking that maybe this would change the #State variable in ContentView and trigger ContentView to reload, but it doesn't work.
In your second screen , change
#StateObject var vm = CoreDataViewModel()
to
#ObservedObject var vm: CoreDataViewModel
then provide for the instances that compiler will ask for
hope it helps
You need to use #FetchRequest instead of #StateObject and NSFetchRequest. #FetchRequest will call body to update the Views when the fetch result changes.
I have a list of fruits. the struct FruitRowView provides the layout for the view of each row. In this FruitRowView, there's a TextField which I want to display the name of each fruit. I am having trouble doing this. The reason why I want to use a TextField to display the name of each fruit rather than a Text is so that users can easily edit the name of the fruit right from that TextField. In this case, fruits are the Core Data entity and the fruit name is an attribute of this entity.
Here is my core data class:
class CoreDataViewModel: ObservableObject {
let container: NSPersistentContainer
#Published var savedEntities: [FruitEntity] = []
init() {
container = NSPersistentContainer(name: "FruitsContainer")
container.loadPersistentStores { (description, error) in
if let error = error {
print("Error with coreData. \(error)")
}
}
fetchFruits()
}
func fetchFruits() {
let request = NSFetchRequest<FruitEntity>(entityName: "FruitEntity")
do {
savedEntities = try container.viewContext.fetch(request)
} catch let error {
print("Error fetching. \(error)")
}
}
func addFruit(text: String) {
let newFruit = FruitEntity(context: container.viewContext)
newFruit.name = text
saveData()
}
func saveData() {
do {
try container.viewContext.save()
fetchFruits()
} catch let error {
print("Error saving. \(error)")
}
}
}
Here is my contentView:
struct ContentView: View {
//sheet variable
#State var showSheet: Bool = false
#StateObject var vm = CoreDataViewModel()
#State var refresh: Bool
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button(action: {
showSheet.toggle()
}, label: {
Text("Add Fruit")
})
List {
ForEach(vm.savedEntities) { fruit in
FruitRowView(vm: vm, fruit: fruit)
}
}
}
.navigationTitle("Fruits")
.sheet(isPresented: $showSheet, content: {
SecondScreen(refresh: $refresh, vm: vm)
})
}
}
}
Here is my popup screen (used to create a new fruit)
struct SecondScreen: View {
#Binding var refresh: Bool
#Environment(\.presentationMode) var presentationMode
#ObservedObject var vm: CoreDataViewModel
#State var textFieldText: String = ""
var body: some View {
TextField("Add fruit here...", text: $textFieldText)
.font(.headline)
.padding(.horizontal)
Button(action: {
guard !textFieldText.isEmpty else { return }
vm.addFruit(text: textFieldText)
textFieldText = ""
presentationMode.wrappedValue.dismiss()
refresh.toggle()
}, label: {
Text("Save")
})
}
}
Here is my FruitRowView:
struct FruitRowView: View {
//instance of core data model
#ObservedObject var vm: CoreDataViewModel
var fruit: FruitEntity
#State var fruitName = fruit.name
var body: some View {
TextField("Enter fruit name", text: $fruitName)
}
}
So the error that I'm getting is: 'Cannot use instance member 'fruit' within property initializer; property initializers run before 'self' is available'. This error occurs in the FruitRowView when I try to assign fruitName to fruit.name. I assume that there's an easy workaround for this but I haven't been able to figure it out.
Since the fruitEntities in the view model is a published property, you don't need a state variable in the row. You need the binding for the row view, and you should pass it in the content view.
You don't need to pass the view model to the row view as well.
struct FruitRowView: View {
// No need to pass view model to child, only pass the data
// #ObservedObject var vm: CoreDataViewModel
#Binding var fruitName: String
// You don't need this.
// #State var fruitName = fruit.name
var body: some View {
TextField("Enter fruit name", text: $fruitName)
}
}
struct ContentView: View {
...
List {
ForEach(vm.savedEntities) { fruit in
FruitRowView(fruitName: $fruit.name)
}
}
...
}
I get to pass back data via closure, so new name is passed, but my UI is not updating. The new name of the user is printed when I go back to original view, but the text above the button is not getting that new value.
In my mind, updating startingUser should be enough to update the ContentView.
my ContentView:
#State private var startingUser: UserData?
var body: some View {
VStack {
Text(startingUser?.name ?? "no name")
Text("Create start user")
.onTapGesture {
startingUser = UserData(name: "Start User")
}
}
.sheet(item: $startingUser) { userToSend in
DetailView(user: userToSend) { newOnePassedFromWhatDoneInEDitView in
startingUser = newOnePassedFromWhatDoneInEDitView
print("✅ \(startingUser?.name)")
}
}
}
my EditView:
struct DetailView: View {
#Environment(\.dismiss) var dismiss
var user: UserData
var callBackClosure: (UserData) -> Void
#State private var name: String
var body: some View {
NavigationView {
Form {
TextField("your name", text: $name)
}
.navigationTitle("edit view")
.toolbar {
Button("dismiss") {
var newData = self.user
newData.name = name
newData.id = UUID()
callBackClosure(newData)
dismiss()
}
}
}
}
init(user: UserData, callBackClosure: #escaping (UserData) -> Void ) {
self.user = user
self.callBackClosure = callBackClosure
_name = State(initialValue: user.name)
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(user: UserData.example) { _ in}
}
}
my model
struct UserData: Identifiable, Codable, Equatable {
var id = UUID()
var name: String
static let example = UserData(name: "Luke")
static func == (lhs: UserData, rhs: UserData) -> Bool {
lhs.id == rhs.id
}
}
update
using these changes solves the matter, but my question remains valid, cannot understand the right reason why old code not working, on other projects, where sheet and text depends on the same #state var it is working.
adding
#State private var show = false
adding
.onTapGesture {
startingUser = UserData(name: "Start User")
show = true
}
changing
.sheet(isPresented: $show) {
DetailView(user: startingUser ?? UserData.example) { newOnePassedFromWhatDoneInEDitView in
startingUser = newOnePassedFromWhatDoneInEDitView
print("✅ \(startingUser!.name)")
}
}
The reason Text is not showing you the updated user name that you are passing in the closure is, your startingUser property will be set to nil when you dismiss the sheet because you have bind that property with sheet. Now after calling callBackClosure(newData) you are calling dismiss() to dismiss the sheet. To overcome this issue you can try something like this.
struct ContentView: View {
#State private var startingUser: UserData?
#State private var updatedUser: UserData?
var body: some View {
VStack {
Text(updatedUser?.name ?? "no name")
Text("Create start user")
.onTapGesture {
startingUser = UserData(name: "Start User")
}
}
.sheet(item: $startingUser) { userToSend in
DetailView(user: userToSend) { newUser in
updatedUser = newUser
print("✅ \(updatedUser?.name ?? "no name")")
}
}
}
}
I would suggest you to read the Apple documentation of sheet(item:onDismiss:content:) and check the example from the Discussion section to get more understanding.
I'm working on an iOS app that track people's medication and I got an add view and an edit view, both look almost the same with the exception that on my edit view I use the .onAppear to load all the medication data into the fields with an existing medication using let medication: Medication
My Form looks something like this:
Form {
Group {
TextField("Medication name", text: $name).disableAutocorrection(true)
TextField("Remaining quantity", text: $remainingQuantity).keyboardType(.numberPad)
TextField("Box quantity", text: $boxQuantity).keyboardType(.numberPad)
DatePicker("Date", selection: $date, in: Date()...).datePickerStyle(GraphicalDatePickerStyle())
Picker(selection: $repeatPeriod, label: Text("Repeating")) {
ForEach(RepeatPeriod.periods, id: \.self) { periods in
Text(periods).tag(periods)
}
.onAppear {
if pickerView {
self.name = self.medication.name != nil ? "\(self.medication.name!)" : ""
self.remainingQuantity = (self.medication.remainingQuantity != 0) ? "\(self.medication.remainingQuantity)" : ""
self.boxQuantity = (self.medication.boxQuantity != 0) ? "\(self.medication.boxQuantity)" : ""
self.date = self.medication.date ?? Date()
self.repeatPeriod = self.medication.repeatPeriod ?? "Nunca"
self.notes = self.medication.notes != nil ? "\(self.medication.notes!)" : ""
}
}
}
I thought of using a binding variable like isEditMode and it works fine but I had some issue related to the moc object when calling the add view that doesn't provide an object.
Here's how my editView preview looks like
struct EditMedicationSwiftUIView_Previews: PreviewProvider {
static let moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
static var previews: some View {
let medication = Medication(context: moc)
return NavigationView {
EditMedicationSwiftUIView(medication: medication)
}
}
}
Any suggestions?
Here is a simplified version of what I think you are trying to do. It uses code from a SwiftUI sample project. Just create an Xcode SwiftUI project with CoreData.
import SwiftUI
import CoreData
//Standard List Screen where you can select an item to see/edit and you find a button to add
struct ReusableParentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
//Keeps work out of the Views so it can be reused
#StateObject var vm: ReusableParentViewModel = ReusableParentViewModel()
var body: some View {
NavigationView{
List{
ForEach(items) { item in
NavigationLink {
//This is the same view as the sheet but witht he item passed fromt he list
ReusableItemView(item: item)
} label: {
VStack{
Text(item.timestamp.bound, formatter: itemFormatter)
Text(item.hasChanges.description)
}
}
}.onDelete(perform: { indexSet in
for idx in indexSet{
vm.deleteItem(item: items[idx], moc: viewContext)
}
})
}
//Show sheet to add new item
.sheet(item: $vm.newItem, onDismiss: {
vm.saveContext(moc: viewContext)
//You can also cancel/get rid of the new item/changes if the user doesn't save
//vm.cancelAddItem(moc: viewContext)
}, content: { newItem in
NavigationView{
ReusableItemView(item: newItem)
}
//Inject the VM the children Views have access to the functions
.environmentObject(vm)
})
.toolbar(content: {
ToolbarItem(placement: .automatic, content: {
//Trigger new item sheet
Button(action: {
vm.addItem(moc: viewContext)
}, label: {
Image(systemName: "plus")
})
})
})
}
//Inject the VM the children Views have access to the functions
.environmentObject(vm)
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter
}()
}
//The Item's View
struct ReusableItemView: View {
//All CoreData objects are ObservableObjects to see changes you have to wrap them in this
#ObservedObject var item: Item
#Environment(\.editMode) var editMode
var body: some View {
VStack{
if editMode?.wrappedValue == .active{
EditItemView(item: item)
}else{
ShowItemView(item: item)
}
}
.toolbar(content: {
ToolbarItem(placement: .automatic, content: {
//If you want to edit this info just press this button
Button(editMode?.wrappedValue == .active ? "done": "edit"){
if editMode?.wrappedValue == .active{
editMode?.wrappedValue = .inactive
}else{
editMode?.wrappedValue = .active
}
}
})
})
}
}
//The View to just show the items info
struct ShowItemView: View {
//All CoreData objects are ObservableObjects to see changes you have to wrap them in this
#ObservedObject var item: Item
var body: some View {
if item.timestamp != nil{
Text("Item at \(item.timestamp!)")
}else{
Text("nothing to show")
}
}
}
//The View to edit the item's info
struct EditItemView: View {
#Environment(\.managedObjectContext) private var viewContext
#EnvironmentObject var vm: ReusableParentViewModel
#Environment(\.editMode) var editMode
//All CoreData objects are ObservableObjects to see changes you have to wrap them in this
#ObservedObject var item: Item
var body: some View {
DatePicker("timestamp", selection: $item.timestamp.bound).datePickerStyle(GraphicalDatePickerStyle())
}
}
struct ReusableParentView_Previews: PreviewProvider {
static var previews: some View {
ReusableParentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
class ReusableParentViewModel: ObservableObject{
//Can be used to show a sheet when a new item is created
#Published var newItem: Item? = nil
//If you dont want to create a CoreData item immediatly just present a sheet with the AddItemView in it
#Published var presentAddSheet: Bool = false
func addItem(moc: NSManagedObjectContext) -> Item{
//You should never create an ObservableObject inside a SwiftUI View unless it is using #StateObject which doesn't apply to a CoreData object
let temp = Item(context: moc)
temp.timestamp = Date()
//Sets the newItem variable
newItem = temp
//And returns the new item for other uses
return temp
}
func cancelAddItem(moc: NSManagedObjectContext){
rollbackChagnes(moc: moc)
newItem = nil
}
func rollbackChagnes(moc: NSManagedObjectContext){
moc.rollback()
}
func deleteItem(item: Item, moc: NSManagedObjectContext){
moc.delete(item)
saveContext(moc: moc)
}
func saveContext(moc: NSManagedObjectContext){
do{
try moc.save()
}catch{
print(error)
}
}
}
And if for some reason you don't want to create a CoreData object ahead of time which seems to be what you are doing you can always Create the temp variables and make a sharable editable view that takes in #Binding for each variable you want to edit.
//The View to Add the item's info, you can show this anywhere.
struct AddItemView: View {
#Environment(\.managedObjectContext) private var viewContext
#EnvironmentObject var vm: ReusableParentViewModel
//These can be temporary variables
#State var tempTimestamp: Date = Date()
var body: some View {
EditableItemView(timestamp: $tempTimestamp)
.toolbar(content: {
ToolbarItem(placement: .navigationBarLeading, content: {
//Create and save the item
Button("save"){
let new = vm.addItem(moc: viewContext)
new.timestamp = tempTimestamp
vm.saveContext(moc: viewContext)
}
})
})
}
}
//The View to edit the item's info
struct EditItemView: View {
#EnvironmentObject var vm: ReusableParentViewModel
#Environment(\.managedObjectContext) private var viewContext
#ObservedObject var item: Item
var body: some View {
VStack{
EditableItemView(timestamp: $item.timestamp.bound)
.onDisappear(perform: {
vm.rollbackChagnes(moc: viewContext)
})
//Just save the item
Button("save"){
vm.saveContext(moc: viewContext)
}
}
}
}
//The View to edit the item's info
struct EditableItemView: View {
#Environment(\.managedObjectContext) private var viewContext
#EnvironmentObject var vm: ReusableParentViewModel
//All CoreData objects are ObservableObjects to see changes you have to wrap them in this
#Binding var timestamp: Date
var body: some View {
DatePicker("timestamp", selection: $timestamp).datePickerStyle(GraphicalDatePickerStyle())
}
}
I have some code like this:
class Data: ObservableObject {
#Published var data = dbContent
init(){
let db = Firestore.firestore()
db.collection("collection").document(userID).addSnapshotListener {
//getting data from DB and storing them as objects by appending them to data
}
}
}
struct 1View: View {
#ObservedObject var myData: Data = Data()
var body: some View {
2View(myData: self.myData)
3View(myData: self.myData)
}
}
struct 2View: View {
#State var myData: Data
var body: some View {
List(){
ForEach(data.count){ data in
Text(data)
}.onDelete(perform: deleteData) //Deletes the item
}
}
}
struct 3View: View {
#State var myData: Data
var body: some View {
List(){
ForEach(data.count){ data in
Text(data)
}.onDelete(perform: deleteData) //Deletes the item
}
}
}
Now the issue is, that I can delete the the item in the 2View. This is then also shown and I implemented the functionality that it deletes the Item in the DB as well.
So the DB data gets altered but this is not shown in the 3View until I refresh it by e.g. revisiting it.
I have no idea what the cause is. Maybe I got a wrong understanding of #Published and ObservedObject ?
#State means that the view owns the data and manages the state. Try using #ObservedObject in your child views as well. Here is an example:
Model
struct Book: Codable, Identifiable {
#DocumentID var id: String?
var title: String
var author: String
var numberOfPages: Int
enum CodingKeys: String, CodingKey {
case id
case title
case author
case numberOfPages = "pages"
}
}
ViewModel
class BooksViewModel: ObservableObject {
#Published var books = [Book]()
private var db = Firestore.firestore()
private var listenerRegistration: ListenerRegistration?
private var cancellables = Set<AnyCancellable>()
init() {
fetchData()
}
deinit {
unregister()
}
func unregister() {
if listenerRegistration != nil {
listenerRegistration?.remove()
}
}
func fetchData() {
unregister()
listenerRegistration = db.collection("books").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.books = documents.compactMap { queryDocumentSnapshot -> Book? in
return try? queryDocumentSnapshot.data(as: Book.self)
}
}
}
func deleteBooks(at offsets: IndexSet) {
self.books.remove(atOffsets: offsets)
}
}
Views
import SwiftUI
struct SampleView: View {
#ObservedObject var viewModel = BooksViewModel()
var body: some View {
VStack {
InnerListView1(viewModel: viewModel)
InnerListView2(viewModel: viewModel)
}
}
}
struct InnerListView1: View {
#ObservedObject var viewModel: BooksViewModel
var body: some View {
List {
ForEach(viewModel.books) { book in
VStack(alignment: .leading) {
Text(book.title)
.font(.headline)
Text(book.author)
.font(.subheadline)
Text("\(book.numberOfPages) pages")
.font(.subheadline)
}
}
.onDelete { indexSet in
self.viewModel.deleteBooks(at: indexSet)
}
}
}
}
struct InnerListView2: View {
#ObservedObject var viewModel: BooksViewModel
var body: some View {
List(viewModel.books) { book in
VStack(alignment: .leading) {
Text(book.title)
.font(.headline)
Text(book.author)
.font(.subheadline)
Text("\(book.numberOfPages) pages")
.font(.subheadline)
}
}
}
}
One thing I noticed when trying to reproduce your issue: if you're using CodingKeys (which you only need to do if your the attribute names on the Firestore documents are different from the attribute names on your Swift structs), you need to make sure that the id is also included. Otherwise, id will be nil, which will result in the List view not being abel to tell the items apart.