Introduction to tvOS Part 2: Core Data + Functionality

Simulator Screen Shot May 22, 2016, 7.24.46 PM

Welcome to part two of our Introduction to tvOS series. In part one we built our first tvOS application, a 1 question quiz app. In this part we will go over the basics of core data, and implement it in our project. By the end of this tutorial you will have a tvOS quiz game that asks multiple questions (pulled from core data).

In order to complete this tutorial successfully you should be familiar with iOS development and Swift. You should also know the basics of core data. If you have never implemented core data before that’s ok we will go slow. If you have never heard of core data before, please read this tutorial and check out Apple’s docs to get familiar. Lastly, you should have completed part 1 of this series, which introduces tvOS.

What is Core Data?

In this section I will give a quick and clear overview of Core Data. Note that Core Data functions the same on iOS as it does tvOS.

Core Data does three basic things:

  1. Allows definition of managed objects
  2. Provides functionality to create, update, delete, and query
  3. Provides multiple back-end storage options

 Basically, Core Data is the Model in the Model View Controller (MVC) pattern used in tvOS, iOS and Mac development. In this tutorial we will use Core Data to store, update and retrieve our questions for our user.

There are four parts of Core Data that you should have a handle on before moving on.

  1. Managed Object Model: How tvOS expects the database to be structured
  2. Managed Object Context: Where tvOS holds the data while its in memory
  3. Persistent Store: Where tvOS is going to write the data on commit.
  4. Persistent Store Coordinator: How tvOS does all of the loading and cashing of data, mediating between the Persistent Store and the Managed Object Context.

 Now that we have gone over Core Data and its various parts, its time to get started.

Creating Our Data Model

To create our data model we must be clear about what data we need to store. Let’s break down the parts of a multiple-choice question.

Note: We must also define the variable type for each attribute.

  1. questionText: String
  2. questionChoice0: String
  3. questionChoice1: String
  4. questionChoice2: String
  5. questionChoice3: String
  6. questionAnswer: String
  7. questionNumber: Int 16
  8. questionUserAns: Int 16

When creating our Entity we will need to make sure we include all of these parts.

Note: If you feel comfortable adding the attributes and creating your managed object context feel free to jump down to the Adding Data Section.

First click on your tvOSStarter.xcdatamodeld in your navigation menu. Next click Add Entity (located at the bottom left of your screen). You should then see an entity pop up, double click on the text and change it to read Questions.

tvOS2-1

Now hit the + in the attributes section. An attribute should pop up and be waiting for you to input the name and type. Enter each of the attributes (defined above) and their corresponding types. Once you are done, add two more attributes, dateAdded and dateUpdated, which are both of type Date. It is good practice to include these two attributes for debugging purposes that may arise.

tvOS2-3

You should have all of your entities in, with their corresponding types. See photo above for confirmation. Now we must create our Managed Object Context and to do this we will click Editor>Create NSManagedObjectContext

tvOS2-4

Then make sure that the box next to tvOSStarter is checked.

tvOS2-5

Click next, then make sure the box next to Questions is checked and hit next again. Now Xcode will have you pick where you will save the files that it is creating. It is very important that you save them inside the project folder where the other files (viewController, Storyboard, etc. are located). Often times xcode defaults to selecting the project itself as opposed to the project folder. Not to worry, just grab the dropdown menu and choose the tvOSStarter folder.

tvOS2-6

Once your screen looks like mine below, you can hit create.

tvOS2-7

Lets examine what just happened. You will notice that Xcode just added two new files. Questions+CoreDataProperties.Swift and Questions.Swift. Now drag both files so that they are next to your data model.

tvOS2-8

‘Right Click’ and choose New Group From Selection. This will create a folder, name it Data. It is important to note that you can create folders and drag files around in the Navigator and it will not harm your project. The Navigator is for file visual file organization, Xcode will maintain all references regardless of how you move things around. Once you have completed this paste these properties into the top of your view controller.

      let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
      let managedObjectContext: NSManagedObjectContext! = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
      var questionArray = [Questions]()

Now add this tempAddQuestions function below your properties.

    //MARK: Temp Add Questions
    func tempAddQuestions () {
        let entityDescriptionQuestions : NSEntityDescription! = NSEntityDescription.entityForName("Questions", inManagedObjectContext: managedObjectContext)
        
        let newQuestion = Questions(entity: entityDescriptionQuestions, insertIntoManagedObjectContext: managedObjectContext)
        newQuestion.questionText = "Which of these were not performance spaces used in the Renaissance?"
        newQuestion.questionChoice0 = "Church Cemetaries"
        newQuestion.questionChoice1 = "Courtyards"
        newQuestion.questionChoice2 = "Open Air Theatres"
        newQuestion.questionChoice3 = "Permanant Theatres"
        newQuestion.questionAnswer = 0
        newQuestion.questionNumber = 0
        
        let newQuestion1 = Questions(entity: entityDescriptionQuestions, insertIntoManagedObjectContext: managedObjectContext)
        newQuestion1.questionText = "Everyman is a _______"
        newQuestion1.questionChoice0 = "Comedy"
        newQuestion1.questionChoice1 = "Morality Play"
        newQuestion1.questionChoice2 = "Tradgedy"
        newQuestion1.questionChoice3 = "Satire"
        newQuestion1.questionAnswer = 1
        newQuestion1.questionNumber = 1
}

Let me explain this function. First, we define our entity that we want to save our data into and then we begin creating questions. When I was in University I minored in Theatre, so for my quiz I am going to ask questions about theatre history. Feel free to ask whatever questions you would like. Let me just make one last thing clear; the questionAnswer is an int and it refers to which questionChoice is the correct answer. questionAnswer should always be equal to 0,1,2, or 3 (corresponding to the correct choice) and nothing else, otherwise your application will crash. The reason we include the question number is for sorting purposes and for user interface clarity, which we will get into in Part 3 of this tutorial series. If you don’t want to take the time to make up your own questions then use mine. Copy the code below and past in our tempAddQuestions function below newQuestion1‘s declaration. Notice that at the bottom of this tempAddQuestions function we have appDelegate.saveObjectContext, which saves the data that we just added. Without that we would add the questions and when the app is closed the data would be lost. 

let newQuestion2 = Questions(entity: entityDescriptionQuestions, insertIntoManagedObjectContext: managedObjectContext)
        newQuestion2.questionText = "Who Aphra Behn?"
        newQuestion2.questionChoice0 = "1st female professional actress"
        newQuestion2.questionChoice1 = "1st female professional playwright"
        newQuestion2.questionChoice2 = "1st female professional dacner"
        newQuestion2.questionChoice3 = "1st female professional doctor"
        newQuestion2.questionAnswer = 0
        newQuestion2.questionNumber = 2
        
        let newQuestion3 = Questions(entity: entityDescriptionQuestions, insertIntoManagedObjectContext: managedObjectContext)
        newQuestion3.questionText = "This fesitval became the most important for theatre in athens greece"
        newQuestion3.questionChoice0 = "Mechane"
        newQuestion3.questionChoice1 = "City Dionysia"
        newQuestion3.questionChoice2 = "Aeschylus"
        newQuestion3.questionChoice3 = "Theatron"
        newQuestion3.questionAnswer = 0
        newQuestion3.questionNumber = 3
        
        let newQuestion4 = Questions(entity: entityDescriptionQuestions, insertIntoManagedObjectContext: managedObjectContext)
        newQuestion4.questionText = "Name the Italian innovation of creating the illusion of visial realism where scenery becomes smaller the farther away it gets"
        newQuestion4.questionChoice0 = "Angled Wings"
        newQuestion4.questionChoice1 = "Painted Shutters"
        newQuestion4.questionChoice2 = "Lighting and shadows"
        newQuestion4.questionChoice3 = "Perspective"
        newQuestion4.questionAnswer = 3
        newQuestion4.questionNumber = 4
        
        let newQuestion5 = Questions(entity: entityDescriptionQuestions, insertIntoManagedObjectContext: managedObjectContext)
        newQuestion5.questionText = "In Oedipus The King, Oedipus killed his father and married his_______?"
        newQuestion5.questionChoice0 = "Mother"
        newQuestion5.questionChoice1 = "Aunt"
        newQuestion5.questionChoice2 = "Sister"
        newQuestion5.questionChoice3 = "Brother"
        newQuestion5.questionAnswer = 0
        newQuestion5.questionNumber = 5
        
        let newQuestion6 = Questions(entity: entityDescriptionQuestions, insertIntoManagedObjectContext: managedObjectContext)
        newQuestion6.questionText = "Who invented the oil lamp?"
        newQuestion6.questionChoice0 = "James Churchhill"
        newQuestion6.questionChoice1 = "Aime Argand"
        newQuestion6.questionChoice2 = "Sara Rosiello"
        newQuestion6.questionChoice3 = "Samuel Griswald"
        newQuestion6.questionAnswer = 1
        newQuestion6.questionNumber = 6
        
        let newQuestion7 = Questions(entity: entityDescriptionQuestions, insertIntoManagedObjectContext: managedObjectContext)
        newQuestion7.questionText = "Who are 2 playwrights from the English Renaissance?"
        newQuestion7.questionChoice0 = "Torelli and Jonson"
        newQuestion7.questionChoice1 = "Betterton and Burbage"
        newQuestion7.questionChoice2 = "Shakespeare and Marlowe"
        newQuestion7.questionChoice3 = "Williams and Aristotle"
        newQuestion7.questionAnswer = 2
        newQuestion7.questionNumber = 7
        
        let newQuestion8 = Questions(entity: entityDescriptionQuestions, insertIntoManagedObjectContext: managedObjectContext)
        newQuestion8.questionText = "Who Was Nell Gwyn?"
        newQuestion8.questionChoice0 = "An Actress"
        newQuestion8.questionChoice1 = "A Singer"
        newQuestion8.questionChoice2 = "A Dutchess"
        newQuestion8.questionChoice3 = "An Actor"
        newQuestion8.questionAnswer = 0
        newQuestion8.questionNumber = 8
        
        let newQuestion9 = Questions(entity: entityDescriptionQuestions, insertIntoManagedObjectContext: managedObjectContext)
        newQuestion9.questionText = "All of the following were actors in the restoration period except"
        newQuestion9.questionChoice0 = "Nell Gwynn"
        newQuestion9.questionChoice1 = "Anne Bracegirdle"
        newQuestion9.questionChoice2 = "Anne Hathaway"
        newQuestion9.questionChoice3 = "Margaret Hughes"
        newQuestion9.questionAnswer = 2
        newQuestion9.questionNumber = 9
        
        let newQuestion11 = Questions(entity: entityDescriptionQuestions, insertIntoManagedObjectContext: managedObjectContext)
        newQuestion11.questionText = "Who was responsible for deciding which plays would be performed during antiquity?"
        newQuestion11.questionChoice0 = "choregoi"
        newQuestion11.questionChoice1 = "playwright"
        newQuestion11.questionChoice2 = "archon"
        newQuestion11.questionChoice3 = "actors"
        newQuestion11.questionAnswer = 3
        newQuestion11.questionNumber = 11
        
        let newQuestion12 = Questions(entity: entityDescriptionQuestions, insertIntoManagedObjectContext: managedObjectContext)
        newQuestion12.questionText = "What types of plays existed in medieval theater?"
        newQuestion12.questionChoice0 = "Mystery, Miracle, Morality"
        newQuestion12.questionChoice1 = "Tragedy, Comedy and Drama"
        newQuestion12.questionChoice2 = "Passion, Comedy, Mystery"
        newQuestion12.questionChoice3 = "Religious, Vernacular and History"
        newQuestion12.questionAnswer = 0
        newQuestion12.questionNumber = 12
        
        let newQuestion13 = Questions(entity: entityDescriptionQuestions, insertIntoManagedObjectContext: managedObjectContext)
        newQuestion13.questionText = "What was NOT a role of the playwright in ancient theater?"
        newQuestion13.questionChoice0 = "Writes the play"
        newQuestion13.questionChoice1 = "Modern day director"
        newQuestion13.questionChoice2 = "Acts in play"
        newQuestion13.questionChoice3 = "Modern day producer"
        newQuestion13.questionAnswer = 3
        newQuestion13.questionNumber = 13
        
        let newQuestion14 = Questions(entity: entityDescriptionQuestions, insertIntoManagedObjectContext: managedObjectContext)
        newQuestion14.questionText = "Who lifted the ban on theater by the Puritan parliament in England in 1642?"
        newQuestion14.questionChoice0 = "King Charles I"
        newQuestion14.questionChoice1 = "Queen Elizabeth I"
        newQuestion14.questionChoice2 = "King James I"
        newQuestion14.questionChoice3 = "King Charles II"
        newQuestion14.questionAnswer = 0
        newQuestion14.questionNumber = 14
        
        let newQuestion15 = Questions(entity: entityDescriptionQuestions, insertIntoManagedObjectContext: managedObjectContext)
        newQuestion15.questionText = "He was the first playwright to add a second actor, making face to face dialogue possible?"
        newQuestion15.questionChoice0 = "Aeschelus"
        newQuestion15.questionChoice1 = "Thespis" //added the first actor to step out from the chorus
        newQuestion15.questionChoice2 = "Euripides"
        newQuestion15.questionChoice3 = "Sophocles"
        newQuestion15.questionAnswer = 0
        newQuestion15.questionNumber = 15
        
        let newQuestion16 = Questions(entity: entityDescriptionQuestions, insertIntoManagedObjectContext: managedObjectContext)
        newQuestion16.questionText = "By the end of 17th century, what variety of lighting was not in use?"
        newQuestion16.questionChoice0 = "Sconces"
        newQuestion16.questionChoice1 = "Natural light"
        newQuestion16.questionChoice2 = "Chandeliers"
        newQuestion16.questionChoice3 = "Footlights"
        newQuestion16.questionAnswer = 1
        newQuestion16.questionNumber = 16
        
        let newQuestion17 = Questions(entity: entityDescriptionQuestions, insertIntoManagedObjectContext: managedObjectContext)
        newQuestion17.questionText = "What was the original name of the theater company Shakespeare led?"
        newQuestion17.questionChoice0 = "The King's Men"
        newQuestion17.questionChoice1 = "The chamberlain's men"
        newQuestion17.questionChoice2 = "The admiral's men"
        newQuestion17.questionChoice3 = "The Queen's men"
        newQuestion17.questionAnswer = 1
        newQuestion17.questionNumber = 17
        
        let newQuestion18 = Questions(entity: entityDescriptionQuestions, insertIntoManagedObjectContext: managedObjectContext)
        newQuestion18.questionText = "This playwirght was the second most popular playright renaissance"
        newQuestion18.questionChoice0 = "William Shakespeare"
        newQuestion18.questionChoice1 = "Christopher Marlowe"
        newQuestion18.questionChoice2 = "George Bernard Shaw"
        newQuestion18.questionChoice3 = "Ben Jonson"
        newQuestion18.questionAnswer = 1
        newQuestion18.questionNumber = 18
        
        let newQuestion19 = Questions(entity: entityDescriptionQuestions, insertIntoManagedObjectContext: managedObjectContext)
        newQuestion19.questionText = "Which of the following is true of the church in the Middle Ages?"
        newQuestion19.questionChoice0 = "It honored actors"
        newQuestion19.questionChoice1 = "It abolished theater"
        newQuestion19.questionChoice2 = "It aimed to teach moral lessons"
        newQuestion19.questionChoice3 = "Playwrights received church burials"
        newQuestion19.questionAnswer = 2
        newQuestion19.questionNumber = 19
        
        let newQuestion20 = Questions(entity: entityDescriptionQuestions, insertIntoManagedObjectContext: managedObjectContext)
        newQuestion20.questionText = "This group, which sang and danced, as well as recited, was a integral and unique feature of greek drama"
        newQuestion20.questionChoice0 = "The Poetics"
        newQuestion20.questionChoice1 = "The Cyclops"
        newQuestion20.questionChoice2 = "The Chorus"
        newQuestion20.questionChoice3 = "Thespis"
        newQuestion20.questionAnswer = 3
        newQuestion20.questionNumber = 20
        
        appDelegate.saveContext()

Now that we have our temp add function completed we need to call it. Replace your viewDidLoad function (towards the bottom of your view controller) with the code below. This will now run our tempAddQuestions function and print “Success” to make sure it runs.

//MARK: System Methods
    override func viewDidLoad() {
        super.viewDidLoad()
        tempAddQuestions()
        print("Success") 
    }

Now let’s set up a way to get the data we just saved.

Fetching Data

Now it is time for us to retrieve the data we have just added. The first step is to comment out the tempAddQuestions function call in our view did load. If we leave it in there we will add our questions every time we run our app. Your viewDidLoad should like this.

tvOS2-10

Now paste this fetch method below your tempAddQuestions function.

    //MARK: Fetch Questions
    func fetchQuestions() {
        let fetchRequest :NSFetchRequest = NSFetchRequest(entityName: "Questions")
        fetchRequest.sortDescriptors = [NSSortDescriptor(key: "questionNumber", ascending: true)]
        do {
            let results = try self.managedObjectContext!.executeFetchRequest(fetchRequest)
            questionArray = (results as! [Questions])
        } catch let error as NSError {
            print("error in fetch handling \(error)")
        } catch {
            print("Error")
        }
    }

This function begins by creating a fetch request and then adding a sortDescriptor so that we get our questions in order of their question number. Then we save our results into our questionArray. Then we check for errors within the fetch. In a more robust application we would want to give the user some information instead of just a print statement. This is so that if there is an error the user knows that something has gone wrong. The concept is called ‘Failing Gracefully’ and it is something that we will talk about in Part 3 of this tutorial series.

Now call the fetch in your viewDidLoad function and modify your print statement to print the number of objects in your questionArray.

//MARK: System Methods
    override func viewDidLoad() {
        super.viewDidLoad()
        //tempAddQuestions()
        fetchQuestions()
        print(questionArray.count)
    }

Run your project and your console should now print 20 (if you used my questions) or if you used your own it should print the number of questions you added to your database. If you are really curious and want to make sure that the questions are in, then place this code in your view did load (below your fetch) and it will print all of the questions (the questionText attribute).

        for ques in questionArray {
            print(ques.questionText)
        }

Now when you run it you should see all of your questions printed in your console.

Congratulations! you have now added data to your database and retrieved it.

Note: If you want to change a question or add more questions after you have already run temp add, the easiest way to do that is this: Delete the app from the Apple TV simulator by opening the simulator and hitting command-shift-h, then scroll down to your app and click and hold on the app once it begins shaking. Hit the play button and confirm that you want to delete it. Now, make whatever changes you would like to your temp add function, uncomment it in your view did load, and run the application. Once you run it, comment it out again.

Now lets add some functionality.

Building Functionality (Using our data)

In this section we will give the user the ability to answer all of the questions in our database. We need to add 5 properties, 4 for the buttons and 1 for our label.

Paste these properties at the top of your view controller

    @IBOutlet var choice0Button     :UIButton!
    @IBOutlet var choice1Button     :UIButton!
    @IBOutlet var choice2Button     :UIButton!
    @IBOutlet var choice3Button     :UIButton!
    @IBOutlet var questionTextLabel :UILabel!

Now open the storyboard and rename the buttons to choice0, choice1, choice2, and choice4. This is just for clarity in our storyboard. Also, click on the label and in the attributes panel increase the number of lines to 3 (in case we have a long question). Then, increase the height of the label to 300px. Now that you have finished that, we need to wire up these buttons and the label to the properties we added above.

tvOS2-11

We want to wire up the new referencing outlet. As an example, here’s what your connections inspector should look like when you click on the first button.

tvOS2-12

Now that we have our UI elements wired up we can open the view controller. Add this variable to the top of your view controller below your @IBOutlets.

var questionCounter = 0

Now paste this method below our showAlert method but above our viewDidLoad method.

    //MARK: Next Question
    func nextQuestion() {
        if questionCounter > questionArray.count - 1 {
            print("Entering If Statement")
            questionCounter = 0
            let gameOverAlert = UIAlertController(title: "Game Over", message:
                "You have completed all of the questions", preferredStyle: UIAlertControllerStyle.Alert)
            gameOverAlert.addAction(UIAlertAction(title: "New Game", style: .Default, handler: { (action) -> Void in
                self.nextQuestion()
            }))
            self.presentViewController(gameOverAlert, animated: true, completion: nil)
        }
        questionTextLabel.text = questionArray[questionCounter].questionText
        choice0Button.setTitle(questionArray[questionCounter].questionChoice0, forState: .Normal)
        choice1Button.setTitle(questionArray[questionCounter].questionChoice1, forState: .Normal)
        choice2Button.setTitle(questionArray[questionCounter].questionChoice2, forState: .Normal)
        choice3Button.setTitle(questionArray[questionCounter].questionChoice3, forState: .Normal)
    }

This is our game logic method. First we check to see if our question counter is greater than the number of objects in our array (minus 1). If it is, that means we are on our last question, so we need a different alert to tell the user the game is over. if it isn’t, we update our label and our buttons to reflect the next question. Now we need to write a function to evaluate the user’s answer. Before we can do that, open your storyboard.

tvOS2-13

Click on your choice 1 button and in the attributes panel you will see a tag option, set it to one. Do the same for choice 2 (set it to two) and choice three (set it to three). Note that choice 0 is already set to 0. This will allow us to write one method for all of our button presses instead of having four separate methods. Now open the connections panel and delete the “primary action triggered” connection for each button, we will re-wire them up once we complete our new @IBAction method. Open your viewcontroller and paste this method below your nextQuestion method.

    //MARK: Evaluating Answers
    @IBAction func choicePressed(sender: UIButton) {
        let selectedButtonIndex = sender.tag
        if questionArray[questionCounter].questionAnswer == selectedButtonIndex {
            
            let correctAlert = UIAlertController(title: "Correct", message:
                "Nice Job!", preferredStyle: UIAlertControllerStyle.Alert)
            correctAlert.addAction(UIAlertAction(title: "Next Question", style: .Default, handler: { (action) -> Void in
                self.nextQuestion()
            }))
            self.presentViewController(correctAlert, animated: true, completion: nil)
        } else {
            var correctAnswer = ""
            switch questionArray[questionCounter].questionAnswer as! Int {
            case 0:
                correctAnswer = questionArray[questionCounter].questionChoice0!
            case 1:
                correctAnswer = questionArray[questionCounter].questionChoice1!
            case 2:
                correctAnswer = questionArray[questionCounter].questionChoice2!
            case 3:
                correctAnswer = questionArray[questionCounter].questionChoice3!
            default:
                print("Default")
                
            }
            let correctAlert = UIAlertController(title: "Wrong", message:
                "The correct answer was: \(correctAnswer)", preferredStyle: UIAlertControllerStyle.Alert)
            correctAlert.addAction(UIAlertAction(title: "Next Question", style: .Default, handler: { (action) -> Void in
                self.nextQuestion()
            }))
            self.presentViewController(correctAlert, animated: true, completion: nil)
        }
        print(questionCounter)
        questionCounter++
    }

This function evaluates whether or not the user got the answer correct. First we get the tag from whatever button was pressed, then we check to see if the question’s correct answer is the same as the users answer. If it is, we give them a ‘correct’ alert. If it isn’t, we use a switch statement to get the text of the correct answer to use in the alert for an incorrect answer. Lastly, we increment the question counter. Before we wire up this method scroll up on your view controller and delete our @IBActions that call the show alert and delete the showAlert Function. Also delete or comment out the print of the array count and the for loop in the viewDidLoad function. Now open your storyboard and wire all of the button’s “primary action triggered” attributes to our new choicePressed method. The wire panel should look like this for button 1.

tvOS2-14

Once you have each button wired up to that method run your tvOS app

Congratulations! You now have an tvOS quiz game that pulls questions from core data.

Conclusion

I know that this has been a long tutorial, but I hope that you now feel comfortable working with tvOS and Core Data. I want to point out that all of the code in this project can be reused to build an iOS version of the quiz game, with the only expection being that you would use touchUpInside as opposed to primary action triggered to register a button press. In the third and final part of this tutorial series we will be talking about user experience and user interface design. We will add color as well as display a user score and a question number. Thanks again and I look forward to seeing you in Part 3!

-Miles