Welcome back to our RubyMotion Tutorial for Beginners series!
In the first part of the series, you learned the basics of getting started with RubyMotion, and created a view controller with a few styled views.
In this second and final part of the series, you will add the rest of the logic to this app, including making the label count down and getting the app fully wrapped up.
Let’s get back in motion! :]
Building a Countdown Timer
At this point, you have a label and a button, and the label has some static text on it. You want the label to count down from 25 minutes.
It’s clear there should be some sort of object responsible for handling the countdown, but you haven’t defined that object yet. While it’s tempting to add that functionality to
MainViewController, that’s not really the controller’s responsibility. The controller’s job is to respond to events and direct what should happen next.
Run the following command in Terminal:
You’ll use the models directory to store the models containing the application logic of your app.
Run the following command in Terminal to create a new class that will serve as your countdown timer:
PomodoroTimer and add the following lines of code:
class PomodoroTimer end
PomodoroTimer has no superclass; it’s just a plain-old-ruby-object, or PORO for short.
The first thing
PomodoroTimer needs is an attribute to store its current value. To do this, add the following lines to app/models/pomodoro_timer.rb:
attr_accessor macro declares a getter and setter for
count. The equivalent to this in Objective-C is:
@interface PomodoroTimer : NSObject @property NSInteger count; @end
Now, add the following method to app/models/pomodoro_timer.rb:
def initialize @count = Time.secsIn25Mins end
count should be set to the number of seconds in 25 minutes, so you use the method you’ve just defined on
NSDate to set this in
PomodoroTimer also needs a delegate to report when certain events occur.
Add the following code just below the spot where you declared
attr_reader here because the default setter for your delegate isn’t appropriate in this case. Using
attr_accessor would create a setter that holds the delegate — in this case, an instance of
MainViewController in a strongly referenced instance variable. But since you’re going to define
PomodoroTimer as a property of
attr_accessor would create a circular dependency leading to memory leaks and crashes!
To avoid that mess, add the following method to pomodoro_timer.rb:
def delegate=(object) @delegate = WeakRef.new(object) end
Here you define your own setter for
delegate and set it as a weak reference. In Ruby, everything is an object, and weak references are no exception.
Add the following property to the PomodoroTimer class:
This property, as the name suggests, will hold an
NSTimer object that handles the countdown by firing once a second for 25 minutes.
Add the following method to the PomodoroTimer class next:
def start invalidate if ns_timer self.ns_timer = NSTimer.timerWithTimeInterval(1, target: self, selector: 'decrement', userInfo: nil, repeats: true) NSRunLoop.currentRunLoop.addTimer(ns_timer, forMode: NSDefaultRunLoopMode) delegate.pomodoro_timer_did_start(self) if delegate end
This handles the creation of a new timer. Here’s what’s going on in the code above:
- If the
PomodoroTimeralready has an
ns_timerto a new
NSTimerinstance that calls
decrementonce per second.
- Add the
NSTimerto the current run loop, and if the delegate has been set then send
pomodoro_timer_did_startto the delegate so it’s aware that the timer started.
You’ve yet to define
Add the following below the
start method you just wrote:
def invalidate ns_timer.invalidate delegate.pomodoro_timer_did_invalidate(self) if delegate end
This method simply passes
invalidate on to
ns_timer and then notifies the
delegate that the timer has been invalidated as long as a delegate has been set.
Finally, define the
decrement method as follows:
private def decrement self.count -= 1 return if delegate.nil? if count > 0 delegate.pomodoro_timer_did_decrement(self) else delegate.pomodoro_timer_did_finish(self) end end
This simple method decrements the value of
count by 1 each time it’s called. If there’s a delegate present and the count is greater than 0, it notifies the delegate that
pomodoro_timer_did_decrement. If the count is 0 then it notifies the delegate that
private directive above; since
decrement should only be used internally within the class itself, you make this method private by adding the directive above the class definition.
rake to build and launch your app; you can now play around with the new class you defined above. To do this, execute the following command in Terminal (with rake and the Simulator active) to initialize a new PomodoroTimer and assign it to a local variable:
p = PomodoroTimer.new
Inspect the value of
p.count using the commands below:
The value should be 10 as expected:
# => 10
start on p to start the countdown sequence as follows:
To see the countdown timer working, evaluate
p.count repeatedly — but don’t wait, you only have 10 seconds! :]
p.count # => 8 p.count # => 6 p.count # => 2
Now that you know your timer is working, you can use it in your app.
Adding a PomodoroTimer to MainViewController
Open main_view_controller.rb and declare the following property on MainViewController:
class MainViewController < UIViewController attr_accessor :pomodoro_timer # ... end
This holds the timer instance for this controller.
In the first part of this tutorial series, you added
MainView as the target for touch actions, with the action name of
timer_button_tapped. It's finally time to define that method.
Still in main_view_controller.rb, add the following code below
def timer_button_tapped(sender) if pomodoro_timer && pomodoro_timer.valid? pomodoro_timer.invalidate else start_new_pomodoro_timer end end
You call the above action when the user taps the
pomodoro_timer has a value — i.e. is not nil — and it references a valid
PomodoroTimer, then invalidate the PomodoroTimer. Otherwise, create a new
private directive just below the method you just added as shown below:
# ... def timer_button_tapped(sender) if pomodoro_timer && pomodoro_timer.valid? pomodoro_timer.invalidate else start_new_pomodoro_timer end end private # ...
This separates the public and private methods.
Finally, add the following method after the
def start_new_pomodoro_timer self.pomodoro_timer = PomodoroTimer.new pomodoro_timer.delegate = self pomodoro_timer.start end
start_new_pomodoro_timer assigns a new
PomodoroTimer instance to
pomodoro_timer, sets its
self, and then starts the timer. Remember, tapping the button calls this method to you need to start the countdown as well.
rake to build and launch your app, then tap the Start Timer button to see what happens:
2014-09-11 16:40:58.276 Pomotion[17757:70b] *** Terminating app due to uncaught exception 'NoMethodError', reason: 'pomodoro_timer.rb:22:in `start': undefined method `pomodoro_timer_did_start' for #<MainViewController:0x93780a0> (NoMethodError)
Hmm, something's wrong with your app. Can you guess what the problem is?
When you start
pomodoro_timer, it calls delegate methods on MainViewController — but those methods don't yet exist. In Ruby, this results in a NoMethodError exception.
Add the following delegate methods above the
private keyword in main_view_controller.rb:
def pomodoro_timer_did_start(pomodoro_timer) NSLog("pomodoro_timer_did_start") end def pomodoro_timer_did_invalidate(pomodoro_timer) NSLog("pomodoro_timer_did_invalidate") end def pomodoro_timer_did_decrement(pomodoro_timer) NSLog("pomodoro_timer_did_decrement") end def pomodoro_timer_did_finish(pomodoro_timer) NSLog("pomodoro_timer_did_finish") end
NSLog statements will print out a line to the console, just to show you that the methods are in fact being called.
rake once again and tap Start Timer; you should see the NSLog statements written out to the console as they're called:
Build ./build/iPhoneSimulator-8.1-Development Build vendor/PixateFreestyle.framework Build vendor/NSDate+SecsIn25Mins Compile ./app/controllers/main_view_controller.rb Link ./build/iPhoneSimulator-8.1-Development/Pomotion.app/Pomotion Create ./build/iPhoneSimulator-8.1-Development/Pomotion.app/Info.plist (main)> 2014-11-13 13:52:44.778 Pomotion[9078:381797] pomodoro_timer_did_start 2014-11-13 13:52:45.779 Pomotion[9078:381797] pomodoro_timer_did_decrement 2014-11-13 13:52:46.779 Pomotion[9078:381797] pomodoro_timer_did_decrement 2014-11-13 13:52:47.779 Pomotion[9078:381797] pomodoro_timer_did_decrement 2014-11-13 13:52:48.779 Pomotion[9078:381797] pomodoro_timer_did_decrement (nil)? 2014-11-13 13:52:49.778 Pomotion[9078:381797] pomodoro_timer_did_decrement 2014-11-13 13:52:50.778 Pomotion[9078:381797] pomodoro_timer_did_decrement (nil)? 2014-11-13 13:52:51.778 Pomotion[9078:381797] pomodoro_timer_did_decrement
If you still get an exception, make sure you followed the instructions above about pasting the methods before the
There's just one more bit of housekeeping before moving on. In
timer_button_tapped you ask if
valid?, but you haven't yet defined a
valid? method on PomodoroTimer; if you tap the button twice RubyMotion will throw a
Add the following code just beneath
start in pomodoro_timer.rb:
def valid? ns_timer && ns_timer.valid? end
In this case, a
valid result means that the
PomodoroTimer has an
NSTimer and that the timer is
valid. Ensure you've added this method above the
private directive, so that you can call this method on any instance of
PomodoroTimer from within other objects.