RubyMotion Tutorial for Beginners: Part 2
In this RubyMotion Tutorial for beginners, you’ll learn how to make a simple Pomodoro app for the iPhone. By Gavin Morrice.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
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:
mkdir app/models
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:
touch app/models/pomodoro_timer.rb
Open 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 :count
The 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
By default 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 initialize
.
Weak References
PomodoroTimer
also needs a delegate to report when certain events occur.
Add the following code just below the spot where you declared count
:
attr_reader :delegate
You’re using 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 MainViewController
, using 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:
attr_accessor :ns_timer
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
PomodoroTimer
already has anns_timer
instance, callinvalidate
. - Set
ns_timer
to a newNSTimer
instance that callsdecrement
once per second. - Add the
NSTimer
to the current run loop, and if the delegate has been set then sendpomodoro_timer_did_start
to the delegate so it’s aware that the timer started.
You’ve yet to define PomodoroTimer#invalidate
and PomodoroTimer#decrement
.
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 pomodoro_timer_did_finish
.
Note the 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.
Run 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:
p.count
The value should be 10 as expected:
# => 10
Call start
on p to start the countdown sequence as follows:
p.start
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 nextResponder
to 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 loadView
:
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 timer_button
. If pomodoro_timer
has a value — i.e. is not nil — and it references a valid PomodoroTimer
, then invalidate the PomodoroTimer. Otherwise, create a new PomodoroTimer
instance.
Add the 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 private
directive:
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 delegate
to self
, and then starts the timer. Remember, tapping the button calls this method to you need to start the countdown as well.
Run 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
The NSLog
statements will print out a line to the console, just to show you that the methods are in fact being called.
Run 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 private
keyword.
There's just one more bit of housekeeping before moving on. In timer_button_tapped
you ask if pomodoro_timer
is valid?
, but you haven't yet defined a valid?
method on PomodoroTimer; if you tap the button twice RubyMotion will throw a NoMethodError
.
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.