RubyMotion Tutorial for Beginners: Part 1

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
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Adding Views Programmatically

Just like controllers, views should have a directory of their own within the app directory.

Execute the following command in Terminal:

mkdir app/views

Next, execute the following command to create a file to contain the MainView of MainViewController:

touch app/views/main_view.rb

Open main_view.rb and add the following code:

class MainView < UIView

  def initWithFrame(frame)
    super.tap do
      self.styleId = 'main_view'
    end
  end
  
end

MainView is a custom view for the MainViewController which inherits from UIView. Although they share the same name prefix of "Main", this isn't a requirement in RubyMotion; it's just a nice convention to keep things organized in your app.

Pixate Freestyle adds two properties to all UIView subclasses: styleId and styleClass. These let you apply styles to any view. In the code above you set styleId on MainView when you override initWithFrame.

Note: tap was introduced in Ruby 1.9; it lets you call methods on an object within a block and returns that object at the end of the block's execution. This comes in really handy with things like initializers in iOS, which should always return self...which you always remember to do, right? :]

It's a heck of a lot easier than writing the following:

  def initWithFrame(frame)
    super
    self.styleId = 'main_view'
    return self
  end
  
  def initWithFrame(frame)
    super
    self.styleId = 'main_view'
    return self
  end
  

You can now move on to styling MainView using simple CSS!

Add a new CSS stylesheet to the resources directory named default.css as follows:

touch resources/default.css

And add the following code to the new stylesheet:

#main_view {
  background-color: white;
}

This is some simple CSS that sets the background color to white for the #main_view id, which will apply to the main view you just created.

Open app/main_view_controller.rb and add the following method to the implementation of MainViewController:

def loadView
  self.view = MainView.alloc.initWithFrame(CGRectZero)
end

This should look familiar; you're re-defining loadView in the view controller to load a custom view instance. In this case, the frame argument is CGRectZero, which sets the width and height of the view to 0.

Run rake to see your view in action; MainViewController now has a white background:

RW_Rubymotion_WhiteBG

Next you need a label on the screen to show the timer countdown as well as a button to start and stop the timer.

Open app/views/main_view.rb and add the following method:

def timer_label
  @timer_label ||= UILabel.alloc.initWithFrame(CGRectZero).tap do |label|
    label.styleId = 'timer_label'
    label.text    = '00:00'
  end
end

Just as before, you've defined a getterfor the label and you've used a memoization pattern to cache the UILabel in an instance variable. The label has a CSS style ID of timer_label and its text shows 00:00 — just like a real timer set to zero.

You set the frame to CGRectZero upon initialization since you'll use using Pixate and CSS to style the label including setting its size and origin.

While you're at it, you should add the timer button to your view as well.

Add the following code to app/views/main_view.rb:

def timer_button
  @timer_button ||= UIButton.buttonWithType(UIButtonTypeCustom).tap do |button|
    button.styleId = 'timer_button'
    button.setTitle('Start Timer', forState: UIControlStateNormal)
    button.setTitle("Interrupt!" , forState: UIControlStateSelected)
    button.addTarget(nextResponder, action: 'timer_button_tapped:',
      forControlEvents: UIControlEventTouchUpInside)
  end
end

In the above method you've set several titles for UIControlStateSelected; you'll be using the selected state shortly. nextResponder is the target object that responds to tap events, which in this case is the MainViewController displaying the view.

You could have just as easily defined a timer_button_tapped: action in MainView but, strictly speaking, that would have been a violation of MVC. Since it's the controller's job to respond to user input, that's where you should define the action.

Still in app/views/main_view.rb, modify initWithFrame as follows:

def initWithFrame(frame)
  super.tap do 
    self.styleId = 'main_view'
    addSubview(timer_label)
    addSubview(timer_button)
  end
ends

Here you add the label and button views you just created as subviews of MainView.

Finally, add the following styles to resources/default.css:

#timer_label {
  top: 160px;
  left: 60px;
  width: 200px;
  height: 60px;
  font-size: 60px;
  text-align: center;
  color: #7F7F7F;      
}
#timer_button {
  top: 230px;
  left: 60px;
  width: 200px;
  height: 40px;
  color: white;
  background-color: #007F00;
}

This styles the two views you added previously.

Run rake to launch your app; your app should now look like the following:

RW_Rubymotion_Timer1

Things are starting to look pretty nice! The timer is a bit static — your next job is to get it to count down.

Getting the Timer to Count Down

The Pomodoro Technique states that work should be accomplished in 25 minute blocks, so that's where your timer value will start.

This value is quite important to the application's logic, and might be referenced in more than one place; therefore it makes sense to write a helper method for NSDate so you don't have to hardcode time calculations throughout your code.

Importing C Code into Your Project

The category below extends NSDate with an extra method, secsIn25Mins:

+ (int) secsIn25Mins  { 
  return TARGET_IPHONE_SIMULATOR ? 10 : 1500;
}

The above method simply defines an extra class method on NSDate that returns the number of seconds in 25 minutes (25 x 60 = 1500 seconds). Since you won't want to wait a full 25 minutes every time you want to test the app during development, the method will return 10 seconds when the app runs in the simulator:

It would be a shame to have to port this code to RubyMotion, just so that you can use it in this app. This particular example is only a few lines, but other libraries or extensions could potentially be several thousand lines. Fortunately, RubyMotion lets you import Objective-C code directly into your app!

Create a new directory within the vendor directory and name it NSDate+SecsIn25Mins:

mkdir vendor/NSDate+SecsIn25Mins/

Then create two files named NSDate+SecsIn25Mins.h and NSDate+SecsIn25Mins.m:

touch vendor/NSDate+SecsIn25Mins/NSDate+SecsIn25Mins.h 
touch vendor/NSDate+SecsIn25Mins/NSDate+SecsIn25Mins.m

Open NSDate+SecsIn25Mins.h and add the following code:

#import <Foundation/Foundation.h>
  
@interface NSDate (SecsIn25Mins)
+ (int) secsIn25Mins;
@end

Now, paste the following code into NSDate+SecsIn25Mins.m:

#import "NSDate+SecsIn25Mins.h"
  
@implementation NSDate (SecsIn25Mins)
+ (int) secsIn25Mins  { 
  return TARGET_IPHONE_SIMULATOR ? 10 : 1500;
}
@end

Finally, add the following line to the bottom of Rakefile, just before the closing "end":

  app.vendor_project('vendor/NSDate+SecsIn25Mins', :static)

Run rake to build your app and launch it in the Simulator; RubyMotion automatically includes the code you added in the vendor directory.

To see your Obj-C methods at work, run the following command in Terminal (sill in rake, with the simulator active):

NSDate.secsIn25Mins

You should see the following result returned in Terminal:

# => 10

Just as you defined in NSDate+SecsIn25Mins, this returns 10 since you're running in the Simulator.

Note: If you run the command Time.secsIn25Mins, you'll get a return value of 10 as well, even though Time is a Ruby class and not part of the iOS API. What's going on?

RubyMotion cleverly merged the class hierarchies of both Ruby and iOS, so the Time class inherits from NSDate; this means the methods from both classes are available to Time objects.

Gavin Morrice

Contributors

Gavin Morrice

Author

Over 300 content creators. Join our team.