Is there a way to update an object in a core data model by pressing bar button item? - swift

My goal here is to update an object in my core data by pressing the done button after editing the text.
The done button and the textfields below:
Here is some of my code,
#objc func doneTapped() {
do {
try context.save()
} catch {
print("Error saving the new information \(error)")
}
dateEditableTextF.resignFirstResponder()
selectedEventDate = dateEditableTextF.text
dateEditableTextF.isEnabled = false
costEditableTextF.resignFirstResponder()
selectedEventCost = costEditableTextF.text
costEditableTextF.isEnabled = false
gradesEditableTextF.resignFirstResponder()
selectedEventGrade = gradesEditableTextF.text
gradesEditableTextF.isEnabled = false
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(editTapped))
}
I expected that when I pressed the done button while running the app after editing the textfields, the information would update and that when I went back to the view controller, the information would be the same and my core data database would be update with an update attribute for that object.
What actually happened was when I finish editing the textfield, the data updates in the view controller, but when I leave the view controller and come back to it, the data reverts to the old entry.
I watched about 4 youtube videos of crud methods and they all were different scenarios and didn't match mine so I was hoping someone here could help. Thanks in advance.
Here's the rest of my view controller.
#IBOutlet weak var costEditableTextF: UITextField!
#IBOutlet weak var dateEditableTextF: UITextField!
#IBOutlet weak var gradesEditableTextF: UITextField!
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
var updateTheEvents = [Event]()
var selectedEventName: String?
var selectedEventDate: String?
var selectedEventCost: String?
var selectedEventGrade: String?
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = selectedEventName
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(editTapped))
if let dateToLoad = selectedEventDate {
dateEditableTextF.text = selectedEventDate
}
if let costToLoad = selectedEventCost {
costEditableTextF.text = selectedEventCost
}
if let gradesToLoad = selectedEventGrade {
gradesEditableTextF.text = selectedEventGrade
}
// Do any additional setup after loading the view.
}
#objc func editTapped() {
dateEditableTextF.becomeFirstResponder()
dateEditableTextF.isEnabled = true
costEditableTextF.isEnabled = true
gradesEditableTextF.isEnabled = true
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped))
}

First you need to create a storage to manage your persistentContainer and the CRUD operations:
class PersistenceManager {
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "your_xcdatamodeld_name") //Here you should type the name of your xcdatamodeld file without the extension .xcdatamodeld
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
return container
}()
}
Now to save data, you'll need a context. I strongly suggest that you use a global one. I had several issues accessing the store context from external functions (ie. stuff wasn't added/edited). Note that, despite it works great for me, I'm not sure wether a global context is the best practice. I have encountered any issues yet, however.
Inside of your PersistenceManager class, before the persistentContainer put the following code
static let shared = PersistenceManager()
var managedObjectContext: NSManagedObjectContext {
persistentContainer.viewContext
}
And, before and outside of the class put the following
let context = PersistenceManager.shared.managedObjectContext
...
class PersistenceManager { [...] }
Now you'll have to create your saving function like this:
func saveContext () {
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
This goes inside of the PersistenceManager class.
Now comes the fun part: You'll have to create the CRUD functions. These will all be inside of your PersistenceManager class. I'm going to show you a small demonstration about creating and editing entities.
Let's assume you have an entity named "Item" and it has the attributes name and price.
To save each item, you'll have a function like the following one:
func creaateNewItem(name: String, price: Int) -> Item {
let entity = NSEntityDescription.entity(forEntityName: Item, in: context)
let newItem = Item(entity: entity!, insertInto: context)
newItem.name = name
newItem.price = price
self.saveContext()
return newItem
}
To edit the item, you'll have to fetch it and then assign it the new values:
func editItem(currentItem: Item, newName: String, newPrice: Int) {
let currentName: String = currentItem.name! //Current name
//Looking for the item to edit
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Item")
request.predicate = NSPredicate(format: "name = %#", currentName)
request.returnsObjectsAsFaults = false
do { //Editing the item
let editedItem = (try context.fetch(request))[0] as! Item
editedItem.name = newName
editedItem.price = newPrice
self.saveContext()
} catch let error {
print("Error \n \((error))")
}
}
As you see here I passed some parameters which will allow you to customise your Items. Obviously if you need to assign default values you can remove those parameters.
Now, in your view controller, you'll create an Item array object:
my item : [Item]?
Which will be filled with your items.
To edit your saved items by pressing a bar button you'll now simply have to do the following:
#objc func editMyItem(){
let newName = "Edited Item"
let newPrice = 15
PersistenceManager().editItem(currentItem: item[indexOfYourChoice], newName: newName, newPrice: newPrice)
}
And your item will be edited!
Note that if you want the text to come from a textfield the newPrice constant will be equal to that textfield's text, for instance.

Related

How to get Custom Cell to update labels to information from core data

I'm not getting any errors, just not the result I am looking for.
Here is where I gather data from the user
(If this is not relevant please let me know so I can declutter the post)
I want my custom cell to display the core data record once it's added, but it keeps displaying placeholder labels instead of updating.
class ViewController: UIViewController,UITableViewDataSource, UITableViewDelegate
{
var parties: [NSManagedObject] = []
#IBOutlet weak var tableView: UITableView!
#IBAction func addParty(_ sender: UIBarButtonItem)
{
/*init alert controller with title, message & .alert style*/
let alert = UIAlertController(title: "New Name",
message: "Add a new name",
preferredStyle: .alert)
/*create a name text field, with placeholder "name"*/
alert.addTextField(configurationHandler: { (textFieldName) in
textFieldName.placeholder = "name"
})
/*create a ssn text field, with placeholder "ssn"*/
alert.addTextField(configurationHandler: { (textFieldSize) in
textFieldSize.placeholder = "size"
})
/*create a ssn text field, with placeholder "ssn"*/
alert.addTextField(configurationHandler: { (textFieldContact) in
textFieldContact.placeholder = "contact"
})
/*create a ssn text field, with placeholder "ssn"*/
alert.addTextField(configurationHandler: { (textFieldLocation) in
textFieldLocation.placeholder = "location"
})
/*create a save action*/
let saveAction = UIAlertAction(title: "Save", style: .default) { [unowned self] action in
/*find textfield's text (name) guard let way to get unwrap value otherwise return early*/
guard let textField = alert.textFields?.first,
let nameToSave = textField.text else {
return
}
/*find textfield's text (ssn) guard let way to get unwrap value otherwise return early*/
guard let textFieldSize = alert.textFields?[1],
let sizeToSave = textFieldSize.text else {
return
}
/*find textfield's text (ssn) guard let way to get unwrap value otherwise return early*/
guard let textFieldContact = alert.textFields?[2],
let contactToSave = textFieldContact.text else {
return
}
/*find textfield's text (ssn) guard let way to get unwrap value otherwise return early*/
guard let textFieldLocation = alert.textFields?[3],
let locationToSave = textFieldLocation.text else {
return
}
/*call save method by passing nameToSave and SSNToSave*/
self.save(name: nameToSave, size: sizeToSave, contact: contactToSave, location: locationToSave)
self.tableView.reloadData()
}
let cancelAction = UIAlertAction(title: "Cancel",
style: .default)
alert.addAction(saveAction)
alert.addAction(cancelAction)
present(alert, animated: true)
}
// Save core data function
func save(name: String, size : String, contact: String, location: String)
{
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
/*1.
Before you can save or retrieve anything from your Core Data store, you first need to get your hands on an NSManagedObjectContext. You can consider a managed object context as an in-memory “scratchpad” for working with managed objects.
Think of saving a new managed object to Core Data as a two-step process: first, you insert a new managed object into a managed object context; then, after you’re happy with your shiny new managed object, you “commit” the changes in your managed object context to save it to disk.
Xcode has already generated a managed object context as part of the new project’s template. Remember, this only happens if you check the Use Core Data checkbox at the beginning. This default managed object context lives as a property of the NSPersistentContainer in the application delegate. To access it, you first get a reference to the app delegate.
*/
let managedContext = appDelegate.persistentContainer.viewContext
/*
An NSEntityDescription object is associated with a specific class instance
Class
NSEntityDescription
A description of an entity in Core Data.
Retrieving an Entity with a Given Name here person
*/
let entity = NSEntityDescription.entity(forEntityName: "Party",
in: managedContext)!
/*
Initializes a managed object and inserts it into the specified managed object context.
init(entity: NSEntityDescription,
insertInto context: NSManagedObjectContext?)
*/
let party = NSManagedObject(entity: entity,
insertInto: managedContext)
/*
With an NSManagedObject in hand, you set the name attribute using key-value coding. You must spell the KVC key (name in this case) exactly as it appears in your Data Model
*/
party.setValue(name, forKeyPath: "name")
party.setValue(size, forKeyPath: "size")
party.setValue(contact, forKeyPath: "contact")
party.setValue(location, forKeyPath: "location")
/*
You commit your changes to person and save to disk by calling save on the managed object context. Note save can throw an error, which is why you call it using the try keyword within a do-catch block. Finally, insert the new managed object into the people array so it shows up when the table view reloads.
*/
do {
try managedContext.save()
parties.append(party)
tableView.reloadData()
} catch let error as NSError {
print("Could not save. \(error), \(error.userInfo)")
}
}
// TABLE VIEW CODE
func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int
{
return parties.count
}
//NEED TO FIX WHY CUSTOM CELL NOT DISPLAYING INFO
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
(print(tableView.dequeueReusableCell(withIdentifier: "PartyCell", for: indexPath)))
let party = parties[indexPath.row] as NSManagedObject
let cell = tableView.dequeueReusableCell(withIdentifier: "PartyCell",
for: indexPath) as! PartyCell
cell.nameLabel?.text = party.value(forKeyPath: "name") as? String
cell.sizeLabel.text = party.value(forKeyPath: "size") as? String
cell.contactLabel.text = party.value(forKeyPath: "contact") as? String
cell.locationLabel.text = party.value(forKeyPath: "location") as? String
return cell
}
override func viewDidLoad()
{
super.viewDidLoad()
}
override func didReceiveMemoryWarning()
{
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}

NSManagedObject collection properties become nil after a while

I'm working in a subclass of NSViewController where in viewDidLoad I fetch my entities from CoreData as below:
let delegate = AppDelegate.init()
let context = delegate.persistentContainer.viewContext
let fetch = PicPathEntity.entityFetchRequest()
var tempEntities: [PicPathEntity] = []
context.performAndWait {
tempEntities = try! fetch.execute()
}
entities = tempEntities
Then during the itemForRepresentedObjectAt function, it appears that the count of entities is the same, but the property values have become nil/empty.
I did some searching and found similar problems with no clear answers (at least not for Swift)
I can't seem to find information on how to properly fetch and retain the data in my entities variable.
I figured out how to solve this after reading the how the relationship between objects and context work.
Turns out all I needed to make this work, was make the context into a property of the ViewController instead of a local variable of viewDidLoad.
var entities: [PicPathEntity] = []
var context: NSManagedObjectContext!
override func viewDidLoad() {
super.viewDidLoad()
let delegate = AppDelegate.init()
context = delegate.persistentContainer.viewContext
let fetch = PicPathEntity.entityFetchRequest()
context.performAndWait {
entities = try! fetch.execute()
}
}
Due to helpful comments to this answer, I have now updated the code to:
var entities: [PicPathEntity] = []
var context: NSManagedObjectContext!
override func viewDidLoad() {
super.viewDidLoad()
let delegate = NSApplication.shared.delegate as! AppDelegate
context = delegate.persistentContainer.viewContext
let fetch = PicPathEntity.entityFetchRequest()
context.perform {
self.entities = try! fetch.execute()
self.picFinderEntityCollectionView.reloadData()
}
configureCollectionView()
}

Why tableView.reloadData() is not triggered after Core Data container.performBackgroundTask()

I am using Swift 4 to build a single view iOS 11 application that has a UITableViewController that is also defined as a delegate for a NSFetchedResultsController.
class MyTVC: UITableViewController, NSFetchedResultsControllerDeleagate {
var container:NSPersistentContainer? =
(UIApplication.shared.delegate as? AppDelegate)?.persistentContainer
var frc : NSFetchedResultsController<Student>?
override func viewDidLoad() {
container?.performBackgroundTask { context in
// adds 100 dummy records in background
for i in 1...100 {
let student = Student(context: context)
student.name = "student \(i)"
}
try? context.save() // this works because count is printed below
if let count = try? context.count(for: Student.fetchRequest()) {
print("Number of students in core data: \(count)") // prints 100
}
} // end of background inserting.
// now defining frc:
if let context = container?.viewContext {
let request:NSFetchRequest<Student> = Student.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
frc = NSFetchedResultsController<Student> (
fetchRequest: request,
managedObjectContext: context,
sectionNameKeyPath: nil,
cacheName: nil )
try? frc?.performFetch() // this works and I get no errors
tableView.reloadData()
frc.delegate = self
} // end of frc definition
}
}
If I add one row of Student using the viewContext, the frc will fire the required methods to show it in the tableView. However, the 100 dummy rows are not shown. In fact, If I try to tell the tableview to reload after the insertion is done, my app starts to behave weirdly and becomes buggy, and does not do what it should do (i.e: does not delete rows, does not edit, etc).
But If I restart my app, without calling the dummy insertion, I can see the 100 rows inserted from the previous run.
The only problem is that I can't call tableView.reloadData() from the background thread, so I tried to do this:
// after printing the count, I did this:
DispatchQueue.main.async { [weak self] in
self?.tableView.reloadData() // causes UI to behave weirdly
}
then I tried to call viewContext.perform to reload the table view in the proper thread
func viewDidLoad() {
// code for inserting 100 dummy rows in background thread
// code for defining frc and setting self as delegate
if let context = container?.viewContext {
context.perform { [weak self] in
self?.tableView.reloadData() // that also causes UI to behave weirdly
}
}
}
How can tell my tableview to reload and display the 100 dummy rows in a thread-safe manner?
override func viewDidLoad() {
super.viewDidLoad()
//Always need your delegate for the UI to be set before calling the UI's delegate functions.
frc.delegate = self
//First we can grab any already stored values.
goFetch()
//This chunk just saves. I would consider putting it into a separate function such as "goSave()" and then call that from an event handler.
container?.performBackgroundTask { context in
//We are in a different queue than the main queue, hence "backgroundTask".
for i in 1...100 {
let student = Student(context: context)
student.name = "student \(i)"
}
try? context.save() // this works because count is printed below
if let count = try? context.count(for: Student.fetchRequest()) {
print("Number of students in core data: \(count)") // prints 100
}
//Now that we are done saving its ok to fetch again.
goFetch()
}
//goFetch(); Your other code was running here would start executing before the backgroundTask is done. bad idea.
//The reason it works if you restart the app because that data you didn't let finish saving is persisted
//So the second time Even though its saving another 100 in another queue there were still at least 100 records to fetch at time of fetch.
}
func goFetch() {
if let context = container?.viewContext {
let request:NSFetchRequest<Student> = Student.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
frc = NSFetchedResultsController<Student> (
fetchRequest: request,
managedObjectContext: context,
sectionNameKeyPath: nil,
cacheName: nil )
try? frc?.performFetch()
//Now that records are both stored and fetched its safe for our delegate to access the data on the main thread.
//To me it would make sense to do a tableView reload everytime data is fetched so I placed this inside o `goFetch()`
DispatchQueue.main.async { [weak self] in
self?.tableView.reloadData()
}
}
}
After a lot of reading about the NSFetchedResultsController and the NSPersistentContainer and finally finding an important piece of information here at SO I think I have a working example.
My code is slightly different since I used a project I had for this. Anyway here is what I did:
In my view controller I had a property for my container
private var persistentContainer = NSPersistentContainer(name: coreDataModelName)
And in viewDidLoad I loaded the persistent store and created my 100 records.
persistentContainer.loadPersistentStores { persistentStoreDescription, error in
if let error = error {
print("Unable to add Persistent Store [\(error)][\(error.localizedDescription)]")
} else {
self.createFakeNotes() // Here 100 elements get created
DispatchQueue.main.async {
self.setupView() // other stuff, not relevant
self.fetchNotes() // fetch using fetch result controller
self.tableView.reloadData()
}
}
}
Below is createFakeNotes() where I use a separate context for inserting the elements in a background thread, this code is pretty much taken from Apple's Core Data programming guide but to make the UI being updated I needed to set automaticallyMergesChangesFromParent to true which I found out in this SO answer
I also delete old notes first to make the testing easier.
private func createFakeNotes() {
let deleteRequest = NSBatchDeleteRequest(fetchRequest: Note.fetchRequest())
do {
try persistentContainer.persistentStoreCoordinator.execute(deleteRequest, with: persistentContainer.viewContext)
} catch {
print("Delete error [\(error)]")
return
}
let privateContext = persistentContainer.newBackgroundContext()
privateContext.automaticallyMergesChangesFromParent = true //Important!!!
privateContext.perform {
let createDate = Date()
for i in 1...100 {
let note = Note(context: privateContext)
note.title = String(format: "Title %2d", i)
note.contents = "Content"
note.createdAt = createDate
note.updatedAt = createDate
}
do {
try privateContext.save()
do {
try self.persistentContainer.viewContext.save()
} catch {
print("Fail saving main context [\(error.localizedDescription)")
}
} catch {
print("Fail saving private context [\(error.localizedDescription)")
}
}
}
You should fetch your data by calling it from viewwillappear and then try to reload your tableview.
override func viewWillAppear(_ animated: Bool) {
getdata()
tableView.reloadData()
}
func getdata() {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
do{
persons = try context.fetch(Person.fetchRequest())
}
catch {
print("fetching failed")
}
}

Switch view controller works if no other buttons are pressed

I have a view that has a user input server info and on submit switches to another view. It works fine if they just hit submit. I have another button that is used to clear the saved server info if they wanted to change it at some point. The issue is that it doesn't work if the resetServerInfo button is pressed prior to the submit button.
User flow that works: Enter server name/Server IP -> testConnection -> submitServer -> Switches view controller
When it fails: clearServerInformation -> Enter server name/Server IP -> testConnection connection -> Submit -> Doesn't do anything
The code:
class serverSelect: UIViewController {
#IBOutlet var serverNameField: UITextField!
#IBOutlet var serverIPField: UITextField!
#IBOutlet var successLabel: UILabel!
#IBOutlet var errorLabel: UILabel!
//Submit button - checks fields aren't empty, then saves info to core data
#IBAction func submitServer(sender: UIButton) {
errorLabel.hidden = true
if(!serverNameField.text!.isEmpty && !serverIPField.text!.isEmpty){
saveName(serverNameField.text!, ip: serverIPField.text!)
switchToViewController("userselect")
}
else {
errorLabel.text = "Fields must contain data"
errorLabel.hidden = false
}
}
//Calls deleteserverinformation on button click
#IBAction func resetServerInfo(sender: UIButton) {
let result = deleteServerInformation()
if(result){
successLabel.hidden = false
successLabel.text = "Server Information Reset"
}
else {
errorLabel.hidden = false
errorLabel.text = "Failed to delete. Try again."
}
}
//Removes all server information stored in core data
func deleteServerInformation() -> Bool {
let appDel = UIApplication.sharedApplication().delegate as! AppDelegate
let context = appDel.managedObjectContext
let coord = appDel.persistentStoreCoordinator
let fetchRequest = NSFetchRequest(entityName: "Server")
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
do {
try coord.executeRequest(deleteRequest, withContext: context)
return true
} catch let error as NSError {
debugPrint(error)
return false
}
}
//Clears old server information and commits new data to core data
func saveName(name: String, ip: String) {
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext
let fetchRequest = NSFetchRequest(entityName: "Server")
let entity = NSEntityDescription.entityForName("Server",
inManagedObjectContext:managedContext)
let server = NSManagedObject(entity: entity!,
insertIntoManagedObjectContext: managedContext)
server.setValue(name, forKey: "serverName")
server.setValue(ip, forKey: "serverIP")
do {
let results =
try managedContext.executeFetchRequest(fetchRequest)
let server = results as! [NSManagedObject]
if(server.count != 0){
deleteServerInformation()
}
do {
try managedContext.save()
} catch let error as NSError {
print("Could not save \(error), \(error.userInfo)")
}
} catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
}
}
//Call to switch view controller on successful submit
func switchToViewController(identifier: String) {
let viewController = self.storyboard!.instantiateViewControllerWithIdentifier(identifier)
self.navigationController?.pushViewController(viewController, animated: true)
}
}
I am new to swift so I imagine I am just missing some conceptual thing. I really don't have a good grasp on the core data stuff yet. From what I have read the view controller flow is all related to app delegate, so could it have to deal with how my deleteServerInfo is dealing with the app del AND my SaveServer also is dealing with it and when used one after another it causes issues? I'm sorry if I am way off, but it is the only thing I can think of that really changes between the two user interaction paths.
So basically the issue is the "submitServer" function runs fine and switches view over to userselect view controller when no other button is pushed in the view controller. However, if I press the button that triggers "resetServerInfo" first (which runs fine), when i click and run "submitServer" again it no longer switches view controllers.

TableView.reloadData() doesn't work after save data into core data entity

I'm trying to insert a default record into my core data entity while the tableview first-time loaded and checked there's no data in the entity.
The data inserted just fine , but the reloadData() didn't work, after navigate to other view and navigate back to the view the data appears. no matter the reloadData() in or out of the .save() method.
override func viewDidLoad() {
let cateContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
let categoryRequest = NSFetchRequest(entityName: "Category")
categoryArray = (try! cateContext.executeFetchRequest(categoryRequest)) as! [Category]
if categoryArray.count == 0 {
let category = NSEntityDescription.insertNewObjectForEntityForName("Category", inManagedObjectContext: cateContext) as! Category
category.sno = "1"
category.category = "General"
category.locate = false
do {
try cateContext.save()
self.categoryTableView.reloadData()
} catch let saveError as NSError {
print("Saving Error : \(saveError.localizedDescription)")
}
}
//self.categoryTableView.reloadData()
}
If your are calling self.categoryTableView.reloadData() in viewDidLoad() method it will not reload your tableView twice. You need to call self.categoryTableView.reloadData() after you have checked if entity existed again.