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.
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:
StrokeThis class represents a single stroke in the painting.
PaintingThis 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.
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
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,
color. It sets
points to an array of points containing
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
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
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.
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
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.
PaintingView class, we've overridden
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.
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.
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 green, and
Don't forget to add a spec to spec/controllers/painting_controller_spec.rb for the
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
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
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
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
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.points.should == [ @point1 ] @painting.strokes.color.should == UIColor.redColor @painting.strokes.points.should == [ @point2 ] @painting.strokes.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.points.should == [ @point1, @point2 ] @painting.strokes.points.should == [ @point3, @point4 ] end end end
Next, modify the
PaintingView class to draw a
Painting object instead of a
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
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
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
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
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
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
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.
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
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 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.should == CGPoint.new(100, 100) controller.painting.strokes.first.points.should == CGPoint.new(150, 150) controller.painting.strokes.first.points.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, 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
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.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
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
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.
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.Update me weekly
Envato Tuts+ tutorials are translated into other languages by our community members—you can be involved too!Translate this post