Tag Archives: User interface

This Old App Episode 1: The Evaluation Stage

Before anyone thought that home improvement could be sexy, WGBH Boston started a series on house restoration. This Old House is  still going strong after 36 seasons. If you’ve never watched, each season a house in bad shape undergoes restoration. The cast goes through the paces of find what’s wrong, finding solutions and doing the work to make a new, beautiful home.

Recently, I headed over to my often neglected iTunes connect account, logged in and was met with the bright and friendly message:

 The following apps have unresolved issues and have been removed from the App Store:

Interval RunCalc

Going into the app, I found the message I expected to see:

Dear Developer,

On September 1, 2016, we announced that we’re implementing an ongoing process of evaluating and removing apps that no longer function as intended, don’t follow current review guidelines, or are outdated.

We noticed that your app has not been updated in a significant amount of time.

Next Steps

To keep your app on the App Store, submit an updated version for review and make sure it follows the latest App Review Guidelines. If you are unable to submit an update within 30 days, your app will be removed from the App Store until you submit an update and it is approved.

If Your App is Removed

Your app will remain fully functional for current users. They will experience no interruption to services, will still be able to buy in-app purchases, and can re-download the app while restoring from an iCloud or iTunes backup. However, we recommend that you update your app as soon as possible to reinstate it on the App Store and ensure that it remains functional and engaging for new and existing customers.

You can continue to use your current app name, as your app has not been deleted from your account.

Yep my app was one of the one removed in the great culling Apple threatened back in the fall of 2016. IntervalRunCalc was one of the apps that hit the chopping block. I wasn’t upset since the app had  only 13 sales in 2016. This isn’t big money, so really its no big deal if it lives or dies. If it had been more popular,  I would have been doing the revisions necessary to keep the app

I’ve just finished writing a piece for LinkedIn on Peanut butter and programming, which was a metaphor on what developers go through on a regular basis. I thought to follow up on that metaphor, I ‘d go through the process with a real app. I could go through the whole development process of Interval RunCalc, rebuilding the app using Swift instead Objective-C. Unlike my usual API of the week column, I thought of a This Old House type series on rebuilding an old app.   So for the next couple of weeks, I’ll be doing just that. We’ll explore the old application, then build models, new interfaces and putting it all together in controllers. We’ll go through the debugging process for the new app, and my adventure in getting the new app processed for Apple.  Along the way I’ll go through some good habits, find some old bad habits, and let you see my decisions as I rebuild the app.

The Original Interval RunCalc

Before we begin, you need to know the story of the app. Knowing the story of your app sets a lot of your design decisions.  You’ll need to know a little bit about running and one particular running coach, Jeff Galloway.  Running is a very popular form of physical exercise around the world. There is a lot of concern among non-runners and beginner runners about injury. Jeff Galloway, a 1972 Olympics  10,000 meter competitor for the U.S. and later running coach formulated a simple solution to the injury problem: don’t run so much by walking a little. His Run/Walk/Run method adds walk breaks or intervals during a run. You could for example run one minute, recover for thirty seconds, and the run another minute. You can workout on more complex patterns.  My current training runs for the upcoming race season is run four sets of  30 seconds run, 30 seconds walk, then pause for an full minute walk break, then repeat those for the length of the run.

For beginner runners, this has made entry into running easier, and has probably been one of the contributing factors to the popularity of race running. For older runners, the impact on bones is far less. There is a lot of evidence for cardio benefits of alternating between high intensity to low intensity activities repeatedly. Some faster distance runners are beginning to experiment with 30-second and less walk breaks once a mile to combat fatigue and run faster late in the race.

If you want to plan out a run walk run interval pattern to achieve a specific time or pace, there was no calculator to do it.  As a runner, I originally did it on spreadsheets, but the measurements systems necessary for pace, time and speed made such efforts clumsy. I wanted a tool, and figured others did too that did the calculations for me quickly. Input some numbers for intervals and walk breaks. Get back a pace, time and distance traveled. That’s why I wrote version 1.0 of Interval RunCalc.

The home screen contained basic information on pace, distance, time and speed. By changing one of these four variables you could see how it affected the other four.

This interface was very confusing, since you need to press the correct button and enter the figures correctly. Even for me, the developer, this was confusing. I fell victim to a very easy trap many developers fall into: Not testing the UI and believing what works is good enough.

Unless you know better, there were two more views in the app.  They were hidden in a button at the top.

 

Tap the button and you got a menu:

The Splits Calculator gave the splits at mile intervals and calculated your time if you decided to run some of those miles slower or faster.

  The interval calculator was the reason for the app. The user would input a series of intervals for the Pace,Speed,distance,and time.The calculator would give average pace, distance, and elapsed time back. The idea was to plan interval training runs using the app. Again, these interfaces worked, but was not easy to use.  I also added a few buttons at the bottom of the app that were confusing for adding and duplicating the intervals.

The last series of scenes I made were input scenes. Users would enter in values for HH:MM:SS time or MM:SS pace. A decimal keypad would enter  distance and speed. These all presented huge data validation problems, particularly the time.

    

Enter the Watch

A few months after the launch of Interval runcalc the Apple Watch came out. As fitness watch, it worked well with Apple’s products but not other companies. As of this writing, many still have annoying latency problems. You might  look at your watch for several seconds before getting an accurate number for your pace, time and distance. When I moved to an Apple watch from my Nike+  GPS watch, this drove me nuts. There is one exception to this latency problem: Apple’s native fitness app, which also has the active heart rate monitor data. Apple’s watch app however has no interval notifications. You can find those on a few of the big run-tracking apps, but not apple’s fitness activity app.

One solution did present itself. One of the 3rd party apps I used did intervals and sent a local notification to the phone when it was time to change intervals. When the phone went to sleep two minutes into a run, the notifications went to the watch — I had my notification by a tap on my wrist. I’d run both the app and the the apple fitness app at the same time.

If we’re going to update runcalc, there’s this one active thing it could do: send local notifications at the run/walk/run intervals. That way you can run this app in the background while using an apple watch to do the run.

Other Possible Changes

The original app was for iPhone only, I’d like to get it working well on a iPad, and iPhone plus in portrait might need a different look. These are both the regular size width class, and thus the issues would be related.

There’s one more feature I might want that isn’t there. Most of the running apps that allow additions of intervals for your workout want to know how many repetitions of the interval you want. For a one minute walk and one minute run, this is relatively easy. But for a 30 second run and 16 second walk, it isn’t so easy.  Knowing how many repetitions of this interval set make up a workout would be a helpful number.

Evaluating the Changes

Given the app’s current condition, let’s summarize the problems and ideas so far:

  1. The initial scene is confusing to use
  2. The two secondary scenes, splits and intervals are hidden
  3. Intervals are not easy to read
  4. I never use the splits
  5. The intervals are hard to use
  6. Input of data is difficult
  7. Add notifications for intervals
  8. Repetition calculation
  9. iPad use(regular width size classes)

I’ll find more as I go on, but this will give me my starting point, and set me planning.

There’s a bit of advice I’d like to start with before evaluating these. When writing an app, keep to your story, the reason your app does what it does. Consistent stories make stronger products. The story of Interval RunCalc is a calculator for running interval calculations. I’m working with the number for planning purposes, not running with this. With that story firmly in my mind, I start to make decisions.

The story is about calculations for intervals, yet I set intervals on a secondary page, not the main page. It needs to be up front. Looking at the first page , I have a lot of extra whitespace. What if I added the intervals in that whitespace?

Here’s another thought: a simple continuous run is a single interval. As long as I always have one interval in the table the data on top summarizes all runs. I’m now doing two pages on a single page.

I’ll start wire framing, sketching what this might look like:

I used the simplest, fastest way to wireframe. In this stage, don’t get overwhelmed by apps and templates letting you lay out a wireframe app. Paper,  pencil, and a few highlighters works fine. As I did here I used the sketching mode of the Apple Notes app. I can clean this all up later. Work fast, or you’ll get so bogged down in distractions you’ll never get anywhere.  I’ll add two buttons on the bottom for adding an interval and clearing the calculator:

Then I get into a debate with myself: Should there be a third button, a totals button? If I select an interval on the lower half to edit it, Should I edit it in the upper Half? If so, I’ll need a button to deselect all intervals and set the upper half to totals? While compact, It again presents me with a problem: A hidden or confusing interface. It would be better to select a row in the intervals, then go to a second view to edit and enter there.

 

This would look nice and consistent on the home screen too.Each might be a button, and tapping it could change it.  I’ll also add a button for the repetitions as well.

In a few user interface changes, I cover most of the issues with 1-5.

What to Cut

My first cut is notifications. That should be a separate app.  I do have data that would make the notifications easy to add, but that does not mean I add it. Notifications are not in the story.  This happens a lot in development, often referred to as feature creep.  One keeps adding functions to an app that it becomes, slow, unwieldy and overall unusuable. One classic example is Microsoft Word, which has so many extra features outside its core story of typing out a document. I’d rather use a simpler general word processor like Pages or Google Docs instead of Word for most documents. In updating an app, unless its a feature demanded by users, avoid adding anything outside the story. If you are an independent developer, this is a lot easier than being in an organization however. You might have teammates, upper management or marketing telling you to put something in that makes no sense. The advantage of getting the entire organization to  buy into the app story is keeping everyone focused.

For the same reasons, I’m dropping the splits calculator.  Since this is an Interval Run calculator, Splits are  not in the story.

Input Views

I’ll make one more change that I have been thinking about. I didn’t like the input in V1.0.  My original notes for 1.0 tell me I hated the time it takes to use a UIPickerview as a user.  In V2.0 I’ll change all my input to picker views to restrict the user, which they are very good at doing. Unlike other picker views I’ve seen, I’ll speed up the picker with more components. Instead of 60 rows in the seconds of minutes picker component, ill have components of 0 to 5 in one and 0 to 9 in the other, so I can quick dial any number.  My time pickers would look like this:

This solves a lot of the validation problems I had with the text fields by restricting input, but speeding up the input.

The Regular Width Devices

Our final problem in our list is the regualr width devices of iPad and iPhone plus in landscape. I have a simple solution to that. I’ll have two major views, the intervals and the stats. In regular width mode, they will be next to each other instead of on top of each other

With all that decided. I have the beginnings of a new version of this app. Many of the user interface decisions were made and those influence the structure. I was a good boy and for the moment I added only one more new feature, and killed several features. I have a few more user interface decisions to make, but I’ll get to those a little later.  My next step is to get into the code, and I usually start that with the models I’ll be using in the app. In the next lesson, we’ll build those new models, and look at the old code to find some of the problems I encountered with the old model I might want to change in the new one.

Swift Swift: Basic Core Graphics for the Ring Graph

Screenshot 2015-03-10 15.42.16If you take a look at many of the cutting-edge designs for mobile User interfaces on Behance or Pinterest, you find a ring, arc or circle graph. The Apple Watch’s fitness activity app uses them extensively. Yet, if you scroll down the object library in Xcode it is a not a control to drag and drop into Xcode. You have to make it yourself.

Actually, it’s not difficult to draw your own ring graph using Core graphics. In this post, we’ll walk through the process of making and using an arc graph.

Create the Project

Start by making a new Single-view project by pressing Command-Shift-N on the keyboard or File>New>Project.. from the drop down menu. Name the project SwiftRingGraph. Use Swift as the language and make it a Universal app.  Save the file somewhere convenient.

Go to the storyboard. Drag three sliders, two labels and a view (not a view controller) onto the scene.  Make both labels 20 point. Change one label to Pizza Power with a background color of white. Change the other label to 0% with a background color to Light gray . Make the view have a background color of white.  Make the background of one slider yellow, one slider red, and one slider white. Make the scene’s background black. Drag and size the objects to look like this:

Screenshot 2015-03-10 14.31.29

Add Auto Layout

You can leave it like this if you want to run in the iPad simulator.  I’ll add some auto layout to let this work on any device. If you haven’t used auto layout before, I suggest reading the Basic Auto Layout post or viewing the video, but you can follow along even if you  don’t.

Select the Pizza Power label and using the pin menu, pin 0 up -16 left, 5 right and 10 down. Also set the height to 50 points like this:

Screenshot 2015-03-10 14.32.12

Select the 0% label. Pin up 0 and right 0 points. Add the constraints.  Control drag from the 0% label to the Pizza Power Label.  In the menu that appears, select Equal Widths and Equal Heights.

Screenshot 2015-03-10 06.24.28

Pin the yellow slider up 20, left 0 and  right 0  points. Pin the white slider up 5, left 0 and right 0 points. Pin the red slider up 5, left 0,and right 0 points.

Select the View. Click the pin menu and set it for 300 width and 300 points height. Click the alignment menu and check on Center Horizontally, making sure the values are 0 points.

Screenshot 2015-03-10 14.46.24

In the resolver, select Update Frames.

Screenshot 2015-03-10 06.30.07

Select the Pizza Power Label. In the Equal width to 0% constraint, click Edit.

Screenshot 2015-03-10 14.52.42

Change the Multiplier from 1 to 1:3 or 3:1 so you get this on the storyboard:

Screenshot 2015-03-10 14.50.48

Connect the Outlets and Actions

Press Command-N. Add a Cocoa Touch Class called CircleGraphView subclassing UIView with Swift as the language.  Save the file and go back to the storyboard. Select the view. In the identity inspector, change the Custom Class to CircleGraphView:

Screenshot 2015-03-10 06.45.05

Close the inspector panel to give yourself room. Open up the Assistant editor. Select the 0% Label. Control drag from the label to the view controller class. Name the label’s outlet percentLabel. Control drag from the view to the viewController class. Name it circleGraph. Control drag from the slider to the code. Make an action named slider with a sender of UISlider. You should have this in your code:

    @IBOutlet weak var percentLabel: UILabel!
    @IBOutlet weak var circleGraph: CircleGraphView!
    @IBAction func slider(sender: UISlider) {
    }

Coding the Arc Graph View

Close the assistant editor and open the CircleGraphView.swift file. You will find some commented out code with a dire warning. We are doing custom drawing, so go ahead and remove the comments so you have this:

class CircleGraphView: UIView {

    override func drawRect(rect: CGRect) {
        // Drawing code
    }

}

Add the following properties to the code.

var endArc:CGFloat = 0.0 // in range of 0.0 to 1.0
var arcWidth:CGFloat = 10.0
var arcColor = UIColor.yellowColor()
var arcBackgroundColor = UIColor.blackColor()

We have the property endArc which is where the arc ends. We have a property arcWidth which will tell us how wide a line we want, with a default value of 10. Both values are CGFloat. Everything has a type CGFloat in Core Graphics, so keeping our properties typed correctly saves us a lot of work later. We also added  two color properties to set the color of the graph, and gave them some default values.

Working with Radians

Drawing always requires some math and geometry. Add the following to the drawRect method.

//Important constants for circle
        let fullCircle = 2.0 * CGFloat(M_PI)
        let start:CGFloat = -0.25 * fullCircle
        let end:CGFloat = endArc * fullCircle + start

Ring or arc graphs are circular arcs drawn in the view. We need a center point, an angle on the circle to start and an angle on the circle to end.
Core graphics does not work in degrees for angles, but radians. If you don’t remember your geometry or trigonometry lessons, radians describes angles around a circle as a measurement of pi. A full revolution around a circle is 2 times pi. There is a Double constant M_PI in Xcode we can use for calculations. We would need to convert it to CGFloat and multiply by 2 every time we use it. I calculated it once as a constant to use in calculations later. Looking at line 3 above, you’ll see how handy this is.  Any point on the circle is a fraction of a whole circle. If I use a CGFloat between 1 and 0, multiplied by fullCircle, I can describe any angle on the circle. Line 3 sets start by moving backwards a quarter of a circle. We do this because people think in clocks. The starting point should be 12 o’clock. The starting point of 0 in Core Graphics is at the 3 o’clock position, so we move back a quarter to start our line at the top.
Line four multiplies the endArc property by our full circle constant giving us where the arc ends. Since we shifted the arc a quarter of a turn we add that as an offset.

Next, Add this code:

//find the centerpoint of the rect
        var centerPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect))

Core Graphics has a set of functions to get dimension information from a CGRect rectangle. Two of these are CGRectGetMidX() and CGRectGetMidY(). I could type in these functions every time I need them, but I find having a CGPoint tends to be useful and better documentation.
Next add these lines of code:

       //define the radius by the smallest side of the view
        var radius:CGFloat = 0.0
        if CGRectGetWidth(rect) > CGRectGetHeight(rect){
            radius = (CGRectGetWidth(rect) - arcWidth) / 2.0
        }else{
            radius = (CGRectGetHeight(rect) - arcWidth) / 2.0
        }

The size of our circle or arc is set by the radius. We are going to automatically fit a circle into the view, based on the smallest side of the view and the width of the arc. We first figure if the width or length is shorter. We take that shorter side. We subtract the width of the line, so it does not go outside of the view, clipping it. We divide that by two, since we need a radius. We now have a line that will always fit inside any size view.

Starting to Draw

Before drawing, you must have a context to draw with. There is a core graphics function to do this UIGraphicsGetCurrentContext. Assign its value to a constant like this:

//starting point for all drawing code is getting the context.
let context = UIGraphicsGetCurrentContext()

Once you have a context, set the color space. For most purposes just use RGB like this:

        //set colorspace
        let colorspace = CGColorSpaceCreateDeviceRGB()

After adding those lines of code, set your line attributes. Add the following code:

//set line attributes
CGContextSetLineWidth(context, arcWidth)
CGContextSetLineCap(context, kCGLineCapRound)
CGContextSetStrokeColorWithColor(context, arcColor.CGColor)

All the functions require the context. Line 2 sets the line width and line 3 the end caps on the line. Since we are drawing a line, we have a few choices what the end of the line looks like. In this case we use the constant kCGLineCapRound to make a round end for the line, just like the Apple Watch uses. There is also kCGLineCapButt and kCGLineCapSquare for straighter lines. If kCGLineCapRound produces a syntax error, you may be on Swift 2.0. Use GCLineCap.Round  or .Round instead if it doesn’t work. The version with k‘s are for Objective-C.

The last line sets the line color. Core graphics does not use UIColor. However, UIColor has a property which is a CGColor, which is what we do here.

After all that, we draw the line. Add this to the code:

CGContextAddArc(context, centerPoint.x, centerPoint.y, radius, start, end, 0)
CGContextStrokePath(context)

Line 1 adds an arc to the path. Paths are invisible, and may have several lines, arcs,  and curves added to them. We have only one arc. End the path and draw the line from the path with the CGContextStrokePath function.

That is all we need to draw one arc in a view. We now have to tell the system to draw it. It is tempting to call the drawRect method from our view controller but there is a rule about Core Graphics: Never call drawRect. It will mess up the system if you do. Instead we call the view’s method setNeedsDisplay. How we call it is another question. For something like this, I’d call it from a change in  properties.  Change the code on the top of the class like this:

var endArc:CGFloat = 0.0{   // in range of 0.0 to 1.0
    didSet{
         setNeedsDisplay()
    }
}

We used a property observer to call setNeedsDisplay any time we change the endArc value.

Go to the ViewController.swift file. In the slider method, change to this:

@IBAction func slider(sender: UISlider) {
    circleGraph.endArc = CGFloat(sender.value)
    percentLabel.text = String(format:" %5.2f %%",sender.value * 100)
}

Every time we change the arc length in the slider, we run the code. We set value of the slider as a percentage by multiplying by 100. Build and run.

Screenshot 2015-03-10 15.03.13

It looks good, but the arc does not show up until you move the slider. We could do several things to fix this. We could set the slider’s initial value to 0. We could make an outlet for the slider, get its value and set it in viewDidLoad. For a short solution in a long post, I’ll hack the solution(I’d add the outlet in a real app). We know that the slider will have a value of 0.5. so in viewDidLoad set this:

circleGraph.endArc = 0.5

The line is a bit thin for the space. also in viewDidLoad, add code to make a bigger line:.

circleGraph.arcWidth = 35.0

Build and run. We now start with an arc on the device.

Screenshot 2015-03-10 15.04.52

Adding a Circle Background

You will find in most applications, like the activity monitor on the Apple Watch a faint circle background behind the arc. It give a user a sense of the track  of your arc. Between the CGContextSetLineCap and CGContextSetStrokeColorWithColor add this code:

//make the circle background
CGContextSetStrokeColorWithColor(context, arcBackgroundColor.CGColor)
CGContextAddArc(context, centerPoint.x, centerPoint.y, radius, 0, fullCircle, 0)
CGContextStrokePath(context)

This makes a dark circle behind the yellow arc. Drawing happens from the bottom of the view up, We need to place this code before the code for the arc in order for it to behind the arc. I changed the start and end for 0 and fulCcircle, creating a full circle.

Back in viewController, in viewDidLoad, set the property arcBackgroundColor to make a very dark gray track.

let backgroundTrackColor = UIColor(white: 0.15, alpha: 1.0)
circleGraph.arcBackgroundColor = backgroundTrackColor

Build and run.

Screenshot 2015-03-10 15.07.15

One more change to make is the yellow arc’s width be slightly smaller than the track. Add this line before just after the second CGContextSetStrokeColorWithColor:

CGContextSetLineWidth(context, lineWidth* 0.8)

This makes the yellow line only 80% of the line width, leaving a small remnant of the track. Build and run:

Screenshot 2015-03-10 15.11.39

Adding More Views

Let’s add some nested arcs like the activity graph for the Apple watch on your phone or iPad .

Move the slider over to about a third of the screen. Add two more sliders in line with the current slider so they line up like this:

Change the current view’s background color to yellow. Now drag a view out on the storyboard directly over the current view keep the background  of the new view white. Resize the new view so you can see the yellow of the first view. The white view, when placed on teh yellow view is now a subview of the yellow view.

Screenshot 2015-03-10 15.16.22

Control drag diagonally up and left until it highlights the current view. Shift-Select Center X, Center Y, Equal Heights and Equal Widths. Keeping the new view selected, Go to the size inspector. Edit the constraints so they all have 0 for a constant.

Screenshot 2015-03-10 15.21.08

Update the constraints for the frame. It will fill in the square.  Change the Equal Width and Equal Height to a multiplier of 2:3 or 3:2 whichever makes a smaller view.

Screenshot 2015-03-10 15.24.09

We have nested the white view inside the yellow view. Repeat these steps to nest a smaller red view inside the white view. You should end up with this.

Screenshot 2015-03-10 15.28.15

 

Select the white view.  in the identity inspector, change the class to CircleGraphView. Select the red view. Change the class to CircleGraphView.

Open the assistant editor and add actions for sliderWhite for the white slider and sliderRed for the red slider. Add and outlet whiteCircle  for the white view and redCircle for the red view.

In the sliderWhite code add this:

whiteCircle.endArc = CGFloat(sender.value)

In the sliderRed code add this:

redCircle.endArc = CGFloat(sender.value)

In viewDidLoad add this:

whiteGraph.arcWidth = 25.0
whiteGraph.arcColor = UIColor.whiteColor()
whiteGraph.endArc = 0.5
whiteGraph.arcBackgroundColor = backgroundTrackColor
        
redGraph.endArc = 0.25
redGraph.arcColor = UIColor.redColor()
redGraph.arcWidth = 20.0
redGraph.arcBackgroundColor = backgroundTrackColor

Build and run. Move the sliders around. This looks a little messy.

Screenshot 2015-03-10 15.36.21

The opaque backgrounds of the views is getting in the way of each other. Change the background color for all three views to Clear Color in the storyboard. Run again.

Screenshot 2015-03-10 15.42.16

Now that looks classy! Note we never touched the view code to do this. We built a sufficiently flexible class with properties to control the graph, then added the graphs to the super view.  This is one way of making multiple arcs. You can also do so as part of a single view, but I will leave that up to the reader to implement.

The Whole Code

Here is the code for the lesson. There is a download of a more robust version at the end of another lesson:    where we add pie wedges and some better background track techniques.

ViewController.swift

//
//  ViewController.swift
//  Swift Ring Graph
//
//  Created by Steven Lipton on 3/10/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var percentLabel: UILabel!
    @IBOutlet weak var circleGraph: CircleGraphView!
    @IBOutlet weak var whiteGraph: CircleGraphView!
    @IBOutlet weak var redGraph: CircleGraphView!
    @IBAction func slider(sender: UISlider) {
        circleGraph.endArc = CGFloat(sender.value)
        percentLabel.text = String(format:" %5.2f %%",sender.value * 100)
    }
    @IBAction func whiteSlider(sender: UISlider) {
        whiteGraph.endArc = CGFloat(sender.value)
    }
    @IBAction func redSlider(sender: UISlider) {
        redGraph.endArc = CGFloat(sender.value)
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        let backgroundTrackColor = UIColor(white: 0.15, alpha: 1.0)
        circleGraph.arcBackgroundColor = backgroundTrackColor
        circleGraph.arcWidth = 35.0
        circleGraph.endArc = 0.5

        whiteGraph.arcWidth = 25.0
        whiteGraph.arcColor = UIColor.whiteColor()
        whiteGraph.endArc = 0.5
        whiteGraph.arcBackgroundColor = backgroundTrackColor

        redGraph.endArc = 0.25
        redGraph.arcColor = UIColor.redColor()
        redGraph.arcWidth = 20.0
        redGraph.arcBackgroundColor = backgroundTrackColor

    }

}

CircleGraphView.swift

//
//  CircleGraphView.swift
//  Swift Ring Graph
//
//  Created by Steven Lipton on 3/10/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import UIKit

class CircleGraphView: UIView {
    var endArc:CGFloat = 0.0{   // in range of 0.0 to 1.0
        didSet{
            setNeedsDisplay()
        }
    }
    var arcWidth:CGFloat = 10.0
    var arcColor = UIColor.yellowColor()
    var arcBackgroundColor = UIColor.blackColor()

    override func drawRect(rect: CGRect) {

        //Important constants for circle
        let fullCircle = 2.0 * CGFloat(M_PI)
        let start:CGFloat = -0.25 * fullCircle
        let end:CGFloat = endArc * fullCircle + start

        //find the centerpoint of the rect
        var centerPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect))

        //define the radius by the smallest side of the view
        var radius:CGFloat = 0.0
        if CGRectGetWidth(rect) > CGRectGetHeight(rect){
            radius = (CGRectGetWidth(rect) - arcWidth) / 2.0
        }else{
            radius = (CGRectGetHeight(rect) - arcWidth) / 2.0
        }
        //starting point for all drawing code is getting the context.
        let context = UIGraphicsGetCurrentContext()
        //set colorspace
        let colorspace = CGColorSpaceCreateDeviceRGB()
        //set line attributes
        CGContextSetLineWidth(context, arcWidth)
        CGContextSetLineCap(context, kCGLineCapRound)
        //make the circle background

        CGContextSetStrokeColorWithColor(context, arcBackgroundColor.CGColor)
        CGContextAddArc(context, centerPoint.x, centerPoint.y, radius, 0, fullCircle, 0)
        CGContextStrokePath(context)

        //draw the arc
        CGContextSetStrokeColorWithColor(context, arcColor.CGColor)
        CGContextSetLineWidth(context, arcWidth * 0.8 )
        //CGContextSetLineWidth(context, arcWidth)
        CGContextAddArc(context, centerPoint.x, centerPoint.y, radius, start, end, 0)
        CGContextStrokePath(context)

    }

}

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()  

 

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

From Apple to Raspberry Pi: Adding Combo Box Selections and Message Boxes

We began to make our penguin data collection interface last week. Today we are going to add Combobox selections to the penguin panel.  The Combobox widget codes similar to the Entry widget. Instead of text for a title it takes a list for values. We also will add our own dialog box using the messagebox module to end the application.

Make the Combo Boxes

In loadView() Under the quitbutton code add the following:

       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)

This will add two combo boxes to the user interface. Lines 1 and 4 make a list of our options. Lines 2 and 5 make our combo box, using the values attributes. Lines 3 and 6 place it on the grid.

We will need a few new control variables. Add this above self.vc = MyViewController(self) in the __init__ method of MyView

        self.penguinType = StringVar()
        self.penguinType.set('Penguin Type')
        self.penguinAction = StringVar()
        self.penguinAction.set('Penguin Action')

That sets some prompts for the combo box. We only need an action to use the combo box. Change the addPressed method to

    def addPressed(self):

        self.parent.labelText.set(self.parent.penguinType.get()+ ' Penguin '+ self.parent.penguinAction.get() + ' Added')

Save and run.
Screenshot 2014-05-19 08.52.50
Try opening the combo boxes,
Screenshot 2014-05-19 08.53.12
Screenshot 2014-05-19 08.54.02
Now click Add:
Screenshot 2014-05-19 08.54.16
and the event registers.

Make a Message Box for Quitting

For the quitting, replace the quitButton target-action with this:

def quitPressed(self):
        self.view.labelText.set('Quitting')
        answer = messagebox.askokcancel('Ok to Quit','This will quit the program. \n Ok to quit?')
        if answer==True:
            self.destroy()

Also add the following import below the line for ttk

from tkinter import messagebox

While other version of Python and tkinter seem to use other import statements, only the one above seems to work on the latest version. Most of the tutorials will lead you down frustrating blind alleys, as I found out the hard way. The above code does work. Messagebox is a module with a set of methods for making dialog boxes. We need the askokcancel one. The first parameter puts a string in the title, and the second is a prompt in the window created. Note that the newline \n escape sequence works in the prompt. this method returns a boolean value. If we answered yes, it returns True. In that case we end the application using the destroy()method. Save and run. Click the Quit button.

Screenshot 2014-05-19 08.54.43

It works well. We have much of our UI in place. Next time we will go back to MVC and separate the controller from the view.

The Code, and Nothing but the Code

Here’s the code for our user interface so far:

# poppers_penguins_01
#Basic Object Oriented GUI 
from tkinter import *
from tkinter import ttk
from tkinter import messagebox


class MyViewController():
    def __init__(self,parent):
        self.parent = parent;
        
        
    #Handlers -- target action
    def addPressed(self):
        
        self.parent.labelText.set(self.parent.penguinType.get()+ ' Penguin '+ self.parent.penguinAction.get() + ' Added')
    def quitPressed(self):
        self.view.labelText.set('Quitting')
        answer = messagebox.askokcancel('Ok to Quit','This will quit the program. \n Ok to quit?')
        if answer==True:
            self.destroy()

class MyView(Frame):
  
    def __init__(self,parent):

        self.frame = Frame.__init__(self, parent, background="#5555ff", takefocus = 0)
        self.parent = parent
        self.parent.title("Popper's Penguins")
        
        self.labelText = StringVar()
        self.labelText.set("Popper's Penguins Ready")
        #some UI variables
        self.penguinType = StringVar()
        self.penguinType.set('Penguin Type')
        self.penguinAction = StringVar()
        self.penguinAction.set('Penguin Action')

        self.vc = MyViewController(self)
        self.loadView()
        self.makeStyle()
        
#Handlers -- our pseudo-controller
    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()
#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.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.addPressed,text = 'Add')
        add_button.grid(row = 2, column = 0)
        quit_button = ttk.Button(self.frame, command = self.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)
        
def main(): 
    root = Tk()
    app = MyView(root)
    root.mainloop()  


if __name__ == '__main__':
    main()  

Living Without Storyboards: Make a Clock with Sprite Kit

Sprite Kit — it’s not just for games anymore.  Sprite Kit is of course meant to write games, a full 2-d game engine waiting for use inside Xcode and iOS7.  While simulating buttons  writing SlippyFlippyPenguin, I began to realize there’s a lot more here that could be applied to a general UI. I’ve often tried to apply animations to elements on the storyboard ending up with flicker. A true built-in game cycle does much for removing flicker. Sprite Kit can create beautiful animated  user interfaces. This post series will make a basic clock app using Sprite Kit as a demonstration.

Before we do, its important to understand the concept of the game cycle.  Games are based on frames, similar to frames of a motion picture. By flickering images at a rate fast enough, the eye and brain treat the images as a smooth motion. Games run somewhere between 30 and 60 frames per second.

The game cycle treats each frame as unique, gets and reacts to events, and then renders an image that frame. There are a few methods involved with Sprite Kit’s game cycle which the developer overrides to make this happen. The most important for this demonstration is update:.

Make a Sprite Kit App

Open Xcode and Start a new project. There are two ways of getting to Sprite Kit,  but the template is a bit easier to start, so choose the Sprite Kit Game template.  Name  the application SpriteTimeClock.

Xcode will load a template with a Hello World demonstration of Sprite Kit. Click over to the MyScene.m implementation file. Start by deleting all the touchesBegan: method.

Make the Label

Create the label node we will use throughout the class.

@implementation MPMyScene{
    SKLabelNode *myTimeLabel;
}

Change the code in initWithSize: to this:

self.backgroundColor = [SKColor colorWithRed:0.15 green:0.15 blue:0.3 alpha:1.0];
myTimeLabel = [SKLabelNode labelNodeWithFontNamed:@"AmericanTypeWriter-Bold"];
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];

Line 1 sets our background color, which we left the same as the template. Line 2 makes a Label node, the Sprite Kit equivalent of a Label View, setting the font to American TypeWriter. Lines 3 and 4 set the text and font size properties, which are self-explanatory.

Line 5 sets the position with a CGPoint, which requires a bit of explanation. As I mentioned in the grids post, using percentages relative to the size of the frame avoids a lot of screen fragmentation problems. The system adjusts for the differences between 3.5″ and 4″ automatically. It will even adjust for iAd banners if they take up part of the view.

A big difference between the coordinate systems of UIView and Sprite Kit is a different origin and coordinate system. The origin is on the lower left corner and increases in value towards the top. While one might expect this label with a Y value of CGRectGetMaxY(self.frame)*0.80 to be in the lower fifth of the screen, it is actually in the upper fifth. Another difference is the node’s positioning anchor for Sprite Kit is not a corner by default, but the center of the node. The x position CGRectGetMidX(self.frame) is centered on the frame.

Line 6 is just good practice. Naming a node allows for searching for the node in the node tree. While I declared an instance variable I don’t have to. I can find the node by its name in any code I write using the childNodeWithName: method. See Making a Basic Obstacle for some examples.

We have not added the node to the node tree. Until we do, it does not exist in the application. Line 7 does this critical step.

Make a Clock

We want to make a clock, and to get the time we have an advantage: the update: method provides one for us. The clock helps a game developer time events in the game, but we will use the raw data from the CFTimeInterval to tell the time. This is a double which has the number of seconds since a reference date. The Core Foundation function CFAbsoluteTimeGetGregorianDate() will convert the time into the struct CFGregorianDate which has the date and time information in a usable form. To adjust for timezone, we use CFTimeZoneCopySystem() in the function. This function needs to be manually released. We can then use our results to make up a formatted string for the label. Add the following to update:

-(void)update:(CFTimeInterval)currentTime {
/* 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;
}

While most of the CFGregorianDate struct is integers, the seconds are a double. Build and run this.

Screenshot 2014-05-07 07.23.04

We have a quick and basic clock in a few lines of code. While this is not impressive by itself, next post we will add animation to the clock to switch between the date and the time.