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.

Leave a rating/review
Save for later
Share

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:

  1. If the PomodoroTimer already has an ns_timer instance, call invalidate.
  2. Set ns_timer to a new NSTimer instance that calls decrement once per second.
  3. Add the NSTimer to the current run loop, and if the delegate has been set then send pomodoro_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.

Gavin Morrice

Contributors

Gavin Morrice

Author

Over 300 content creators. Join our team.