Core Data Insert one to many with multiples relationships - swift

Good afternoon!
I am using the core date to save the following entity.
Training Program > Activities > Series
When saving the activities within the program, I am using the method.
let programTranningDB = NSEntityDescription.insertNewObject(forEntityName: "ProgramTranningDB", into: self.getContext())
(programTranningDB as! ProgramTranningDB).addToActivities(activities)
do{
try self.getContext().save()
} catch {
print(error)
}
How can I add series into activities?
Can someone help me?

Assuming your model looks a little like this:
A TrainingProgram can have many activities.
An Activity can have many series.
You will add an instance of Series to an Activity in the same way as you add the Activity to a TrainingProgram.
You can use the auto-generated method on Activity of addToSeries(_:)
I usually keep all my data domain logic contained in a class to aid testing and separation of concerns.
class DomainLogic {
var context: NSManagedObjectContext
init(context: NSManagedObjectContext) {
self.context = context
}
func addNewProgram(named: String) -> TrainingProgram {
guard let program = NSEntityDescription.insertNewObject(forEntityName: "TrainingProgram", into: context) as? TrainingProgram else {
preconditionFailure("TrainingProgram should be created here")
}
program.name = named
return program
}
func addNewActivityToProgram(_ program: TrainingProgram) -> Activity {
guard let activity = NSEntityDescription.insertNewObject(forEntityName: "Activity", into: context) as? Activity else {
preconditionFailure("Activity should be created here")
}
program.addToActivities(activity)
return activity
}
func addNewSeriesToActivity(activity: Activity) -> Series {
guard let series = NSEntityDescription.insertNewObject(forEntityName: "Series", into: context) as? Series else {
preconditionFailure("Series should be created here")
}
activity.addToSeries(series)
return series
}
}
Which leads to a simple sequence to create a new Series and add it down the chain.
var context: NSManagedObjectContext!
func createSeries() -> Series {
let logic = DomainLogic(context: context)
let program = logic.addNewProgram(named: "Fatima's Program")
let activity = logic.addNewActivityToProgram(program)
let series = logic.addNewSeriesToActivity(activity: activity)
return series
}
You'll also need logic to recover existing TrainingProgram objects from the database.

Related

Any clean way for batch inserting coredata objects with relationships?

I have been observing high CPU times in background threads while inserting the coredata objects, and from analyser i could find that it's coming majorly because of some relationships i was creating one by one, and those could be in thousands.
So i thought if i could create them with batch insert. I can do that easily for objects using without relationships using NSBatchInsertRequest, but with relationships, I can't seem to find any clean way.Without relationships, i can easily create dictionaries and insert using the above request.
​
With relationships, i also tried using the object handler method of NSBatchInsertRequest, but even that is giving me an exception
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Illegal attempt to establish a relationship 'run' between objects in different contexts
This is how i am trying to make sure that the trackpoint getting added is using the run object from the same context as the one in which its being created
func addTrackPoints(run: RunModel, objectId: NSManagedObjectID) async throws {
let locations:[CLLocation] = run.getLocations()
let count = run.getLocations().count
var index = 0
let batchInsert = NSBatchInsertRequest(entity: TrackPoint.entity()) { (managedObject: NSManagedObject) -> Bool in
guard index < count else { return true }
if let trackPoint = managedObject as? TrackPoint {
let data = locations[index]
guard let run = try? StorageService.shared.getBackgroundContext().object(with: objectId) as? Run else {
fatalError("failed to get run object")
}
trackPoint.run = run
}
index += 1
return false
}
try await StorageService.shared.batchInsert(entity: TrackPoint.entity(), batchInsertRequest: batchInsert, context: StorageService.shared.getBackgroundContext())
}
I also tried it without accessing the object from same context but instead tried directly using the Run object that i had created. It didn't crash, but it still didn't create the relationship.Also it forced me to remove the concurrencydebug run argument.
func addTrackPoints(run: RunModel, object: Run) async throws {
let locations = run.getLocations()
let count = run.getLocations().count
var index = 0
let batchInsert = NSBatchInsertRequest(entity: TrackPoint.entity()) { (managedObject: NSManagedObject) -> Bool in
guard index < count else { return true }
if let trackPoint = managedObject as? TrackPoint {
let data:CLLocation = locations[index]
trackPoint.run = object
}
index += 1
return false
}
try await StorageService.shared.batchInsert(entity: TrackPoint.entity(), batchInsertRequest: batchInsert, context: StorageService.shared.getBackgroundContext()) }
StorageService
public func batchInsert(entity: NSEntityDescription, batchInsertRequest: NSBatchInsertRequest, context: NSManagedObjectContext? = nil) async throws {
var taskContext:NSManagedObjectContext? = context
if(taskContext == nil) {
taskContext = StorageService.shared.newTaskContext()
// Add name and author to identify source of persistent history changes.
taskContext?.name = "importContext"
taskContext?.transactionAuthor = "import\(entity.name ?? "entity")"
}
/// - Tag: performAndWait
try await taskContext?.perform {
// Execute the batch insert.
do{
let fetchResult = try taskContext?.execute(batchInsertRequest)
if let batchInsertResult = fetchResult as? NSBatchInsertResult,
let success = batchInsertResult.result as? Bool, success {
return
}
} catch {
self.logger.error("Failed to execute batch insert request. \(error)")
}
throw SSError.batchInsertError
}
logger.info("Successfully inserted data for \(entity.name ?? "entity")")
}
Any help would be deeply appreciated :-)
How app works, I send request to server, get some results and want data to be saved in core data for further usage to send request to server only when needed. so next time I will query data from database.
Here is sample:
I always save data in background context, which is configured like this:
func getBgContext() -> NSManagedObjectContext {
let bgContext = self.persistenceController.container.newBackgroundContext()
bgContext.automaticallyMergesChangesFromParent = true
bgContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return bgContext
}
Next I construct my data models like this so decoder will handle entity creation and data parsing + insertion in dbContext:
public class SomeDataModel: NSManagedObject, Codable {
var entityName: String = "SomeDataModel"
enum CodingKeys: String, CodingKey {
case id = "id"
case someData = "someData"
}
public required convenience init(from decoder: Decoder) throws {
guard
let context = decoder.userInfo[CodingUserInfoKey.managedObjectContext] as? NSManagedObjectContext,
let entity = NSEntityDescription.entity(forEntityName: "SomeDataModel", in: context)
else {
throw DecoderConfigurationError.missingManagedObjectContext
}
self.init(entity: entity, insertInto: context)
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decode(Int32.self, forKey: .id)
someData = try values.decodeIfPresent(String.self, forKey: .someData)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(someData, forKey: .someData)
}
func toExternalModel() -> SomeExternalUsableModel {
return SomeExternalUsableModel(id: id, someData: someData)
}
}
extension SomeDataModel {
#nonobjc public class func fetchRequest() -> NSFetchRequest<SomeDataModel> {
return NSFetchRequest<SomeDataModel>(entityName: "SomeDataModel")
}
#NSManaged public var someData: String?
#NSManaged public var id: Int32
}
extension SomeDataModel: Identifiable {
}
to pass dbcontext to decoder I do next:
extension CodingUserInfoKey {
static let managedObjectContext = CodingUserInfoKey(rawValue: "managedObjectContext")!
}
dbContext - create background context somewhere in API helper class, and use this context for all the parsings below.
and next I do parsing with decoder when respond from server comes:
let model = try self.dbContext.performAndWait {
let jsonDecoder = JSONDecoder()
let jsonEncoder = JSONEncoder()
// pass context to decoder/encoder
jsonDecoder.userInfo[CodingUserInfoKey.managedObjectContext] = self.dbContext
jsonEncoder.userInfo[CodingUserInfoKey.managedObjectContext] = self.dbContext
// parse model, used generic for reuse for other models
let model = try jsonDecoder.decode(T.self, from: result.data)
// after this line - all the data is parsed from response from server, and saved to dbContext, and contained in model as well
if self.dbContext.hasChanges {
do {
try self.dbContext.save()
self.dbContext.refreshAllObjects() // refresh context objects to ELIMINATE all outdated db objects in memory (specially when you will have relations, they could remain in memory until updated)
} catch let error {
// process error
}
}
return model
}
// do with saved and ready to use data in models whatever needed:
return model
and extensions used for performAndWait
extension NSManagedObjectContext {
func performAndWait<T>(_ block: () throws -> T) throws -> T? {
var result: Result<T, Error>?
performAndWait {
result = Result { try block() }
}
return try result?.get()
}
func performAndWait<T>(_ block: () -> T) -> T? {
var result: T?
performAndWait {
result = block()
}
return result
}
}

save increase int as multiple core data entities

In my swift code below I am trying to save ints to core data. Every time a user hits a button a new int is created. So if the user hits the button twice there are know 2 int entities in core data. My code below is having a runtime error and I dont know how to solve it.
pic
var pageNumber = 0
var itemName : [NSManagedObject] = []
func enterData() {
let appDeldeaget = UIApplication.shared.delegate as! AppDelegate
let context = appDeldeaget.persistentContainer.viewContext
let entity = NSEntityDescription.entity(forEntityName: "Player", in: context)
let theTitle = NSManagedObject(entity: entity!, insertInto: context)
theTitle.setValue(pageNumber, forKey: "positon")
do {
try context.save()
itemName.append(theTitle)
pageNumber += 1
}
catch {
}
self.theScores.reloadData()
positionTextField.text = ""
positionTextField.resignFirstResponder()
}
You are introducing a few complications that might be causing the issue.
First, if I understood your purpose, the itemName should not be an array of NSManagedObject, but rather an array of Player. Also, creating the theTitle can be simplified.
Try this instead of the code you proposed:
var pageNumber = 0
// If I understand correctly, you should have an array of Players
var itemName : [Player] = []
func enterData() {
let appDeldeaget = UIApplication.shared.delegate as! AppDelegate
let context = appDeldeaget.persistentContainer.viewContext
// Simpler way to create a new Core Data object
let theTitle = Player(context: context)
// Simpler way to set the position attribute
theTitle.position = pageNumber // pageNumber must be of type Int64, otherwise use Int64(pageNumber)
do {
try context.save()
itemName.append(theTitle)
pageNumber += 1
} catch {
// handle errors
}
// rest of the code
}

Swift: Saving Multiple Images to Core Data

I have a Recipe entity with a one-to-many relationship to RecipeImage. The problem I'm having is how to properly save multiple images. What I'm trying to do is once I selected the images, I store them in a tempImageArray. Once I select "Done" I try to convert images within tempImageArray into an array of data since Core Data only accept images as Binary Data. I then set my recipe.images as that array of data.
Currently, I'm only able to save 1 photo instead multiple.
I think im close to the solution but just needs some guidance. Any help you be much appreciated!
class ListDetailViewController: UITableViewController {
var recipeToEdit: Recipe?
var recipeImages = [RecipeImage]()
//image collection
var tempImageArray = [UIImage]()
private func createRecipe() {
let context = CoreDataManager.shared.persistentContainer.viewContext
let recipe = NSEntityDescription.insertNewObject(forEntityName: "Recipe", into: context) as! Recipe
recipe.setValue(textField.text, forKey: "name")
let image = NSEntityDescription.insertNewObject(forEntityName: "RecipeImage", into: context) as! RecipeImage
for i in tempImageArray {
image.imageData = i.jpegData(compressionQuality: 0.8)
recipeImages.append(image)
}
recipe.images?.setValue(recipeImages, forKey: "images")
image.recipe = recipe
do {
try context.save()
//Perform save
navigationController?.popViewController(animated: true)
self.delegate?.didAddRecipe(recipe: recipe)
} catch let saveErr {
print("Failed to save recipe", saveErr)
}
}

swift element is empty

I am trying to get the first or current exercise from my core data but swift keeps telling me that the element is empty. When i run the app and set the break points the debugger shows that the element is empty but no errors. here are the functions i am using to get the element data.
func currentWorkout() -> Workout? {
let client = currentClient()
return (appointment?.workouts as? Set<Workout>)?.first(where: { $0.client == client })
}
private func currentCard() -> Card? {
return currentWorkout()?.card
}
private func currentClientPlannedExercises() -> [ExerciseInfo] {
if let currentCard = currentCard(), let template = currentCard.template, let exerciseSets = template.exerciseSets?.array as? [ExerciseSet] {
let numCardsWithThis = (template.cardsWithThisTemplate as? Set<Card>)?.filter { $0.client != currentClient() }.count ?? 0
let exercsiseSetNumber = numCardsWithThis % exerciseSets.count
if let result = exerciseSets[exercsiseSetNumber].exercises?.array as? [ExerciseInfo] {
return result
}
}
return [ExerciseInfo]()
}
private func currentExercise() -> Exercise? {
// we can't have an exercise without a selection
guard let selectedExercise = currentExerciseInfo(), let currentCard = currentCard() else{
return nil
}
// get the first exercise on the current card that has the same exercise info as the one selected
if let exercises = currentWorkout()?.exercises as? Set<Exercise>{
return exercises.first(where: { $0.exerciseInfo == selectedExercise })
}
let exercise = Exercise(context: context)
exercise.workout = currentWorkout()
exercise.exerciseInfo = selectedExercise
//TODO: Set Seat
return exercise
}
private func currentExerciseInfo() -> ExerciseInfo? {
guard let selectedRow = exercisesTableView.indexPathForSelectedRow else {
return nil
}
return currentClientPlannedExercises()[selectedRow.row]
}
if the Issue is fetching then You can use this Code:
For Fetching the data from Core Data
var tasks: [Task] = [] //Your Entity Name in Bracket
func getData() {
do {
tasks = try context.fetch(Task.fetchRequest()) //Instead of Task your Entity Name
} catch {
print("Fetching Failed")
}
}
And use it like:
for data in tasks{
print(data.name) // All the Attributes name after data.attributename
print(data.address)
}
If it is in tableView:
let data = tasks[indexPath.row]
print(data.name)
You will get the data if it is there.
Edit to Check if Data entered or not
Print the Path like this:
let paths = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)
print(paths[0])
Go to sqlite file and open and check if there is Data or not inside that.
Edit If you are facing the issue in Adding Data to Core Data
Simple code to add Data
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let task = Task(context: context) //Entity Name here instead of Task
task.name = taskTextField.text! // Attribute name here after task.attributename
// Save the data to coredata
(UIApplication.shared.delegate as! AppDelegate).saveContext()
Hope this help.
I found the issue was in the currentExercise function it wasn't calling the first exercise until the it had an exercise. I fixed by rewriting the function
private func currentExercise() -> Exercise? {
// we can't have an exercise without a selection
guard let selectedExercise = currentExerciseInfo() else{
return nil
}
// get the first exercise on the current card that has the same exercise info as the one selected
if let exercises = currentWorkout()?.exercises as? Set<Exercise>, let firstExercise = exercises.first(where: { $0.exerciseInfo == selectedExercise }) {
return firstExercise
}
let exercise = Exercise(context: context)
exercise.workout = currentWorkout()
exercise.exerciseInfo = selectedExercise
//TODO: Set Seat
return exercise
}

Swift Firebase Reuse a function throughout the app

In my app, in a few places I am loading data from Firebase Firestore database and showing the data. The problem is I am not adopting the DRY technique and I know I shouldn't, but I am reusing this same load function in different places in my app.
func loadData() {
let user = Auth.auth().currentUser
db.collection("users").document((user?.uid)!).collection("children").getDocuments() {
QuerySnapshot, error in
if let error = error {
print("\(error.localizedDescription)")
} else {
// get all children into an array
self.childArray = QuerySnapshot!.documents.flatMap({Child(dictionary: $0.data())})
DispatchQueue.main.async {
self.childrenTableView.reloadData()
}
}
}
}
The function simply grabs all the children from the database and adds them to my child array.
Is there some better way to do this or a central place I can put this function where it can be called as and when I need it in the app instead of repeatedly adding it in multiple view controllers?
I thought about a helper class, and just calling the function, but then not sure how to add the result to the childArray in the viewcontroller I needed it?
my Child model
import UIKit
import FirebaseFirestore
protocol DocumentSerializable {
init?(dictionary:[String:Any])
}
// Child Struct
struct Child {
var name: String
var age: Int
var timestamp: Date
var imageURL: String
var dictionary:[String:Any] {
return [
"name":name,
"age":age,
"timestamp":timestamp,
"imageURL":imageURL
]
}
}
//Child Extension
extension Child : DocumentSerializable {
init?(dictionary: [String : Any]) {
guard let name = dictionary["name"] as? String,
let age = dictionary["age"] as? Int,
let imageURL = dictionary["imageURL"] as? String,
let timestamp = dictionary["timestamp"] as? Date else {
return nil
}
self.init(name: name, age: age, timestamp: timestamp, imageURL: imageURL)
}
}
EDIT: I have updated to safely unwrap the optionals. You may still have to modify as I am not sure what your Firebase structure is, nor do I know your Child initializer.
You could just write this as a static function and then reuse it everywhere. I assume you might have some class related to whatever "children" is, and that'd be the best place to implement. You could pass the results (as an option array of Child) in a completion handler so that you can do whatever you need with those results. It'd look something like this:
static func loadData(_ completion: (_ children: [Child]?)->()) {
guard let user = Auth.auth().currentUser else { completion(nil); return }
Firestore.firestore().collection("users").document(user.uid).collection("children").getDocuments() {
querySnapshot, error in
if let error = error {
print("\(error.localizedDescription)")
completion(nil)
} else {
guard let snapshot = querySnapshot else { completion(nil); return }
// get all children into an array
let children = snapshot.documents.flatMap({Child(dictionary: $0.data())})
completion(children)
}
}
}
Assuming you have this implemented in your Child class you would use it like this:
Child.loadData { (children) in
DispatchQueue.main.async {
if let loadedChildren = children {
//Do whatever you need with the children
self.childArray = loadedChildren
}
}
}