So I am trying to create a TableViewCell with Core Data, but when defining the cells, they all turn in the last input at the Core Data. The app is taking the user textfield input and turning into the table view cell label, and the zipInStr to the TableViewCell detail.
This is the function that add the values to the CoreData:
#IBAction func createFav(sender: AnyObject) {
//Defining variables to save at core data
var newTextInput = textInput.text
var trimmNewTextInput = newTextInput.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet())
var zipInStr: String = zipCode.text!
var appDel: AppDelegate = (UIApplication.sharedApplication().delegate as AppDelegate)
var context:NSManagedObjectContext = appDel.managedObjectContext!
var newFav = NSEntityDescription.insertNewObjectForEntityForName("Cells", inManagedObjectContext: context) as NSManagedObject
newFav.setValue(zipInStr, forKey: "favsDictKey")
newFav.setValue(trimmNewTextInput, forKey: "favsDictValues")
context.save(nil)
println(newFav)
textInput.text = String()
}
}
And this is the function that is creating the TableViewCells:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var appDel: AppDelegate = (UIApplication.sharedApplication().delegate as AppDelegate)
var context:NSManagedObjectContext = appDel.managedObjectContext!
var request = NSFetchRequest(entityName: "Cells")
request.returnsObjectsAsFaults = false
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell
var results: NSArray = context.executeFetchRequest(request, error: nil)!
println(results)
if (results.count == 0){
println("Error")
} else {
for res in results{
cell.textLabel?.text = res.valueForKey("favsDictValues") as? String
cell.detailTextLabel?.text = res.valueForKey("favsDictKey") as? String
}
}
return cell
}
I am pretty sure the error have something to do with the loop, since when I print the results I can see all the inputs with its respective values
Your setup is not correct. You should employ a NSFetchedResultsController (see Xcode template for easy code to copy) - this is the best way to populate a table view with Core Data.
In cellForRowAtIndexPath you simply retrieve the correct item for the index path. Don't put fetch request into this method, as it is potentially called many times (e.g. while scrolling).
let cell = self.fetchedResultsController.objectAtIndexPath(indexPath) as Cells
BTW, "Cells" seems to be an inappropriate name for an entity. You need a singular that describes the data you are displaying. Perhaps, "Favorite"?
As per my comment, to get your current setup working, make results into a stored property. Add
var results : NSArray = NSArray()
at the beginning of your class definition. Then move the code to populate the array from your cellForRowAtIndexPath to viewDidLoad:
var appDel: AppDelegate = (UIApplication.sharedApplication().delegate as AppDelegate)
var context:NSManagedObjectContext = appDel.managedObjectContext!
var request = NSFetchRequest(entityName: "Cells")
request.returnsObjectsAsFaults = false
results = context.executeFetchRequest(request, error: nil)!
println("\(results)")
Now, the tableView will call cellForRowAtIndexPath multiple times, once for each row that it wants to display. So the code in that method needs to configure it with the data for the relevant row. The indexPath provided as an argument to the call indicates which row (and section, if you break your table view into sections) the tableView is requesting. So your code does not need to loop through all the values in results - it just needs to reference the correct element of the array.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell
let res = results[indexPath.row] as NSManagedObject
cell.textLabel?.text = res.valueForKey("favsDictValues") as? String
cell.detailTextLabel?.text = res.valueForKey("favsDictKey") as? String
return cell
}
That should get your tableView working (unless I've messed up some optionals or casts - if so, sorry, I'm still getting up to speed with Swift).
This approach will work fine provided your data is relatively static (i.e. doesn't change whilst the tableView is being displayed) and provided you don't want to break the table into sections. In either of these situations (and, some would say, in any case), you should use an NSFetchedResultsController. That will automatically keep track of how many (and which) items appear in each section, etc, and can also be configured to automatically update the tableView if new items are added to the data whilst it is being displayed.
Related
I'm working on a Core Data project and I got a complier error for deleting item from a TableViewController:
Cannot convert value of type String to expected argument type NSManagedObject
Here's the code:
var listArr:[String] = []
override func viewDidLoad() {
super.viewDidLoad()
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
let request = NSFetchRequest<NSFetchRequestResult>(entityName:"Entity")
request.returnsObjectsAsFaults = false
do {
let results = try context.fetch(request)
if results.count > 0 {
for result in results as! [NSManagedObject] {
if let listName = result.value(forKey: "listName") {
listArr.append(listName as! String)
}
}
}
} catch {
// Handle error
print(error)
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let identifier = "cellID"
let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath)
// Configure the cell...
cell.textLabel?.text = listArr[indexPath.row]
return cell
}
// Override to support editing the table view.
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
// Delete the row from the data source
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
context.delete(listArr[indexPath.row]) //The error is here
listArr.remove(at: indexPath.row)
}
self.tableView.reloadData()
}
What do I need to change?
The delete() method that you're calling takes an NSManagedObject as its sole argument. You're passing in an element from your listArr array, which is an array of strings.
It seems obvious to say it, but if you want to delete a managed object, you need to tell Core Data which one to delete. It doesn't know what to do with a string.
I'm guessing (since you didn't say) that your listArr is made up of strings which are stored in Core Data as a property of an entity. You're fetching data somewhere and saving only the strings. That's fine as long as your data store is read-only, at least in this part of the app. You can display the strings but you can't go back and update Core Data, because you don't have the necessary information.
If that's true, what you should do is use an array of managed objects instead of an array of strings. When you need the string value, get it from one of the managed objects. When you need to delete an entry, delete that managed object.
There are two ways that you can populate a table from core-data. Either you can use a NSFetchedResultsController that tracks changes from core data and keeps your data in sync. Or you can do a single fetch and store the values you want to display in our own datasource (such as an array).
With a NSFetchedResultsController you will get updates as your data changes. With this model the correct way to delete a row is to delete it from core-data and wait for a callback from the NSFetchedResultsController to update your view.
With a single fetch (as you are doing) you do not get updates as core-data changes. In this model you can simply remove the object from your array and update the view. Updating core-data is another task unrelated to your view or datasource. In this case you should use persistentContainer performBackgroundTask to fetch the object you want to delete and then delete it in the background. For this to work you need to have a way to fetch the managedObject that you which to delete. I don't know what kind of strings you are storing - but if they are unique you could use them for your fetch. Otherwise you would also have to store some unique ID of the object such as it's objectID.
I have a static table that is bound to some Core Data values, I'm not sure how I would use NSFetchedResultsController in this instance, though I have seen discussions about how much more recommended it is.
I grab my Core Data object which is passed via Segue.
I also have a model that is setup to contain questions, with one of the properties containing the Core Data value (this is why I don't think I can use NSFetchedResultsController, as even though my Core Data entity contains some of the values I need, I'm not sure I would need a full data set)
self.surveyQuestion.append(SurveyQuestion(question: "Does the customer have a 'Proof of ownership'?", answer: coreDataEntity.isProofOfOwnership as Bool))
The questions are Survey related such as "Is your property X?" with a UiSwitch which is mapped to a Core Data value:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Configure the cell...
let cell : SurveyQuestionTableViewCell = tableView.dequeueReusableCellWithIdentifier("SurveyQuestionCell") as! SurveyQuestionTableViewCell
cell.lblQuestion.textColor = UIColor.grayColor()
let surveyQuestion = self.surveyQuestion[indexPath.row]
cell.lblQuestion.text = surveyQuestion.question
cell.toggQuestion.on = surveyQuestion.answer
if cell.toggQuestion.on {
cell.lblQuestion.textColor = UIColor.blackColor()
cell.accessoryType = .DetailDisclosureButton
}
return cell
}
Now, when I tap on the UISwitch I need it to update the Core Data value, and reload the table, its hooked up to a CustomTableViewCell like so:
*edit - Nearly got this thing working! heres my UITableViewCell class
class SurveyQuestionTableViewCell: UITableViewCell {
let managedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
#IBOutlet weak var lblQuestion: UILabel!
#IBOutlet weak var toggQuestion: UISwitch!
var surveyQuestionReference : SurveyQuestion?
var tableViewReference : UITableView?
#IBAction func toggledQuestion(sender: AnyObject) {
let tempContext: NSManagedObjectContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
tempContext.parentContext = self.managedObjectContext
tempContext.performBlock({
let entityName = "CoreDataEntity"
let request = NSFetchRequest(entityName: entityName)
request.predicate = NSPredicate(format: "id = %#", self.surveyQuestionReference!.id)
do {
let results = try tempContext.executeFetchRequest(request) as? [NSManagedObject]
if results!.count > 0{
if let moc = self.managedObjectContext{
moc.performBlockAndWait({
for result in results!{
result.setValue(self.toggQuestion.on, forKey: (self.surveyQuestionReference?.property)!)
}
})
}
}
do {
try tempContext.save()
//completion(finished: true)
} catch let error {
print(error)
}
}catch{
print("error")
}
})
print(sender)
dispatch_async(dispatch_get_main_queue()) {
self.tableViewReference!.reloadData()
}
}
I can obviously access the bit where the toggle is triggered, but this class doesn't know anything about the Core Data bit, I was thinking about using notifications but that just seems kind of messy...
when you create your cell, pass in a reference to the coredata object, and the tableView itself and store them as attributes of SurveyQuestionTableViewCell, then you can do everything you need to in setSelected()
in your custom cell class, add an attribute for the question
class SurveyQuestionTableViewCell: UITableViewCell {
#IBOutlet weak var lblQuestion: UILabel!
#IBOutlet weak var toggQuestion: UISwitch!
var surveyQuestionReference : SurveyQuestionType
vat tableViewReference : UITableView
...
and then in cellForRowAtIndexPath after you create the cell
cell.surveyQuestionReference = surveyQuestion
cell.tableViewReference = tableView
where SurveyQuestionType is whatever you have previously defined
in setSelected, you can use those stored attributes
surveyQuestionReference = self.toggQuestion.on
tableViewReference.reloadData()
Here's another option, using a shared Instance
import Foundation
import MapKit
import CoreData
class DataModelInstance : NSObject, NSCoding
{
var appDelegate : AppDelegate?
var managedContext : NSManagedObjectContext?
var persistentStoreCoordinator : NSPersistentStoreCoordinator?
// plus whatever else you need
class var sharedInstance : DataModelInstance
{
struct Singleton
{
static let instance = DataModelInstance()
}
return Singleton.instance
}
and then in any class which needs access to this data model
var dataModel = DataModelInstance.sharedInstance
I know there are those who just won't ever use singletons, but it can be a much more elegant solution to making these attributes available where they are needed
With a shared data model, you can simply move all of your data attributes out of the class they are currently in, and reference them through the data model - then if you have the same data model in your custom cell class, you can do whatever you can do in the main view. To keep your GUI and processing logic separate, you can put everything in the data model
dataModel.refreshTable()
and then define a function in the data model that takes care of your table view - you could save all current edits to the data, and reload, without having to put any of that logic in individual cell classes
for updating any record in core data try to use this code:
let managedObjectContext:NSManagedObjectContext=(UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext!
let req=NSFetchRequest(entityName: "Entity Name")
req.returnsObjectsAsFaults=false
let result:NSArray=try! managedObjectContext.executeFetchRequest(req)
if result.count>0{
let res=result[Int(indexPath.row!]as! NSManagedObject
res.setValue("The Value", forKey: "Key Name")
do {
try managedObjectContext.save()
} catch _ { print("Update Unsuccessful") }
You must use [unowned self] in within the closure. See Apple's docs. This is how it's done. See also CoreDataKit, a 28-star github repo Core Data stack. It's available on cocoapods and honestly, why not just drop something like this into your app and not worry about "unowned selves" and other philosophical brain twisters, eh?
if let moc = self.managedObjectContext{
moc.performBlockAndWait({
/* In here we are in a closure (Swift version of a block), so "self" is problematic. Use unowned self instead (in objective c you'd have to do weak self). */
[unowned self] in
for result in results!{
result.setValue(self.toggQuestion.on,
forKey: (self.surveyQuestionReference?.property)!)
}
})
}
In Parse I accidentally deleted a column called "likes" that counts the number of a likes a user receives for their blog post. I created the column again with the same name but now when I run my app it crashes and I receive this message "unexpectedly found nil while unwrapping an Optional value". It points to my code where its suppose to receive the "likes" in my cellForRowAtIndexPath. I pasted my code below. Is there any way I could fix this issue and stop it from crashing?
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath?) -> PFTableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("BCell", forIndexPath: indexPath!) as! BlogCell
if let object : PFObject = self.blogPosts.objectAtIndex(indexPath!.row) as? PFObject {
cell.author.text = object["blogger"] as? String
cell.post.text = object["blogPost"] as? String
let dateUpdated = object.createdAt! as NSDate
let dateFormat = NSDateFormatter()
dateFormat.dateFormat = "h:mm a"
cell.timer.text = NSString(format: "%#", dateFormat.stringFromDate(dateUpdated)) as String
let like = object[("likes")] as! Int
cell.likeTracker.text = "\(like)"
}
return cell
}
I would inspect what's going on with the object if I were you. You clearly aren't getting data that you expected to be there. As a stopgap, you can change let like = object["likes"] as! Int to
if let like = object["likes"] as? Int {
cell.likeTracker.text = "\(like)"
}
If you do that, you will also want to implement the prepareForReuse method in BlogCell to set that label's text to nil or else you might have some weird cell reuse bugs.
Where you delete a column from uitableview , you need to delete data from data source, and update the delete index or reload the whole table .
Look for if you are missing that step
I'm new to core data and I'm trying to sort my UITableView from data fetched from CoreData. I know how to do this using an array, but I'm struggling to find out how to do it with CoreData. I've read similar questions but it's either in Objective-c -which I haven't learned- or it's not solving my specific problem. My CoreData entity is named numbers and it stores numbers of type Float that the user passes in. My code here is in my viewDidAppear method, the first line is my declaration of myNumbers array which is globally available in that class. ***Take note that my entity has more than one attribute, I have an attribute "number" and an attribute "name" which is a string.
var myNumbers: Array<AnyObject> = []
override func viewDidAppear(animated: Bool) {
let appDeleg: AppDelegate = UIApplication.sharedApplication().delegate as AppDelegate
let context: NSManagedObjectContext = appDeleg.managedObjectContext!
let fetchreq = NSFetchRequest(entityName: "Numbers")
//Would I be adding the sort descriptor here?
let sortDescriptor = NSSortDescriptor(key: "numbersAttribute", ascending: true)
fetchreq.sortDescriptors = [sortDescriptors]
myNumbers = context.executeFetchRequest(fetchreq, error: nil)!
tableView.reloadData()
}
Now this is the code in my cellForRowAtIndexPath method:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let CellID: NSString = "myCell"
var cell: CustomCell = tableView.dequeueReusableCellWithIdentifier(CellID) as CustomCell
if let indexpth = indexPath as NSIndexPath! {
var data: NSManagedObject = myNumbers[indexpth.row] as NSManagedObject
cell.theTextLabel?.text = data.valueForKeyPath("tName") as? String
cell.theDetailTextLabel?.text = data.valueForKeyPath("tNumber") as? String
}
I want to order the data in my table view based on the "tNumber" attribute in the Numbers entity.
You can do this using an NSSortDescriptor. Do the following before doing the fetch request to populate myNumbers.
// Note that the key is the attribute of your Numbers entity that you are sorting
let sortDescriptor = NSSortDescriptor(key: "tNumber", ascending: true)
fetchreq.sortDescriptors = [sortDescriptors]
This should work, but the best way to accomplish loading data from Core Data into a table is to use a NSFetchedResultsController. This is especially true if you're dealing with a very large or transient data set.
Recently, I'm learning about CoreData in Swift. My purpose is about to send the value of one object "editContact" in class "AllContactTableViewController" as code below.
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let appDelegate:AppDelegate = UIApplication.sharedApplication().delegate as AppDelegate
let managedObjectContext:NSManagedObjectContext = appDelegate.managedObjectContext!
let entity = NSEntityDescription.entityForName("Contact", inManagedObjectContext: managedObjectContext)
var request = NSFetchRequest(entityName: "Contact")
request.returnsObjectsAsFaults = false
var results: NSArray = managedObjectContext.executeFetchRequest(request, error: nil)!
let editContactView : EditContactTableViewController = EditContactTableViewController()
var editContact : Contact = results.objectAtIndex(indexPath.row) as Contact
editContactView.editContact = editContact
println("\(editContactView.editContact)")
}
to another viewcontroller called "EditContactTableViewController" (as code below)
class EditContactTableViewController: UITableViewController {
var editContact : Contact!
override func viewDidLoad() {
super.viewDidLoad()
firstNameField.text = self.editContact.firstName
lastNameField.text = self.editContact.lastName
phoneField.text = self.editContact.phone
emailField.text = self.editContact.email
companyField.text = self.editContact.company
addressField.text = self.editContact.address
}
}
then it caused the error as "fatal error: unexpectedly found nil while unwrapping an Optional value"
in the log. It seems the value of object called "editContact" in this class has changed to nil.
Do you have idea how to fix this problem?
First, use a NSFetchedResultsController. There are many advantages, and you also will safely get the object you need with
self.fetchedResultsController.objectAtIndexPath(indexPath)
If your contact is displayed in the cell you must already have fetched it. So it does not make any sense to fetch it again. Also you have an unused variable (entity) in your very verbose but unnecessary code.
The best way is to use a segue in storyboard from the cell to the edit controller. You can then set up the object in prepareForSegue.
One nice setup is to subclass the table view cell and give it a property of type Contact. The displaying of the attributes in the cell can be handled by the subclass, helping you to uncluttered the table view controller. You can then easily retrieve the object in prepareForSegue from the sender parameter.