Video icon 64
Learning to code? Skill up faster with our practical video courses. Start your free trial today.
Advertisement

Let's Write a RubyMotion App: Part 2

by
Final product image
What You'll Be Creating

RubyMotion is a fantastic framework for building performant iOS applications using the Ruby language. In the first part this tutorial, you learned how to set up and implement a RubyMotion application. You worked with Interface Builder to create the application's user interface, implemented a view controller, and learned how to write tests for your application.

In this tutorial, you'll learn about the Model-View-Controller or MVC design pattern and how you can use it to structure your application. You'll also implement a painting view and add a gesture recognizer that allows the user to draw on the screen. When you're done, you'll have a complete, fully-working application.

1. Model-View-Controller

Apple encourages iOS developers to apply the Model-View-Controller design pattern to their applications. This pattern breaks classes into one of three categories, models, views, and controllers.

  • Models contain your application's business logic, the code that determines the rules for managing and interacting with data. Your model is where the core logic for you application lives.
  • Views display information to the user and allow them to interact with the application.
  • Controllers are responsible for tying the models and views together. The iOS SDK uses view controllers, specialized controllers with a little more knowledge of the views than other MVC frameworks.

How does MVC apply to your application? You've already started implementing the PaintingController class, which will connect your models and views together. For the model layer, you'll add two classes:

  • Stroke This class represents a single stroke in the painting.
  • Painting This class represents the entire painting and contains one or more strokes.

For the view layer, you'll create a PaintingView class that is responsible for displaying a Painting object to the user. You'll also add a StrokeGestureRecongizer that captures touch input from the user.

2. Strokes

Let's start with the Stroke model. A stroke will consist of a color and several points representing the stroke. To start, create a file for the Stroke class, app/models/stroke.rb, and another one for its spec, spec/models/stroke.rb.

Next, implement the stroke class skeleton and a constructor.

class Stroke
  attr_reader :points, :color
end

The Stroke class has two attributes, points, a collection of points, and color, the color of the Stroke object. Next, implement a constructor.

class Stroke
  attr_reader :points, :color

  def initialize(start_point, color)
    @points = [ start_point ]
    @color = color
  end
end

That looks great so far. The constructor accepts two arguments, start_point and color. It sets points to an array of points containing start_point and color to the provided color.

When a user swipes their finger across the screen, you need a way to add points to the Stroke object. Add the add_point method to Stroke.

def add_point(point)
  points << point
end

That was easy. For convenience, add one more method to the Stroke class that returns the start point.

def start_point
  points.first
end

Of course, no model is complete without a set of specs to go along with it.

describe Stroke do

  before do
    @start_point = CGPoint.new(0.0, 50.0)
    @middle_point = CGPoint.new(50.0, 100.0)
    @end_point = CGPoint.new(100.0, 0.0)
    @color = UIColor.blueColor

    @stroke = Stroke.new(@start_point, @color)
    @stroke.add_point(@middle_point)
    @stroke.add_point(@end_point)
  end

  describe "#initialize" do
    before do
      @stroke = Stroke.new(@start_point, @color)
    end

    it "sets the color" do
      @stroke.color.should == @color
    end
  end

  describe "#start_point" do
    it "returns the stroke's start point" do
      @stroke.start_point.should == @start_point
    end
  end

  describe "#add_point" do
    it "adds the points to the stroke" do
      @stroke.points.should == [ @start_point, @middle_point, @end_point ]
    end
  end

  describe "#start_point" do
    it "returns the start point" do
      @stroke.start_point.should == @start_point
    end
  end
end

This should start to feel familiar. You've added four describe blocks that test the initialize, start_point, add_point, and start_point methods. There's also a before block that sets a few instance variables for the specs. Notice the describe block for #initialize has a before block that resets the @stroke object. That's fine. With specs, you don't have to be as concerned with performance as you do with a regular application.

3. Drawing

It's the moment of truth, it's time to make your application draw something. Start by create a file for the PaintingView class at app/views/painting_view.rb. Because we're doing some specialized drawing, the PaintingView class is tricky to test. For the sake of brevity, I'm going to skip the specs for now.

Next, implement the PaintingView class.

class PaintingView < UIView
  attr_accessor :stroke

  def drawRect(rectangle)
    super

    # ensure the stroke is provided
    return if stroke.nil?

    # set up the drawing context
    context = UIGraphicsGetCurrentContext()
    CGContextSetStrokeColorWithColor(context, stroke.color.CGColor)
    CGContextSetLineWidth(context, 20.0)
    CGContextSetLineCap(context, KCGLineCapRound)
    CGContextSetLineJoin(context, KCGLineJoinRound)

    # move the line to the start point
    CGContextMoveToPoint(context, stroke.start_point.x, stroke.start_point.y)

    # add each line in the path
    stroke.points.drop(1).each do |point|
      CGContextAddLineToPoint(context, point.x, point.y)
    end

    # stroke the path
    CGContextStrokePath(context);
  end
end

Phew, that's a lot code. Let's break it down piece by piece. The PaintingView class extends the UIView class. This allows PaintingView to be added as a subview of PaintingController's view. The PaintingView class has one attribute, stroke, which is an instance of the Stroke model class.

With regards to the MVC pattern, when working with the iOS SDK, it's acceptable for a view to know about a model, but it's not okay for a model to know about a view.

In the PaintingView class, we've overridden UIView's drawRect: method. This method allows you to implement custom drawing code. The first line of this method, super, calls the method on the super class, UIView in this example, with the provided arguments.

In drawRect:, we also check that the stroke attribute isn't nil. This prevents errors if stroke hasn't been set yet. We then fetch the current drawing context by invoking UIGraphicsGetCurrentContext, configure the stroke that we're about to draw, move the drawing context to the start_point of the stroke, and adds lines for each point in the stroke object. Finally, we invoke CGContextStrokePath to stroke the path, drawing it in the view.

Add an outlet to PaintingController for the painting view.

outlet :painting_view

Fire up Interface Builder by running bundle exec rake ib:open and add a UIView object to the PaintingController's view from the Ojbect Library on the right. Set the view's class to PaintingView in the Identity Inspector. Make sure that the painting view is positioned underneath the buttons you added earlier. You can adjust the ordering of the subviews by changing the positions of the view's in the view hierarchy on the left.

Control and drag from the view controller to the PaintingView and select the painting_view outlet from the menu that appears.

Select the painting view and set its background color to 250 red, 250 green, and 250 blue.

Don't forget to add a spec to spec/controllers/painting_controller_spec.rb for the painting_view outlet.

describe "#painting_view" do
  it "is connected in the storyboard" do
    controller.painting_view.should.not.be.nil
  end
end

To make sure your drawing code works correctly, add the following code snippet to the PaintingController class and run your application. You can delete this code snippet when you've verified everything is working as expected.

def viewDidLoad
  stroke = Stroke.new(CGPoint.new(80, 100), '#ac5160'.uicolor)
  stroke.add_point(CGPoint.new(240, 100))
  stroke.add_point(CGPoint.new(240, 428))
  stroke.add_point(CGPoint.new(80, 428))
  stroke.add_point(CGPoint.new(80, 100))
  painting_view.stroke = stroke
  painting_view.setNeedsDisplay
end

4. Painting

Now that you can draw a stroke, it's time to level up to the entire painting. Let's start with the Painting model. Create a file for the class at app/models/painting.rb and implement the Painting class.

class Painting
  attr_accessor :strokes

  def initialize
    @strokes = []
  end

  def start_stroke(point, color)
    strokes << Stroke.new(point, color)
  end

  def continue_stroke(point)
    current_stroke.add_point(point)
  end

  def current_stroke
    strokes.last
  end
end

The Painting model is similar to the Stroke class. The constructor initializes strokes to an empty array. When a person touches the screen, the application will start a new stroke by calling start_stroke. Then, as the user drags their finger, it will add points with continue_stroke. Don't forget the specs for the Painting class.

describe Painting do

  before do
    @point1 = CGPoint.new(10, 60)
    @point2 = CGPoint.new(20, 50)
    @point3 = CGPoint.new(30, 40)
    @point4 = CGPoint.new(40, 30)
    @point5 = CGPoint.new(50, 20)
    @point6 = CGPoint.new(60, 10)

    @painting = Painting.new
  end

  describe "#initialize" do

    before do
      @painting = Painting.new
    end

    it "sets the stroke to an empty array" do
      @painting.strokes.should == []
    end
  end

  describe "#start_stroke" do

    before do
      @painting.start_stroke(@point1, UIColor.redColor)
      @painting.start_stroke(@point2, UIColor.blueColor)
    end

    it "starts new strokes" do
      @painting.strokes.length.should == 2
      @painting.strokes[0].points.should == [ @point1 ]
      @painting.strokes[0].color.should == UIColor.redColor
      @painting.strokes[1].points.should == [ @point2 ]
      @painting.strokes[1].color.should == UIColor.blueColor
    end
  end

  describe "#continue_stroke" do

    before do
      @painting.start_stroke(@point1, UIColor.redColor)
      @painting.continue_stroke(@point2)
      @painting.start_stroke(@point3, UIColor.blueColor)
      @painting.continue_stroke(@point4)
    end

    it "adds points to the current strokes" do
      @painting.strokes[0].points.should == [ @point1, @point2 ]
      @painting.strokes[1].points.should == [ @point3, @point4 ]
    end
  end
end

Next, modify the PaintingView class to draw a Painting object instead of a Stroke object.

class PaintingView < UIView
  attr_accessor :painting

  def drawRect(rectangle)
    super

    # ensure the painting is provided
    return if painting.nil?

    painting.strokes.each do |stroke|
      draw_stroke(stroke)
    end
  end

  def draw_stroke(stroke)

    # set up the drawing context
    context = UIGraphicsGetCurrentContext()
    CGContextSetStrokeColorWithColor(context, stroke.color.CGColor)
    CGContextSetLineWidth(context, 20.0)
    CGContextSetLineCap(context, KCGLineCapRound)
    CGContextSetLineJoin(context, KCGLineJoinRound)

    # move the line to the start point
    CGContextMoveToPoint(context, stroke.start_point.x, stroke.start_point.y)

    # add each line in the path
    stroke.points.drop(1).each do |point|
      CGContextAddLineToPoint(context, point.x, point.y)
    end

    # stroke the path
    CGContextStrokePath(context);
  end
end

You've changed the stroke attribute to painting. The drawRect: method now iterates over all of the strokes in the painting and draws each one using draw_stroke, which contains the drawing code you wrote previously.

You also need to update the view controller to contain a Painting model. At the top of the PaintingController class, add attr_reader :painting. As the name implies, the viewDidLoad method of the UIViewController class—the superclass of the PaintingController class—is called when the view controller has finished loading its view. The viewDidLoad method is therefore a good place to create a Painting instance and set the painting attribute of the PaintingView object.

def viewDidLoad
  @painting = Painting.new
  painting_view.painting = painting
end

As always, don't forget to add tests for viewDidLoad to spec/controllers/painting_controller_spec.rb.

describe "#viewDidLoad" do

  it "sets the painting" do
    controller.painting.should.be.instance_of Painting
  end

  it "sets the painting attribute of the painting view" do
    controller.painting_view.painting.should == controller.painting
  end
end

5. Gesture Recognizers

Your application will be pretty boring unless you allow people to draw on the screen with their fingers. Let's add that piece of functionality now. Create a file for the StrokeGestureRecognizer class along with its spec by running the following commands from the command line.

touch app/views/stroke_gesture_recognizer.rb
touch spec/views/stroke_gesture_recognizer_spec.rb

Next, create the skeleton for the class.

class StrokeGestureRecognizer < UIGestureRecognizer
  attr_reader :position
end

The StrokeGestureRecognizer class extends the UIGestureRecognizer class, which handles touch input. It has a position attribute that the PaintingController class will use to determine the position of the user's finger.

There are four methods you need to implement in the StrokeGestureRecognizer class, touchesBegan:withEvent:, touchesMoved:withEvent:, touchesEnded:withEvent:, and touchesCancelled:withEvent:. The touchesBegan:withEvent: method is called when the user starts touching the screen with their finger. The touchesMoved:withEvent: method is called repeatedly when the user moves their finger and the touchesEnded:withEvent: method is invoked when the user lifts their finger from the screen. Finally, the touchesCancelled:withEvent: method is invoked if the gesture is cancelled by the user.

Your gesture recognizer needs to do two things for each event, update the position attribute and change the state property.

class StrokeGestureRecognizer < UIGestureRecognizer
  attr_accessor :position

  def touchesBegan(touches, withEvent: event)
    super
    @position = touches.anyObject.locationInView(self.view)
    self.state = UIGestureRecognizerStateBegan
  end

  def touchesMoved(touches, withEvent: event)
    super
    @position = touches.anyObject.locationInView(self.view)
    self.state = UIGestureRecognizerStateChanged
  end

  def touchesEnded(touches, withEvent: event)
    super
    @position = touches.anyObject.locationInView(self.view)
    self.state = UIGestureRecognizerStateEnded
  end

  def touchesCancelled(touches, withEvent: event)
    super
    @position = touches.anyObject.locationInView(self.view)
    self.state = UIGestureRecognizerStateEnded
  end
end

Both the touchesEnded:withEvent: and touchesCancelled:withEvent: methods set the state to UIGestureRecognizerStateEnded. This is because it doesn't matter if the user is interrupted, the drawing should remain untouched.

In order to test the StrokeGestureRecognizer class, you need to be able to create an instance of UITouch. Unfortunately, there's no publicly available API to accomplish this. To make it work, we'll make use of the Facon mocking library.

Add gem 'motion-facon' to your Gemfile and run bundle install. Then, add require "motion-facon" below require "sugarcube-color" in the project's Rakefile.

Next, implement the StrokeGestureRecognizer spec.

describe StrokeGestureRecognizer do
  extend Facon::SpecHelpers

  before do
    @stroke_gesture_recognizer = StrokeGestureRecognizer.new
    @touch1 = mock(UITouch, :"locationInView:" => CGPoint.new(100, 200))
    @touch2 = mock(UITouch, :"locationInView:" => CGPoint.new(300, 400))
    @touches1 = NSSet.setWithArray [ @touch1 ]
    @touches2 = NSSet.setWithArray [ @touch2 ]
  end

  describe "#touchesBegan:withEvent:" do

    before do
      @stroke_gesture_recognizer.touchesBegan(@touches1, withEvent: nil)
    end

    it "sets the position to the gesture's position" do
      @stroke_gesture_recognizer.position.should == CGPoint.new(100, 200)
    end

    it "sets the state of the gesture recognizer" do
      @stroke_gesture_recognizer.state.should == UIGestureRecognizerStateBegan
    end
  end

  describe "#touchesMoved:withEvent:" do

    before do
      @stroke_gesture_recognizer.touchesBegan(@touches1, withEvent: nil)
      @stroke_gesture_recognizer.touchesMoved(@touches2, withEvent: nil)
    end

    it "sets the position to the gesture's position" do
      @stroke_gesture_recognizer.position.should == CGPoint.new(300, 400)
    end

    it "sets the state of the gesture recognizer" do
      @stroke_gesture_recognizer.state.should == UIGestureRecognizerStateChanged
    end
  end

  describe "#touchesEnded:withEvent:" do

    before do
      @stroke_gesture_recognizer.touchesBegan(@touches1, withEvent: nil)
      @stroke_gesture_recognizer.touchesEnded(@touches2, withEvent: nil)
    end

    it "sets the position to the gesture's position" do
      @stroke_gesture_recognizer.position.should == CGPoint.new(300, 400)
    end

    it "sets the state of the gesture recognizer" do
      @stroke_gesture_recognizer.state.should == UIGestureRecognizerStateEnded
    end
  end

  describe "#touchesCancelled:withEvent:" do

    before do
      @stroke_gesture_recognizer.touchesBegan(@touches1, withEvent: nil)
      @stroke_gesture_recognizer.touchesCancelled(@touches2, withEvent: nil)
    end

    it "sets the position to the gesture's position" do
      @stroke_gesture_recognizer.position.should == CGPoint.new(300, 400)
    end

    it "sets the state of the gesture recognizer" do
      @stroke_gesture_recognizer.state.should == UIGestureRecognizerStateEnded
    end
  end
end

extend Facon::SpecHelpers makes several methods available in your specs, including mock. mock is a simple way to create test objects that work exactly the way you want them to. In the before block at the beginning of the specs, you're mocking instances of UITouch with the locationInView: method that returns a predefined point.

Next, add a stroke_gesture_changed method to the PaintingController class. This method will receive an instance of the StrokeGestureRecognizer class whenever the gesture is updated.

def stroke_gesture_changed(stroke_gesture_recognizer)

  if stroke_gesture_recognizer.state == UIGestureRecognizerStateBegan
    painting.start_stroke(stroke_gesture_recognizer.position, selected_color)
  else
    painting.continue_stroke(stroke_gesture_recognizer.position)
  end

  painting_view.setNeedsDisplay
end

When the gesture recognizer's state is UIGestureRecognizerStateBegan, this method starts a new stroke in the Painting object using the StrokeGestureRecognizer's position and selected_color. Otherwise, it continues the current stroke.

Add the specs for this method.

describe "#stroke_gesture_changed" do

  before do
    drag(controller.painting_view, :points => [ CGPoint.new(100, 100), CGPoint.new(150, 150), CGPoint.new(200, 200) ])
  end

  it "adds the points to the stroke" do
    controller.painting.strokes.first.points[0].should == CGPoint.new(100, 100)
    controller.painting.strokes.first.points[1].should == CGPoint.new(150, 150)
    controller.painting.strokes.first.points[2].should == CGPoint.new(200, 200)
  end

  it "sets the stroke's color to the selected color" do
    controller.painting.strokes.first.color.should == controller.selected_color
  end
end

RubyMotion provides several helper methods to simulate user interaction, including drag. Using drag, you can simulate a user's interaction with the screen. The points option allows you to provide an array of points for the drag.

If you were to run the specs now, they would fail. That's because you need to add the gesture recognizer to the storyboard. Launch Interface Builder by running bundle exec rake ib:open. From the Object Library, drag an Object into your scene, and change its class to StrokeGestureRecognizer in the Identity Inspector on the right.

Control and drag from the StrokeGestureRecognizer object to the PaintingController and choose the select_color method from the menu that appears. This will ensure the select_color method is called whenever the gesture recognizer is triggered. Then, control and drag from the PaintingView object to the StrokeGestureRecognizer object and select gestureRecognizer from the menu that appears.

Add a spec for the gesture recognizer to the PaintingController specs in the #painting_view describe block.

describe "#painting_view" do

  it "is connected in the storyboard" do
    controller.painting_view.should.not.be.nil
  end

  it "has a stroke gesture recognizer" do
    controller.painting_view.gestureRecognizers.length.should == 1
    controller.painting_view.gestureRecognizers[0].should.be.instance_of StrokeGestureRecognizer
  end
end

That's it. With these changes your application should now allow a person to draw on the screen. Run your application and have fun.

6. Final Touches

There are a few final touches left to add before your application is finished. Because your application is immersive, the status bar is a bit distracting. You can remove it by setting the UIStatusBarHidden and UIViewControllerBasedStatusBarAppearance values in the application's Info.plist. This is easy to do in the RubyMotion setup block inside the project's Rakefile.

Motion::Project::App.setup do |app|
  app.name = 'Paint'
  app.info_plist['UIStatusBarHidden'] = true
  app.info_plist['UIViewControllerBasedStatusBarAppearance'] = false
end

The application's icons and launch images are included in the source files of this tutorial. Download the images and copy them to the resources directory of the project. Then, set the application icon in the Rakefile configuration. You may have to clean the build by running bundle exec rake clean:all in order to see the new launch image.

Motion::Project::App.setup do |app|
  app.name = 'Paint'
  app.info_plist['UIStatusBarHidden'] = true
  app.info_plist['UIViewControllerBasedStatusBarAppearance'] = false
  app.icons = [ "icon.png" ]
end

Conclusion

That's it. You now have a complete app that's ready for a million downloads in the App Store. You can view and download the source for this application from GitHub.

Even though your app is finished, there's so much more you could add to it. You can add curves between the lines, more colors, different line widths, saving, undo, and redo, and anything else you can imagine. What will you do to make your app better? Let me know in the comments below.

Advertisement