0

I am creating custom data and I want to save them into CoreData when I make favorites. In order to do that I use the Combine framework by subscribing CoreData values back into my custom data. The problem is when I try to map both CoreData and custom data, there is something wrong and I couldn't display even my custom data on the canvas. To be honest, I don't even know what I am doing because most of the ViewModel codes are based on Nick's tutorial video (from the Swiftful Thinking Youtube channel). Please help me with what is wrong with my codes. Thanks in advance.

I create my CoreData with a name "DataContainer" with entity name "DataEntity". In DataEntity, there are three attributes:

'id' with a type "Integer32"
'isFavorite' with a type "Boolean"
'timestamp' with a type "Date"

import Foundation
import CoreData

// This is CoreData class without using Singleton
class CoreDataManager {

    private let container: NSPersistentContainer
    @Published var savedEntities: [DataEntity] = []
    
    init() {
        container = NSPersistentContainer(name: "DataContainer")
        container.loadPersistentStores { _, error in
            if let error = error {
                print("Error loading CoreData! \(error)")
            }
        }
        fetchData()
    }
    
    // MARK: Privates
    
    private func fetchData() {
        let request = NSFetchRequest<DataEntity>(entityName: "DataEntity")
        
        do {
            savedEntities = try container.viewContext.fetch(request)
        } catch let error {
            print("Error fetching DataEntity! \(error)")
        }
    }
    
    // Add to CoreData
    private func addFavorite(dataID: DataArray, onTappedFavorite: Bool) {
        let newFavorite = DataEntity(context: container.viewContext)
        newFavorite.id = Int32(dataID.id)
        newFavorite.isFavorite = onTappedFavorite
        
        applyChanges()
    }
    
    // Update time
    private func updateTime() {
        let newTime = DataEntity(context: container.viewContext)
        newTime.timestamp = Date()
    }
    
    // Save to CoreData
    private func save() {
        do {
            try container.viewContext.save()
        } catch let error {
            print("Error saving to CoreData! \(error)")
        }
    }
    
    private applyChanges() {
        save()
        updateTime()
        fetchData()
    }
    
    private func update(entity: DataEntity, updateFavorite: Bool) {
        entity.isFavorite = updateFavorite
        applyChanges()
    }
    
    private func delete(entity: DataEntity) {
        container.viewContext.delete(entity)
        applyChanges()
    }
    
    // MARK: Public
    func updateFavorite(dataID: DataArray, onTappedFavorite: Bool) {
    
        // Checking the data is already taken
        if let entity = savedEntities.first(where: { $0.id == dataID.id }) {
            if onTappedFavorite {
                update(entity: entity, updateFavorite: onTappedFavorite)
            } else {
                delete(entity: entity)
            }
        } else {
            addFavorite(dataID: dataID, onTappedFavorite: onTappedFavorite)
        }
    }

}

This will be my Model:

import Foundation
import SwiftUI

struct DataArray: Identifiable {
    let id: Int
    let cities: String
    let name1: String
    let name2: String
    
    let isFavorite: Bool
    
    func updateFavorite(favorited: Bool) -> DataArray {
        return DataArray(id: id, cities: cities, name1: name1, name2: name2, isFavorite: favorited)
    }
}

public struct ListDataArray {
    static let dot = [
    DataArray(id: 1,
        cities: "Baltimore"
        name1: "John",
        name2: "Mike",
        isFavorite: False),
        
    DataArray(id: 2,
        cities: "Frederick"),
        name1: "Joe",
        name2: "Swift",
        isFavorite: False),
        
    DataArray(id: 3,
        cities: "Catonsville"
        name1: "Susan",
        name2: "Oliver",
        isFavorite: False),
        
    // There will be a lot of data     
    ]
}

This will be my Home ViewModel:

import Foundation
import SwiftUI
import Combine

class Prospect: ObservableObject {

    @Published var datas: [DataArray] = []
    
    private let coreDataServices = CoreDataManager()
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        fetchDataArrays()
        fetchCoreData()
    }
    
    private func fetchDataArrays() {
        let items = ListDataArray.dot
        datas = items
    }
    
    private func fetchCoreData() {
        coreDataServices.$savedEntities
            .map({ (coreData) -> [DataArray] in
                
                // Here is something wrong when I check and try to convert CoreData to DataArray
                let arrays: [DataArray] = []
                return arrays
                    .compactMap { (data) -> DataArray? in
                    
                        guard let entity = coreData.first(where: { $0.id == data.id }) else {
                            return nil
                        }
                        return data.updateFavorite(favorited: entity.isFavorite)
                    }
            })
            .sink {[weak self] (receivedEntities) in
                self?.datas = receivedEntities
            }
            .store(in: &cancellables)
    }
    
    func updateFavoriteData(dataID: DataArray, isFavorite: Bool) {
        coreDataServices.updateFavorite(dataID: dataID, onTappedFavorite: isFavorite)
    }
    
    
    // To View Favorite
    @Published var showFavorite: Bool = false
}

This is my View:

import SwiftUI
struct Home: View {

    @EnvironmentObject var items: Prospect
    
    var body: some View {
        ScrollView {
        
            LazyVStack {
                ForEach(items.datas) { data in
                    
                    VStack {
                        HStack {
                            Button {
                                //Action for making favorite or unfavorite
                                items.updateFavoriteData(dataID: data, isFavorite: data.isFavorite)
                            } label: {
                                
                                Image(systemName: data.isFavorite ? "suit.heart.fill" : "suit.heart")
                            }
                            
                            Spacer()
                            
                            Button {
                                items.showFavorite.toggle()
                            } label: {
                                
                                Image(systemName: "music.note.house.fill")
                            }
                            .sheet(isPresented: $items.showFavorite) {
                                FavoriteView()
                                    .environmentObject(items)
                            }
                        }
                        
                        Text("\(data.id)")
                            .font(.title3)
                        Text(data.cities)
                            .font(.subheadline)
                            
                        Spacer()
                    }
                    padding()
                }
                .padding()
            }
        }
    }
}


struct FavoriteView: View {

    @EnvironmentObject var items: Prospect
    
    var body: some View {
        VStack {
            List {
                ForEach(items.datas) { data in
                    
                    if data.isFavorite {
                    
                        VStack(spacing: 10) {
                            
                            Text(data.cities)
                            Text(data.name1)
                            Text(data.name2)
                            
                        }
                        .font(.body)
                    
                    }
                }
                .padding()
            }
            Spacer()
        }
    }
}

struct Home_Previews: PreviewProvider {
    static var previews: some View {
        Home()
            .environmentObject(Prospect())
    }
}
10
  • @jnpdx where are you? Commented Oct 30, 2021 at 3:35
  • You define arrays as an empty array (let arrays: [DataArray] = []) and then you run compactMap on it. It will never return anything, because arrays is empty. Maybe you mean to be mapping coreData instead? It's unclear to me what the objective is in that function. Commented Oct 30, 2021 at 4:48
  • Yes it is you are right. I want map coreData but don't know how to do it. Will you able to help me. Also there is some typo at "@Private var savedEntities: [DataEntity] = []". I am fixing now. Thanks so much for the reply. Commented Oct 30, 2021 at 4:52
  • I don't know how to help your other problem because I don't really know what you're trying to do. I know that the closure passes a coreData, which is [DataArray]. What you want to do with that is unclear to me. Commented Oct 30, 2021 at 4:55
  • What I would like to perform is mapping core data and my custom DataArray[]. So that when I add an item to favorite, that will be saved to core data and I can access back in FavoriteView(). Commented Oct 30, 2021 at 5:03

1 Answer 1

1

Here is a different approach

//extend DataEntity
extension DataEntity{
    //Have a dataArray variable that links with your array
    var dataArray: DataArray?{
        ListDataArray.dot.first(where: {
            $0.id == self.id
        })
    }
}
//extend DataArray
extension DataArray{
    //have a data entity variable that retrieves the CoreData object
    var dataEntity: DataEntity{
        //Use DataEntity Manager to map
        //Find or Create
        return DataEntityManager().retrieve(dataArray: self)
    }
}
//Have an entity manager
class DataEntityManager{
    let container = PersistenceController.previewAware
    //convenience to create
    func create(id: Int32) -> DataEntity{
        let entity = DataEntity(context: container.container.viewContext)
        entity.id = id
        save()
        return entity
    }
    //for updating to centralize work
    func update(entity: DataEntity){
        //I think this is what you intend to update the timestamp when the value changes
        entity.timestamp = Date()
        //get the array variable
        var dataArry = entity.dataArray
        //See if they match to prevent loops
        if dataArry?.isFavorite != entity.isFavorite{
            //if they dont update the array
            dataArry = dataArry?.updateFavorite(favorited: entity.isFavorite)
        }else{
            //Leave alone
        }
        save()
    }
    //for updating to centralize work
    func update(dataArray: DataArray){
        //get the entity
        let entity = dataArray.dataEntity
        //See if they match to prevent loops
        if entity.isFavorite != dataArray.isFavorite{
            //if they dont update the entity
            DataEntityManager().update(entity: entity)
        }else{
            //leave alone
        }
    }
    func retrieve(dataArray: DataArray) -> DataEntity{
        let request: NSFetchRequest = DataEntity.fetchRequest()
        request.predicate = NSPredicate(format: "id == %@", dataArray.id)
        
        do{
            let result =  try controller.container.viewContext.fetch(request).first
            //This is risky because it will create a new one
            //You can handle it differently if you prefer
            let new = create(id: Int32(dataArray.id))
            update(entity: new)
            return result ?? new
        }catch{
            print(error)
            //This is risky because it will create a new one
            //You can handle it differently if you prefer
            let new = create(id: Int32(dataArray.id))
            update(entity: new)
            return new
        }
    }
    
    func save() {
        do{
            try container.container.viewContext.save()
        }catch{
            print(error)
        }
    }
}


struct DataArray: Identifiable {
    let id: Int
    let cities: String
    let name1: String
    let name2: String
    
    var isFavorite: Bool
    //Mostly your method
    mutating func updateFavorite(favorited: Bool) -> DataArray {
        //get the new value
        isFavorite = favorited
        //update the entity
        DataEntityManager().update(dataArray: self)
        //return the new
        return self
    }
}

With this you can now access the matching variable using either object

dataEntity.dataArray 

or

dataArray.dataEntity

Remember to update using the methods in the manager or the array so everything stays in sync.

Something to be aware of. CoreData objects are ObservableObjects where ever you want to see changes for the DataEntity you should wrap them in an @ObservedObject

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

2 Comments

Thanks @lorem upsum. I haven't seen this approach and I feel this is much simpler and much understandable for me. Again, thank you for sharing your knowledge and your help.
@Lian I'm glad it helped. People have different methods of doing stuff, a lot of it is styling. I use extension whenever I can to keep the logic in the model. People won't like me saying this stick to apple documentation. Some of the old guides are a little outdated but you get the basics. developer.apple.com/library/archive/documentation/Cocoa/…

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.