Search code examples
swiftuitableviewcore-datansfetchedresultscontroller

CoreData not saving - TableViewCell returning nill


This is my code:

    import UIKit
    import CoreData

    class ExerciseViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

        override func viewDidLoad() {
            super.viewDidLoad()

            //sets stepper configs
            setsStepper.wraps = false
            setsStepper.autorepeat = true
            setsStepper.continuous = true
            setsStepper.tintColor = UIColor.redColor()
            setsStepper.minimumValue = 0
            setsStepper.maximumValue = 500
            setsStepper.value = 0

            //reps stepper configs
            repsStepper.wraps = false
            repsStepper.autorepeat = true
            repsStepper.continuous = true
            repsStepper.tintColor = UIColor.orangeColor()
            repsStepper.minimumValue = 0
            repsStepper.maximumValue = 500
            repsStepper.value = 0

            exerciseTableView.reloadData()
        }

        var moc = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
        var fetchedResultsController: NSFetchedResultsController?

        @IBOutlet var exerciseTableView: UITableView!

        @IBOutlet var dayName: UITextField!
        @IBOutlet var exerciseName: UITextField!
        @IBOutlet var setsStepper: UIStepper!
        @IBOutlet var repsStepper: UIStepper!

        @IBOutlet var setsNumber: UILabel!
        @IBOutlet var repsNumber: UILabel!

        var daysArray = [String]()
        var namesArray = [String]()
        var setsArray = [Int]()
        var repsArray = [Int]()


        func appendDaysToArray() {
            let dayLabel = dayName.text
            daysArray.append(dayLabel)

            let entityDescription = NSEntityDescription.entityForName("TrainingDay", inManagedObjectContext: moc!)
            let trainingday = TrainingDay(entity: entityDescription!, insertIntoManagedObjectContext: moc)
            trainingday.day = dayName.text

            var error: NSError?
            moc?.save(&error)

            if let err = error {
                var status = err.localizedFailureReason
                println("\(status)")
            } else {
                println("Day #\(dayName.text) saved successfully!")
            }
        }

        func appendNamesToArray () {
            let nameLabel = exerciseName.text
            namesArray.append(nameLabel)

            let entityDescription = NSEntityDescription.entityForName("TrainingDetails", inManagedObjectContext: moc!)
            let trainingdetails = TrainingDetails(entity: entityDescription!, insertIntoManagedObjectContext: moc)
            trainingdetails.exerciseName = exerciseName.text

            var error: NSError?
            moc?.save(&error)

            if let err = error {
                var status = err.localizedFailureReason
                println("\(status)")
            } else {
                println("Exercise: #\(exerciseName.text) saved successfully!")
            }

        }

        func appendNumberToSets () {
            let numberOfSets = setsNumber.text?.toInt()
            setsArray.append(numberOfSets!)

            let entityDescription = NSEntityDescription.entityForName("TrainingDetails", inManagedObjectContext: moc!)
            let trainingdetails = TrainingDetails(entity: entityDescription!, insertIntoManagedObjectContext: moc)
            trainingdetails.setsNumber = setsNumber.text!

            var error: NSError?
            moc?.save(&error)

            if let err = error {
                var status = err.localizedFailureReason
                println("\(status)")
            } else {
                println("Exercise: #\(setsNumber.text) saved successfully!")
            }

        }

        func appendNumberOfReps () {
            let numberOfReps = repsNumber.text?.toInt()
            repsArray.append(numberOfReps!)

            let entityDescription = NSEntityDescription.entityForName("TrainingDetails", inManagedObjectContext: moc!)
            let trainingdetails = TrainingDetails(entity: entityDescription!, insertIntoManagedObjectContext: moc)
            trainingdetails.repsNumber = repsNumber.text!

            var error: NSError?
            moc?.save(&error)

            if let err = error {
                var status = err.localizedFailureReason
                println("\(status)")
            } else {
                println("Exercise: #\(repsNumber.text) saved successfully!")
            }

        }

        @IBAction func doneButton(sender: AnyObject) {
            println("\(dayName.text)")
            appendDaysToArray()
            println("\(exerciseName.text)")
            appendNamesToArray()
            println("\(setsNumber.text)")
            appendNumberToSets()
            println("\(repsNumber.text)")
            appendNumberOfReps()
            exerciseTableView.reloadData()
        }

        @IBAction func setsStepperAction(sender: UIStepper) {
            println("\(Int(sender.value))")
            setsNumber.text = Int(sender.value).description
        }

        @IBAction func repsStepper(sender: UIStepper) {
            println("\(Int(sender.value))")
            repsNumber.text = Int(sender.value).description
        }

        func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return namesArray.count
        }

        func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

            let cellIdentifier = "exerciseCell"
    var cell  = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as? UITableViewCell
            if cell == nil {
                cell = UITableViewCell(style: UITableViewCellStyle.Value2, reuseIdentifier: cellIdentifier)
            }
            let row = indexPath.row
                let name = namesArray[indexPath.row]
            let numberReps = repsArray[indexPath.row]
            let numberSets = setsArray[indexPath.row]
                cell!.textLabel!.text = name
            cell?.detailTextLabel?.text = "Sets: #\(numberSets) Reps: #\(numberReps)"
            return cell!
        }
    }

and

class ViewExercisesViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, NSFetchedResultsControllerDelegate {

    override func viewDidLoad() {

        fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchTrainingDetails(), managedObjectContext: moc!, sectionNameKeyPath: nil, cacheName: nil)
        fetchedResultsController?.delegate = self
        fetchedResultsController?.performFetch(nil)
        self.viewExerciseTableView.reloadData()


        sundayButton.frame = CGRectMake(-30,150,125,125)
        sundayButton.addTarget(self, action: "sundayButtonTouch:", forControlEvents: UIControlEvents.TouchDown)
        sundayButton.setImage(imageSunday, forState: .Normal)
        sundayButton.imageEdgeInsets = UIEdgeInsetsMake(30,30,30,30)
        self.view.addSubview(sundayButton)

        mondayButton.frame = CGRectMake(120,150,125,125)
        mondayButton.addTarget(self, action: "mondayButtonTouch:", forControlEvents: UIControlEvents.TouchDown)
        mondayButton.setImage(imageMonday, forState: .Normal)
        mondayButton.imageEdgeInsets = UIEdgeInsetsMake(30,30,30,30)
        self.view.addSubview(mondayButton)

        tuesdayButton.frame = CGRectMake(270,150,125,125)
        tuesdayButton.addTarget(self, action: "tuesdayButtonTouch:", forControlEvents: UIControlEvents.TouchDown)
        tuesdayButton.setImage(imageTuesday, forState: .Normal)
        tuesdayButton.imageEdgeInsets = UIEdgeInsetsMake(30,30,30,30)
        self.view.addSubview(tuesdayButton)

        wednesdayButton.frame = CGRectMake(-30,250,125,125)
        wednesdayButton.addTarget(self, action: "wednesdayButtonTouch:", forControlEvents: UIControlEvents.TouchDown)
        wednesdayButton.setImage(imageWednesday, forState: .Normal)
        wednesdayButton.imageEdgeInsets = UIEdgeInsetsMake(30,30,30,30)
        self.view.addSubview(wednesdayButton)

        thursdayButton.frame = CGRectMake(70,250,125,125)
        thursdayButton.addTarget(self, action: "thursdayButtonTouch:", forControlEvents: UIControlEvents.TouchDown)
        thursdayButton.setImage(imageThursday, forState: .Normal)
        thursdayButton.imageEdgeInsets = UIEdgeInsetsMake(30,30,30,30)
        self.view.addSubview(thursdayButton)

        fridayButton.frame = CGRectMake(170,250,125,125)
        fridayButton.addTarget(self, action: "fridayButtonTouch:", forControlEvents: UIControlEvents.TouchDown)
        fridayButton.setImage(imageFriday, forState: .Normal)
        fridayButton.imageEdgeInsets = UIEdgeInsetsMake(30,30,30,30)
        self.view.addSubview(fridayButton)

        saturdayButton.frame = CGRectMake(270,250,125,125)
        saturdayButton.addTarget(self, action: "saturdayButtonTouch:", forControlEvents: UIControlEvents.TouchDown)
        saturdayButton.setImage(imageSaturday, forState: .Normal)
        saturdayButton.imageEdgeInsets = UIEdgeInsetsMake(30,30,30,30)
        self.view.addSubview(saturdayButton)


    }

    //VAR AND LET

    var sundayButton = UIButton.buttonWithType(UIButtonType.Custom) as! UIButton
    var imageSunday = UIImage(named: "day.png")

    var mondayButton = UIButton.buttonWithType(UIButtonType.Custom) as! UIButton
    var imageMonday = UIImage(named: "day.png")

    var tuesdayButton = UIButton.buttonWithType(UIButtonType.Custom) as! UIButton
    var imageTuesday = UIImage(named: "day.png")

    var wednesdayButton = UIButton.buttonWithType(UIButtonType.Custom) as! UIButton
    var imageWednesday = UIImage(named: "day.png")

    var thursdayButton = UIButton.buttonWithType(UIButtonType.Custom) as! UIButton
    var imageThursday = UIImage(named: "day.png")

    var fridayButton = UIButton.buttonWithType(UIButtonType.Custom) as! UIButton
    var imageFriday = UIImage(named: "day.png")

    var saturdayButton = UIButton.buttonWithType(UIButtonType.Custom) as! UIButton
    var imageSaturday = UIImage(named: "day.png")

    @IBOutlet var viewExerciseTableView: UITableView!

    var moc = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
    var fetchedResultsController: NSFetchedResultsController?

    // FUNCTIONS

    func sundayButtonTouch(sender: UIButton!) {
        println("future event will be added, button working fine - sunday")
    }

    func mondayButtonTouch(sender: UIButton!) {
        println("future event will be added, button working fine - monday")
    }

    func tuesdayButtonTouch(sender: UIButton!) {
        println("future event will be added, button working fine - tuesday")
    }

    func wednesdayButtonTouch(sender: UIButton!) {
        println("future event will be added, button working fine - wednesday")
    }

    func thursdayButtonTouch(sender: UIButton!) {
        println("future event will be added, button working fine - thursday")
    }

    func fridayButtonTouch(sender: UIButton!) {
        println("future event will be added, button working fine - friday")
    }

    func saturdayButtonTouch(sender: UIButton!) {
        println("future event will be added, button working fine - saturday")
    }

    // FETCH REQUEST METHODS
    func fetchTrainingDay() -> NSFetchRequest {
        let fetchRequest = NSFetchRequest(entityName: "TrainingDay")
       // let predicate = NSPredicate(format: "day == %@")
        let sortDescriptor = NSSortDescriptor(key: "day", ascending: true)
        fetchRequest.predicate = nil
        fetchRequest.sortDescriptors = [sortDescriptor]
        fetchRequest.fetchBatchSize = 20
        return fetchRequest
    }

    func fetchTrainingDetails() -> NSFetchRequest {
        let fetchRequest = NSFetchRequest(entityName: "TrainingDetails")
        fetchRequest.predicate = nil
        let sortDescriptor1 = NSSortDescriptor(key: "exerciseName", ascending: true)
        let sortDescriptor2 = NSSortDescriptor(key: "repsNumber", ascending: true)
        let sortDescriptor3 = NSSortDescriptor(key: "setsNumber", ascending: true)
        fetchRequest.sortDescriptors = [sortDescriptor1, sortDescriptor2, sortDescriptor3]
        fetchRequest.fetchBatchSize = 20
        return fetchRequest
    }



    //TABLE VIEW DELEGATE METHODS
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return fetchedResultsController?.sections?[section].numberOfObjects ?? 0
    }

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

        let cellIdentifier = "exCell"
        var cell  = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as? UITableViewCell

        if let exCell = fetchedResultsController?.objectAtIndexPath(indexPath) as? TrainingDetails {
            println("THE ERROR IS HERE #\(exCell.exerciseName) \(exCell.repsNumber) \(exCell.setsNumber)")
            cell!.textLabel?.text = exCell.exerciseName
            cell?.detailTextLabel?.text = "Sets: #\(exCell.setsNumber) Reps: #\(exCell.repsNumber)"
        }
        return cell!
    }

    // MARK: NSFetchedResultsControllerDelegate
    func controllerWillChangeContent(controller: NSFetchedResultsController) {
        self.viewExerciseTableView.beginUpdates()
    }
    func controller(controller: NSFetchedResultsController,
        didChangeObject anObject: AnyObject,
        atIndexPath indexPath: NSIndexPath?,
        forChangeType type: NSFetchedResultsChangeType,
        newIndexPath: NSIndexPath?)
    {
        switch(type) {
        case .Insert:
            if let newIndexPath = newIndexPath {
                viewExerciseTableView.insertRowsAtIndexPaths([newIndexPath],
                    withRowAnimation:UITableViewRowAnimation.Fade)
            }
        case .Delete:
            if let indexPath = indexPath {
                viewExerciseTableView.deleteRowsAtIndexPaths([indexPath],
                    withRowAnimation: UITableViewRowAnimation.Fade)
            }
        case .Update:
            break
        case .Move:
            if let indexPath = indexPath {
                if let newIndexPath = newIndexPath {
                    viewExerciseTableView.deleteRowsAtIndexPaths([indexPath],
                        withRowAnimation: UITableViewRowAnimation.Fade)
                    viewExerciseTableView.insertRowsAtIndexPaths([newIndexPath],
                        withRowAnimation: UITableViewRowAnimation.Fade)
                }
            }
        }
    }

    func controller(controller: NSFetchedResultsController,
        didChangeSection sectionInfo: NSFetchedResultsSectionInfo,
        atIndex sectionIndex: Int,
        forChangeType type: NSFetchedResultsChangeType)
    {
        switch(type) {
        case .Insert:
            viewExerciseTableView.insertSections(NSIndexSet(index: sectionIndex),
                withRowAnimation: UITableViewRowAnimation.Fade)
        case .Delete:
            viewExerciseTableView.deleteSections(NSIndexSet(index: sectionIndex),
                withRowAnimation: UITableViewRowAnimation.Fade)
        default:
            break
        }
    }
    func controllerDidChangeContent(controller: NSFetchedResultsController) {
        viewExerciseTableView.endUpdates()
    }





}

and

import Foundation
import CoreData

class TrainingDetails: NSManagedObject {

    @NSManaged var exerciseName: String
    @NSManaged var setsNumber: String
    @NSManaged var repsNumber: String
    @NSManaged var relationship2: NSManagedObject

}

The logic is this: In "ExerciseViewController", I put some stuff in CoreData, this view works fine. Now, in ViewExercisesViewController, I should retrieve the exerciseName and set it as the cell's textLabel, but it crashes right here: println("THE ERROR IS HERE #\(exCell.exerciseName) \(exCell.repsNumber) \(exCell.setsNumber)"). It seems that I'm not retrieving it correctly, but I'm certain it is being saved in CoreData.

Anyone has any idea? I have an app that does something similar, but this one is not working.

Thanks for your help, I'm looking forward to figure out what's wrong here.

EDIT -> image with crush error:

enter image description here

EDIT 2

Doing this: enter image description here

leads to this:

enter image description here

FINAL EDIT -> SOLUTION

As Tom said, doing this:

func appendTrainingDetails () {
        let nameLabel = exerciseName.text
        namesArray.append(nameLabel)
        let numberOfSets = setsNumber.text?.toInt()
        setsArray.append(numberOfSets!)
        let numberOfReps = repsNumber.text?.toInt()
        repsArray.append(numberOfReps!)

        let entityDescription = NSEntityDescription.entityForName("TrainingDetails", inManagedObjectContext: moc!)
        let trainingdetails = TrainingDetails(entity: entityDescription!, insertIntoManagedObjectContext: moc)
        trainingdetails.exerciseName = exerciseName.text
        trainingdetails.setsNumber = setsNumber.text!
        trainingdetails.repsNumber = repsNumber.text!

        var error: NSError?
        moc?.save(&error)

        if let err = error {
            var status = err.localizedFailureReason
            println("\(status)")
        } else {
            println("Exercise: #\(exerciseName.text) saved successfully!")
            println("Number of sets: #\(setsNumber.text) saved successfully!")
            println("Number of reps: #\(repsNumber.text) saved successfully!")
        }
    }

with the methods, led me to fix what I was doing wrong! Thank you very very much!!


Solution

  • Here's what it looks like is happening...

    Your methods appendNumberToSets(), appendNumberOfReps(), and appendNamesToArray() all have this code:

    let entityDescription = NSEntityDescription.entityForName("TrainingDetails", inManagedObjectContext: moc!)
    let trainingdetails = TrainingDetails(entity: entityDescription!, insertIntoManagedObjectContext: moc)
    

    That means that each one of these is creating a new instance of TrainingDetails. However,

    • appendNumberToSets() only sets a value for the setsNumber property
    • appendNamesToArray() only sets a value for the exerciseName property
    • appendNumberOfReps() only sets a value for the repsNumber property

    In all three methods you're creating new TrainingDetails instances but leaving most of the properties with nil values. That would be OK except that later on you attempt to look up exerciseName, setsNumber, and repsNumber on the same instance. Since none of your instances have values for all three, your app crashes.

    This is happening because Core Data has an idea of what an optional value is, and Swift also has an idea of what "optional" means, but these are not the same thing. Core Data doesn't care that you're leaving these properties with nil values, but Swift does. The conflict between these two ideas is leading to your problem.

    What you should do:

    • If these values should be optional, that is, they're allowed to be nil, you should change your TrainingDetails class to make them Swift optionals. It looks like you're using code that Xcode generated, but that code is wrong and it's completely OK to fix it. The compiler will then force you to check whether the attributes have values, and you won't crash.
    • If these values should not be optional, that is, any TrainingDetails must have values for every one of these attributes, you should do two things. First, edit your Core Data model. For each of these attributes, un-check the "Optional" box in the model editor. That way Core Data will know that nil isn't allowed and it will prevent you from saving changes if you have unexpected nils. Second, you need to change the three methods mentioned above to either (a) always assign values to all of the attributes, or (b) re-use the same TrainingDetails instance instead of creating a new one in each method (the choice depends on your requirements, so it's up to you to decide which is correct).