My app consists of a ProductFamily entity with a name attribute and an array of PartDetail dictionaries defined as a one-to-many relationship in CoreData. For each ProductFamily, I can have many PartDetail entities (PartNumbers) but for each PartDetail, it can only be associated with one ProductFamily. My example has 5 ProductFamilies, each with an array of 5 PartDetail dictionaries. I'm struggling to get my JSON decoder correct. It's not importing any data into CoreData. You can clone my sample project here:
https://github.com/jegrasso19/ProductFinder-Test2.git
A sample of my JSON data looks like this:
[
{
"Product Family 1": [
{
"partNumber": "160-9013-900",
"orderable": true,
"pnDescription": "Part Number Description"
},
{
"partNumber": "160-9104-900",
"orderable": true,
"pnDescription": "Part Number Description"
},
{
"partNumber": "160-9105-900",
"orderable": false,
"pnDescription": "Part Number Description"
},
{
"partNumber": "160-9108-900",
"orderable": true,
"pnDescription": "Part Number Description"
},
{
"partNumber": "160-9109-900",
"orderable": true,
"pnDescription": "Part Number Description"
}
]
},
{
"Product Family 2": [
{
"partNumber": "160-9113-900",
"orderable": true,
"pnDescription": "Part Number Description"
},
{
"partNumber": "160-9114-900",
"orderable": true,
"pnDescription": "Part Number Description"
},
{
"partNumber": "160-9115-900",
"orderable": false,
"pnDescription": "Part Number Description"
},
{
"partNumber": "160-9116-900",
"orderable": true,
"pnDescription": "Part Number Description"
},
{
"partNumber": "160-9201-900",
"orderable": true,
"pnDescription": "Part Number Description"
}
]
}
]
My ProductFamilyJSON Decoder file and ProductFamilyProperties looks like this:
import Foundation
struct ProductFamilyJSON: Decodable {
// Struct that conforms with CodingKey so we can retrieve the product family name as a key
//
private struct JSONCodingKeys: CodingKey {
var stringValue: String
var intValue: Int?
init?(stringValue: String) {
self.stringValue = stringValue
}
init?(intValue: Int) {
self.init(stringValue: "\(intValue)")
self.intValue = intValue
}
}
// This is the dictionary that contains the JSON data
// The key is the ProductFamily name, and the value is an array of PartDetailInfo.
//
private(set) var productFamilies = [ProductFamilyProperties]()
init(from decoder: Decoder) throws {
var rootContainer = try decoder.unkeyedContainer()
let nestedProductFamilyContainer = try rootContainer.nestedContainer(keyedBy: JSONCodingKeys.self)
// This is where my code fails. When decoding the JSON file,
// it never goes into the while loop.
var productFamily = try ProductFamilyProperties(from: decoder)
while !rootContainer.isAtEnd {
let productFamilyKey = nestedProductFamilyContainer.allKeys.first!
if var partNumberArrayContainer = try? nestedProductFamilyContainer.nestedUnkeyedContainer(forKey: productFamilyKey) {
var partNumbers = Array<PartDetailInfo>()
while !partNumberArrayContainer.isAtEnd {
if let partNumber = try? partNumberArrayContainer.decode(PartDetailInfo.self) {
partNumbers.append(partNumber)
}
}
productFamily.code = UUID().uuidString
productFamily.name = productFamilyKey.stringValue
productFamily.partNumbers = partNumbers
productFamilies.append(productFamily)
}
}
print(productFamilies)
}
}
import Foundation
struct ProductFamilyProperties : Decodable {
var code: String
var name: String
var partNumbers: Array<PartDetailInfo>
enum CodingKeys: String, CodingKey {
case code
case name
case partNumbers
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let rawCode = try? values.decode(String.self, forKey: .code)
let rawName = try? values.decode(String.self, forKey: .name)
let rawPartNumbers = try? values.decode(Array<PartDetailInfo>.self, forKey: .partNumbers)
guard let code = rawCode,
let name = rawName,
let partNumbers = rawPartNumbers
else {
throw myError.programError("Missing Data from Product Family")
}
self.code = code
self.name = name
self.partNumbers = partNumbers
}
var dictionaryValue: [String: Any] {
[
"code": code,
"name": name,
"partNumbers": partNumbers
]
}
}
In my ProductFamilyJSON file, it seems to quit at defining the productFamily variable, which is based on my ProductFamilyProperties. This is apparently wrong but I don't know what it should be defined as. This is my first iOS app I'm trying to develop and learn from. I've spent a while learning CoreData and I've seen so many examples but very few use NSBatchInsertRequest and everyone seems to do this a little differently. I would appreciate some insight on getting this to work. Thanks.
Here is my CoreDataManager class, which contains the NSBatchInsertRequest for reference.
import Foundation
import CoreData
class CoreDataManager: ObservableObject {
let persistentContainer: NSPersistentContainer
static var shared = CoreDataManager()
var viewContext: NSManagedObjectContext {
return persistentContainer.viewContext
}
private init() {
persistentContainer = NSPersistentContainer(name: "ProductFinderTest")
persistentContainer.loadPersistentStores { (description, error) in
if let error = error {
fatalError("Unable to initialize Core Data \(error)")
}
}
let directories = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
print(directories[0])
}
func newTaskContext() -> NSManagedObjectContext {
let taskContext = persistentContainer.newBackgroundContext()
taskContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
taskContext.undoManager = nil
return taskContext
}
}
extension CoreDataManager {
func fetchProductData() async throws {
guard let url = Bundle.main.url(forResource: "ProductFamilies", withExtension: "json"),
let jsonData = try? Data(contentsOf: url)
else {
throw myError.programError("Failed to receive valid response and/or Product Family data.")
}
do {
let jsonDecoder = JSONDecoder()
// ProductFamilyJSON uses this code
let productFamilyJSON = try jsonDecoder.decode(ProductFamilyJSON.self, from: jsonData)
let productFamilyList = productFamilyJSON.productFamilies
print("Received \(productFamilyList.count) Product records.")
print("Start importing product data to the store...")
try await importProductData(from: productFamilyList)
print("Finished importing product data.")
} catch {
throw myError.programError("Wrong Data Format for Product Families")
}
}
private func importProductData(from productList: [ProductFamilyProperties]) async throws {
guard !productList.isEmpty else { return }
let taskContext = newTaskContext()
taskContext.name = "importProductDataContext"
taskContext.transactionAuthor = "importProductData"
try await taskContext.perform {
let batchInsertRequest = self.productListBatchInsertRequest(with: productList)
if let fetchResult = try? taskContext.execute(batchInsertRequest),
let batchInsertResult = fetchResult as? NSBatchInsertResult,
let success = batchInsertResult.result as? Bool, success {
return
}
else {
throw myError.programError("Failed to execute ProductList batch import request.")
}
}
print("Successfully imported Product data.")
}
private func productListBatchInsertRequest(with productList: [ProductFamilyProperties]) -> NSBatchInsertRequest {
var index = 0
let total = productList.count
let batchInsertRequest = NSBatchInsertRequest(entity: ProductFamily.entity(), dictionaryHandler: { dictionary in
guard index < total else { return true }
dictionary.addEntries(from: productList[index].dictionaryValue)
index += 1
return false
})
return batchInsertRequest
}
func requestProductFamilies() -> NSFetchedResultsController<ProductFamily> {
var fetchedResultsController: NSFetchedResultsController<ProductFamily>!
let request: NSFetchRequest = ProductFamily.fetchProductFamilyRequest()
request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
fetchedResultsController = NSFetchedResultsController(fetchRequest: request,
managedObjectContext: viewContext,
sectionNameKeyPath: nil,
cacheName: nil)
try? fetchedResultsController.performFetch()
return fetchedResultsController
}
func deleteProductData() async throws {
let taskContext = self.newTaskContext()
let fetchedResultsController = requestProductFamilies()
try fetchedResultsController.performFetch()
let productFamilies = (fetchedResultsController.fetchedObjects ?? []).map(ProductFamilyViewModel.init)
guard !productFamilies.isEmpty else {
print("ProductFamily database is empty.")
return
}
let objectIDs = productFamilies.map { $0.objectId }
print("Start deleting Product data from the store...")
try await taskContext.perform {
let batchDeleteRequest = NSBatchDeleteRequest(objectIDs: objectIDs)
guard let fetchResult = try? taskContext.execute(batchDeleteRequest),
let batchDeleteResult = fetchResult as? NSBatchDeleteResult,
let success = batchDeleteResult.result as? Bool, success
else {
throw myError.programError("Failed to execute Product Family batch delete request.")
}
}
print("Successfully deleted Product data.")
}
}
The problem was with how I was initializing the productFamily variable. I needed to initialize it with the actual values instead of as an empty variable. I also needed to move the nestedProductFamilyContainer inside the while loop. Here is the correct ProductFamilyJSON decoder file. In addition, I changed the partNumber attribute in my ProductFamily entity from NSSet to Array, which allowed more flexibility.
@vadian - I did remove the init(from:) and CodingKeys from ProductFamilyProperties as you suggested and it works just fine. Thanks for the input.
import Foundation
struct ProductFamilyJSON: Decodable {
// Struct that conforms with CodingKey so we can retrieve the product family name as a key
//
private struct JSONCodingKeys: CodingKey {
var stringValue: String
var intValue: Int?
init?(stringValue: String) {
self.stringValue = stringValue
}
init?(intValue: Int) {
self.init(stringValue: "\(intValue)")
self.intValue = intValue
}
}
// This is the dictionary that contains the JSON data
// The key is the ProductFamily name, and the value is an array of PartDetailInfo.
//
private(set) var productFamilies = [ProductFamilyProperties]()
init(from decoder: Decoder) throws {
var rootContainer = try decoder.unkeyedContainer()
while !rootContainer.isAtEnd {
let nestedProductFamilyContainer = try rootContainer.nestedContainer(keyedBy: JSONCodingKeys.self)
let productFamilyKey = nestedProductFamilyContainer.allKeys.first!
if var partNumberArrayContainer = try? nestedProductFamilyContainer.nestedUnkeyedContainer(forKey: productFamilyKey) {
var partNumbers = Array<PartDetailProperties>()
while !partNumberArrayContainer.isAtEnd {
if let partNumber = try? partNumberArrayContainer.decode(PartDetailProperties.self) {
partNumbers.append(partNumber)
}
}
let partNumbersSorted = partNumbers.sorted(by: { $0.partNumber < $1.partNumber })
let productFamily = ProductFamilyProperties(code: UUID().uuidString, name: productFamilyKey.stringValue, partNumbers: partNumbersSorted)
productFamilies.append(productFamily)
}
}
print(productFamilies)
}
}