Change Value of Attribute Through Core Data Relationship - xcode

I'm struggling with updating a value stored in an entity that has a "one-to-many" relationship with another entity.
My project is a budgeting app. Using NSFetchedResultsController, I can successfully add a transaction (which populates in the table) and delete transactions which the FRC automatically saves.
I have an entity that stores the transactions with attributes "name" and "amount", and I have a separate entity with one attribute "theDate" (NSDate) that stores the date of the transaction.
The reason it is separate is because I believed this would organizationally make it simpler-many transactions can occur on the same date.
In the below I create the entities and store the values after creating a new transaction, then set the relationship:
#IBAction func done(segue: UIStoryboardSegue) {
let addTransactionVC = segue.sourceViewController as! AddTransaction
let newTrans = addTransactionVC.newTransaction
let date = addTransactionVC.datePicker.date
// Create Entities
let entity = NSEntityDescription.entityForName("DailyTransactions", inManagedObjectContext: self.managedContext)
let relatedEntity = NSEntityDescription.entityForName("TransDates", inManagedObjectContext: self.managedContext)
// Initialize Record
let record = NSManagedObject(entity: entity!, insertIntoManagedObjectContext: self.managedContext)
let recordDate = NSManagedObject(entity: relatedEntity!, insertIntoManagedObjectContext: self.managedContext)
// Is new transaction being created or an old one being edited?
if !addTransactionVC.isEditingTransaction {
// Populate Record
record.setValue(newTrans.name, forKey: "transName")
record.setValue(newTrans.amount, forKey: "transAmount")
recordDate.setValue(date, forKey: "theDate")
// Create Relationship of Date to Transaction
record.setValue(recordDate, forKey: "date")
} else {
// If user was editing a transaction:
let updatedObject = addTransactionVC.managedObject
let newDate = addTransactionVC.datePicker.date
// How do I change the date associated with this transaction?
updatedObject.setValue(newTrans.name, forKey: "transName")
updatedObject.setValue(newTrans.amount, forKey: "transAmount")
// This line was meant to access the attribute in the "date" relationship:
updatedObject.setValue(newDate, forKey: "date")
Everything above else works fine. After else triggers if the cell (transaction) was selected to be edited. This line: updatedObject.setValue(newDate, forKey: "date") was meant to simply update the "theDate" attribute of the TransDates entity ("date" is the name of the relationship). But I see now why that won't work.
So I tried this in the else statement:
let fetchRequestDates = NSFetchRequest(entityName: "TransDates")
do {
let dateObjects = try self.managedContext.executeFetchRequest(fetchRequestDates) as! [TransDates]
for dateObject in dateObjects {
// These 2 lines were to test that a date was being returned:
let tempDate = dateObject.theDate as! NSDate
print("dateObject is:\n\(String(tempDate))")
if dateObject.valueForKey("theDate") as! NSDate == newDate {
updatedObject.setValue(dateObject, forKey: "date")
break
}
}
} catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
}
I thought the above would simply return an array of all the TransDates objects and if a match was found (b/w what's in the array and the new NSDate) the updatedObject would get added to it, otherwise I would add some code to create the newDate as a new NSManagedObject, etc.
However when executing if dateObject.valueForKey("theDate") as! NSDate == newDate { this happens: "fatal error: unexpectedly found nil while unwrapping an Optional value".
My two questions:
In terms of what I'm trying to accomplish-update/change a date that's associated with a transaction (if the date already exists and contains other transactions, this transaction just moves over to join them), is this the best way to do it?
Do I have the purpose/functionality of relationships all wrong?
Sorry for this long-winded question, but I've been stuck here for days. So thanks in advance!

The reason you are getting this error is because you are trying to force unwrap a nil value. You should instead use an if let statement:
if let date dateObject.valueForKey("theDate") as? NSDate {
if date == newDate {
...
}
}

Related

I'm trying to delete a record out of Core Data in xCode 8/Swift 3 & latest core data syntax

I'm trying to delete an entire record out of coreData. I've retrieved the data and placed it in an array for manipulation (I have another function that lets the user edit the data using this method and it works fine) But I can't figure out how to just delete the record. [.remove(at: index)] doesn't work and neither does the code below. I can set all the fields to empty but that's not what I want, I want the record gone completely.
I went through the solutions given for similar problems but to no avail
#IBAction func Delete(_ sender: UIButton) { // The delete function
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "DestinationsOne")
let context = appDelagate.persistentContainer.viewContext
var destArray = [DestinationsOne]() // The data array
do {
try destArray = context.fetch(request) as! [DestinationsOne]} //Fetching the data and placing it in the array
catch{
//error message
}
for index in (0..<destArray.count - 1){ //Go through the records
if destArray[index].destID == IDTitle!{ //Picks the record to edit
let object = destArray[index]
context.delete(object
}
appDelagate.saveContext()
}
I figured this one out. I'm posting the solution in case anyone else has the same question
func deleteRecords() -> Void { //The function to delete the record
let moc = getContext()
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "DestinationsOne")
let result = try? moc.fetch(fetchRequest)
let resultdata = result as! [DestinationsOne] // result as entity
for object in resultdata { // Go through the fetched result
if object.destID == self.IDTitle{ // If there is a match
moc.delete(object) // delete the object
}
}
do {
try moc.save() // Save the delete action
} catch let error as NSError {
print("Could not save \(error), \(error.userInfo)")
} catch {
}
}
func getContext () -> NSManagedObjectContext {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
return appDelegate.persistentContainer.viewContext
}
deleteRecords() // Call the function
Why not applying a predicate to search this particular record. It's much more efficient than looping through a huge list.
func deleteRecords() { //The function to delete the record
let moc = getContext()
let fetchRequest = NSFetchRequest<DestinationsOne>(entityName: "DestinationsOne")
let predicate = NSPredicate(format: "destID == %#", self.IDTitle)
fetchRequest.predicate = predicate
do {
let resultdata = try moc.fetch(fetchRequest) // no type cast needed
if let objectToDelete = resultdata.first {
moc.delete(objectToDelete) // delete the object
try moc.save() // Save the delete action
}
} catch {
print("Could not save error: ", error)
}
}
Here are some issues with your code:
viewContext should be treated as readonly - you should use performBackgroundTask for all changes to core-data
You are fetching ALL of the entities and then then going through each one to find the one you want to delete. It is a lot faster to have core-data only fetch the one you want. You can do this by setting a predicate to the fetch request.
Instead of displaying your records by doing a fetch and using the array as a model, it is better to use a NSFetchedResultsController to do the fetch and manage the results. The fetchedResultsController will keep the data in sync when objects are changed, inserted or deleted. It also has delegate methods that will inform you when there are changes so you can update your view.
remove appDelagate.saveContext from your project. Apple's template code is wrong. You should never be writing to the viewContext so you should never have a reason to save it.
where is IDTitle being set? are you sure it is not nil?
(minor) for index in (0..<destArray.count - 1){ can be replaced with for (index, element) in destArray.enumerated() { which is clearer to read.

Swift 2 + Parse: Array index out of range

I have a UITableViewController which is basically a news feed. I have also implemented a pull to refresh feature. However sometimes when I pull to refresh it gives me the error
'Array index out of range'.
I know this means an item it is trying to get does not exist but can you tell me why? Here is my code:
override func viewDidLoad() {
super.viewDidLoad()
refresher = UIRefreshControl()
refresher.attributedTitle = NSAttributedString(string: "Pull to refresh")
refresher.addTarget(self, action: "refresh", forControlEvents: UIControlEvents.ValueChanged)
self.tableView.addSubview(refresher)
refresh()
tableView.delegate = self
tableView.dataSource = self
}
and the refresh() function:
func refresh() {
//get username and match with userId
let getUser = PFUser.query()
getUser?.findObjectsInBackgroundWithBlock({ (objects, error) -> Void in
if let users = objects {
//clean arrays and dictionaries so we don't get indexing error
self.messages.removeAll(keepCapacity: true)
self.usernames.removeAll(keepCapacity: true)
for object in users {
if let user = object as? PFUser {
//make userId = username
self.users[user.objectId!] = user.username!
}
}
}
})
let getPost = PFQuery(className: "Posts")
getPost.findObjectsInBackgroundWithBlock { (objects, error) -> Void in
if let objects = objects {
self.messages.removeAll(keepCapacity: true)
self.users.removeAll(keepCapacity: true)
self.usernames.removeAll(keepCapacity: true)
for object in objects {
self.messages.append(object["message"] as! String)
self.usernames.append(self.users[object["userId"] as! String]!)
self.tableView.reloadData()
}
}
}
self.refresher.endRefreshing()
}
It appears you are removing everything from users and then trying to access it in getPost.findObjectsInBackgroundWithBlock.
Specifically, self.users.removeAll(keepCapacity: true) is called just before self.usernames.append(self.users[object["userId"] as! String]!).
Also, be aware that the two queries execute asynchronously. You have no guarantee that users has been populated when the second query's completion block is reached. You will most likely want to restructure your two queries into one compound query or nest them (not recommended).
Lastly, from the looks of it, you may want to use removeAll(keepCapacity: false) rather than keeping the capacity with removeAll(keepCapacity: true).

How to append new values to an already existing value in a parse database?

I have a table in parse.com database called as test. I have a column called phone number in it. So i successfully inserted a value in the column (ex. 123456789) now i want to append another value in the same field. (Ex. 123456789,987654321) How do I do that ? I am using Parse and Xcode together. The language which I am using is swift.
I know it's Swift, but you can do it like this in Objective-c, (I'm sure it's close to the swift version):
PFQuery *query = [PFQuery queryWithClassName:#"className"];
[query getFirstObjectInBackgroundWithBlock:^(PFObject *object, NSError *error) {
if (object) {
// let's update your phone number
NSString *numbers = #"123";
NSString *newStr = [NSString stringWithFormat:#"%# %# ",object[#"telNumber"], numbers]
object[#"telNumber"] = newStr;
[object saveInBackground];
}
}];
Do you want to add another column or do you want to change the current value in the column phone number?
To add another column you can create a new colmn in parse or you create it programatically:
var stream = PFObject(className: "test")
stream["phoneNumber"] = "123"
stream["anotherValue"] = "abc"
func saveInBackgroundWithBlock(stream: PFObject) {
stream.saveInBackgroundWithBlock {
(succeeded: Bool, error: NSError?) -> Void in
var withStatus = "succ"
if !succeeded {
withStatus = "not succ"
}
print("Upload: \(withStatus)")
}
}
If you want to edit the current value you have to edit it in the following way:
let phoneNumber = "345"
stream.setValue(phoneNumber, forKey: "phoneNumber")
user?.saveInBackgroundWithBlock {
(succeeded: Bool, error: NSError?) -> Void in
var withStatus = "succ Change"
if !succeeded {
withStatus = "not SuccChange"
}
print("Upload: \(withStatus)")
}
If I answered or understood your question wrong, please feel free to correct me. Regards, Alex

Could not cast value of type 'RealmSwift.DynamicObject' to MyCustomObject during migration

During RealmSwift migration, i want to migrate from dynamic var customObject: CustomObject to let customObjectList = List<CustomObject>(). CustomObject is type of Object
This is the chunk of code within migration
let newList = List<CustomObject>()
if oldObject!["customObject"] != nil {
print(oldObject!["customObject"])
var obj = oldObject!["customObject"]
var result: CustomObject = obj as! CustomObject //Crash
farList.append(result)
}
newObject!["customObjectList"] = newList
Could not cast value of type 'RealmSwift.DynamicObject' (0x1015c82d0) to 'AppName.CustomObject' (0x1006e5550).
How do i achieve what i want? Currently what i can think of is to create a CustomObject & manually assign the values to it.
EDIT 1
I want to add a primaryKey to CustomObject. I keep getting duplicate primary key error, i'm pretty sure the key assigned is unique.
fatal error: 'try!' expression unexpectedly raised an error: Error Domain=io.realm Code=0 "Primary key property 'resultKey' has duplicate values after migration."
migration.deleteData(CustomObject.className())
if oldObject!["customObject"] != nil {
let oldSubFar = oldObject!["customObject"] as! MigrationObject
var newFarDict = oldSubFar.dictionaryWithValuesForKeys(["firstName","secondName"])
newFarDict["resultKey"] = NSUUID().UUIDString + "v1"
let newSubFar = migration.create(CustomObject.className(), value: newFarDict )
print(newSubFar) //its the updated object that i want
let subFarList = newObject!["customObjectList"] as! List<MigrationObject>
subFarList.append(newSubFar)
}
EDIT 2
I manage to figure out what is the error by making resultKey not a primary key. The app runs perfectly and when i open .realm to see the values, there are some fields with "" under resultKey -> The duplicated primary key. ><
I think what you'd like to do is like following:
Delete all CustomObject data if needed, because Migration List object cannot append existing objects.
Then you can enumerate User objects, and create each CustomObject from User's property. And new User object has customObject property, then append the CustomObject object to the list.
migration.deleteData(CustomObject.className()) // If needed
migration.enumerate(User.className()) { oldObject, newObject in
if let oldObject = oldObject,
let newObject = newObject {
let oldCustomObject = oldObject["customObject"] as! MigrationObject
let newCustomObject = migration.create(CustomObject.className(), value: oldCustomObject)
let customObjectList = newObject["customObjectList"] as! List<MigrationObject>
customObjectList.append(newCustomObject)
}
}

Variable Substitution with FetchRequests stored in a CoreData Model

I've always created my NSFetchRequests entirely in-code. Now I'm looking at the Xcode GUI for building a fetch request and storing it in the model.
I'm following an example from the Xcode Documentation. I added a Fetch Request to my model, and the predicate that has been created through the Modelling GUI is:
firstName LIKE[c] "*SUBSTRING*"
I then retrieve that request with these two lines:
NSDictionary *substituionDictionary = [NSDictionary dictionaryWithObject:#"woody" forKey:#"SUBSTRING"];
NSFetchRequest *fetchRequest = [mom fetchRequestFromTemplateWithName:#"firstNameContains" substitutionVariables:substituionDictionary];
An NSLog of the resulting NSFetchRequest outputs this:
(entity: Customer; predicate: (firstName LIKE[c] "*SUBSTRING*"); sortDescriptors: (null); limit: 0)
.. which indicates that the variable is not being substituted prior to the return of the stored FetchRequest.
So, how does one specify that text entered in the Xcode Data Modelling Fetch Request Predicate Builder GUI is intended to be substituted at runtime by NSFetchRequest:fetchRequestFromTemplateWithName:substitutionVariables: ?
Thank you!
Woody
You need to right-click on the row of the fetch request predicate editor containing the intended variable and select "VARIABLE" from the popup. Where you right-click is sometimes picky in the Xcode editor, so I tend to click just to the left of the +/- buttons.
Here is the example of substitution Variables.
First create the fetchRequest Template in the Fetch Requests Section.
Then write the code for fetch employee by firstName.
func employeeByFirstName(fName: String) -> [Employee]{
let viewContext = self.persistentContainer.viewContext
var substitutionVariables: [String: Any] = [String : Any]()
substitutionVariables["FIRSTNAME"] = fName
let fetchRequest = fetchRequestBy(name: "fetchByFirstName", variables: substitutionVariables) as! NSFetchRequest<Employee>
do {
let objects = try viewContext.fetch(fetchRequest)
return objects
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
func fetchRequestBy(name: String, variables: [String : Any]) -> NSFetchRequest<NSFetchRequestResult>? {
let model = self.persistentContainer.managedObjectModel
let fetchRequest = model.fetchRequestFromTemplate(withName: name, substitutionVariables: variables)
return fetchRequest
}

Resources