Delete duplicated object in core data (swift) - ios

I'm saving objects to core data from a JSON, which I get using a for loop (let's say I called this setup function.
Because the user might stop this loop, the objects saved in core data will be partial. The user can restart this setup function, restarting the parsing and the procedure to save object to core data.
Now, I'm getting duplicated objects in core data if I restart the setup().
The object has an attribute which is id.
I've thought I could fetch first objects that could eventually already exist in core data, save them to an array (a custom type one), and test for each new object to add to core data if already exist one with the same id.
The code used is the following:
if !existingCards.isEmpty {
for existingCard in existingCards {
if id == existingCard.id {
moc.deleteObject(existingCard)
println("DELETED \(existingCard.name)")
}
}
}
...
// "existingCards is the array of object fetched previously.
// Code to save the object to core data.
Actually, the app return
EXC_BAD_ACCESS(code=1, address Ox0)
Is there an easier way to achieve my purpose or what should I fix to make my code work? I'm quite new to swift and I can't figure other solution.
The main purpose is to delete duplicated core data, BTW.

Swift 4 code to delete duplicate object:
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Card")
var resultsArr:[Card] = []
do {
resultsArr = try (mainManagedObjectContext!.fetch(fetchRequest) as! [Card])
} catch {
let fetchError = error as NSError
print(fetchError)
}
if resultsArr.count > 0 {
for x in resultsArr {
if x.id == id {
print("already exist")
mainManagedObjectContext.deleteObject(x)
}
}
}

At the end, I managed to make it work.
I had to rewrite my code, because I realized moc.deleteObject() works with a fetch before, which in my previous code wasn't in the same function, but it was in viewDidLoad().
// DO: - Fetch existing cards
var error: NSError?
var fetchRequest = NSFetchRequest(entityName: "Card")
if let results = moc.executeFetchRequest(fetchRequest, error: &error) as? [Card] {
if !results.isEmpty {
for x in results {
if x.id == id {
println("already exist")
moc.deleteObject(x)
}
}
}
} else {
println(error)
}
No more existingCards, the result of the the fetch is now processed as soon as possible. Something isn't clear to me yet, but now my code works. If you have any improvements/better ways, they're welcome.
P.S.: I actually found Apple reference useful but hard to understand because I don't know Obj-C. Often I can figure what the code do, but in swift functions and properties are a bit different.

Related

How to get ObjectID and search for specific ObjectID in CoreData in Swift 5?

I am currently working on a project with a multi user system. The user is able to create new profiles which are saved persistently using CoreData.
My problem is: Only one profile can be the active one at a single time, so I would like to get the ObjectID of the created profile and save it to UserDefaults.
Further I was thinking that as soon as I need the data of the active profile, I can simply get the ObjectID from UserDefaults and execute a READ - Request which only gives me back the result with that specific ObjectID.
My code so far for SAVING THE DATA:
// 1. Create new profile entry to the context.
let newProfile = Profiles(context: context)
newProfile.idProfileImage = idProfileImage
newProfile.timeCreated = Date()
newProfile.gender = gender
newProfile.name = name
newProfile.age = age
newProfile.weight = weight
// 2. Save the Object ID to User Defaults for "activeUser".
// ???????????????????
// ???????????????????
// 3. Try to save the new profile by saving the context to the persistent container.
do {
try context.save()
} catch {
print("Error saving context \(error)")
}
My code so far for READING THE DATA
// 1. Creates an request that is just pulling all the data.
let request: NSFetchRequest<Profiles> = Profiles.fetchRequest()
// 2. Try to fetch the request, can throw an error.
do {
let result = try context.fetch(request)
} catch {
print("Error reading data \(error)")
}
As you can see, I haven't been able to implement Part 2 of the first code block. The new profile gets saved but the ObjectID isn't saved to UserDefaults.
Also Party 1 of the second code block is not the final goal. The request just gives you back all the data of that entity, not only the one with the ObjectID I stored in User Defaults.
I hope you guys have an idea on how to solve this problem.
Thanks for your help in advance guys!
Since NSManagedObjectID does not conform to one of the types handled by UserDefaults, you'll have to use another way to represent the object id. Luckily, NSManagedObjectID has a uriRepresentation() that returns a URL, which can be stored in UserDefaults.
Assuming you are using a NSPersistentContainer, here's an extension that will handle the storage and retrieval of a active user Profile:
extension NSPersistentContainer {
private var managedObjectIDKey: String {
return "ActiveUserObjectID"
}
var activeUser: Profile? {
get {
guard let url = UserDefaults.standard.url(forKey: managedObjectIDKey) else {
return nil
}
guard let managedObjectID = persistentStoreCoordinator.managedObjectID(forURIRepresentation: url) else {
return nil
}
return viewContext.object(with: managedObjectID) as? Profile
}
set {
guard let newValue = newValue else {
UserDefaults.standard.removeObject(forKey: managedObjectIDKey)
return
}
UserDefaults.standard.set(newValue.objectID.uriRepresentation(), forKey: managedObjectIDKey)
}
}
}
This uses a method on NSPersistentStoreCoordinator to construct a NSManagedObjectID from a URI representation.

Core Data Creates A New Object When Updating An Old One

To start with, I don't believe this is a duplicate of: Updating object value in core data is also creating a new object (It's in Obj-C and they were calling insertNewObject every time they segued.)
Background Info: I learned how to use CoreData from the Ray Wenderlich book and referred to it when writing this code. I rolled my own custom stack as outlined in Chapter 3 if you have the book. I can show the code for this if needed.
Queue is the Entity I'm trying to update.
It has 1 property: name - String
And 1 to-many relationship: tasks: Task
My CoreData logic is in a Struct which contains the managedContext.
I have a basic find/create function to create a Queue object. This works. It creates 1 and only 1 object.
func findOrCreateMainQueue() -> Queue? {
let queue = Queue(context: managedContext)
queue.name = "Queue32"
let queueFetch: NSFetchRequest<Queue> = Queue.fetchRequest()
queueFetch.predicate = NSPredicate(format: "%K == %#", #keyPath(Queue.name), "Queue32" as CVarArg)
do {
let results = try managedContext.fetch(queueFetch)
print(results.count)
if results.count > 0 {
return results.first!
} else {
try managedContext.save()
}
} catch let error as NSError {
print("Fetch error: \(error) description: \(error.userInfo)")
}
return nil
}
(As you can see by the queue.name suffix number I have tried a lot of different things.)
I have tried just about everything I can think of:
This code is basically copy/pasted from: How do you update a CoreData entry that has already been saved in Swift?
func addTaskToMainQueue2(task: Task) {
let queueFetch: NSFetchRequest<Queue> = Queue.fetchRequest()
queueFetch.predicate = NSPredicate(format: "%K == %#", #keyPath(Queue.name), "Queue32" as CVarArg)
do {
let results = try managedContext.fetch(queueFetch)
print(results.count)
if results.count > 0 {
var tasks = results[0].tasks?.mutableCopy() as? NSMutableOrderedSet
tasks?.add(task)
results[0].setValue(tasks, forKey: "tasks")
}
} catch let error as NSError {
print("Fetch error: \(error) description: \(error.userInfo)")
}
do {
try managedContext.save()
} catch let error as NSError {
print("Save error: \(error),description: \(error.localizedDescription)")
}
}
Which causes a second Queue object to be created with the "Queue32" name.
Here is another thing I tried:
func addTaskToMainQueue(task: Task) {
if var queue = findOrCreateMainQueue() {
var tasks = queue.tasks?.mutableCopy() as? NSMutableOrderedSet
tasks?.add(task)
queue.tasks = tasks
do {
try managedContext.save()
} catch let error as NSError {
print("Save error: \(error),description: \(error.localizedDescription)")
}
}
}
For the sake of space I won't add code for other things I've tried.
I've tried using the find/create function and updating in that method.
I've tried saving the queue as a local object and passing it to the addTask function which causes duplication as well.
It also doesn't matter if I pass in the Task or create one in the addTask function.
I am starting to believe my issue is something in my dataModel file causing this as I've tried a number of 'How to update a Core Data object' tutorials online and I get the same result each time.
awakeFromInsert() is called whenever I try to update an object. Not sure if this should be happening.
In other places in my app updating works. For example, if I add a Subtask to a Task. It works fine. However, if I want to change the name of another entity called Project the object duplicates. (Project has an id attribute which is fetched, then the name attribute is changed.)
Thank you in advance. I've been pulling my hair out for hours.
I admit not having read all of your code but if you create a new managed object like this
let queue = Queue(context: managedContext)
then it will be added to the managedContext and will be saved to disk at some point. So this code
if results.count > 0 {
return results.first!
} else {
try managedContext.save()
}
is irrelevant in regard to the queue object created earlier because it will be saved even if results.count is > 0, although at a later point. So this means you will have to delete queue when the fetch is successful which feels unnecessary, better to wait with creating it
if results.count > 0 {
return results.first!
} else {
let queue = Queue(context: managedContext)
queue.name = "Queue32"
try managedContext.save()
}
Off topic but I see you return nil if a new object was created rather than fetched, is this intended?

Why I couldn't assign fetched values from Firestore to an array in Swift?

I tried several times with various ways to assign the collected values of documents from firestore into an array. Unfortunately, I could't find a way to solve this issue. I attached the code that I recently tried to implement. It includes before Firestore closure a print statement which print the whole fetched values successfully. However, after the closure and I tried to print the same array and the result is an empty array.
I tried to implement this code
var hotelCities: [String] = []
func getCities() {
db.collection("Hotels").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
var found = false
let documentDetails = document.data() as NSDictionary
let location = documentDetails["Location"] as! NSDictionary
let city = location["city"]!
if (self.hotelCities.count == 0) {
self.hotelCities.append(String(describing: city))
}
else{
for item in self.hotelCities {
if item == String(describing: city){
found = true
}
}
if (found == false){
self.hotelCities.append(String(describing: city))
}
}
}
}
print(self.hotelCities)
}
print(self.hotelCities)
}
That's actually the expected result, since data is loaded from Firestore asynchronously.
Once you call getDocuments(), the Firestore client goes of and connects to the server to read those documents. Since that may take quite some time, it allows your application to continue running in the meantime. Then when the documents are available, it calls your closure. But that means the documents are only available after the closure has been called.
It's easiest to understand this flow, by placing a few print statements:
print("Before starting to get documents");
db.collection("Hotels").getDocuments() { (querySnapshot, err) in
print("Got documents");
}
print("After starting to get documents");
When you run this code, it will print:
Before starting to get documents
After starting to get documents
Got documents
Now when you first saw this code, that is probably not the output your expected. But it completely explains why the print(self.hotelCities) you have after the closure doesn't print anything: the data hasn't been loaded yet.
The quick solution is to make sure that all code that needs the documents is inside of the close that is called when the documents are loaded. Just like your top print(self.hotelCities) statement already is.
An alternative is to define your own closure as shown in this answer: https://stackoverflow.com/a/38364861

Managed Object Context as Singleton?

I am facing issues with my app where if i create or delete a new object, then save the object within a different entity object, then go back and try to make a new object of the first entity type, my app will crash.
I can then reopen then app and make the object that crashed the app with no issue.
This is all being done via core data, there is an exercise, exercises are saved as a routine, then creating a new exercise after having created a routine will crash the app. Furthermore, deleting an exercise and a routine then trying to create a new one straight after will also crash the app
I have spend a long time reading around this and believe the likely cause is managed object context and wondered if creating it as a singleton was the solution? I set up the MoC by running the below in each VC's viewdidload:
func getMainContext() -> NSManagedObjectContext {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
return appDelegate.persistentContainer.viewContext
}
I then reference this VC level variable via .self wherever i need to reference the MoC to avoid clashes with creating further MoC within a VC.
I believed this should prevent issues as all core data work is linked to the shared MoC. However as documented above, there are still crashes occurring.
Below is a console print of the crash which hopefully will narrow down the source.
fatal error: Failure to save context: Error Domain=NSCocoaErrorDomain Code=134020 "(null)" UserInfo={NSAffectedObjectsErrorKey= (entity: UserExercise; id: 0x600000025060
The code block this is triggering off as an example as 1 location in the app it occurs is included below, to clarify this only occurs when i just deleted other objects, if i reloaded the app now this code would work and save just fine:
func createExercise() {
print("SAVE EXERCISE PRESSED")
if userExercise == nil {
print("SAVING THE NEW EXERCISE")
let newUserExercise = UserExercise(context: self.managedObjectContext!)
newUserExercise.name = userExerciseName.text
newUserExercise.sets = Int64(userSetsCount)
newUserExercise.reps = Int64(userRepsCount)
newUserExercise.dateCreated = NSDate()
newUserExercise.hasBeenTickedDone = false
} if self.associatedRoutineToAddTo != nil {
let fetchRequest: NSFetchRequest<UserRoutine> = UserRoutine.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "name == %#", self.associatedRoutineToAddTo!)
do {
let existingUserRoutine = try self.managedObjectContext!.fetch(fetchRequest).first
print("RETRIVED ROUTINES ARRAY CONTAINING \(existingUserRoutine)")
existingUserRoutine?.addToUserexercises(newUserExercise)
print("EXERCISE SUCESSFULLY ADDED TO ROUTINE")
} catch {
print("Fetching Routine Failed")
}
} else if self.associatedRoutineToAddTo == nil {
print("THIS IS A FRESH EXERCISE WITHOUT A PARENT ROUTINE")
}
} else if let userExercise = userExercise {
print("UPDATING THE EXISTING EXERCISE")
userExercise.name = userExerciseName.text
userExercise.sets = Int64(userSetsCount)
userExercise.reps = Int64(userRepsCount)
}
do {
try self.managedObjectContext?.save()
print("THE EXERCISE HAS BEEN SAVED")
} catch {
fatalError("Failure to save context: \(error)")
}
The variable declarations are:
var managedObjectContext: NSManagedObjectContext!
var userExercise: UserExercise?
var associatedRoutineToAddTo : String?
var editingUserExerciseID: NSManagedObjectID?
var editingUserExercise: UserExercise?
I was receiving the "NSCocoaErrorDomain Code=134020 (null)" error because my new entity was not added to the proper CoreData Configuration.

Realm best practices: How to handle asynchronous HTTP object update?

I have a model which is a swift object.
I retrieve data from the web and then I need to update my object but there are different cases to handle:
I create an object, fetch the data, update the properties, save it in realm
I create an object, save it in realm, fetch the data, update the properties, save it again
I create an object, save it in realm, start to fetch the data, delete it from realm, receive the data, do nothing.
And this is how I handle it:
If self.invalidated == false & self.realm == nil -> update the properties on self
If self.invalidated == false & self.realm != nil -> Fetch the object from Realm in a background thread, set the properties, Refresh Realm on main thread before completion
If self.invalidated == true -> Stop (object has been deleted so it's not needed anymore)
One solution to simplify this code is to save the object in realm, but I don't want to save an object that could be dirtier than a potential one in realm. Or I could fetch whatever I have in realm before fetching the data online, so that I'm sure I save something at least as dirty as one in realm (but performance is not as optimal as it could be)
Could you give me some insight about what is the cleanest way to handle such a case?
Here is my code at the moment:
func fetchDataOnline(completion:(success : Bool)->()){
let params = ["tmdb_id":self.tmdbId,"lang":kLang]
let tmdbId = self.tmdbId
let invoker = AWSLambdaInvoker.defaultLambdaInvoker()
invoker.invokeFunction("getMovie", JSONObject: params).continueWithBlock { (task) -> AnyObject? in
guard self.invalidated == false else{
DDLogWarn("Movie has been invalidated while fecthing data")
completion(success: false)
return nil
}
if let dic = task.result as? NSDictionary{
var objectToUpdate = self
if self.realm != nil{ //Use new realm instance
guard let newRealmInstance = try! Realm().objectForPrimaryKey(MovieNew.self, key: tmdbId) else{
DDLogError("self.realm not nil but can't find movie in realm")
completion(success: false)
return nil
}
objectToUpdate = newRealmInstance
}
try! Realm().write{
objectToUpdate.setProperties(dic: dic)
objectToUpdate.lastUpdate = NSDate()
}
}
else{ //No dictionary found from result
if let error = task.error{
DDLogError(error.description)
}
DDLogError("Error getting movie")
}
Async.main{
try! Realm().refresh()
completion(success : task.error == nil)
}
return nil
}
}
Given your specific use-case scenarios, I think this looks like the best way to go about doing it. While you could do state tracking within the Realm objects themselves, it's much better to simply track their status against their parent Realm objects and respond to that accordingly.
The main best practice for Realm is mainly to try and minimize the number of write transactions as much as possible., so the only thing I could potentially question here is if it's absolutely necessary to add an object to a Realm before performing the request, only to potentially delete it again before the download is complete. If that's necessary because you're using those objects as placeholders in your UI, then that's perfectly fine.
Either way, all of this is really a matter of opinion. This solution you've put forward is competent and fills all of your requirements, so I'm not sure if it's worth trying to find something better. :)

Resources