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

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.
}
}

Related

valid if an object already exists in coreData swift

I have a record of students and grades, where it is saved:
Name
Spanish rating
science grade
math qualification
physical qualification
etc.
My question is how can I check if a student has already been registered, all this is done with CoreData.
I want to avoid duplicate records.
I want to use the name to check if a record with that same name already exists or not.
if it already exists, an alert is launched and if not, it is saved.
I have little time programming.
in this way the data is saved in CoreData.
data register.
private func saveStudent(){
do{
let currentStudent = Student(context: self.context)
currentStudent.name = self.name
currentStudent.spanish = self.spanish
currentStudent.science = self.science
currentStudent.math = self.math
currentStudent.physical = self.physical
currentStudent.average = self.average
try context.save()
SCLAlertView().showSuccess("Registrado", subTitle: "Estudiante registrado.", closeButtonTitle: "Aceptar", animationStyle: .bottomToTop)
}catch{
SCLAlertView().showError("Error", subTitle: "Error al guardar los datos.", closeButtonTitle: "Aceptar", animationStyle: .bottomToTop)
}
}
so the data from Core data is retrieved, an array is filled and the array fills a tableView.
Swift
class ViewController: UIViewController {
//MARK: - Private
private var arrayOfRatings = [Student]()
private let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
recuperarDatos()
}
private func recuperarDatos(){
do{
try self.arrayOfRatings = self.context.fetch(Student.fetchRequest())
DispatchQueue.main.async {
self.tableView.reloadData()
}
}catch{
SCLAlertView().showError("Error", subTitle: "Error al recuperar datos.", closeButtonTitle: "Aceptar", animationStyle: .bottomToTop)
}
}
retrieve data and reload tableView
my problem is that it should not be possible to register 2 students with the same name.
Before registering a new student I want to check if it has not been saved before.

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

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.

Slow CloudKit table scrolling - altering existing code?

Below I have my existing query download and cell for table row code...
publicDB.perform(query, inZoneWith: nil)
{
(results, error) -> Void in
if (error != nil)
{
self.present(alert, animated: true, completion: nil)
}
else
{
for result in results!
{
self.restaurantArray.append(result)
}
OperationQueue.main.addOperation( { () -> Void in
self.tableView.reloadData()
}) } }}
downloadRestaurants()
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "restaurantcell") as? RestaurantTableCell
let restaurant: CKRecord = restaurantArray[indexPath.row]
cell?.name?.text = restaurant.value(forKey: "Name") as? String
let asset = restaurant.value(forKey: "Picture") as! CKAsset
let data = try! Data(contentsOf: asset.fileURL)
_ = UIImage(data: data)
cell?.picture?.image = UIImage(data: data)
return cell!
}
When I run this code, the app remains functional but scrolling through the 10 or so table cells is incredibly choppy. I am unsure what is causing this - all records, each containing an image, are downloaded during the query download portion of the top function. However, a problem or concept I'm missing is ever present during runtime. What am I missing here? Lazy loading? cache? something else? Unsure at this point, so any help would be incredibly helpful.
Update 1:
I've updated my code with a large thank you going to Pierce. I've had to update my code ever so slightly from his answer to maintain a ckrecord array to segue over to another controller via - restaurantArray but also create a new array for the NSObject class - tablerestaurantarray to be displayed in the current table controller.
var restaurantArray: Array<CKRecord> = []
var tablerestaurantarray: [Restaurant] = []
for result in results!
{
let tablerestaurant = Restaurant()
if let name = result.value(forKey: "Name") as! String? {
tablerestaurant.name = name
}
// Do same for image
if let imageAsset = result.object(forKey: "Picture") as! CKAsset? {
if let data = try? Data(contentsOf: imageAsset.fileURL) {
tablerestaurant.image = UIImage(data: data)
}
}
self.tablerestaurantarray.append(tablerestaurant)
self.restaurantArray.append(result)
}
OperationQueue.main.addOperation( { () -> Void in
self.tableView.reloadData()
})
}
}
}
downloadRestaurants()
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return restaurantArray.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "restaurantcell") as? RestaurantTableCell
let restaurant: Restaurant = tablerestaurantarray[indexPath.row]
cell?.name?.text = restaurant.name
cell?.picture?.image = restaurant.image
return cell!
}
The way your code is setup, whenever you scroll in your UITableView, your program is converting a CKAsset into Data, and then converting that into a UIImage, and that's within every cell! That's a rather inefficient process, so try creating an NSObject called something like Restaurant that has an image property, and when you go through all the records returned from your CKQuery, parse each record into a new Restaurant object. To create a new NSObject, go to File -> New -> File -> select 'Swift File' and add something like this:
import UIKit
class Restaurant: NSObject {
// Create a UIImage property
var image: UIImage?
// Add any other properties, i.e. name, address, etc.
var name: String = ""
}
Now for your query:
// Create an empty array of Restaurant objects
var restaurantArray: [Restaurant] = []
publicDB.perform(query, inZoneWith: nil) { (results, error) -> Void in
if (error != nil) {
self.present(alert, animated: true, completion: nil)
} else {
for result in results! {
// Create a new instance of Restaurant
let restaurant = Restaurant()
// Use optional binding to check if value exists
if let name = result.value(forKey: "Name") as! String? {
restaurant.name = name
}
// Do same for image
if let imageAsset = result.object(forKey: "Picture") as! CKAsset? {
if let data = try? Data(contentsOf: imageAsset.fileURL) {
restaurant.image = UIImage(data: data)
}
}
// Append the new Restaurant to the Restaurants array (which is now an array of Restaurant objects, NOT CKRecords)
self.restaurantArray.append(restaurant)
}
OperationQueue.main.addOperation( { () -> Void in
self.tableView.reloadData()
})
}
}
Now your cell setup is much simpler:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "restaurantcell") as? RestaurantTableCell
let restaurant: Restaurant = restaurantArray[indexPath.row]
cell?.name?.text = restaurant.name
cell?.picture?.image = restaurant.image
return cell!
}
You should use CKQueryOperation in order to implements pagination for your UITableView.
You have to set the resultLimit property to a number equals to the cell quantity visiable at one time on you table plus 3 or 4
Set recordFetchedBlock property where you have to implement the code that will apply to one CKRecord
Set queryCompletionBlock property. This is the most important part on your pagination code because this closure receive an Optional CKQueryCursor parameter.
If this CKQueryCursor is nil then you have reach the last record available for you query but if it's a non nil value, then you have more records to fetch using this CKQueryCursor as indicator to your next fetch.
When user scroll on your TableView and reach the last element you should perform another fetch with CKQueryCursor.
Other performance advice is CKAssets should be treated on separated execution queues.

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.

Primary entity vs secondary entities about Core Data saving objects

I am working on a core data application and currently I have the methods setup correctly to save the primary object saves the name of the users deck but it doesn't save recall the secondary object even though the method used to save both is identical. The primary does save second though and I am wondering if it matters the order that objects are saved. I know it is a relational but I figured it wouldn't matter if the secondary was called to save prior to the primary. I am still new to core data so just a simple answer is enough. If I need to save the primary entity object first then I will build the app in such a way that such occurs, else I may have to relook at the code to figure out why it isn't recalling.
This is the code that is supposed to save prior to the name being saved in a relational manner:
#IBAction func buttonWarrior(sender: AnyObject) {
let entity = NSEntityDescription.entityForName("ClassSelection", inManagedObjectContext: classMOC!)
let newObject = ClassSelection(entity: entity!,insertIntoManagedObjectContext: classMOC)
newObject.classname = "Warrior"
var error: NSError?
classMOC?.save(&error)
if let err = error {
println(err)
} else {
self.performSegueWithIdentifier("popOver", sender: self)
}
}
This is the code used to store the primary object which is a different viewcontroller.swift file than the other one. This is presented as a popover box over the secondary object. This part works fine and recalls correctly :
#IBAction func enterButton(sender: AnyObject) {
let entityDescription = NSEntityDescription.entityForName("Deck",inManagedObjectContext: managedObjectContext!)
let storeDeck = Deck(entity: entityDescription!,insertIntoManagedObjectContext: managedObjectContext)
storeDeck.deckname = usersDeckName.text
var error: NSError?
managedObjectContext?.save(&error)
if let err = error {
status.text = err.localizedFailureReason
} else {
usersDeckName.text = ""
status.text = "Deck Saved"
self.performSegueWithIdentifier("showCardSelection", sender: self)
}
}
The recall method I am trying to use may not make sense in it's current iteration as I have been trying many different methods :
#IBOutlet weak var decksListed: UITableView!
let managedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
var savedDecksClass = [ClassSelection]()
var frc: NSFetchedResultsController = NSFetchedResultsController()
var frcClasses: NSFetchedResultsController = NSFetchedResultsController()
func getFetchedResultsController() -> NSFetchedResultsController {
frc = NSFetchedResultsController(fetchRequest: listFetchRequest(), managedObjectContext: managedObjectContext!, sectionNameKeyPath: nil, cacheName: nil)
return frc
}
func getClassesFetchedResultsController() -> NSFetchedResultsController {
frcClasses = NSFetchedResultsController(fetchRequest: classFetchRequest(), managedObjectContext: managedObjectContext!, sectionNameKeyPath: nil, cacheName: nil)
return frcClasses
}
func listFetchRequest() -> NSFetchRequest {
let fetchRequest = NSFetchRequest(entityName: "Deck")
let sortDescriptor = NSSortDescriptor(key: "deckname", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
return fetchRequest
}
func classFetchRequest() -> NSFetchRequest {
let fetchRequestClasses = NSFetchRequest(entityName: "Deck")
let classSortDescriptor = NSSortDescriptor(key: "classname", ascending: true)
fetchRequestClasses.sortDescriptors = [classSortDescriptor]
return fetchRequestClasses
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let numberofRowsInSection = frc.sections?[section].numberOfObjects
return numberofRowsInSection!
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("usersDeck", forIndexPath: indexPath) as! UITableViewCell
let listed = frc.objectAtIndexPath(indexPath) as! Deck
cell.textLabel?.text = listed.deckname
let listedClass = frcClasses.objectAtIndexPath(indexPath) as! ClassSelection
cell.detailTextLabel!.text = listedClass.classname
return cell
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
decksListed.reloadData()
}
override func viewDidLoad() {
super.viewDidLoad()
frcClasses = getClassesFetchedResultsController()
frcClasses.delegate = self
frc.performFetch(nil)
frc = getFetchedResultsController()
frc.delegate = self
frc.performFetch(nil)
}
I hope this is enough to give you an idea. I checked the relationships out and they all seem to be correct in the model. I apologize in advanced for the way some of the code looks I plan on shrinking it down after all the editing is done and working.
Thanks to pbasdf for helping me with this one. The chat he opened actually contained exactly what was needed to be done. I just wasn't saving the relationship and passing the object from one view controller to the next. After showing me exactly how to do so with an example I figured out the rest! Basically it would never have been able to recall the object as it never knew that they were related....foolish me! Thanks again!