Tag Archives: GUI

The Joys of Beta Swift: A Change to Outlets

I went to open the pizza demo to get ready for my next post and was greeted by 10 compiler error messages, all the same.

'IBOutlet' property has non-optional type 'UILabel'

Just after I finished the optionals post, It turns out there a change in Beta 4. Before Beta 4, and as described in Apple’s iBook Using Swift with Cocoa and Objective-C.

When you declare an outlet in Swift, the compiler automatically converts the type to a weak implicitly unwrapped optional and assigns it an initial value of nil. In effect, the compiler replaces @IBOutlet var name: Type with @IBOutlet weak var name: Type! = nil.

When I began to write the last post, and every post before then, this was valid code:

 @IBOutlet var priceLabel : UILabel

and Xcode would change this to

 @IBOutlet weak var priceLabel : UILabel! = nil

However, if you control drag an outlet for a label in beta 4 this happens:

@IBOutlet var priceLabel : UILabel!

It’s an understandable change, and does make for clarity. UILabels are optionals, and should be marked as such. So every outlet you enter into code now needs to be declared optional, Xcode is no longer doing it for you, unless you control drag the outlet.

I’ll go back and fix code for previous lessons as soon as I can, but that is what going on if you get ten or more error messages on some of my previous code.

Oh, and one other, small thing:

The range operator was changed too. The .. is now ..< Xcode will suggest that change for you when you load code.

Update August 4, 2014 — I changed all the github code to the correct versions. I now have to go back and do the same for the blogs themselves.

The Swift Swift Tutorial: How to Use Split View on iPad (Round 1)

Back in July of 2014, I wrote this first post on using UISplitViewController in Swift. I gave how to use Apple’s template, which has a few interesting quirks, such as ARC killing@IBOutlets at the drop of a hat. I said I would write another post on the subject. I ended up making this section 2 of Chapter six of my Book Swift Swift: UI View Controllers . iOS8 makes split views universal, not just iPad. It does different things depending on class sizes. I thought I’d share with you a preview of that book.It came out as three projects, which if you want and updated version without ARC killing your detail’s outlets, go here to start the series: Swift Swift: Split View Controllers (Round Two) Part 1: The Master-Detail template For more previews and interesting thoughts, sign up for my newsletter. –Steve

Two and a half weeks ago I had a request from a reader to cover split views and split view controllers in Swift. My apologies for taking this long, but now I know the reason for this user’s request. For those not familiar with split view controllers, they are a view controller meant for the iPad. They take two view controllers and place them in the same window. On the left  is the master, and is almost always a table view controller. The other view controller is known as the detail, which is a standard UIView controller.

Portrait Split Image View
Portrait Split Image View

I will admit I had a lot of problems implementing split views in Swift. I finally did get it to work. There is a change in Swift’s implementation that requires a bit of getting used to. This will be the first round of split views. In this post, I’ll show how to use the master-detail template to get them to work. After I cover a few other topics which will be useful in a more advanced version, I’ll revisit split views in Swift.

The Problem with the Detail Controller

The way I learned to make a split view with Objective-C was by dragging out a split view controller on a blank storyboard, and writing everything myself. That doesn’t seem to work too well and I was getting one of Swift’s most common error messages: fatal error: Can't unwrap Optional.None This is the run-time error when you unwrap a nil value from an optional.  This has to do with very weak references. The error happens in @IBOutlets in the DetailViewController . Outlets are by definition weak, and by Swift’s definition anything weak is also an optional. According to Apple’s book Using Swift with Cocoa and Objective-C. but @IBOutlet var name:Type is all we need to write. The compiler assumes the rest for us and defines an outlet as @IBOutlet weak var name: Type! = nil

Outlets in the detail controller cannot be directly accessed like an outlet hooked up anywhere else on a storyboard. In Swift, outlets are so weak in this case they are usually nil. Somehow the meager connections to the split view controller are not enough to keep them alive, though with the equivalent code in Objective-C there are enough strong pointers. In Swift, if you try something simple like this in the detail:

pizzaLabel.text = "Veggie"

That run-time error fatal error: Can't unwrap Optional.None. appears.

Apple’s Master-Detail template does work however.  There is a way to do this. In this lesson, we’ll set up the template to make a split view for PizzaDemo. We’ll cut and paste a lot of code from our last post on table views, and add a few new parts for the Split controller. As an iPad application, we will change our UI a bit from the iPhone. We’ll make our master the list of pizza types, and replace the segmented control with the master. In this session, we will not change the prices, but just get the data displayed in the model.

Get the template

In Xcode start a new project on the welcome screen, press command-shift-N or go to File>New>Project… Select in the iOS Application the Master-Detail Application. Name the app SwiftSplitPizzaDemo select Swift for the language and iPad for the device. Keep Core Data unchecked.

Select your file location and click Create. In the files created click the story board and look at what Xcode loaded for you.

The split view controller splits into two navigation controllers. One navigation controller has a UITableViewController subclass found in MasterViewController.swift. The other navigation controller has a UIViewController  subclass, found in DetailViewController.swift.

Xcode provides some sample code in the template to run an small application. Change to the iPad2 simulator, since it will fit on any screen nicely. Build and run. You get a pretty blank screen with a label on it. On your keyboard press the command key and the left or right arrow keys together a few times to rotate the ipad. As you change the orientation, the screen changes. Go back to portrait, and swipe right. the master view slides out. Press the plus button in the corner of the master view to add to the table. Rotate to a landscape view and hit plus again. Now select one of your two selections in the table view. The label in the detail changes.

We know what is there works. Now to replace this code with our own. Go to the storyboard. Start by selecting the dynamic table cell, changing the table cell to tableCell in the storyboard.  Also change the cell style to right detail.

Add the Model

Open the PizzaDemo application with the table view in it from our last lesson, if you want to copy and paste from there. If you do not have it, you can download it from github or you can copy what I have below.

We first need a Model. if you have the PizzaDemo code open, drag the Pizza.swift file over to SwiftSplitPizzaDemo. make sure Copy items if needed has a check next to it, and hit Finish. If you do not have the PizzaDemo open and don’t want to load it, hit command-N on the keyboard, and make a Cocoa Touch class called Pizza in Swift which subclasses NSObject. In the new Pizza.swift file, add this code:

import Foundation
/* --------

Our model for MVC
keeps data  and calculations
about pizzas

------------*/

class Pizza {
    var pizzaPricePerInSq = ["Cheese": 0.03 , "Sausage": 0.06 , "Pepperoni": 0.05 , "Veggie": 0.04]
    var typeList:Array {  //computed property 7/7/14
    get{
        return Array(pizzaPricePerInSq.keys)
    }
    }

    let pi = 3.1415926

    var pizzaDiameter = 0.0
    let maxPizza = 24.0
    var pizzaType = "Cheese"

    var radius : Double {  //computed property
    get{   //must define a getter
        return pizzaDiameter/2.0
    }
    set(newRadius){ //optionally define a setter
        pizzaDiameter = newRadius * 2.0
    }
    }

    var area :  Double {
    get{
        return pizzaArea()
    }
    }

    func pizzaArea() -> Double{
        return radius * radius * pi
    }

    func unitPrice() ->Double{
        let unitPrice = pizzaPricePerInSq[pizzaType]    //optional type ?Double
        if unitPrice != nil {
            return unitPrice!
        }                               //optional type ?Double checking for nil
        else {
            return 0.0
        }
    }

    func pizzaPrice() ->Double{
        let unitPrice = pizzaPricePerInSq[pizzaType]    //optional type ?Double
        if unitPrice != nil {                                   //optional type ?Double checking for nil
            return pizzaArea() * unitPrice!             //unwrapping the optional type
        }
        return 0.0
    }

    func diameterFromString(aString:NSString) -> Double {
        switch aString {
        case "Personal":
            return 8.0
        case "10\"":
            return 10.0
        case "12\"":
            return 12.0
        case "16\"","15\"":
            return 16.0
        case "18\"":
            return 18.0
        case "24\"":
            return 24.0
        default:
            return 0.0
        }
    }

}

Add the Master

Go to the MasterViewController.swift file. Go into the MasterViewController class and comment out the following line:

var objects = NSMutableArray()

This will produce a lot of error messages. We will need to change the code in each place there is an error message. To add our model, just under where you commented out, objects add:

var pizza = Pizza()

Add the TableView Code

Scroll down to where the table view data source is:

   //MARK:  - Table View

    override func numberOfSectionsInTableView(tableView: UITableView?) -> Int {
        return 1
    }

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

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath?) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell

        let object = objects[indexPath.row] as NSDate
        cell.textLabel.text = object.description
        return cell
    }

Replace it with the following code.

//MARK: -Table View
    override func numberOfSectionsInTableView(tableView: UITableView?) -> Int {
        // Return the number of sections.
        return 1
    }

    override func tableView(tableView: UITableView?, numberOfRowsInSection section: Int) -> Int {
        // Return the number of rows in the section.
        return pizza.pizzaPricePerInSq.count
    }

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath?) -> UITableViewCell
        let row = indexPath!.row   //get the array index from the index path
        let cell = tableView.dequeueReusableCellWithIdentifier("tableCell", forIndexPath: indexPath) as UITableViewCell  //make the cell
        let myRowKey = pizza.typeList[row]  //the dictionary key
        cell.textLabel!.text = myRowKey
        let myRowData = pizza.pizzaPricePerInSq[myRowKey]  //the dictionary value
        cell.detailTextLabel!.text = String(format: "%6.3f",myRowData!)
        return cell
    }

    override func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath!) -> CGFloat {
        return 44.0
    }

We will not be editing the table. Comment out this method for insertion:

/*
    func insertNewObject(sender: AnyObject) {
        if objects == nil {
            objects = NSMutableArray()
        }
        objects.insertObject(NSDate.date(), atIndex: 0)
        let indexPath = NSIndexPath(forRow: 0, inSection: 0)
        self.tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
    }
*/

Also comment out this method for deleting rows:

/*
    override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
        if editingStyle == .Delete {
            objects.removeObjectAtIndex(indexPath.row)
            tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
        } else if editingStyle == .Insert {
            // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view.
        }
    }
*/

Disable editing of the table by returning false here:

    override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
        // Return false if you do not want the specified item to be editable.
        return false
    }

We just made a table. We covered how to do that last time. Here we begin to see something new. Change tableview(didSelectRowAtIndexpath:indexPath:) to this:

    override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        //let object = objects[indexPath.row] as NSDate
        //self.detailViewController!.detailItem = object
        pizza.pizzaType = pizza.typeList[indexPath.row] //set to the selected pizza
        if (detailViewController != nil){
            self.detailViewController!.detailItem = pizza //send the model to the detailItem
        }

    }

Looking at the top of our code the master view controller makes an instance of the DetailViewController class. Here we set the detailItem property of the detail view controller, whose type is AnyObject!. This is key to everything, but we’ll discuss the property more in a few minutes. We set the pizzaType property to the selected pizza, and then send the model to the detail controller via this property.

To be consistent, handle an ios8 issue, and to get rid of the last error, change prepareForSegue() like this:

 override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
        if segue.identifier == "showDetail" {
            let indexPath = self.tableView.indexPathForSelectedRow()
            //    let object = objects[indexPath.row] as NSDate
            //((segue.destinationViewController as UINavigationController).topViewController as DetailViewController).detailItem = object
            pizza.pizzaType = pizza.typeList[indexPath!.row] //set to the selected pizza
            ((segue.destinationViewController as UINavigationController).topViewController as DetailViewController).detailItem = pizza
        }
    }

Line 7 will not make a lot of sense if you are unfamiliar with split view controllers. Take a look at the storyboard:

The storyboard
The storyboard

One of a split view controller’s properties is an array called viewControllers. At most it has two objects,  navigation controllers for the master and the detail views. Embedded in each of these navigation controllers is a master and detail view. For the segue, we go from the master controller to the navigation controller, which is the segue’s destination. The first view controller in the destination navigation view controller’s array of controllers is the detail view. We access that by ((segue.destinationViewController as UINavigationController).topViewController Once we have the detail view controller, we assign the model to the detailItem property.

Now change viewDidLoad to this:

override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
/* Removed code for editing and use of bar buttons.
        self.navigationItem.leftBarButtonItem = self.editButtonItem()

        let addButton = UIBarButtonItem(barButtonSystemItem: .Add, target: self, action: "insertNewObject:")
        self.navigationItem.rightBarButtonItem = addButton
*/
        let controllers = self.splitViewController!.viewControllers
        self.detailViewController = controllers[controllers.endIndex-1].topViewController as? DetailViewController

        }

Lines 10 and 11 set up the detail view controller in the master. It takes the detail view from the viewControllers array we assigned to controllers We also removed the bar button item navigation bar stuff since we will not be using it in this tutorial.

We now have a completed master view controller. Essentially it is a table view with some odd connections to a property in the detail view controller. Build and run. In portrait swipe from left to right. Try rotating the iPad and see what happens.

Add the Detail

The next step is to design the detail view controller. We will be showing a lot more information on this scene, so a good layout is important.

Design and Connect the Scene

Go into the storyboard and go to the detail view controller there. Add five labels  with the text Pizza:, Size:, Anchovy, 12″, and Pizza Info And Price. Add one button with a blue background and white foreground labeled 10″. Copy the button five times. Resizing as necessary, lay out and label everything something like this:

Layout for the detail view

I did use auto layout here since we will be rotating the iPad.

Open the assistant editor to the detail view controller and add the following outlets by control dragging from the Anchovy, 12″, and Pizza Info And Price labels on the storyboard.

    @IBOutlet var pizzaSizeLabel: UILabel!
    @IBOutlet var pizzaTypeLabel: UILabel!
    @IBOutlet var pizzaPriceLabel: UILabel!

Adding Code for the DetailViewController

first, add the model to the view

var pizza = Pizza()

Next, add this code for the size buttons.

@IBAction func pizzaSizeButton(sender: UIButton) {
pizza.pizzaDiameter = pizza.diameterFromString(sender.titleLabel.text)
configureView()
}

Drag from the circle to each of the buttons to connect them all to this method.

Take a look at this code:

    var detailItem: AnyObject? {
        didSet {
            // Update the view.
            self.configureView()
//comment out this if clause  if you want the master to not disappear.
            if self.masterPopoverController != nil {
                self.masterPopoverController!.dismissPopoverAnimated(true)
            }
        }
    }

Lines 1-4 are at the heart of the way we get outlets to work. The MasterViewController instantiated a detailViewController. The master then set this property detailItem with the model, which is the data we wanted to pass to the detail controller. While just displaying is not strong enough a connection to keep the outlets alive, this connection is. To use an outlet we refer to detailItem. When set, detailItem calls configureView(), which looks like this:

    func configureView() {
        // Update the user interface for the detail item.
        if let detail: AnyObject = self.detailItem {
            if let label = self.detailDescriptionLabel {
                label.text = detail.description
            }
        }
    }
  

This is Apple’s template code to put text on a label. In line 2 we do two things at once. First we assign detailItem to a new constant detail. If that is successful, the app moves on to assigning another variable label to a UILabel. If that is successful, the app uses the assigned label to change the label properties. We sneakily assigned a strong pointer to a bunch of weak ones, at least for the life of the method. This is the way to access IBOutlets. Replace this code with the following:

     func configureView() {
        // Update the user interface for the detail item.
        if let detail = self.detailItem as? Pizza {     //if we have an object in the detail item
            pizza = detail
            if let label = self.pizzaTypeLabel {
                label.text = detail.pizzaType
            }
            let pizzaSizeString = NSString(format:"%6.1fin Pizza",detail.pizzaDiameter)
            let priceString = NSString(format:"%6.2f sq in at $%6.2f is $%6.2f",detail.pizzaArea(),detail.unitPrice(),detail.pizzaPrice())
            if let label = self.pizzaSizeLabel{
                label.text = pizzaSizeString
            }
            if let label = self.pizzaPriceLabel{
                label.text = priceString
            }

        }

    }

A few things changed  in configureView() for this example. In line 2, since there is a single detail view,  detailItem receives only the model.  I cast this directly to a instance of Pizza. I assign this to the model in the detail controller. While the outlets can be set here in configureView() everything else won’t be. If we want the properties from the buttons to work, we need to set the model here. After setting detail, we follow the if let label= pattern for updating our labels. That’s the trick to getting outlets to work in split views.

The Model initializes to the same values on both controllers, head over to the master view controller’s viewDidLoad() and add the following line:

pizza.pizzaPricePerInSq["Pepperoni"] = 9.99

This places a marker to prove our connections work.
Build and run. Play around with the app in different orientations.
splitview demo 4

There’s Always One More Bug

If you try to change the size of a pizza before selecting one in the master view, you’ll find nothing happens. We need to run configureView() in the detailViewController to get it to work. Go back to the MasterViewController‘s viewDidLoad() and add the following:

//send the model to the detailItem
        if (detailViewController){
            self.detailViewController!.detailItem = pizza

The pizza = detail assignment buried in line 4 of configureView() connects up enough to get the buttons to work.

This code works, and using the master-detail template is easy, though cumbersome, once you know the trick I showed here. There are still a lot of unanswered questions. We’ll explore some of those in the upcoming installments.

The Whole Code

Here is the code for the model, master view controller and detail view controller for copying and review. You can find a copy of the completed  project at Github as well.

Pizza.Swift

//
//  Pizza.swift
//  pizzaDemo
//
//  Created by Steven Lipton on 7/1/14.
//  Copyright (c) 2014 Steven Lipton. All rights reserved.
//

import Foundation
/* --------

Our model for MVC
keeps data  and calcualtions
about pizzas

------------*/

class Pizza {
    var pizzaPricePerInSq = ["Cheese": 0.03 , "Sausage": 0.06 , "Pepperoni": 0.05 , "Veggie": 0.04]
    var typeList:Array {  //computed property 7/7/14
    get{
        return Array(pizzaPricePerInSq.keys)
    }
    }

    let pi = 3.1415926

    var pizzaDiameter = 0.0
    let maxPizza = 24.0
    var pizzaType = "Cheese"

    var radius : Double {  //computed property
    get{   //must define a getter
        return pizzaDiameter/2.0
    }
    set(newRadius){ //optionally define a setter
        pizzaDiameter = newRadius * 2.0
    }
    }

    var area :  Double {
    get{
        return pizzaArea()
    }
    }

    func pizzaArea() -> Double{
        return radius * radius * pi
    }

    func unitPrice() ->Double{
        let unitPrice = pizzaPricePerInSq[pizzaType]    //optional type ?Double
        if (unitPrice != nil) {
            return unitPrice!
        }                               //optional type ?Double checking for nil
        else {
            return 0.0
        }
    }

    func pizzaPrice() ->Double{
        let unitPrice = pizzaPricePerInSq[pizzaType]    //optional type ?Double
        if (unitPrice != nil) {                         //optional type ?Double checking for nil
            return pizzaArea() * unitPrice!             //unwrapping the optional type
        }
        return 0.0
    }

    func diameterFromString(aString:NSString) -> Double {
        switch aString {
        case "Personal":
            return 8.0
        case "10\"":
            return 10.0
        case "12\"":
            return 12.0
        case "16\"","15\"":
            return 16.0
        case "18\"":
            return 18.0
        case "24\"":
            return 24.0
        default:
            return 0.0
        }
    }

}

MasterViewController.swift

//
//  MasterViewController.swift
//  SwiftSplitPizzaDemo
//
//  Created by Steven Lipton on 7/17/14.
//  Copyright (c) 2014 Steven Lipton. All rights reserved.
//

import UIKit

class MasterViewController: UITableViewController {

//MARK: Life Cycle
    var detailViewController: DetailViewController? = nil
    //var objects = NSMutableArray()
    var pizza = Pizza()

    override func awakeFromNib() {
        super.awakeFromNib()
        self.clearsSelectionOnViewWillAppear = false
        self.preferredContentSize = CGSize(width: 320.0, height: 600.0)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
/* Removed code for editing and use of bar buttons.
        self.navigationItem.leftBarButtonItem = self.editButtonItem()

        let addButton = UIBarButtonItem(barButtonSystemItem: .Add, target: self, action: "insertNewObject:")
        self.navigationItem.rightBarButtonItem = addButton
*/
        let controllers = self.splitViewController!.viewControllers
        self.detailViewController = controllers[controllers.endIndex-1].topViewController as? DetailViewController

        pizza.pizzaPricePerInSq["Pepperoni"] = 9.99

        //send the model to the detailItem
        if (detailViewController != nil ){
            self.detailViewController!.detailItem = pizza
        }

    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
/*
    func insertNewObject(sender: AnyObject) {
        if objects == nil {
            objects = NSMutableArray()
        }
        objects.insertObject(NSDate.date(), atIndex: 0)
        let indexPath = NSIndexPath(forRow: 0, inSection: 0)
        self.tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
    }
*/
//MARK: - Segues

    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
        if segue.identifier == "showDetail" {
            let indexPath = self.tableView.indexPathForSelectedRow()
            //    let object = objects[indexPath.row] as NSDate
            //((segue.destinationViewController as UINavigationController).topViewController as DetailViewController).detailItem = object
            pizza.pizzaType = pizza.typeList[indexPath!.row] //set to the selected pizza
            ((segue.destinationViewController as UINavigationController).topViewController as DetailViewController).detailItem = pizza
        }
    }
//MARK:  - Table View Delegates
    override func numberOfSectionsInTableView(tableView: UITableView?) -> Int {
        // #warning Potentially incomplete method implementation.
        // Return the number of sections.
        return 1
    }

    override func tableView(tableView: UITableView?, numberOfRowsInSection section: Int) -> Int {
        // #warning Incomplete method implementation.
        // Return the number of rows in the section.
        return pizza.pizzaPricePerInSq.count
    }

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath?) -> UITableViewCell {
        //note I did not check for nil values. Something has to be really broken for these to be nil.
        let row = indexPath!.row   //get the array index from the index path
        let cell = tableView.dequeueReusableCellWithIdentifier("tableCell", forIndexPath: indexPath!) as UITableViewCell  //make the cell
        let myRowKey = pizza.typeList[row]  //the dictionary key
        cell.textLabel!.text = myRowKey
        let myRowData = pizza.pizzaPricePerInSq[myRowKey]  //the dictionary value
        cell.detailTextLabel!.text = String(format: "%6.3f",myRowData!)
        return cell
    }

    override func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
        return 44.0
    }

    override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
        // Return false if you do not want the specified item to be editable.
        return false
    }
/*
    override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
        if editingStyle == .Delete {
            objects.removeObjectAtIndex(indexPath.row)
            tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
        } else if editingStyle == .Insert {
            // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view.
        }
    }
*/
    override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        //let object = objects[indexPath.row] as NSDate
        //self.detailViewController!.detailItem = object
        pizza.pizzaType = pizza.typeList[indexPath.row] //set to the selected pizza
        if (detailViewController != nil){
            self.detailViewController!.detailItem = pizza //send the model to the detailItem
        }

    }

}

DetailViewController.Swift

//
//  DetailViewController.swift
//  SwiftSplitPizzaDemo
//
//  Created by Steven Lipton on 7/17/14.
//  Copyright (c) 2014 Steven Lipton. All rights reserved.
//

import UIKit

class DetailViewController: UIViewController, UISplitViewControllerDelegate {

    @IBOutlet var pizzaSizeLabel: UILabel!
    @IBOutlet var pizzaTypeLabel: UILabel!
    @IBOutlet var pizzaPriceLabel: UILabel!
    @IBOutlet var detailDescriptionLabel: UILabel!
    var pizza = Pizza()

    var masterPopoverController: UIPopoverController? = nil

    var detailItem: AnyObject? {
        didSet {
            // Update the view.
            self.configureView()

            //Comment out this if clause if you don't want the Master to disappear.
             if self.masterPopoverController != nil {
                self.masterPopoverController!.dismissPopoverAnimated(true)
            }
        }
    }

    @IBAction func pizzaSizeButton(sender: UIButton) {
         pizza.pizzaDiameter = pizza.diameterFromString(sender.titleLabel!.text!)
        configureView()
    }

    func configureView() {
        // Update the user interface for the detail item.
        if let detail = self.detailItem as? Pizza {     //if we have an object in the detail item
            pizza = detail
            if let label = self.pizzaTypeLabel {
                label.text = detail.pizzaType
            }
            let pizzaSizeString = NSString(format:"%6.1fin Pizza",detail.pizzaDiameter)
            let priceString = NSString(format:"%6.2f sq in at $%6.2f is $%6.2f",detail.pizzaArea(),detail.unitPrice(),detail.pizzaPrice()) //added 6/29/14
            if let label = self.pizzaSizeLabel{
                label.text = pizzaSizeString
            }
            if let label = self.pizzaPriceLabel{
                label.text = priceString //added 6/29/14
            }

        }

    }

    //MARK: - Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        self.configureView()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    //MARK: - Split View

    func splitViewController(splitController: UISplitViewController, willHideViewController viewController: UIViewController, withBarButtonItem barButtonItem: UIBarButtonItem, forPopoverController popoverController: UIPopoverController) {
        barButtonItem.title = "Master" // NSLocalizedString(@"Master", @"Master")
        self.navigationItem.setLeftBarButtonItem(barButtonItem, animated: true)
        self.masterPopoverController = popoverController
    }

    func splitViewController(splitController: UISplitViewController, willShowViewController viewController: UIViewController, invalidatingBarButtonItem barButtonItem: UIBarButtonItem) {
        // Called when the view is shown again in the split view, invalidating the button and popover controller.
        self.navigationItem.setLeftBarButtonItem(nil, animated: true)
        self.masterPopoverController = nil
    }

}

The Swift Swift Tutorial: Using Segues and Delegates in Navigation Controllers (Part 1 — The Template)

[Updating to Swift 2.0  SJL 9/17/15]

Click here for the Swift 3.0 version of this post
It should be one of the easiest things we do, and yet for many it is the most confusing. Getting data from one view controller to another as you switch views never seems easy. Segues might be slightly difficult, but delegates to get the information back have given many a developer a headache or two. Swift does streamline the process a bit, but much of the setup is the same as Objective-C. Today we’ll discuss this often used technique.

A basic map of Segues and Delegates for UI
A basic map of Segues and Delegates for UI

As this can get a bit convoluted, I decided to break this topic down into two posts. This post will make a new very tiny app which is nothing but a segue, a delegate, and a few controls to prove they work. The next post we will add all this to the pizza demo app, and discuss more practical issues with using delegates. For those looking for factory delegates, we’ll discuss that in two posts from now when I discuss something that uses a lot of them: UITableViews.

Make a New App

The First Scene

Make a new single-view Swift project named Swift2DelegateFoo. In the storyboard click on the view controller, and embed it in a navigation controller by selecting Editor>Embed In> Navigation Controller. Add a label in the center of the view, centered and with the text Unknown Color. Drag a bar button item to the right side of the navigation bar. Change its title to Color. It should look something like this.

2015-09-17_08-00-36

In the view controller, remove everything but the viewDidLoad() method. Using the assistant editor, connect an outlet for the UILabel called colorLabel. Your code should look like this:

 import UIKit

class ViewController: UIViewController {
    @IBOutlet var colorLabel : UILabel!
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }
}

The Second Scene

Make a new Cocoa Touch class that subclasses UIViewController from File>New>File…. Call it FooTwoViewController. In the code, remove everything but the viewDidLoad() method.

Go back to the storyboard and drag in another View controller. From the Foo One Navigation bar, control-drag the Color bar button item to the new controller to connect the segue. Set the segue to show and in the property inspector set the identifier to mySegue. Select the new view controller again in the storyboard. Once we’ve set the segue, in the identity inspector set the custom class for this view controller to be FooTwoViewController.

Now we’ll add a label, a navigation item, a bar button item, and three buttons. First add the label and place it towards the center of the view. Make the label the width of the view. Place the three buttons under the label. Title the buttons Red, Green and Blue respectively. Drag a navigation item into the view and in the label, title it Foo Two. Finally add the bar button item to the navigation bar. Your finished view should look like this:

2015-09-17_08-07-39

Open the assistant editor if not already open. Control-drag the label into the FooTwoViewController class. Create an outlet named colorLabel. Select the bar button, and control-drag it to the class code.  Make an action named saveColor. Select the red, green and blue buttons, and control drag them into FooTwoViewController. Make an action for a UIButton called colorSelectionButton. In the colorSelectionButton method add this code:

colorLabel.text = sender.titleLabel!.text!

Above the colorLabel outlet add a string variable colorString:

var colorString = ""

Finally, add this to viewDidLoad() to set the label correctly from the property:

colorLabel.text = colorString

Your code for FooTwoViewController should look like this:

import UIKit

class FooTwoViewController: UIViewController {
    var colorString = ""
    @IBOutlet var colorLabel : UILabel!

    @IBAction func saveColor(sender : UIBarButtonItem) {
    }

    @IBAction func colorSelectionButton(sender: UIButton) {
         colorLabel.text = sender.titleLabel!.text!
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        colorLabel.text = colorString
    }
}

Build and Run. Go back and forth between the two views.

Adding prepareForSegue

To move data from Foo One to Foo Two, we will override prepareForSegue(). Overridden methods in the editor are a bit tricky at first, since the auto fill-in has a new feature. In Swift a method that is overridden starts with the keyword override, as you can see with the viewDidload method above. However when typing in any defined method getting overridden, you don’t start by typing override. Instead you type the function or method name and Xcode adds override func to it. Go into the viewController.swift file and under the viewdidload method but before the end of the class, type prepareforS. Xcode will automatically populate the rest for you. Press tab, or click the selection on the drop down. Add braces to the end so you have this:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
    }

Add the following code to this method so the prepareForSegue() reads:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
        if segue.identifier == "mySegue"{
            let vc = segue.destinationViewController as! FooTwoViewController
            vc.colorString = colorLabel.text
        }
    }

In line 1, the first parameter is segue, which is a UIStoryboardSegue object. This is the segue we call for our transition between controllers. The exclamation point means that it is not an optional value, and we can use it as is (see below for more on optional values). In line 2 we check to see if this is the correct segue. Line 3 we create vc which is the pointer to the segue’s destination view controller. destinationviewcontroller is of type AnyObject! a Swift equivalent to Objective-C’s id. We need to cast, or more technically downcast to the correct object type with the as operator, and make vc an instance of FooTwoController. Once we do that, we can access the colorString property in line 4, sending our current text to the property in the new controller.

Build and Run. We now can send “Unknown Color” to the new controller.

The segue working correctly
The segue working correctly

Now comes the slightly more challenging part: getting stuff back. For that we will need a delegate.

Protocols and Delegates in Swift

Like Objective-C, delegation uses protocols. Protocols for those who are not familiar are properties and methods that while declared in one place, another class implements. They allow for a layer of abstraction. A class adopts a protocol to do something. The protocol defines what the something is.  The adopting class will have the code how it gets done.

We are going to set up a protocol to dismiss the Foo Two controller, and then adopt it in the main view controller. This way we can indirectly access properties of the Foo Two controller. Such an access is a delegate. In the Foo Two controller above the class definition for FooTwoViewController, add the following:

protocol FooTwoViewControllerDelegate{
    func myVCDidFinish(controller:FooTwoViewController,text:String)
}

This sets up the protocol FooTwoViewControllerDelegate with a required method myVCDidFinish(). I used a incredibly generic name here, you can of course make it more specific for readability. Next, in the FooTwoViewController class, add the following just after the class definition:

var delegate:FooTwoViewControllerDelegate? = nil

We define a delegate of optional type FooTwoViewControllerDelegate and set its value to nil. Although I covered optional values last time, they are so important I’ll review this important part of Swift. We are familiar with simple values such as a Double or Int. If we want a state for any object that has a nil state, we use an optional value. Declare an optional with a question mark after the type, as it is above. An Optional value of a Double? can be nil, along with any number for example. There is a cost to this extra power. To get back that number you have to unwrap the optional value with the ! operator. The ! operator will return a value as long as the optional is non-nil. If nil, it will break program execution with a run-time error. Before using a ! operator almost always check for nil. For example, add the last part of our code to the saveColor method:

    @IBAction func saveColor(sender : UIBarButtonItem) {
        if (delegate != nil) {
            delegate!.myVCDidFinish(self, text: colorLabel!.text!)
        }
    }

The if statement in line 2 tests an optional for nil. In the code above we first test our delegate, then unwrap the delegate to use the myVCDidFinish method. As a protocol, it isn’t defined here. The adopting class defines it, so our next stop is adopting the protocol back at the original view controller.

Open the ViewController.swift file. Change the class definition to:

class ViewController: UIViewController,FooTwoViewControllerDelegate

You should immeiately get the expected error:
Type ViewController does not conform to protocol FooTwoViewControllerDelegate
We need to write the protocol. On a blank line just above prepareForSegue(), start typing myVCDidFinish until auto complete finds it, press tab, add braces and the rest of this:

func myVCDidFinish(controller: FooTwoViewController, text: String) {
        colorLabel.text = "The Color is " +  text
        controller.navigationController?.popViewControllerAnimated(true)
    }

We take the value of text and place it in our label. We then use the controller to dismiss the controller with popViewControllerAnimated. Note that in Swift, there is no YES and NO but true and false for Boolean values.

We need to set up the delegate in the segue. Add to prepareForSegue(), just under colorString = colorLabel.text:

vc.delegate = self

Build and run. You should be able to send “unknown” to Foo Two. Click a color and send the color back.

Segue and Delegate working properly.
Segue and Delegate working properly.

This is the basic segue and delegate template, which you can try to use on your own projects. Next time We’ll add it to the Pizza Demo app and edit the dictionary.

If you are not uderstand what we did here, you might need a little help with a few few concepts of delegation.  this is mostly how, not why. For the why,  read Why Do We Need Delegates

The Whole Code

Here is the code for the viewController.swift and FooTwoViewController so you can see everything in context.

//
//  ViewController.swift
//  Swift2DelegateFoo
//
//  Created by Steven Lipton on 6/29/14.
//  Copyright (c) 2014 Steven Lipton. All rights reserved.
// Updated 9/17/15 SJL for Swift 2.0

import UIKit

class ViewController: UIViewController,FooTwoViewControllerDelegate {
    @IBOutlet var colorLabel : UILabel!
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }
    func myVCDidFinish(controller: FooTwoViewController, text: String) {
        colorLabel.text = "The Color is " +  text
        controller.navigationController?.popViewControllerAnimated(true)
    }
    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
        if segue.identifier == "mySegue"{
            let vc = segue.destinationViewController as! FooTwoViewController
            vc.colorString = colorLabel.text!
            vc.delegate = self
        }
    }
}
//
//  FooTwoViewController.swift
//  Swift2DelegateFoo
//
//  Created by Steven Lipton on 6/29/14.
//  Copyright (c) 2014 Steven Lipton. All rights reserved.
// updated 9/17/15 SJL for Swift 2.0

import UIKit

protocol FooTwoViewControllerDelegate{
    func myVCDidFinish(controller:FooTwoViewController,text:String)
}

class FooTwoViewController: UIViewController {
    var delegate:FooTwoViewControllerDelegate? = nil
    var colorString = ""
    @IBOutlet var colorLabel : UILabel!

    @IBAction func saveColor(sender : UIBarButtonItem) {
        if (delegate != nil) {
            delegate!.myVCDidFinish(self, text: colorLabel!.text!)
        }
    }

    @IBAction func colorSelectionButton(sender: UIButton) {
         colorLabel.text = sender.titleLabel!.text!
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        colorLabel.text = colorString
    }
}

From Apple to Raspberry Pi: Adding a Tkinter Text Box in Python

Tkinter lacks a good list box. There is a widget, but as shown in earlier posts in this series, it doesn’t do the best job. We had to use underscores to get the list box to work.  There is another widget that might do what we really want: the Text widget.

The Text widget is a full text editor widget. In this post, we will add one to the Popper’s Penguins application to create a report of found penguins.  In future posts, We’ll build some methods to select a line of text  and return the index of the line.

If you would like to follow along and do not have the Popper’s penguins code so far, you can get it from Github here

Adding the Text Box to the View

Add a text box  to the Popper’s Penguins code loadView() the same way we added all our other widgets:

#set up the text box
        self.dataList_textbox = scrolledtext.ScrolledText(listFrame, tabs= ("10c"))
        self.dataList_textbox.grid(row=1, column = 0, sticky = NSEW);
        self.dataList_textbox.insert("0.0","Penguin\tPenguin Action") #test data

Python Tkinter gives us two versions of  the Text widget: one with(ScrolledText) and one without(Text) scrollbars. To save us from doing the mucking around with scrollbars we did last time, we’ll use the ScrolledText version.

While most of our widgets have some control variable, the text box uses methods to do most of the setting and getting of data.  We set up a 10cm tab stop in line 2 with tabs= ("10c") which we will use later.  Using .grid in line 3, we added the text box below our current list box in the listFrame frame. Until we have full functionality, we might want to compare our result to the list box. Line 4 is not necessary, but will help us test our application.

Save and run.

A empty Text Widget with test data
A empty Text Widget with test data

Adding Setters and Getters

Like the rest of our view, we need a setter and getter to use the text in the Text widget. Text widgets do not use control variables for the data.  Text widgets use a range of data indexes. The Text widget’s methods work on the ranges we specify.

Selecting Indexes in a Text Box

Two indexes set a Text box range. An index is a string of the form “line.column” specifying a coordinate in characters in the text box.  The coordinate "0.0" would be the upper left corner. The coordinate "3.2" would be the 3rd line, second character. There are some values calculated for us.  The value of END gives us the coordinate of the last character in Text widget.

We will keep our setters and getters  with the same behavior as  other  text-based widgets like Label for now. It will get all the text and change all the text.  The range is a beginning index and an ending index. The range  ("0.0",END) will select everything in the text box.

Adding the Methods

Add the getter first:

    def getTextBoxtext(self):
    #return a string of all the boxes contents
        return self.dataList_textbox.get('0.0',END)

Using a range of everything, we get all the text and return it as a string.  Our setter is a bit more complicated. Add under the getter the following:

    def setTextboxText(self,listString):
        #delete everything in a text box and replace with the text
        self.dataList_textbox.delete("0.0",END)
        self.dataList_textbox.insert('0.0',listString)

The Text widget does not have a set method. Instead it has an insert method, which inserts at a coordinate a specified string. For us to have the setter we want, we delete everything first in line 3, then insert at the origin in line 4.

Connect the Text Box in the View Controller

Our code for the Text Box is very similar to the code for the List Box. Copy and paste the code for elementListToString() from its current location to under the method listToListString(), then change the method as follows:

#Add list to string processing for the Text widget
    def elementListToString(self,aList): #moved from above code for clarity
        listString = str()
        for  element in aList:
            if len(listString)>0:
                listString = listString + '\t' + element
            else:
                listString = element
        return listString

We added an if clause  to prevent the string from getting a tab before an entry. when empty, listString gets assigned the value for element, and not a tab followed by element. The string starts with the name of a penguin.

Now add the following method below elementListToString():

    def listToTextString(self,aList):
        #method for making lists compatible
        #with the text box
        #returns a string we can insert in the text box
        listString = self.reportHeader 
        for record in aList:
            elementString = self.elementListToString(record)
            listString = listString + '\n' + elementString
        return listString

This one works almost identically to its list box counterpart elementListToString(). Instead of initializing  the listString to blank string, we added a header to it.  We set the header in the __init__() method for the class. Add this to __init__() in the view controller:

        self.reportHeader = "Penguin Type\tPenguin Action\n"+ "="*35 +"\t"+"="*35+"\n"

Python has a few really convenient string and list processing operators.  Concatenation, taking two strings and joining them together uses the + operator used for addition with numbers. Repetition of a string uses the * operator used for multiplication with numbers.  The string  “="*35  is 35 equals signs.  With the \t escape sequence taking us to the tab stop we specified in the view, the header sets up two underlined headings for a table listing our penguins.

Save and run. Add a few penguins.

The text widget with a few penguins added
The text widget with a few penguins added

We now have a working  list of penguins.  You can select in the list, type in the box.  Next time, we will select a line of text.

The Whole Code

Here is a listing of our application so far. you can also download it from Github here

# poppers_penguins_MVC_05
#MVC Version 2014 May 28
#Change: Adds the text box

from tkinter import *
from tkinter import ttk
from tkinter import messagebox
from tkinter import scrolledtext
 
 
class MyViewController():

    
    def __init__(self,parent):

        self.reportHeader = "Penguin Type\tPenguin Action\n"+ "="*35 +"\t"+"="*35+"\n"
        self.parent = parent;       
        self.view = MyView(self)
        self.model = MyModel(self)
        
    #Handlers -- target action
    def addPressed(self):
        self.view.setLabelText(self.view.getPenguinType()+ ' Penguin '+ self.view.getPenguinAction() + ' Added')
        self.model.addRecordToList(self.view.getPenguinType(),self.view.getPenguinAction())
    def quitPressed(self):
        self.view.setLabelText('Quitting')
        answer = messagebox.askokcancel('Ok to Quit','This will quit the program. \n Ok to quit?')
        if answer==True:            self.parent.destroy()

#Add list to string processing for the scrollbox.
    def spaceToUnderscore(self,aList, width):
        # convert spaces to underscores in list items
        # with a triple underscore between list items
        listString = str()
        for  element in aList:
            elementString = '{:{width}}'.format(element, width=width) 
            elementString = elementString.replace(' ','_')
            listString = listString + '___' + elementString
        return listString
    
    def listToListString(self,aList):
        #a temporary method for making lists compatible
        #with listbox till I find something better
        #returns a string we can place in a StringVar
        listString = str() 
        for record in aList:
            elementString = self.spaceToUnderscore(record,20)
            listString = listString + '\n' + elementString
        return listString
#Add list to string processing for the Text widget
    def elementListToString(self,aList): #moved from above code for clarity
        listString = str()
        for  element in aList:
            if len(listString)>0:
                listString = listString + '\t' + element
            else:
                listString = element
        return listString

    
    def listToTextString(self,aList):
        #method for making lists compatible
        #with the text box
        #returns a string we can insert in the text box
        listString = self.reportHeader 
        for record in aList:
            elementString = self.elementListToString(record)
            listString = listString + '\n' + elementString
        return listString
            
#delegate for the model            
    def modelDidChangeDelegate(self):
        aList = self.model.getList()
        aListString = self.listToListString(aList)
        self.view.setDataList(aListString)
        self.view.setTextboxText(self.listToTextString(aList))
    
class MyView(Frame):
#Change parent to vc and add a frame 2014 May 26 a  
    def __init__(self,vc):
        self.frame=Frame()
        self.frame.grid(row=0,column=0)
        self.vc = vc
        
        #properties of the view 
        self.labelText = StringVar()
        self.labelText.set("Popper's Penguins Ready")
        self.penguinType = StringVar()
        self.penguinType.set('Penguin Type')
        self.penguinAction = StringVar()
        self.penguinAction.set('Penguin Action')

#Add the control variable
        self.dataList = StringVar()
        self.dataList.set ('')
# instantiate the text box
        self.dataList_textbox = Text(self.frame)       
        self.loadView()

 #       self.makeStyle()
         

#Setters and getters for the properties 2014-May 26
    def setLabelText(self,newText):
        self.labelText.set(newText)
    def getLabelText(self):
        return self.labelText.get()
    def setPenguinType(self,newType):
        self.penguinType.set(newType)
    def getPenguinType(self):
        return self.penguinType.get()
    def setPenguinAction(self,newAction):
        self.penguinAction.set(newAction)
    def getPenguinAction(self):
        return self.penguinAction.get()
#Add setters and getter for data list
    def getDataList(self):
        #returns a string
        return self.dataList.get()
    def setDataList(self,listString):
        #needs a list string delimtied by a space
        self.dataList.set(listString)
#Setters and getters for the text widget 2014-Jun-13
    def getTextBoxtext(self):
    #return a string of all the boxes contents
        return self.dataList_textbox.get('0.0',END)
            
    def setTextboxText(self,listString):
        #delete everything in a text box and replace with the text
        self.dataList_textbox.delete("0.0",END)
        self.dataList_textbox.insert('0.0',listString)
    
#Style Sheet
    def makeStyle(self):
        self.s = ttk.Style()
        self.s.configure('TFrame',background = '#5555ff')
        self.s.configure('TButton',background = 'blue', foreground = '#eeeeff', font = ('Sans','14','bold'), sticky = EW)
        self.s.configure('TLabel',font=('Sans','16','bold'),background = '#5555ff', foreground = '#eeeeff')
        self.s.map('TButton', foreground = [('hover','#5555ff'), ('focus', 'yellow')])
        self.s.map('TButton', background = [('hover', '#eeeeff'),('focus','orange')])
        self.s.configure('TCombobox',background = '#5555ff',foreground ='#3333ff',font = ('Sans',18))
   
#loading the view
#changed the command= to refer to the view controller 2014-May-26
    # self.addPressed now is self.vc.addPressed
    # self.quitPressed now is self.vc.quitPressed
    def loadView(self):
        #label
        buttonFrame = ttk.Frame(self.frame)
        buttonFrame.grid(row = 1, column = 0)
        
        status_label = ttk.Label(self.frame, textvariable = self.labelText)
        #status_label.configure(font=('Sans','16','bold'),background = 'blue', foreground = '#eeeeff')
        status_label.grid(row=0,column=0,sticky=NSEW)
 
        add_button = ttk.Button(buttonFrame,command= self.vc.addPressed,text = 'Add')
        add_button.grid(row = 0, column = 0,sticky = NSEW)
        quit_button = ttk.Button(buttonFrame, command = self.vc.quitPressed, text = 'Quit')
        quit_button.grid(row = 0, column = 1,sticky = NSEW)
 
        penguinType_values = ['Adele','Emperor','King','Blackfoot','Humboldt','Galapagos','Macaroni','Tux','Oswald Cobblepot','Flippy Slippy']
        penguinType_combobox = ttk.Combobox(buttonFrame,values = penguinType_values, textvariable = self.penguinType)
        penguinType_combobox.grid(row =1, column = 0,sticky = EW)
 
        penguinAction_values = ['Happy','Sad','Angry','Standing','Swimming','Eating','Sleeping','On Belly','Plotting Evil','Singing','Dancing','Being Cute']
        penguinAction_combobox = ttk.Combobox(buttonFrame, values = penguinAction_values, textvariable = self.penguinAction)
        penguinAction_combobox.grid(row=1, column = 1,sticky = EW)

        listFrame = ttk.Frame(self.frame)
        listFrame.grid(row =2,column = 0)
        
        dataList_yScroll = Scrollbar(listFrame, orient=VERTICAL)
        dataList_yScroll.grid(row=0,column=1,sticky = NS)
    
        dataList_listbox = Listbox(listFrame, listvariable= self.dataList, width = 40)
        dataList_listbox.grid(row=0, column = 0, sticky=NSEW)
#bind the listbox and scrollbar
        dataList_listbox.configure(yscrollcommand = dataList_yScroll.set)
        dataList_yScroll.configure(command=  dataList_listbox.yview)
#set up the text box
        self.dataList_textbox = scrolledtext.ScrolledText(listFrame, tabs= ("10c"))
        self.dataList_textbox.grid(row=1, column = 0, sticky = NSEW);
        self.dataList_textbox.insert("0.0","Penguin\tPenguin Action") #test data


#(1)adding the model 2014-May-28 with convenience method
class MyModel():
    def __init__(self,vc):
        self.vc = vc
        self.myList = []
        self.count = 0
#(2)Delegate goes here. Model would call this on internal change
    def modelDidChange(self):
        self.vc.modelDidChangeDelegate()
#Setters and getters for the model   
    def getList(self):
        return self.myList
    def addToList(self,item):
        myItem = item
        myList = self.myList
        myList.append(myItem)
        self.myList=myList
        self.modelDidChange()
    def getItemAtIndex(self,index):
        return myList[index]
#other methods
    def addRecordToList(self,penguin,action):
        self.addToList([penguin,action])
    

         
def main():
    root = Tk()
#Set up the main loop 2014 May 26
    frame = Frame(root, background="#5555ff", takefocus = 0)
    root.title("Popper's Penguins")         
    app = MyViewController(root)
    root.mainloop() 
 
 
if __name__ == '__main__':
    main()  

 

From Apple To Raspberry Pi: An Example of Implementing a Python MVC (Part 2)

appleberryIn the last part we implemented the view and controller for the Popper’s Penguins application. Now we add the model.  Along the way, we will make a delegate method for the model and change our target actions to add our data to a list.

Add the Model Class

We want to keep a list of our penguin observations. As a beginning data structure, the list data structure in Python will do this. Lists are mutable collections of sequential data. Lists can take any data type, including more lists. This will come in handy for us. We have two pieces of data we want to keep together: the penguin type and the penguin action.  We can then send that list to the list in our model.

Add this code:

#(1)adding the model 2014-May-28 with convenience method
class MyModel():
    def __init__(self,vc):
        self.vc = vc
        self.myList = []
        self.count = 0
#Setters and getters for the model
    def getList(self):
        return self.myList
    def addToList(self,item):
        myItem = item
        myList = self.myList
        myList.append(myItem)
        self.myList=myList
    def getItemAtIndex(self,index):
        return myList[index]
#other methods
    def addRecordToList(self,penguin,action):
        self.addToList([penguin,action])

We included a method addRecordToList() to take the penguin data and make it a list that then ends up in the model’s list. We are trying not to handle data manipulations in the controller, so making methods like this keeps all of those manipulations in the model.  The rest of the code is pretty simple, and is very similar to the example code from the MVC template post. We make some getters and setters for handling lists. There are many more we could create, but we will make what we need as we build the application.

Add the Delegate

In the View, we had the command attribute to signal that a change occurred the controller needs to handle. In the model, there is no such notification, we use a delegate instead. Add this code above the setters and getters in the model:

#(2)Delegate goes here. Model would call this on internal change
    def modelDidChange(self):
        self.vc.modelDidChangeDelegate()

This refers  to a method in the view controller that will handle the change. Now add this as the last line to the addList() method:

self.modelDidChange()

We now will jump to the delegate when there is an entry added. Note the one problem with delegates: We need to remember to put them in the model’s methods where there is change to the model.

Add a Model Instance to the Controller

We have set up our model. Now to use it.  Add this to the view controller, just under the instantiation for the view:

#added instantiation of view 2014 May 26
        self.view = MyView(self)
#(3)added instantiation of model 2014 May 28
        self.model = MyModel(self)

There is now a connection between the controller and the model.

Add the Delegate in the Controller

We have a delegate that needs attention. The model will call a delegate in the controller and we need to do something in that delegate to make sure it is working. Our next post will cover what to do with changing data in a GUI, but for now we’ll just print to the console.  Add this to the code for the controller, under the getters and setters.

#(4) added a delegate for the model 2014 May 28
    def modelDidChangeDelegate(self):
        print("Gork!!")
        print(self.model.getList())

Use the Model

We now have a model, so add the data collected to the list. In the controller, change the  addPressed() method to the following:

    def addPressed(self):
#Change getters and setters for the view
        self.view.setLabelText(self.view.getPenguinType()+ ' Penguin '+ self.view.getPenguinAction() + ' Added')
#(5)added true functionality -- this adds the entry to the model. 2014 May 28
        self.model.addRecordToList(self.view.getPenguinType(),self.view.getPenguinAction())

See If It works

Build and Run. Again there is no change when using the user interface.

Screenshot 2014-05-19 08.53.39
Press the Add button. The console does make a change.

>> ================================ RESTART ================================
>>>
Gork!!
[['Flippy Slippy', 'Sad']]

Add a few more, and the list increases:

>> ================================ RESTART ================================
>>>
Gork!!
[['Flippy Slippy', 'Sad']]
Gork!!
[['Flippy Slippy', 'Sad'], ['Adele', 'Singing']]
Gork!!
[['Flippy Slippy', 'Sad'], ['Adele', 'Singing'], ['Oswald Cobblepot', 'Plotting Evil']]
Gork!!
[['Flippy Slippy', 'Sad'], ['Adele', 'Singing'], ['Oswald Cobblepot', 'Plotting Evil'], ['Tux', 'Happy']]

This is something we should see in the window with a scrolling list. Next time we’ll add a scrolling list to the application.

The Whole Code

Here’s what our code looks like after this lesson. I have also posted code on github for the Popper’s Penguins and MVC  if you want to download it here.

# poppers_penguins_MVC_02
#MVC Version 2014 May 28
#Change: Implements the Model according to the MVC template

from tkinter import *
from tkinter import ttk
from tkinter import messagebox

class MyViewController():
    def __init__(self,parent):
        self.parent = parent;      

#added instantiation of view 2014 May 26
        self.view = MyView(self)
#(3)added instantiation of model 2014 May 28
        self.model = MyModel(self)

    #Handlers -- target action
    def addPressed(self):
#Change getters and setters for the view
        self.view.setLabelText(self.view.getPenguinType()+ ' Penguin '+ self.view.getPenguinAction() + ' Added')
#(5)added true functionality -- this adds the entry to the model. 2014 May 28
        self.model.addRecordToList(self.view.getPenguinType(),self.view.getPenguinAction())
    def quitPressed(self):
#Change getters and setters for the view
        self.view.setLabelText('Quitting')
        answer = messagebox.askokcancel('Ok to Quit','This will quit the program. \n Ok to quit?')
        if answer==True:
            self.parent.destroy()
#(4) added a delegate for the model 2014 May 28
    def modelDidChangeDelegate(self):
        print("Gork!!")
        print(self.model.getList())

class MyView(Frame):
#Change parent to vc and add a frame 2014 May 26 a
    def __init__(self,vc):
        self.frame=Frame()
        self.frame.grid(row=0,column=0)
        self.vc = vc

        #properties of the view
        self.labelText = StringVar()
        self.labelText.set("Popper's Penguins Ready")
        self.penguinType = StringVar()
        self.penguinType.set('Penguin Type')
        self.penguinAction = StringVar()
        self.penguinAction.set('Penguin Action')
#remove self.vc here as vc calls this, not the othere way around.   2014 May 26
#       self.vc = MyViewController(self)
        self.loadView()
        self.makeStyle()

#Handlers -- our pseudo-controller
#(3)removed from application 2014-May-26

#added setters and getters for the properties 2014-May 26
    def setLabelText(self,newText):
        self.labelText.set(newText)
    def getLabelText(self):
        return self.labelText.get()
    def setPenguinType(self,newType):
        self.penguinType.set(newType)
    def getPenguinType(self):
        return self.penguinType.get()
    def setPenguinAction(self,newAction):
        self.penguinAction.set(newAction)
    def getPenguinAction(self):
        return self.penguinAction.get()

#Style Sheet
    def makeStyle(self):
        self.s = ttk.Style()
        self.s.configure('TFrame',background = '#5555ff')
        self.s.configure('TButton',background = 'blue', foreground = '#eeeeff', font = ('Sans','14','bold'), sticky = EW)
        self.s.configure('TLabel',font=('Sans','16','bold'),background = '#5555ff', foreground = '#eeeeff')
        self.s.map('TButton', foreground = [('hover','#5555ff'), ('focus', 'yellow')])
        self.s.map('TButton', background = [('hover', '#eeeeff'),('focus','orange')])
        self.s.configure('TCombobox',background = '#5555ff',foreground ='#3333ff',font = ('Sans',18))

#loading the view
#changed the command= to refer to the view controller 2014-May-26
    # self.addPressed now is self.vc.addPressed
    # self.quitPressed now is self.vc.quitPressed
    def loadView(self):
        #label
        status_label = ttk.Label(self.frame, textvariable = self.labelText)
        #status_label.configure(font=('Sans','16','bold'),background = 'blue', foreground = '#eeeeff')
        status_label.grid(row=0,column=0,columnspan=4,sticky=EW)

        add_button = ttk.Button(self.frame,command= self.vc.addPressed,text = 'Add')
        add_button.grid(row = 2, column = 0)
        quit_button = ttk.Button(self.frame, command = self.vc.quitPressed, text = 'Quit')
        quit_button.grid(row = 2, column = 3)

        penguinType_values = ['Adele','Emperor','King','Blackfoot','Humboldt','Galapagos','Macaroni','Tux','Oswald Cobblepot','Flippy Slippy']
        penguinType_combobox = ttk.Combobox(values = penguinType_values, textvariable = self.penguinType)
        penguinType_combobox.grid(row =1, column = 0)

        penguinAction_values = ['Happy','Sad','Angry','Standing','Swimming','Eating','Sleeping','On Belly','Plotting Evil','Singing','Dancing','Being Cute']
        penguinAction_combobox = ttk.Combobox(values = penguinAction_values, textvariable = self.penguinAction)
        penguinAction_combobox.grid(row=1, column = 3)

#(1)adding the model 2014-May-28 with convenience method
class MyModel():
    def __init__(self,vc):
        self.vc = vc
        self.myList = []
        self.count = 0
#(2)Delegate goes here. Model would call this on internal change
    def modelDidChange(self):
        self.vc.modelDidChangeDelegate()
#Setters and getters for the model
    def getList(self):
        return self.myList
    def addToList(self,item):
        myItem = item
        myList = self.myList
        myList.append(myItem)
        self.myList=myList
        self.modelDidChange()
    def getItemAtIndex(self,index):
        return myList[index]
#other methods
    def addRecordToList(self,penguin,action):
        self.addToList([penguin,action])

def main():
    root = Tk()
#(8) Set up the main loop 2014 May 26
    frame = Frame(root, background="#5555ff", takefocus = 0)
    root.title("Popper's Penguins")
    app = MyViewController(root)
    root.mainloop() 

if __name__ == '__main__':
    main()  

From Apple To Raspberry Pi: An Example of Implementing a Python MVC (Part 1)

appleberryI gave a template for MVC in the last post, This time I’m going to use it in the Popper’s Penguins application. We’ll re-organize what we already did with views and controllers to match the template.  We’ll add the model in the next installment of this series.

Changing the View

Let’s start by modifying the view. The code in the view controller is dependent on this code.

Change the View’s Constructor

Change the __init__ for the view to this:

#(1) Change parent to vc and add a frame 2014 May 26 a  
    def __init__(self,vc):
        self.frame=Frame()
        self.frame.grid(row=0,column=0)
        self.vc = vc
        
        #properties of the view 
        self.labelText = StringVar()
        self.labelText.set("Popper's Penguins Ready")
        self.penguinType = StringVar()
        self.penguinType.set('Penguin Type')
        self.penguinAction = StringVar()
        self.penguinAction.set('Penguin Action')
#(2)remove self.vc here as vc calls this, not the other way around.   2014 May 26
#       self.vc = MyViewController(self)
        self.loadView()
        self.makeStyle()

Lines 3 and 4 creates the frame for the view. We set the pointer to the view controller to self.vc in line 5 which the view controller passes to the view’s constructor when it instantiates the view object. We left lines 8-13 alone. There is a valid point to set them in the view controller, but there is an equally valid point they are initial values, and thus part of the view. In Interface Builder terms, this is the same as setting the default values in the properties window.

Since the view controller instead of the view is the first class called by main() we remove self.vc in line 15. I commented it out here but it perfectly okay to remove it completely.

Remove the Pseudo-Controller

We have in our code the redundant pseudo-controller for target-actions within the view. We want target actions to happen in our controller, not our view. Remove the code or comment it out like this:

Handlers -- our pseudo-controller
#(3)removed from application 2014-May-26
#    def addPressed(self):
#        self.labelText.set(self.penguinType.get()+ ' Penguin '+ self.penguinAction.get() + ' Added')
#    def quitPressed(self):
#        self.labelText.set('Quitting')
#        answer = messagebox.askokcancel('Ok to Quit','This will quit the program. \n Ok to quit?')
#        if answer==True:
#            self.parent.destroy()

Add Setters and Getters for Our Properties

Next we need setters and getters for the controller to communicate with the view. Add the following methods after the __init__() method:

#(4)added setters and getters for the properties 2014-May 26
    def setLabelText(self,newText):
        self.labelText.set(newText)
    def getLabelText(self):
        return self.labelText.get()
    def setPenguinType(self,newType):
        self.penguinType.set(newType)
    def getPenguinType(self):
        return self.penguinType.get()
    def setPenguinAction(self,newAction):
        self.penguinAction.set(newAction)
    def getPenguinAction(self):
        return self.penguinAction.get()

None of this special. At the moment they are simple setters and getters. We may make some form of type checking for safety at some point, but for simplicity, we’ll skip that for now.

Change the Target-Action Calls

The command attributes are not calling the correct methods. They are calling the target action we just removed. We need to change the calls so they call the controller’s versions.  Change the loadView() for the buttons as follows:

#(5)changed the command= to refer to the view controller 2014-May-26
    # self.addPressed now is self.vc.addPressed
    # self.quitPressed now is self.vc.quitPressed
    #buttons
        add_button = ttk.Button(self.frame,command= self.vc.addPressed,text = 'Add')
        add_button.grid(row = 2, column = 0)
        quit_button = ttk.Button(self.frame, command = self.vc.quitPressed, text = 'Quit')
        quit_button.grid(row = 2, column = 3)

The View Controller

We’ve made the change to the view. Now to make the changes to the view controller. We’ll instantiate the view, make target actions and refer to view properties by setters and getters.

Instantiate the View in the View Controller

Change the constructor first so we have a view to use. Change the __init__ in MyController to this:

    def __init__(self,parent):
        self.parent = parent;               
#(6)added instantiation of view 2014 May 26
        self.view = MyView(self)

Any time we need to use a property of a view, we can now access it from self.view.

Change the Target Actions

We moved the target actions into the view controllers.  We’ll need to set up those in the View Controller.   There are a few place-maker versions already there. Replace them with this:

#Handlers -- target action
    def addPressed(self):
#(7a) Change getters and setters for the view
        self.view.setLabelText(self.view.getPenguinType()+ ' Penguin '+ self.view.getPenguinAction() + ' Added')
        
    def quitPressed(self):
#(7b) Change getters and setters for the view
        self.view.setLabelText('Quitting')
        answer = messagebox.askokcancel('Ok to Quit','This will quit the program. \n Ok to quit?')
        if answer==True:
            self.parent.destroy() 

In lines 4 and 8, we are now using the setters and getters we made in the view, and not directly using Tkinter set() and get(). Line 11 calls the root node to destroy the window when we indicate in the dialog box to quit.

Change the Main Method

We deleted references to the window in our view constructor. We also have to change what class we instantiate in main() from MyView to MyViewController. It makes a window, sets a title, starts the view controller and starts mainloop(). Change it to this:

def main():
    root = Tk()
#(8) Set up the main loop 2014 May 26
    root.title("Popper's Penguins")         
    app = MyViewController(root)
    root.mainloop() 
 

Test the Application

Build and run:

Screenshot 2014-05-19 08.54.16

If all works right, nothing will change.  In our next lesson, we’ll discuss a little about lists and will add our first iteration of the  model.

The Whole Code.

# poppers_penguins_MVC_01
#MVC Version 2014 May 26
#implements the View and View Controller according to the MVC template

from tkinter import *
from tkinter import ttk
from tkinter import messagebox
 
 
class MyViewController():
    def __init__(self,parent):
        self.parent = parent;      
         
#(6)added instantiation of view 2014 May 26
        self.view = MyView(self)

#Handlers -- target action
    def addPressed(self):
#(7a) Change getters and setters for the view
        self.view.setLabelText(self.view.getPenguinType()+ ' Penguin '+ self.view.getPenguinAction() + ' Added')
        
    def quitPressed(self):
#(7b) Change getters and setters for the view
        self.view.setLabelText('Quitting')
        answer = messagebox.askokcancel('Ok to Quit','This will quit the program. \n Ok to quit?')
        if answer==True:
            self.parent.destroy()
 
class MyView(Frame):
#(1) Change parent to vc and add a frame 2014 May 26 a  
    def __init__(self,vc):
        self.frame=Frame()
        self.frame.grid(row=0,column=0)
        self.vc = vc
        
        #properties of the view 
        self.labelText = StringVar()
        self.labelText.set("Popper's Penguins Ready")
        self.penguinType = StringVar()
        self.penguinType.set('Penguin Type')
        self.penguinAction = StringVar()
        self.penguinAction.set('Penguin Action')
#(2)remove self.vc here as vc calls this, not the othere way around.   2014 May 26
#        self.vc = MyViewController(self)
        self.loadView()
#       self.makeStyle()
         
#Handlers -- our pseudo-controller
#(3)removed from application 2014-May-26
#    def addPressed(self):
#        self.labelText.set(self.penguinType.get()+ ' Penguin '+ self.penguinAction.get() + ' Added')
#    def quitPressed(self):
#        self.labelText.set('Quitting')
#        answer = messagebox.askokcancel('Ok to Quit','This will quit the program. \n Ok to quit?')
#        if answer==True:
#            self.parent.destroy()

#(4)added setters and getters for the properties 2014-May 26
    def setLabelText(self,newText):
        self.labelText.set(newText)
    def getLabelText(self):
        return self.labelText.get()
    def setPenguinType(self,newType):
        self.penguinType.set(newType)
    def getPenguinType(self):
        return self.penguinType.get()
    def setPenguinAction(self,newAction):
        self.penguinAction.set(newAction)
    def getPenguinAction(self):
        return self.penguinAction.get()
    

#Style Sheet
    def makeStyle(self):
        self.s = ttk.Style()
        self.s.configure('TFrame',background = '#5555ff')
        self.s.configure('TButton',background = 'blue', foreground = '#eeeeff', font = ('Sans','14','bold'), sticky = EW)
        self.s.configure('TLabel',font=('Sans','16','bold'),background = '#5555ff', foreground = '#eeeeff')
        self.s.map('TButton', foreground = [('hover','#5555ff'), ('focus', 'yellow')])
        self.s.map('TButton', background = [('hover', '#eeeeff'),('focus','orange')])
        self.s.configure('TCombobox',background = '#5555ff',foreground ='#3333ff',font = ('Sans',18))
   
#loading the view
    def loadView(self):
        #label
        status_label = ttk.Label(self.frame, textvariable = self.labelText)
        status_label.grid(row=0,column=0,columnspan=4,sticky=EW)
#(5)changed the command= to refer to the view controller 2014-May-26
    # self.addPressed now is self.vc.addPressed
    # self.quitPressed now is self.vc.quitPressed

    #buttons
        add_button = ttk.Button(self.frame,command= self.vc.addPressed,text = 'Add')
        add_button.grid(row = 2, column = 0)
        quit_button = ttk.Button(self.frame, command = self.vc.quitPressed, text = 'Quit')
        quit_button.grid(row = 2, column = 3)
    #combobox
        penguinType_values = ['Adele','Emperor','King','Blackfoot','Humboldt','Galapagos','Macaroni','Tux','Oswald Cobblepot','Flippy Slippy']
        penguinType_combobox = ttk.Combobox(values = penguinType_values, textvariable = self.penguinType)
        penguinType_combobox.grid(row =1, column = 0)
        penguinAction_values = ['Happy','Sad','Angry','Standing','Swimming','Eating','Sleeping','On Belly','Plotting Evil','Singing','Dancing','Being Cute']
        penguinAction_combobox = ttk.Combobox(values = penguinAction_values, textvariable = self.penguinAction)
        penguinAction_combobox.grid(row=1, column = 3)


         
def main():
    root = Tk()
#(8) Set up the main loop 2014 May 26
    root.title("Popper's Penguins")         
    app = MyViewController(root)
    root.mainloop() 
 
 
if __name__ == '__main__':
    main()  

Living without Storyboards: Make a Working Stopwatch in Sprite Kit

Over the last few posts we’ve made a clock and stopwatch application in Sprite Kit. Last time, we added an animated button display for the stopwatch. This time we get the stopwatch buttons working.

Get Our Variables in Order

We will use several variables in our changes. Change our variables to the following:

@implementation MPMyScene{
    SKLabelNode *myTimeLabel;
    SKLabelNode *myDateLabel;
    BOOL isShowingTime;
    BOOL isStopwatch;           //changed from isStopWatch for clarity
    BOOL isResettingStopwatch;  //changed from isStartingStopwatch for clarity.
    BOOL isRunningStopwatch;
    CFTimeInterval startTime;
    CFTimeInterval previousElapsedTime;
    CFTimeInterval elapsedTimeInterval;
    CFTimeInterval totalElapsedTime;
}

We changed two variables used in earlier iterations. One was a misspelling, the other makes more sense as isResettingStopwatch.

We will also  initialize these. Add  into initWithSize:

        //initialize everything else
        isShowingTime = YES;
        isStopwatch = NO;
        previousElapsedTime = 0.0;
        isRunningStopwatch = NO;
        isResettingStopwatch = NO;

How to Make a Stopwatch

Our previous stopwatch started and stopped using the mode button. We set a variable startTime when we began the stopwatch and subtracted that value from the currentTime parameter of the update: method. That will not work when adding start and stop buttons. When the clock stops, currentTime will continue. Starting again gives us the elapsed time from entering the stopwatch mode, not from when we started and stopped the stopwatch. We therefore have another variable previousElapsedTime, This variable, when we stop the stopwatch, sets the time from our last start and stop. The currentTime and previous time then get added into a third variable elapsedTimeInterval. When we stop the watch, we set the previousElapsedTime to the value for elapsedTimeInterval.

Check for Events with touchesBegan:

Next is a big re-write of the touchesBegan: method. Replace the earlier method with this one:

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

    //find where the buttons are
    CGRect modeRect=[[self childNodeWithName:@"Mode"] frame];
    CGRect startRect=[[self childNodeWithName:@"Start"] frame];
    CGRect stopRect=[[self childNodeWithName:@"Stop"] frame];
    CGRect resetRect=[[self childNodeWithName:@"Reset"] frame];

    //scan the touches for button presses
    for (UITouch *touch in touches) {
        CGPoint touchPoint =[touch locationInNode:self];
        //--------------------------code for mode button
        if (CGRectContainsPoint(modeRect, touchPoint)){
            [self modePressed];
        }
        //--------------------------code for starting Stopwatch button
        else if (CGRectContainsPoint(startRect, touchPoint)){
            [self startPressed];
        }else if (CGRectContainsPoint(stopRect, touchPoint )){
        //--------------------------code for stopping Stopwatch button
            [self stopPressed];
        }else if (CGRectContainsPoint(resetRect, touchPoint)) {
        //--------------------------code for resetting Stopwatch button
            [self resetPressed];
        }else{
// shut down Stopwatch if touching anywhere else on screen.
            [self screenPressed];
        }
    }
}

The code had two major parts:

  • Identify the locations of the buttons
  • Parse the touch events from the location of the buttons

We removed all the handling code and made it separate methods. This is just good practice. While not quite MVC, it helps organize the code into parts for detecting the event and parts for handling it. If we implemented a more formal MVC, breaking things down this way makes transitioning to seperate classes much easier.

Make Event Handlers

Next, make  the event handlers. Add this above the method touchesBegan:

#pragma mark -----target action
-(void)moveDateOnTop:(BOOL)isDate{

    //set positions for display elements
    CGPoint topDisplay = [self gridPointX:2.5 pointY:4];
    CGPoint bottomDisplay = [self gridPointX:2.5 pointY:3.0];

    //make two actions for use
    SKAction *labelHideAction = [SKAction group:@[
                                                  [SKAction fadeAlphaTo:0.2 duration:3.0],
                                                  [SKAction moveToY:bottomDisplay.y duration:3.0]
                                                  ]];
    SKAction *labelShowAction = [SKAction group:@[
                                                  [SKAction fadeInWithDuration:3],
                                                  [SKAction moveToY:topDisplay.y duration:3.0]]];

    //remove the previous action.
    [myTimeLabel removeActionForKey:@"timeLabelAction"];
    [myDateLabel removeActionForKey:@"dateLabelAction"];

    //run the appropriate action
    if (isDate) {
        [myTimeLabel runAction:labelHideAction withKey:@"timeLabelAction"];
        [myDateLabel runAction:labelShowAction withKey:@"dateLabelAction"];
    }else{

        [myTimeLabel runAction:labelShowAction withKey:@"timeLabelAction"];
        [myDateLabel runAction:labelHideAction withKey:@"dateLabelAction"];

    }

}

-(void)screenPressed{
    isStopwatch = NO;
    previousElapsedTime = totalElapsedTime ;
    //move buttons back
    [self animatedStopwatchMenuOpen:NO];
    //toggle action
    [self moveDateOnTop: isShowingTime];
    isShowingTime = !isShowingTime;

}

This was the code we originally had in touchesBegan: to move the date and time up and down. It is now two methods. In screenPressed: we handle the event, adding one assignment from our earlier version which saves our stopwatch time if we hit the screen. The moveDateOnTop: animates the date and time transition.

Next we have the handler for the mode button, which again is  code from the old version of touchesBegan: moved into a method:

-(void)modePressed{
    //---------------------code for stopwatch here
    //show the stopwatch buttons
    [self animatedStopwatchMenuOpen:YES];
    //display the date on top
    [self moveDateOnTop:YES];
    isStopwatch = YES;
    isResettingStopwatch = YES;
    isShowingTime = NO;
    isRunningStopwatch = NO;
}

We’ve split the variable isStartingStopwatch into two flags: isResettingStopwatch and isRunningStopwatch. When we had only one button, these were the same function. Now that we have the start, stop and reset buttons, they are different functions.

Add the three handlers for the buttons.

-(void)startPressed{
    isRunningStopwatch = YES;
}
-(void)stopPressed{
    isRunningStopwatch = NO;
    previousElapsedTime += elapsedTimeInterval;
}
-(void)resetPressed{
    isResettingStopwatch = YES;
    isRunningStopwatch = NO;
    previousElapsedTime = 0;
}

Change the Game Loop Method

Finally, we change the stopwatch code in the update: method to the following:

if(isStopwatch){
        CFGregorianDate elapsedTime;
        //start the stopwatch by getting a point for elapsed time
        if(isResettingStopwatch){
            startTime=currentTime;
            isResettingStopwatch=NO;
        }

        if (isRunningStopwatch) { //get the time interval
        //update the time in the stopwatch
            elapsedTimeInterval =currentTime - startTime;
        } else { //keep start and current time the same for the next start of the watch
            elapsedTimeInterval = 0;
            startTime = currentTime;
        }

        totalElapsedTime =(elapsedTimeInterval + previousElapsedTime);
        //format and display the time
        elapsedTime  = CFAbsoluteTimeGetGregorianDate(totalElapsedTime, nil);
        NSString *formattedDateString = [NSString stringWithFormat:@"%02d:%02d:%04.2f", elapsedTime.hour, elapsedTime.minute, elapsedTime.second];
        myDateLabel.text = formattedDateString;

    }else{

Lines 12 through 17 give us our start/stop stopwatch functionality. When the user stops the stopwatch, the startTime updates to currentTime The next time  isStopwatchRunning is YES, elapsedTimeInterval will be zero, giving us a proper time.

We can now build and run.

stopwatch_2

And we have a working stopwatch. Next time, we’ll turn the pendulum into a flaming ball of fire with emitter nodes.

The Whole Code

Here’s the whole code for the scene:

//
//  MPMyScene.m
//  SpriteTimeClock
//
//  Created by Steven Lipton on 5/6/14.
//  Copyright (c) 2014 Steven Lipton. All rights reserved.
//

#import "MPMyScene.h"

@implementation MPMyScene{
    SKLabelNode *myTimeLabel;
    SKLabelNode *myDateLabel;
    BOOL isShowingTime;
    BOOL isStopwatch;           //changed from isStopWatch for clarity
    BOOL isResettingStopwatch;  //changed from isStartingStopwatch for clarity.
    BOOL isRunningStopwatch;
    CFTimeInterval startTime;
    CFTimeInterval previousElapsedTime;
    CFTimeInterval elapsedTimeInterval;
    CFTimeInterval totalElapsedTime;

}

-(CGPoint)gridPointX:(float)xPoint pointY:(float)yPoint{
    CGFloat xDivision = CGRectGetMaxX(self.frame) /5.0;
    CGFloat yDivision = CGRectGetMaxY(self.frame)/5.0;
    return CGPointMake(xPoint * xDivision, yPoint * yDivision);
}

-(id)initWithSize:(CGSize)size {
    if (self = [super initWithSize:size]) {
        /* Setup your scene here */
        self.backgroundColor = [SKColor colorWithRed:0.15 green:0.15 blue:0.3 alpha:1.0];

        myTimeLabel = [SKLabelNode labelNodeWithFontNamed:@"HelveticaNeue"];
        myTimeLabel.text = @"00:00:00";
        myTimeLabel.fontSize = 40;
        myTimeLabel.position = CGPointMake(CGRectGetMidX(self.frame),
                                       CGRectGetMaxY(self.frame)*0.80);
        myTimeLabel.name = @"myTimeLabel";

         [self addChild:myTimeLabel];

        myDateLabel = [SKLabelNode labelNodeWithFontNamed:@"HelveticaNeue"];
        myDateLabel.text = @"Date Goes Here";
        myDateLabel.fontSize = 40;
        myDateLabel.alpha = 0.20;
        myDateLabel.position = [self gridPointX:2.5 pointY:3.0];
        myDateLabel.name = @"myDateLabel";

        [self addChild:myDateLabel];

        //make a switch to a stopwatch
        //SKSpriteNode *modeButton= [SKSpriteNode spriteNodeWithImageNamed:@"solidcircle"];
        //modeButton.name = @"modeButton";
        //modeButton.position = [self gridPointX:4.0 pointY:2.0];
        //[self addChild:modeButton];

        //add the stopwatch buttons
        [self makeStopWatchButtonsAtGridPoint:CGPointMake(4.0, 2.0)];

        //Make the ball
        CGPoint tick = [self gridPointX:0.5 pointY:1];
        CGPoint tock = [self gridPointX:4.5 pointY:1];
        SKSpriteNode *bouncingBall = [SKSpriteNode spriteNodeWithImageNamed:@"solidcircle"];
        bouncingBall.position = tock;
        [bouncingBall setScale:0.75];
        bouncingBall.name=@"bouncingBall";
        [self addChild:bouncingBall];

/*
        //simple animation to the ball
        SKAction *bounceBall = [SKAction moveToX:tick.x duration:1.0];
        [bouncingBall runAction:bounceBall withKey:@"bounceBall"];

*/
        //add animation to the ball
        SKAction *bounceBall = [SKAction sequence:@[
                                                    [SKAction moveToX:tock.x duration:1.0],
                                                    [SKAction moveToX:tick.x duration:1.0],
                                                    ]];
        bounceBall.timingMode = SKActionTimingEaseInEaseOut;
        [bouncingBall runAction:[SKAction repeatActionForever:bounceBall] withKey:@"bounceBall"];

        //initialize everything else
        isShowingTime = YES;
        isStopwatch = NO;
        previousElapsedTime = 0.0;
        isRunningStopwatch = NO;
        isResettingStopwatch = NO;
    }

    return self;
}

-(SKSpriteNode *)makeButtonWithTitleAndName:(NSString *)title withImageNamed:(NSString *)imageName{
    SKSpriteNode *aSprite = [SKSpriteNode spriteNodeWithImageNamed:imageName];
    aSprite.name = title;
    [aSprite setScale:0.75];
    SKLabelNode *titleNode = [SKLabelNode labelNodeWithFontNamed:@"HelveticaNeue"];
    titleNode.text = title;
    titleNode.name = @"title";
    titleNode.fontSize = 16;
    titleNode.verticalAlignmentMode = SKLabelVerticalAlignmentModeCenter;
    titleNode.position = CGPointMake(CGRectGetMidX(aSprite.frame), CGRectGetMidY(aSprite.frame));
    titleNode.fontColor = [UIColor blackColor];
    [aSprite addChild:titleNode];
    return aSprite;

}

-(void)makeStopWatchButtonsAtGridPoint:(CGPoint)gridPoint{
    NSString *image = @"solidcircle";
    //make a base layeranchor is on the right side center.
    //anchor is right middle
        //make button 1 -- start
    SKSpriteNode *startButton = [self makeButtonWithTitleAndName:@"Start" withImageNamed:image];
    startButton.position = [self gridPointX:gridPoint.x pointY:gridPoint.y];
    [self addChild:startButton];
    //make button 2 -- stop
    SKSpriteNode *stopButton = [self makeButtonWithTitleAndName:@"Stop" withImageNamed:image];
    stopButton.position = [self gridPointX:gridPoint.x pointY:gridPoint.y];
    [self addChild:stopButton];
    //make button 3 -- reset
    SKSpriteNode *resetButton = [self makeButtonWithTitleAndName:@"Reset" withImageNamed:image];
    resetButton.position = [self gridPointX:gridPoint.x pointY:gridPoint.y];
    [self addChild:resetButton];

    //make a switch to a stopwatch
    SKSpriteNode *modeButton= [self makeButtonWithTitleAndName:@"Mode" withImageNamed:image];
    modeButton.position = [self gridPointX:gridPoint.x pointY:gridPoint.y];
    modeButton.scale = 1.0;
    [self addChild:modeButton];

}

-(SKAction *)moveToGridPositionX:(CGFloat)gridPointX fadeIn:(BOOL)isFadein duration:(NSTimeInterval)duration{
    CGPoint point = [self gridPointX:gridPointX pointY:0.0];
    SKAction *move =[SKAction moveToX:point.x duration:duration];
    SKAction *fadeInOut;
    if (isFadein){
        fadeInOut = [SKAction fadeInWithDuration:duration ];
    }else{
        fadeInOut = [SKAction fadeOutWithDuration:duration];
    }
    return [SKAction group:@[move,fadeInOut]];
}

-(void)animatedStopwatchMenuOpen:(BOOL)isOpen{
    //a method to make buttons appear for the stopwatch
    NSTimeInterval duration = 1.0;
    SKAction *moveButton;
    //get our buttons
    SKNode *modeButton =[self childNodeWithName:@"Mode"];
    SKNode *startButton = [self childNodeWithName:@"Start"];
    SKNode *stopButton = [self childNodeWithName:@"Stop"];
    SKNode *resetButton = [self childNodeWithName:@"Reset"];
    SKNode *title;

    if (isOpen){
    //push the on off to the side
        moveButton = [self moveToGridPositionX:5 fadeIn:YES duration:duration];
        [modeButton runAction:moveButton];
        title = [modeButton childNodeWithName:@"title"];
        [title runAction:[SKAction fadeOutWithDuration:duration]];
    //start to poistion 1
        moveButton = [self moveToGridPositionX:1 fadeIn:YES duration:duration];
        [startButton runAction:moveButton];
    //stop to position 2
        moveButton = [self moveToGridPositionX:2 fadeIn:YES duration:duration];
        [stopButton runAction:moveButton];
    //reset to position 3
        moveButton = [self moveToGridPositionX:3 fadeIn:YES duration:duration];
        [resetButton runAction:moveButton];
    }else{
    //close the menu
        moveButton = [self moveToGridPositionX:4 fadeIn:NO duration:duration];
        title = [modeButton childNodeWithName:@"title"];
        [title runAction:[SKAction fadeInWithDuration:duration]];

        [startButton runAction:moveButton];
        [stopButton runAction:moveButton];
        [resetButton runAction:moveButton];
        moveButton = [self moveToGridPositionX:4 fadeIn:YES duration:duration];
        [modeButton runAction:moveButton];
    }
}

#pragma mark -----target action
-(void)moveDateOnTop:(BOOL)isDate{
    //set positions for display elements
    CGPoint topDisplay = [self gridPointX:2.5 pointY:4];
    CGPoint bottomDisplay = [self gridPointX:2.5 pointY:3.0];

    //make two actions for use
    SKAction *labelHideAction = [SKAction group:@[
                                                  [SKAction fadeAlphaTo:0.2 duration:3.0],
                                                  [SKAction moveToY:bottomDisplay.y duration:3.0]
                                                  ]];
    SKAction *labelShowAction = [SKAction group:@[
                                                  [SKAction fadeInWithDuration:3],
                                                  [SKAction moveToY:topDisplay.y duration:3.0]]];

    //remove the previous action.
    [myTimeLabel removeActionForKey:@"timeLabelAction"];
    [myDateLabel removeActionForKey:@"dateLabelAction"];

    //run the appropriate action
    if (isDate) {
        [myTimeLabel runAction:labelHideAction withKey:@"timeLabelAction"];
        [myDateLabel runAction:labelShowAction withKey:@"dateLabelAction"];
    }else{

        [myTimeLabel runAction:labelShowAction withKey:@"timeLabelAction"];
        [myDateLabel runAction:labelHideAction withKey:@"dateLabelAction"];

    }

}

-(void)screenPressed{
    isStopwatch = NO;
    previousElapsedTime = totalElapsedTime ;
    //move buttons back
    [self animatedStopwatchMenuOpen:NO];
    //toggle action
    [self moveDateOnTop: isShowingTime];
    isShowingTime = !isShowingTime;

}

-(void)modePressed{
    //---------------------code for stopwatch here
    //show the stopwatch buttons
    [self animatedStopwatchMenuOpen:YES];
    //display the date on top
    [self moveDateOnTop:YES];
    isStopwatch = YES;
    isResettingStopwatch = YES;
    isShowingTime = NO;
    isRunningStopwatch = NO;
}

-(void)startPressed{
    isRunningStopwatch = YES;
}
-(void)stopPressed{
    isRunningStopwatch = NO;
    previousElapsedTime += elapsedTimeInterval;
}
-(void)resetPressed{
    isResettingStopwatch = YES;
    isRunningStopwatch = NO;
    previousElapsedTime = 0;
}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

    //find where the buttons are
    CGRect modeRect=[[self childNodeWithName:@"Mode"] frame];
    CGRect startRect=[[self childNodeWithName:@"Start"] frame];
    CGRect stopRect=[[self childNodeWithName:@"Stop"] frame];
    CGRect resetRect=[[self childNodeWithName:@"Reset"] frame];

    //scan the touches for button presses
    for (UITouch *touch in touches) {
        CGPoint touchPoint =[touch locationInNode:self];
        //--------------------------code for mode button
        if (CGRectContainsPoint(modeRect, touchPoint)){
            [self modePressed];
        }
        //--------------------------code for starting Stopwatch button
        else if (CGRectContainsPoint(startRect, touchPoint)){
            [self startPressed];
        }else if (CGRectContainsPoint(stopRect, touchPoint )){
        //--------------------------code for stopping Stopwatch button
            [self stopPressed];
        }else if (CGRectContainsPoint(resetRect, touchPoint)) {
        //--------------------------code for resetting Stopwatch button
            [self resetPressed];
        }else{
// shut down Stopwatch if touching anywhere else on screen.
            [self screenPressed];
        }
    }
}

#pragma mark ------game loop
-(void)update:(CFTimeInterval)currentTime {

    // time display
    /* Called before each frame is rendered */
    CFGregorianDate currentDate = CFAbsoluteTimeGetGregorianDate(CFAbsoluteTimeGetCurrent(), CFTimeZoneCopySystem());
    CFRelease(CFTimeZoneCopySystem());
    NSString *formattedTimeString = [NSString stringWithFormat:@"%02d:%02d:%02.0f", currentDate.hour, currentDate.minute, currentDate.second];
    myTimeLabel.text = formattedTimeString;

    /* ------- date or stopwatch display ---------------------  */

    if(isStopwatch){
        CFGregorianDate elapsedTime;
        //start the stopwatch by getting a point for elapsed time
        if(isResettingStopwatch){
            startTime=currentTime;
            isResettingStopwatch=NO;
        }

        if (isRunningStopwatch) { //get the time interval
        //update the time in the stopwatch
            elapsedTimeInterval =currentTime - startTime;
        } else { //keep start and current time the same for the next start of the watch
            elapsedTimeInterval = 0;
            startTime = currentTime;
        }

        totalElapsedTime =(elapsedTimeInterval + previousElapsedTime);
        //format and display the time
        elapsedTime  = CFAbsoluteTimeGetGregorianDate(totalElapsedTime, nil);
        NSString *formattedDateString = [NSString stringWithFormat:@"%02d:%02d:%05.2f", elapsedTime.hour, elapsedTime.minute, elapsedTime.second];
        myDateLabel.text = formattedDateString;

    }else{
        NSString *formattedDateString =[NSString stringWithFormat:@"%04d %02d %02d", (int)currentDate.year, currentDate.month, currentDate.day];
    myDateLabel.text = formattedDateString;
    }

}

@end