I'm new to SwiftUI and Combine, and have a simple project that I'm using to test. I'm using the CDPublisher
class outlined in this article to create the bridge between Core Data and Combine. I have declared a Core Data entity class called ItemEntity
. It has a single property, a String
, containing a serialized JSON object. This object deserializes to a struct called Item
. My SwiftUI view simply displays a list of items returned from my ViewModel:
struct ContentView: View {
@ObservedObject
var viewModel: MyListViewModel
var body: some View {
NavigationView {
List {
ForEach(viewModel.items, id: .self.id) { item in
Text("(item.name) - (item.createdDate)")
}
}
.onAppear {
self.viewModel.fetchItems()
}
.toolbar {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
}
private func addItem() {
let entity = ItemEntity(context: viewContext)
let rand = arc4random()
var model = Item(name: "Item (rand)", createdDate: Date())
model.id = UUID().uuidString
let jsonData = try! JSONEncoder().encode(model)
entity.modelJSON = String(data: jsonData, encoding: .utf8)
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("(nsError) - (nsError.userInfo)")
}
}
}
My ViewModel
makes use of the CDPublisher
class to fetch the entities from Core Data. It also has a map function that deserializes the JSON string in each entry to an Item
instance. Eventually, an Array
of Item
objects is what is made available via my ViewModel
. It looks like this:
import Foundation
import CoreData
import Combine
import SwiftUI
class MyListViewModel: ObservableObject {
private var viewContext: NSManagedObjectContext = PersistenceController.shared.container.viewContext
@Published
var items: [Item] = []
private var cancellables = [AnyCancellable]()
func fetchItems() {
print("FETCHING...")
let decoder = JSONDecoder()
let fetchReq: NSFetchRequest<ItemEntity> = ItemEntity.fetchRequest()
CoreDataPublisher(request: fetchReq, context: self.viewContext)
.map({ (entities: [ItemEntity]) -> [Item] in
var items: [Item] = []
entities.forEach { (entity: ItemEntity) in
if let json = entity.modelJSON?.data(using: .utf8) {
if let item = try? decoder.decode(Item.self, from: json) {
items.append(item)
}
}
}
return items
})
.receive(on: DispatchQueue.main)
.replaceError(with: [])
.eraseToAnyPublisher()
.sink { completion in
print("COMPLETION : (completion)")
} receiveValue: { items in
print("SUCCESS")
items.forEach { (item) in
print("(item)")
}
self.items = items
}.store(in: &cancellables)
}
}
My code compiles and runs successfully. I see print statements originating via my fetchItems()
method, indicating that my items array contains all of the objects expected:
FETCHING...
SUCCESS
Item(name: "Item 854277542", createdDate: 2021-01-25 19:19:16 +0000)
Item(name: "Item 92334228", createdDate: 2021-01-25 19:19:17 +0000)
Item(name: "Item 405319813", createdDate: 2021-01-25 19:19:18 +0000)
Item(name: "Item 121330574", createdDate: 2021-01-25 19:19:18 +0000)
Item(name: "Item 3025980536", createdDate: 2021-01-25 19:19:19 +0000)
Item(name: "Item 1278077958", createdDate: 2021-01-25 19:19:19 +0000)
Item(name: "Item 4274618146", createdDate: 2021-01-25 19:19:19 +0000)
Item(name: "Item 2320455869", createdDate: 2021-01-25 19:19:19 +0000)
Item(name: "Item 3542559526", createdDate: 2021-01-25 19:19:22 +0000)
Item(name: "Item 4217121551", createdDate: 2021-01-25 19:19:23 +0000)
Item(name: "Item 4139555338", createdDate: 2021-01-25 19:19:24 +0000)
Item(name: "Item 1345067436", createdDate: 2021-01-25 19:20:49 +0000)
However, my UI is not displaying the items as expected. I would expect a row for each Item
object. Instead, I see multiple rows but they all have the exact same text matching the first Item
in my array. It's essentially the same row repeated over and over again.
I'm obviously doing something wrong here, but I'm not experienced enough to know exactly what. Why is it that my items property has the correct values, but I'm not seeing that reflected in my UI? Is it due to my map function and the JSON decoding? (Also, is there a better way to accomplish mapping an array of ItemEntity
entities to an array of Item
objects?) Any hints?
UPDATE: Below is the definition for my Item
struct
import Foundation
struct Item {
var name: String = ""
var createdDate: Date = Date()
}
extension Item: Codable {
}
extension Item: Identifiable {
private struct IdentifiableHolder {
static var _id: String = ""
}
var id: String {
get {
return IdentifiableHolder._id
}
set(newId) {
IdentifiableHolder._id = newId
}
}
}
question from:
https://stackoverflow.com/questions/65891565/swiftui-combine-and-core-data-items-not-being-mapped-displayed-properly