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
PomodoroTimeralready has anns_timerinstance, callinvalidate. - Set
ns_timerto a newNSTimerinstance that callsdecrementonce per second. - Add the
NSTimerto the current run loop, and if the delegate has been set then sendpomodoro_timer_did_startto 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.