Search code examples
iosswiftcore-datauipickerviewrelationship

UIPickerView + Core Data


UPDATE WITH ANSWER

import UIKit
import CoreData

class ExerciseViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UIPickerViewDataSource, UIPickerViewDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()

        VDL()

        //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 daysPickerView: UIPickerView!

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

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

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

    func VDL () {
        let fetchRequest = NSFetchRequest(entityName: "TrainingDay")
        let sort = NSSortDescriptor(key: "dayIndex", ascending: true)
        fetchRequest.sortDescriptors = [sort]
        daysArray = (moc!.executeFetchRequest(fetchRequest, error: nil) as? [TrainingDay])!
        if daysArray.count == 0 { // nothing there
            let dayEntity = NSEntityDescription.entityForName("TrainingDay", inManagedObjectContext: moc!)
            let days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
            for (index, name) in enumerate(days) {
                let newDay = TrainingDay(entity: dayEntity!, insertIntoManagedObjectContext: moc)
                newDay.day = name
                newDay.dayIndex = index
                daysArray.append(newDay)
            }
            var error: NSError?
            moc!.save(&error)
        }

    }


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

        let row = daysPickerView.selectedRowInComponent(0)
        let currentDay = daysArray[row]


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

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

        if let err = error {
            var status = err.localizedFailureReason
            println("\(status)")
        } else {
            println("CURRENT SETTING: \(trainingdetails.trainingDay)")
        }
    }

    @IBAction func doneButton(sender: AnyObject) {
        appendTrainingDetailsToArray()

        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
        println("\(row)")
        let details = detailsArray[indexPath.row]
        cell!.textLabel!.text = details.exerciseName
        cell?.detailTextLabel?.text = "Sets: #\(details.setsNumber) Reps: #\(details.repsNumber)"
        return cell!
    }

    //PICKER VIEW DELEGATE AND DATASOURCE METHODS
    func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
        return 1
    }

    func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return daysArray.count
    }

    func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String! {
            let trainingDay = daysArray[row]
            return trainingDay.day
    }

    func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {

        let currentDay = daysArray[row]
        let fetchRequest = NSFetchRequest(entityName: "TrainingDetails")
        let predicate = NSPredicate(format: "trainingDay = %@", currentDay)
        fetchRequest.predicate = predicate
        let sort = NSSortDescriptor(key: "exerciseName", ascending: true)
        fetchRequest.sortDescriptors = [sort]
        detailsArray = (moc!.executeFetchRequest(fetchRequest, error: nil) as? [TrainingDetails])!
        exerciseTableView.reloadData()

    }
}

Previously, I asked help so I could adapt with UIPickerView. Like @pbasdf said, the code needed some adjustments to conform with core data and the picker view. You can see his explanation in his answer!


Solution

  • There are several things to address:

    The TrainingDay entity

    You've taken a wrong turn with the awakeFromInsert code. It doesn't insert new objects; it is run whenever you insert a new TrainingDay object. From what I can see in your code you never do actually insert new TrainingDays (which is lucky because the awakeFromInsert will crash - you are assigning an array of strings to a property which is a string).

    You need instead to create 7 TrainingDay objects, one for each day of the week. Since these do not change, you can do this as a one-off task when your app is first run, or (as I do below) "lazily" when you try to fetch them. You do not need the awakeFromInsert code, but I would recommend adding another attribute to the entity, say "dayIndex", which will enable you to sort the TrainingDay objects into a logical order. Your code might look something like this:

    // load the TrainingDays data...
    let fetchRequest = NSFetchRequest(entityName: "TrainingDay")
    let sort = NSSortDescriptor(key: "dayIndex", ascending: true)
    fetchRequest.sortDescriptors = [sort]
    daysArray = (moc!.executeFetchRequest(fetchRequest, error: nil) as? [TrainingDay])!
    if daysArray.count == 0 { // nothing there
        let dayEntity = NSEntityDescription.entityForName("TrainingDay", inManagedObjectContext: moc!)
        let days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
        for (index, name) in enumerate(days) {
            let newDay = TrainingDay(entity: dayEntity!, insertIntoManagedObjectContext: moc)
            newDay.day = name
            newDay.dayIndex = index
            daysArray.append(newDay)
        }
        moc.save(&error)
    }
    

    You could put this in viewDidLoad or in a func called from VDL. Then you can use daysArray as the data source for your picker view...

    Populating the picker view

    Abandon daysOfPickerView and use daysArray instead...

    //PICKER VIEW DELEGATE AND DATASOURCE METHODS
    func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
        return 1
    }
    
    func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return daysArray.count
    }
    
    func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String! {
        let trainingDay = daysArray[row]
        return trainingDay.day
    }
    

    Assigning the TrainingDetails object to the correct day

    In the appendTrainingDetailsToArray method, you need to determine which day is currently selected in the picker view. For that purpose, you will need to add an @IBOutlet var for the pickerView and hook it up in your storyboard. Then you can access it and set the relationship for the TrainingDetails...

    let row = pickerView.selectedRowInComponent(0)
    let currentDay = daysArray[row]
    trainingDetails.trainingDay = currentDay
    

    Populating your table view Currently you are using three separate arrays. I would use just one:

    var detailsArray = [TrainingDetails]()
    

    Then use the properties of TrainingDetails to populate the table view cells, ie.

    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 details = detailsArray[indexPath.row]
        cell!.textLabel!.text = details.exerciseName
        cell?.detailTextLabel?.text = "Sets: #\(details.setsNumber) Reps: #\(details.repsNumber)"
        return cell!
    }
    

    Amend the other table view data source methods likewise to use detailsArray.

    Your three arrays are currently built up using the append... method, but I think what you want is to populate the table view with the correct 'TrainingDetails` for the chosen day...

    Responding to the picker view changing

    Currently your code seems to create new TrainingDetails every time the picker view changes. Instead, just fetch the TrainingDetails for the chosen day, and reload the table:

    func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        let currentDay = daysArray[row]
        let fetchRequest = NSFetchRequest(entityName: "TrainingDetails")
        let predicate = NSPredicate(format: "trainingDay = %@", currentDay)
        fetchRequest.predicate = predicate
        let sort = NSSortDescriptor(key: "exerciseName", ascending: true)
        fetchRequest.sortDescriptors = [sort]
        detailsArray = (moc!.executeFetchRequest(fetchRequest, error: nil) as? [TrainingDetails])!
        exerciseTableView.reloadData()
    }
    

    Further thoughts

    You have declared an NSFetchedResultsController but not used it. It would make sense to use it instead of the raw NSFetchRequest, because it will automatically respond to you adding new TrainingDetails and will add them to the table - rather than needing to use reloadData(). But that's for another day...

    Also, if you'll forgive me, you're a bit liberal with optionals: ! and ?. I've left them in unchanged where I've copied your code, but you could tidy them up. Likewise with the error argument for the context operations - it's always worth using it. That said, I've drafted the above without testing it in Xcode, so forgive me if I've made the some mistakes in places; hopefully you can see the idea.