Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
258 views
in Technique[技术] by (71.8m points)

swift - SwiftUI, Combine, and Core Data -- Items not being mapped/displayed properly

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

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

Nothing about the code you have shown for fetchItems ensures that your decoded Item objects have unique id values — but the List explicitly depends upon these (id: .self.id). Instead, all your Item objects have the default id, which is "". Thus the id of every Item object is the id of the first Item object, explaining the phenomenon.

You might have been after something like this:

struct Item : Codable {
    var name: String = ""
    var createdDate: Date = Date()
    private var idHolder = IdentifiableHolder(id: UUID().uuidString)
}

extension Item: Identifiable {
    private struct IdentifiableHolder : Codable {
        var id: String = ""
    }
    var id: String {
        get {
            return idHolder.id
        }
        set(newId) {
            idHolder.id = newId
        }
    }
}

Now every Item has a unique ID and it is stored and retrieved as part of its Codable implementation.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...