2. About me
• Life-long polyglot programmer
• Rubyist since 2005
• CEO & Founder of Integrallis Software, circa 2002
• From David, Panama
• Lives in Scottsdale, Arizona
4. Pre-Requisites
• Follow http://www.rubymotion.com/developer-center/guides/getting-started/
• 64-bit Mac running OS X Mavericks
• Apple’s Xcode
• Command Line Tools Package
• Download RubyMotion Product Installer
• Register as an iPhone developer to test on a physical device
• RVM installed on your machine
• Use your favorite code editor!
6. THE MOTION COMMAND
The motion command is our entry point in the world of RubyMotion:
~/rm_workshop/code
/> motion
RubyMotion lets you develop native iOS and OS X applications using the awesome Ruby language.
!
Commands:
!
* account Access account details.
* activate Activate software license.
* changelog View the changelog.
* create Create a new project.
* device-console Print iOS device logs
* ri Display API reference.
* support Create a support ticket.
* update Update the software.
!
Options:
!
--version Show the version of RubyMotion
--no-ansi Show output without ANSI codes
--verbose Show more debugging information
--help Show help banner of specified command
8. hello world
Let’s use the motion create command to get started:
~/rm_workshop/code
/> motion create hello
Create hello
Create hello/.gitignore
Create hello/app/app_delegate.rb
Create hello/Gemfile
Create hello/Rakefile
Create hello/resources/Default-568h@2x.png
Create hello/spec/main_spec.rb
!
9. RM PROJECT
• Gemfile: Your project’s Gem dependencies (RM supported gems)
• Rakefile: The entry point to build, launch, release and test your App
• app/app_delegate.rb: Glues your custom code (your application)
into the RubyMotion framework
• spec/main_spec.rb: A sample test that verifies that your App has at
least one window
• Miscellaneous: A .gitignore to avoid checking build artifacts and a
default splash screen PNG image
10. RM BUILD SYSTEM
Let’s CD to the hello and run: rake -T
~/rm_workshop/code/hello
/> rake -T
rake clean # Clear local build objects
rake config # Show project config
rake default # Build the project, then run the simulator
rake device # Deploy on the device
rake spec # Same as 'spec:simulator'
rake spec:device # Run the test/spec suite on the device
rake spec:simulator # Run the test/spec suite on the simulator
...
!
The default task (which runs if we simply type rake), builds the project
(in the build directory) and runs the simulator
11. iOS overview
• iOS is the operating system that runs on iPhone, iPod Touch, and iPad
devices
• Apple provides the iOS SDK, which provides the tools and interfaces
needed to interact with iOS' layered architecture
• iOS provides a set of packages called Frameworks, which are dynamic
shared libraries and resources that can be linked to your App
12. Cocoa Touch
Core Services
Core OS
UIKit
Hardware
Core Animation
Foundation
Core Data
Media Layer
Core Graphics
Open GL ES
Applications
13. The APP DELEGATE
class AppDelegate
def application(application, didFinishLaunchingWithOptions:launchOptions)
true
end
end
• UIKit manages the app’s core behavior (event loop and interaction w/OS)
• UIKit uses subclassing, delegation and callbacks to allow you to modify
default behaviors and add your own
• UIApplication object dispatches events to your code in the app delegate
• AppDelegate is responsible for handling state transitions and app events
14. BUILD AND LAUNCH
To build and launch simple type: rake
~/rm_workshop/code/hello
/> rake
Build ./build/iPhoneSimulator-7.1-Development
Compile ./app/app_delegate.rb
Create ./build/iPhoneSimulator-7.1-Development/hello.app
Link ./build/iPhoneSimulator-7.1-Development/hello.app/hello
Create ./build/iPhoneSimulator-7.1-Development/hello.app/PkgInfo
Create ./build/iPhoneSimulator-7.1-Development/hello.app/
Info.plist
Copy ./resources/Default-568h@2x.png
Create ./build/iPhoneSimulator-7.1-Development/hello.dSYM
Simulate ./build/iPhoneSimulator-7.1-Development/hello.app
(main)>
Our empty application should be launched in the simulator…
15. BUILD AND LAUNCH
The empty app doesn’t have any views yet. All we get is a blank screen…
16. BUILD AND LAUNCH
Let’s simulate pressing the device ‘Home’ button.
From the iOS Simulator menu select: Hardware | Home
Our application is installed
on the simulator
Along with other “core”
iOS apps
18. INTERACTING WITH YOUR APP
After the application launched, we were left with a prompt on the console; the REPL!
~/rm_workshop/code/hello
(main)> self
=> main
(main)> alert = UIAlertView.new
=> #<UIAlertView:0x90d39e0>
(main)> alert.title = 'RubyMotion'
=> "RubyMotion"
(main)> alert.message = 'RubyMotion is in da house'
=> "RubyMotion is in da house"
(main)> alert.show
=> #<UIAlertView:0x90d39e0>
(main)>
Let’s follow the interaction shown above
The top level is the
main object Many RM classes are counterpart/
wrappers for their iOS peers
Let’s construct a UIAlertView
which is an iOS class in UIKit
19. INTERACTING WITH YOUR APP
The UIAlertView has a few properties, like title and
message, and several methods, including show:
We can learn more about UIAlertView in Apple’s developer library at:
http://developer.apple.com/library/ios/#documentation/uikit/reference/UIAlertView_Class/UIAlertView/UIAlertView.html
20. INTERACTING WITH YOUR APP
Let’s dismiss the previous alert and create a new one…
~/rm_workshop/code/hello
(main)> alert.dismiss
=> #<UIAlertView:0x90d39e0>
(main)> alert = UIAlertView.new
=> #<UIAlertView:0x8ddcef0>
(main)> alert.addButtonWithTitle 'Kaboom!'
=> 0
(main)> alert.message = 'Foo Bar'
=> "Foo Bar"
(main)> alert.show
=> #<UIAlertView:0x8ddcef0>
(main)>
This time we’ll use the addButtonWithTitle method
21. INTERACTING WITH YOUR APP
Clicking the button
dismisses the alert
We can learn more about UIAlertView#addButtonWithTitle method at:
https://developer.apple.com/library/ios/documentation/uikit/reference/UIAlertView_Class/UIAlertView/UIAlertView.html#//apple_ref/doc/uid/TP40006802-CH3-SW6
22. WHEN TO USE THE REPL
• The REPL gives us a playground to experiment and learn about the
RubyMotion/iOS API
• When doing TDD, the REPL is where I try my raw ideas before
committing them to code
• RubyMotion REPL is unique to the iOS development ecosystem and
one of the best ways to speed up your development process
23. FROM THE REPL to your App
class AppDelegate
def application(application, didFinishLaunchingWithOptions:launchOptions)
alert = UIAlertView.new
alert.title = 'RubyMotion'
alert.message = 'RubyMotion is that da house!'
alert.addButtonWithTitle 'Kaboom!'
!
alert.show
!
true
end
end
app/app_delegate.rb
• We can apply what we learned on the REPL to our application
• Modify your AppDelegate as show above and run the rake command again
24. The application method
• In the application method, we create objects that inherit from UIView (as
UIAlertView does)
• UIView's define an interface for managing the content of a rectangular area
• At runtime the app is represented by a UIApplication object which
“delegates” to your custom code in the AppDelegate class
• A delegate is a class that typically implements one or more protocols
• Nearly all UI classes have a delegate that you can use to receive callbacks from
the framework
25. that method looks weird!
def application(application, didFinishLaunchingWithOptions:launchOptions)
...
end
• You probably noticed the signature of application method
• The second parameter is camel-cased and it has a right colon smack in the
middle
• The camel-casing is just a direct port of the Objective-C parameter names
• RubyMotion is mostly based on Ruby 1.9. In the upcoming Ruby 2.0, we get
named parameters which use the : syntax, aligning perfectly with
Objective-C parameters
26. FROM THE REPL to your App
• Let’s make the variable alert an instance variable
• Modify your AppDelegate as show above and run the rake command again
class AppDelegate
def application(application, didFinishLaunchingWithOptions:launchOptions)
@alert = UIAlertView.new
@alert.title = 'RubyMotion'
@alert.message = 'RubyMotion is that da house!'
@alert.addButtonWithTitle 'Kaboom!'
!
@alert.show
!
true
end
end
app/app_delegate.rb
27. …and back to the repL
We can also interact with the running application elements:
~/rm_workshop/code/hello
(main) app = UIApplication.sharedApplication
=> #<UIApplication:0x901ab90>
(main)> delegate = app.delegate
=> #<AppDelegate:0x8e2d940 @alert=#<UIAlertView:0x8e33ac0>>
(main)> alert = delegate.instance_variable_get('@alert')
=> #<UIAlertView:0x8e33ac0>
(main) alert.message = 'Chunky Bacon is the best!'
=> "Chunky Bacon is the best!"
(main)>
!
!
We can access the singleton application instance via the sharedApplication
class method, which gets us access to the delegate and the instance variable via
instance_variable_get.
29. TEST-driven Development
• TDD creates a tight loop of development that cognitively engages us
• TDD gives us lightweight rigor by making development, goal-oriented
with a clear goal setting, goal reaching and improvement stages
• The stages of TDD are commonly known as the
Red-Green-Refactor loop
30. TEST-driven Development
RED
REFACTOR GREEN
Clean up & improve without adding functionality Write the minimal code to pass the test
Eliminate Redundancy
Write a failing test for new functionality
31. TDD with BACON
• The Ruby Community has embrace the practice of Test-Driven
Development (TDD)
• RubyMotion comes bundled with MacBacon, a Mac specific version
of Bacon which is a small clone of RSpec
• In this section we’ll embark on building the Okonawa application, a
simple ToDo list manager for iOS
32. okonawa
Okonawa (行わ) which translates roughly to “Done” will be a simple To Do app:
~/rm_workshop/code
/> motion create okonawa
Create okonawa
Create okonawa/.gitignore
Create okonawa/app/app_delegate.rb
Create okonawa/Gemfile
Create okonawa/Rakefile
Create okonawa/resources/Default-568h@2x.png
Create okonawa/spec/main_spec.rb
!
Let’s start by creating a new RM application!
33. RM SPECS
• The file spec/main_spec.rb, contains a simple Bacon test that verifies
that your app has at least one window
• In the before block we access the sharedApplication singleton in order to
verify the size of the windows property
describe "Application 'okonawa'" do
before do
@app = UIApplication.sharedApplication
end
!
it "has one window" do
@app.windows.size.should == 1
end
end
spec/main_spec.rb
34. kicking off the TDD LOOP
Change directories into the newly created application directory and type rake spec:
~/rm_workshop/code/okonawa
Application 'okonawa'
- has one window [FAILED - 0.==(1) failed]
!
Bacon::Error: 0.==(1) failed
spec.rb:700:in `satisfy:': Application 'okonawa' - has one window
spec.rb:714:in `method_missing:'
spec.rb:316:in `block in run_spec_block'
spec.rb:440:in `execute_block'
spec.rb:316:in `run_spec_block'
spec.rb:331:in `run'
!
1 specifications (1 requirements), 1 failures, 0 errors
Starting with Failure! A Good Thing
35. getting to green
• We’ll make minimal modifications to app/app_delegate.rb in order to
pass our failing spec
• We’ll add a window (UIWindow) which is a manager for other views your
app displays on the device screen
• An iOS application has only one window (unless your app can use an
external display).
class AppDelegate
def application(application, didFinishLaunchingWithOptions:launchOptions)
@window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
@window.makeKeyAndVisible
end
end
app/app_delegate.rb
36. alloc?
@window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
• The alloc method is a class method of NSObject that returns a new
instance of the receiving class
• To complete the initialization process you must call one of it’s initializers
• Objective-C provides designated initializers and secondary initializers
• An initializer (init and friends) is coupled with alloc in the same line of
code
• We pass a CGRect (the size of the mainScreen) to the initWithFrame
37. what about ruby’s new?
• NSObject provides a new method which allocates and calls init
• When we call .new on an Objective-C class it is the equivalent
of .alloc.init
• Since initWithFrame is the designated initializer for UIWindow, calling init
will call it (passing as a param the constant CGRectZero - a zero rectangle)
• We could change the line above to (which is not exactly the same) but
would make the spec pass:
@window = UIWindow.alloc.init
# or
@window = UIWindow.new
38. getting to green
Let’s run rake spec again:
~/rm_workshop/code/okonawa
Application 'okonawa'
- has one window
!
1 specifications (1 requirements), 0 failures, 0 errors
!
!
!
!
!
!
!
We’ve reached the GREEN state. Yay!
39. STORY #1
• We have passing set of tests and a very uninteresting application :-(
• Let’s kick the TDD cycle into full gear by implementing a simple story:
“As a User I should be able to see a
list of all To-Dos”
40. • We’ll craft a very generic test following the formula we used to access the
delegate from the REPL
• In a before block, we’ll grab the application, the delegate, and something I’m
calling table, which is yet to be created
• Our first test will just test that this table exists in spec/todos_spec.rb
describe "Todos View" do
before do
@app = UIApplication.sharedApplication
@delegate = @app.delegate
@table = @delegate.instance_variable_get("@table")
end
!
it 'should exist' do
@table.should.not == nil
end
end
spec/todos_spec.rb
41. confirm failure
Let’s run the tests again to get back to our “Red” state:
~/rm_workshop/code/okonawa
Application 'okonawa'
- has one window
!
Todos View
- should exist [FAILED - not nil.==(nil) failed]
!
Bacon::Error: not nil.==(nil) failed
spec.rb:700:in `satisfy:': Todos View - should exist
spec.rb:714:in `method_missing:'
spec.rb:316:in `block in run_spec_block'
spec.rb:440:in `execute_block'
spec.rb:316:in `run_spec_block'
spec.rb:331:in `run'
!
2 specifications (2 requirements), 1 failures, 0 errors
Alright. Let’s go slay this dragon!
42. REPL Detective Work
• To make our application pass this story we need to go into design mode.
We’ll need a view that can display a list of things
• Amongst the descendants of UIView there is a class called UITableView
• how do we add this UITableView to our application’s window? Inspecting
the methods of UIWindow we see that there is a method called addSubview
~/rm_workshop/code/okonawa
(main)> UIWindow.instance_methods.sort.keep_if { |m| m =~ /S*viewS*/ }
=>
[:"addSubview:", :autoresizesSubviews, :"bringSubviewToFront:", :deliversTouchesForGestu
resToSuperview, :"didAddSubview:", :didMoveToSuperview, :"exchangeSubviewAtIndex:withSub
viewAtIndex:", :"insertSubview:above:", :"insertSubview:aboveSubview:", :"insertSubview:
atIndex:", :"insertSubview:below:", :"insertSubview:belowSubview:", :layoutSubviews, :"m
ovedFromSuperview:", :"movedToSuperview:", :removeFromSuperview, :"resizeSubviewsWithOld
Size:", :"resizeWithOldSuperviewSize:", :"sendSubviewToBack:", :"setAutoresizesSubviews:
", :"setClipsSubviews:", :"setDeliversTouchesForGesturesToSuperview:", :"setSkipsSubview
Enumeration:", :skipsSubviewEnumeration, :subviews, :superview, :viewDidMoveToSuperview,
:viewForBaselineLayout, :viewPrintFormatter, :viewTraversalMark, :"viewWillMoveToSupervi
ew:", :"viewWithTag:", :"willMoveToSuperview:", :"willRemoveSubview:"]
(main)>
!
43. using UITableView
• Let’s expand our AppDelegate by creating the instance variable @table as a
UITableView sized to the screen’s bounds:
class AppDelegate
def application(application, didFinishLaunchingWithOptions:launchOptions)
@window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
@window.makeKeyAndVisible
!
@table = UITableView.alloc.initWithFrame(UIScreen.mainScreen.bounds)
@window.addSubview(@table)
end
end
app/app_delegate.rb
45. and back to green
Let’s run rake spec again:
~/rm_workshop/code/okonawa
Application 'okonawa'
- has one window
!
Todos View
- should exist
!
2 specifications (2 requirements), 0 failures, 0 errors
!
!
!
!
Not a very robust test, but it’s allowing us to
move forward in a controlled fashion
46. displaying some TODOs
• Looking at the reference for UITableView there is a method
visibleCells that returns the table cells that are visible in the receiver
• The visibleCells method returns an array containing UITableViewCell
objects
• Let’s check the visibleCells method on the REPL by getting to our
@table UITableView
~/rm_workshop/code/okonawa
(main)> (nil)? app = UIApplication.sharedApplication
=> #<UIApplication:0x8d8b6b0>
(main)> delegate = app.delegate
=> #<AppDelegate:0x8fae550 @window=#<UIWindow:0x8da87d0> @table=#<UITableView:
0x9b7ae00>>
(main)> table = delegate.instance_variable_get("@table")
=> #<UITableView:0x9b7ae00>
(main)> table.visibleCells
=> []
(main)>
!!
47. displaying some TODOs
• With that information on hand, let’s write a spec:
it 'displays the given ToDos' do
@table.visibleCells.should.not.be.empty
end
~/rm_workshop/code/okonawa
Todos View
- should exist
- displays the given ToDos [FAILED - not [].empty?() failed]
!
Bacon::Error: not [].empty?() failed
spec.rb:700:in `satisfy:': Todos View - displays the given ToDos
spec.rb:714:in `method_missing:'
spec.rb:316:in `block in run_spec_block'
spec.rb:440:in `execute_block'
spec.rb:316:in `run_spec_block'
spec.rb:331:in `run'
!
3 specifications (3 requirements), 1 failures, 0 errors
spec/todos_spec.rb
48. displaying some TODOs
• So how are we going to pass our test? It seems that we might want to get
some UITableViewCell objects in our table
• If we inspect the UITableView reference we learn that a UITableView
gets its data from a UITableViewDataSource which is set via the
dataSource attribute
• The UITableViewDataSource protocol is adopted by an object that
mediates the application’s data model for a UITableView object
49. displaying some TODOs
• To create a class that can serve as a UITableViewDataSource we must
provide two method implementations:
(Integer) tableView(tableView, numberOfRowsInSection:section)
• Tells the table how many rows of data we have.
(UITableViewCell) tableView(tableView, cellForRowAtIndexPath:indexPath)
• Returns a UITableViewCell for a given row index
50. UITableViewDataSource
• We will implement the two required methods in the contract, and serve our
data from a simple array @data
• Let’s create a TodosDataSource class in app/todos_data_source.rb:
class TodosDataSource
!
attr_writer :data
!
def tableView(tableView, numberOfRowsInSection:section)
@data.size
end
!
def tableView(tableView, cellForRowAtIndexPath:indexPath)
cell = UITableViewCell.alloc.initWithStyle(UITableViewCellStyleDefault,
reuseIdentifier:nil)
cell.textLabel.text = @data[indexPath.row]
cell
end
end app/todos_data_source.rb
51. UITableViewDataSource
• Let’s modify the AppDelegate to use the TodosDataSource by simply
setting its data attribute with an array of strings:
def application(application, didFinishLaunchingWithOptions:launchOptions)
@window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
@window.makeKeyAndVisible
!
@table = UITableView.alloc.initWithFrame(UIScreen.mainScreen.bounds)
!
todos = %w(Milk Orange Juice Apples Bananas Broccoli Carrots Beef
Chicken Enchiladas Hot Dogs Butter Bread Pasta Rice)
!
todos.map! { |thing| "Buy #{thing}"}
!
@data_source = TodosDataSource.new
@data_source.data = todos
@table.dataSource = @data_source
!
@window.addSubview(@table)
end app/app_delegate.rb
52. displaying some TODOs
• Run rake to see the UITableView populated with data from our
UITableViewDataSource:
53. and back to green
Let’s run rake spec again:
~/rm_workshop/code/okonawa
Application 'okonawa'
- has one window
!
Todos View
- should exist
- displays the given ToDos
!
3 specifications (3 requirements), 0 failures, 0 errors
!
!
!
54. UITableViewDataSource
• Let’s complement the previous test with a check for contents of as row:
it 'displays the correct label for a given ToDo' do
first_cell = @table.visibleCells.first
first_cell.textLabel.text.should == 'Buy Milk'
end
~/rm_workshop/code/okonawa
Application 'okonawa'
- has one window
!
Todos View
- should exist
- displays the given ToDos
- displays the correct label for a given ToDo
!
4 specifications (4 requirements), 0 failures, 0 errors
!
!
spec/todos_spec.rb
55. Time to Refactor
• We’ve done a few red-green short loops; now let’s do a full red-green-refactor
• You’ve probably noticed that when we launched the console we got a
warning message that read:
Application windows are expected to have a root view
controller at the end of application launch
• iOS has a pretty robust implementation of the MVC model-view-controller
pattern
• The message above hints that we should have delegated the initial display
of the view to a “view controller”
56. • The first custom object created at launch time is the app delegate, which
handles any events that are not handled by by the UIApplication. The app
delegate is the entry point into the custom code that “controls” the app, the
entry point into the “C” of M-V-C
Data Objects Document
Model
UIApplication Application Delegate UI Window
View Controllers Views and UI Objects
Event
Loop
Controller View
57. View Controllers
• View controllers in RubyMotion are classes that extend
UIViewController
• Using views directly in the AppDelegate is typically frowned upon
• Let’s make a directory for our controllers under the app directory. In there
we’ll create the the TodosController in todos_controller.rb
58. TODOs Controller
• Controller including the data source contracts previously provided:
class TodosController < UIViewController
attr_writer :data
!
def viewDidLoad
super
self.title = 'Okonawa'
@table = UITableView.alloc.initWithFrame(self.view.bounds)
@table.dataSource = self
self.view.addSubview(@table)
!
@data = %w(Milk Orange Juice Apples Bananas Broccoli Carrots Beef Chicken
app/controllers/todos_controller.rb
Enchiladas Hot Dogs Butter Bread Pasta Rice).map { |thing| "Buy #{thing}" }
end
!
def tableView(tableView, numberOfRowsInSection: section)
@data.size
end
!
def tableView(tableView, cellForRowAtIndexPath: indexPath)
cell = UITableViewCell.alloc.initWithStyle(UITableViewCellStyleDefault, reuseIdentifier:nil)
cell.textLabel.text = @data[indexPath.row]
cell
end
end
59. TODOs Controller
• We’ll also need to refactor the AppDelegate to use the controller:
class AppDelegate
def application(application, didFinishLaunchingWithOptions:launchOptions)
@window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
!
@todos_controller = TodosController.alloc
.initWithNibName(nil, bundle:nil)
!
@window.rootViewController =
UINavigationController.alloc
.initWithRootViewController(@todos_controller)
!
@window.makeKeyAndVisible
!
app/app_delegate.rb
true
end
end
• We accomplish this by setting the window root view controller to an
instance of our TodosController
60. TODOs Controller
• If we run the tests: boom! We get one failure and two errors:
~/rm_workshop/code/okonawa
Application 'Todo'
- has one window
ToDos View
- should exist@Table is ==>
[FAILED]
- displays the given ToDos@Table is ==>
[ERROR: NoMethodError]
- displays the correct label for a give ToDo@Table is ==>
[ERROR: NoMethodError]
Bacon::Error: not nil.==(nil) failed
Boo, our refactoring broke our tests!
61. Test Refactoring
• The @table variable is nil. There is no @table in the delegate anymore!
• RubyMotion provides the ability to declare the context of our tests
• Let’s refactor the test with the class method tests, which gives us access
to the controller variable
describe "Todos Controller" do
tests TodosController
!
before do
spec/todos_controller.rb
@table = controller.instance_variable_get("@table")
end
...
• Let’s also rename the file from todos_spec.rb to todos_controller.rb
62. TODOs Controller
• Run rake to reveal the Todos list backed by the Todos Controller:
64. data in IOS
Isolated applications are mostly non-existent.
Most mobile apps keep their data in an
external service and keep a local user data
cache that needs to be synchronized from time
to time
65. data in IOS
There are many mechanisms by which to persist data locally in an iOS
application, including but not limited to:
• NSUserDefaults: Is a key-value local storage, capable of storing both objects and primitive data
types
• NSCache: A cache that stores key-value pairs. NSCache automatically evicts objects in order to
free up space in memory as needed.
• Archives: Serialize an object graph into an “architecture independent stream of bytes”. Archives
are available in sequential and keyed archives backed by NSArchiver and NSKeyArchiver
respectively.
• Core Data: The 'official' persistence framework that can serializes an object graph, provide object
life-cycle management, relationships, lazy loading, validation, change tracking, undo support,
schema migrations and queries.
66. break out the gems!
• Since RubyMotion is a dialect of Ruby that is statically compiled, most
regular Ruby gems won't work right out of the gate
• iOS typically provides an appropriate counter-part to a pure Ruby gem that
has been wrapped in “RubyMotion goodness”
• Just like regular Ruby apps, RM apps use Bundler
• The big difference is that the require method is only allowed inside your
project’s Rakefile
• Gems required in the Rakefile are compiled into the target executable in
alphabetical order. The generated Rakefile will pull all Gemfile dependencies
automatically
67. break out the gems!
For our ToDo App we'll be using a few libraries that will allow us to follow 'The
Ruby Way' of development. Let’s add the following gems to our Gemfile:
• MotionModel: Simple Model and Validation Mixins for RubyMotion
• Formotion: A simple DSL to create iOS Forms
source 'https://rubygems.org'
!
gem 'rake'
gem 'motion_model'
gem 'formotion'
Gemfile
68. break out the gems!
Let’s also isolate our dependencies from other projects in the system by adding
a .ruby-version and a .ruby-gemset files at the root so that RVM and Bundler
can manage our project:
1.9.3
baruco-okonawa
.ruby-version
.ruby-gemset
69. break out the gems!
Now simply CD out and back into the project and type: bundle
~/rm_workshop/code/okonawa
/>bundle
Resolving dependencies...
Using rake 10.3.2
Installing bubble-wrap-http 1.7.1
Installing bubble-wrap 1.7.1
Installing motion-require 0.2.0
Installing formotion 1.8
Installing motion-support 0.2.6
Installing motion_model 0.5.4
Using bundler 1.6.3
Your bundle is complete!
Use `bundle show [gemname]` to see where a bundled gem is installed.
70. Motion Model
• MotionModel provides Ruby Objects with light persistence and
validation abilities, similar to ActiveRecord
• MotionModel's default persistence strategy relies on NSCoder to
serialize data. It then uses NSKeyedArchiver to create a NSData
representation
• MotionModel is more like DataMapper than ActiveRecord, allowing
you to declare a model’s columns directly in the Ruby class
71. the todo model
• We'll start to TDD our models at the lowest level with a simple spec
(todo_model_spec.rb) to test that the Todo model class exists:
describe "Todo Model" do
it "exists" do
Object.const_defined?('Todo').should.be.true
end
end
spec/todo_model_spec.rb
72. the todo model
• Running rake spec will kick start or model development TDD loop:
~/rm_workshop/code/okonawa
Todo Model
- exists [FAILED - false.true?() failed]
!
...
!
Bacon::Error: false.true?() failed
spec.rb:700:in `satisfy:': Todo Model - exists
spec.rb:714:in `method_missing:'
spec.rb:316:in `block in run_spec_block'
spec.rb:440:in `execute_block'
spec.rb:316:in `run_spec_block'
spec.rb:331:in `run
73. The Todo Model
• Let’s create an app/models folder to house our new Todo model (todo.rb)
class Todo
end
~/rm_workshop/code/okonawa
Application 'okonawa'
- has one window
!
Todo Model
- exists
!
Todos Controller
- should exist
- displays the given ToDos
- displays the correct label for a given ToDo
!
5 specifications (5 requirements), 0 failures, 0 errors
app/models/todo.rb
74. the todo model
Let’s write a spec to define what we expect from our Todo model:
• Add a before block and instantiate a Todo
• A Todo model should know its name, description, due_date, and
whether it is done or not
before do
@todo = Todo.new
end
...
!
it 'has a name, description, a due date and whether is done or not' do
@todo.should.respond_to :name
@todo.should.respond_to :description
@todo.should.respond_to :due_date
@todo.should.respond_to :done
end
spec/todo_model_spec.rb
75. the todo model
• Running rake spec again:
~/rm_workshop/code/okonawa
Todo Model
- exists
- has a name, description, a due date and whether is done or not [FAILED -
#<Todo:0xa6c8a90>.respond_to?(:name) failed]
...
Bacon::Error: #<Todo:0xa6c8a90>.respond_to?(:name) failed
spec.rb:700:in `satisfy:': Todo Model - has a name, description, a due date and
whether is done or not
spec.rb:714:in `method_missing:'
spec.rb:316:in `block in run_spec_block'
spec.rb:440:in `execute_block'
spec.rb:316:in `run_spec_block'
spec.rb:331:in `run'
!
6 specifications (6 requirements), 1 failures, 0 errors
76. motion model
To pass the spec we’ll have to:
• Mix in the main MotionModel functionality contained in
MotionModel::Model
• Use the default persistence adapter provided by the module
MotionModel::ArrayModelAdapter
• Define the 4 columns using the columns method
77. class Todo
include MotionModel::Model
include MotionModel::ArrayModelAdapter
!
columns :name => :string,
:details => :string,
:due_date => {:type => :date,
:formotion => {:picker_type => :date_time}},
:done => {:type => :boolean, :default => false,
:formotion => {:type => :switch}}
end
Motion Model
• The Todo model enhanced with MotionModel:
app/models/todo.rb
78. motion Model
• Running rake spec again to confirm the newly added functionality:
~/rm_workshop/code/okonawa
Application 'okonawa'
- has one window
!
Todo Model
- exists
- has a name, description, a due date and whether is done or not
!
Todos Controller
- should exist
- displays the given ToDos
- displays the correct label for a given ToDo
!
6 specifications (9 requirements), 0 failures, 0 errors
79. validations w/ motion Model
• Let’s specify that a Todo must have a name to be valid:
it 'is invalid without a name' do
@todo.name = nil
@todo.should.not.be.valid
end
~/rm_workshop/code/okonawa
spec/todo_model_spec.rb
Todo Model
- exists
- has a name, description, a due date and whether is done or not
- is invalid without a name [ERROR: NoMethodError - undefined method `valid?' for
Todo#3:0xa7b8b50:Todo]
!
...
NoMethodError: undefined method `valid?' for Todo#3:0xa7b8b50:Todo
model.rb:863:in `method_missing:': Todo Model - is invalid without a name
spec.rb:697:in `satisfy:'
spec.rb:714:in `method_missing:'
spec.rb:316:in `block in run_spec_block'
spec.rb:440:in `execute_block'
spec.rb:316:in `run_spec_block'
spec.rb:331:in `run'
80. validations w/ motion Model
• Including the MotionModel::Validatable mixin to use the validates
method:
class Todo
include MotionModel::Model
include MotionModel::ArrayModelAdapter
include MotionModel::Validatable
!
!
columns :name => :string,
:details => :string,
:due_date => {:type => :date,
:formotion => {:picker_type => :date_time}},
:done => {:type => :boolean, :default => false,
:formotion => {:type => :switch}}
!
validates :name, :presence => true
end
app/models/todo.rb
81. motion Model
• Running rake spec again to confirm that the Todo model validates the
presence of its name:
~/rm_workshop/code/okonawa
Application 'okonawa'
- has one window
!
Todo Model
- exists
- has a name, description, a due date and whether is done or not
- is invalid without a name
!
Todos Controller
- should exist
- displays the given ToDos
- displays the correct label for a given ToDo
!
7 specifications (10 requirements), 0 failures, 0 errors
82. a couple more validations
• Let's round out the Todo model development with a couple of
specifications: “A Todo is not done by default” and “A Todo knows if its
overdue”
before do
@now = NSDate.new
@todo = Todo.new :name => "Buy Milk",
:description => "We need some Milk",
:due_date => @now
end
!
it 'is not done by default' do
@todo.done.should.not.be.true
end
!
it 'knows if its overdue' do
@todo.should.be.overdue
end
spec/todo_model_spec.rb
83. a couple more validations
• Now we have to implement the overdue? method:
~/rm_workshop/code/okonawa
Todo Model
...
- knows if its overdue [ERROR: NoMethodError - undefined method `overdue?' for
Todo#5:0xa69ab50:Todo]
...
!
NoMethodError: undefined method `overdue?' for Todo#5:0xa69ab50:Todo
model.rb:863:in `method_missing:': Todo Model - knows if its overdue
spec.rb:697:in `satisfy:'
spec.rb:714:in `method_missing:'
spec.rb:316:in `block in run_spec_block'
spec.rb:440:in `execute_block'
spec.rb:316:in `run_spec_block'
spec.rb:331:in `run'
!
9 specifications (11 requirements), 0 failures, 1 errors
84. a couple more validations
• Add the overdue? method to the Todo model and provide an implementation:
def overdue?
# TODO: Implement me!
end
spec/todo_model_spec.rb
85. integrating the model
• The next step is to refactor the TodosController controller to
make use of the new model
• Our refactoring will involve replacing the @data string array with a
collection of Todo model instances
• Also, since we are using a full-fledge model we’ll have to extract the
property we want to use to display on the table
86. integrating the model
• Replace the @data array contents with a call to Todo.all
• Use the name attribute to be used in the cell label in cellForRowAtIndexPath
87. integrating the model
• Refactor the controller spec to clean up the DB and create a new Todo
using the create method
88. integrating the model
• Refactor the seeding of the DB to a separate seed method
• Refactor the AppDelegate to seed the DB in non-test mode
90. FORMOTION
https://github.com/clayallsopp/formotion
• FormMotion “Making iOS Forms insanely great with RubyMotion"
• Provides a builder DSL to quickly create iOS Forms
• Provides custom controllers to display the forms
• Integrates with MotionModel!
91. STORY #2
• We now have a list of Todos but we also need to see a Todo’s details
• Our next story will concentrate on the Todo’s details view:
“As a User I should be able to see a
To-Do’s details”
92. TODO controller
• We want a form displaying the Todo's details.
• That functionality will be the responsibility of the (yet to be created)
TodoController
• Let's start with the spec below:
describe "Todo Controller" do
it 'exists' do
Object.const_defined?('TodoController').should.be.true
end
end
spec/todo_controller_spec.rb
93. TODO controller
• Let’s use Formotion to implement our model-driven form:
class TodoController < Formotion::FormController
end
app/controllers/todo_controller.rb
~/rm_workshop/code/okonawa
Todo Controller
- exists
!
...
!
10 specifications (13 requirements), 0 failures, 0 errors
!
!
!
94. TODO controller
• Let's add a spec to test that our controller can indeed display a Todo model
• The spec does require some prior knowledge of how to access the soon to
be created rows of our form
it 'displays a Todo's details' do
@name_row.value.should.equal 'Buy Milk'
@details_row.value.should.equal 'We need some Milk'
@due_date_row.object.date_value.hour.should.equal @now.hour
@due_date_row.object.date_value.min.should.equal @now.min
@done_row.value.should.equal false
end
spec/todo_controller_spec.rb
95. • Although we have a failure, it’s not very telling (a sign we might be testing
to close to the implementation):
~/rm_workshop/code/okonawa
Todo Controller
- exists
- displays a Todo's details [ERROR: NoMethodError - undefined method `value' for nil:NilClass]
…
!
NoMethodError: undefined method `value' for nil:NilClass
spec.rb:316:in `block in run_spec_block': Todo Controller - displays a Todo's details
spec.rb:440:in `execute_block'
spec.rb:316:in `run_spec_block'
spec.rb:331:in `run'
!
11 specifications (13 requirements), 0 failures, 1 errors
!!!!!!
TODO controller
96. FORM-AWARE MODELS
• By adding the MotionModel::Formotion Mixin allow the ability to create
a FormMotion::Form from an instance of Todo
• E.g. Formotion::Form.new(todo.to_formotion('Edit your ToDo'))
class Todo
include MotionModel::Model
include MotionModel::ArrayModelAdapter
include MotionModel::Validatable
include MotionModel::Formotion
!
...
app/models/todo.rb
97. TODO controller
• Let’s expand the TodoController with the ability to initialize itself with an
instance of a FormMotion::Form (which we’ll build with our form-aware
model):
class TodoController < Formotion::FormController
attr_accessor :todo
attr_accessor :form
!
def initialize(todo)
self.form = Formotion::Form.new(todo.to_formotion('Edit your ToDo'))
self.initWithForm(self.form)
self.todo = todo
end
end
app/controllers/todo_controller.rb
98. TODO controller spec
• Finally, we’ll need to refactor the spec to create a Todo model, initialize a
controller instance with it and make that available to the spec
describe "Todo Controller" do
tests TodoController
!
before do
@now = NSDate.new
@todo = Todo.create :name => "Buy Milk",
:details => "We need some Milk",
:due_date => @now
@controller = TodoController.new(@todo)
!
@form = @controller.instance_variable_get("@form")
@name_row = @form.sections[0].rows[0]
@details_row = @form.sections[0].rows[1]
@due_date_row = @form.sections[0].rows[2]
@done_row = @form.sections[0].rows[3]
end
!
def controller
@controller
end
...
spec/todo_controller_spec.rb
99. TODO controller
• A subclass of UIViewController, UITableViewController is designed
to host and manage a UITableView
• By replacing the base class we don’t have to manually wire the
UITableView, set the datasource and add the view:
class TodosController < UITableViewController
attr_writer :data
!
def viewDidLoad
super
self.title = 'Okonawa'
# @table = UITableView.alloc.initWithFrame(self.view.bounds)
# @table.dataSource = self
# self.view.addSubview(@table)
!
@data = Todo.all
end
...
app/controllers/todo_controller.rb
100. TODO controller
• Let’s add the tableView#didSelectRowAtIndexPath event handler
• We’ll deselect the row the user just selected (clean up), grab the index of
the selection (via indexPath param), find the Todo in our Array, create the
TodoController and use the navigationController to push the new
view:
def tableView(tableView, didSelectRowAtIndexPath: indexPath)
tableView.deselectRowAtIndexPath(indexPath, animated: true)
todo = @data[indexPath.row]
todo_controller = TodoController.new(todo)
self.navigationController.pushViewController(todo_controller, animated: true)
end
app/controllers/todo_controller.rb
101. TODO controller spec
• If you run the specs the Todos Controller spec will fail since there is no
longer an instance variable @table in the controller
• Instead we can access the tableView property of the UIViewController
describe "Todos Controller" do
tests TodosController
!
before do
Todo.delete_all
@now = NSDate.new
@todo = Todo.create(:name => 'Buy Milk',
:description => 'Get some 1% to rid yourself of the muffin top',
:due_date => @now)
#@table = controller.instance_variable_get("@table")
@table = controller.tableView
end
spec/todo_controller_spec.rb
103. editing / saving
• Now, let's add the ability to save any modifications made to a Todo
• This section is going to be “Lab Style”. I’ll provide a failing spec for you to
pass
• Feel free to work in groups!
it 'saves changes made to a Todo' do
@name_row.object.row.value = 'Buy 1% Milk'
controller.save
!
saved_todo = Todo.find(@todo.id)
!
saved_todo.name.should.equal 'Buy 1% Milk'
end
spec/todo_controller_spec.rb
104. editing / saving
• Now, on to implement the save method!
~/rm_workshop/code/okonawa
Todo Controller
- exists
- saves changes made to a Todo [ERROR: NoMethodError - undefined method
`save' for #<TodoController:0xd3e2d90>]
...
!
NoMethodError: undefined method `save' for #<TodoController:0xd3e2d90>
spec.rb:316:in `block in run_spec_block': Todo Controller - saves changes
made to a Todo
spec.rb:440:in `execute_block'
spec.rb:316:in `run_spec_block'
spec.rb:331:in `run'
!
11 specifications (13 requirements), 0 failures, 1 errors
105. editing / saving - hints
• Implement the controller save method:
• Find how to retrieve the data from the form (what methods does @form
support?)
• Find how to update a Todo model the form data (what methods does
@todo support?)
• Consult the Formotion and MotionModel documentation if necessary
106. save button
• It doesn’t help our users that we have a working save method in the
controller if they can't use it
• We need to add a “Save” button to the TodoController view:
def viewDidLoad
super
saveButton = UIBarButtonItem.alloc.initWithTitle("Save",
style: UIBarButtonItemStyleBordered,
target: self, action: 'save')
self.navigationItem.rightBarButtonItem = saveButton
end
app/controllers/todo_controller.rb
107. save button
• Launching the app we can see that the changed values are persisted when
we tap the ‘Save’ button, but they are not reflected on the Todos list
• Model Motion supports notifications that are issued on object save, update,
and delete. We can use the NSNotificationCenter default instance to
register an observer, which will invoke the todoChanged method
def viewDidLoad
super
self.title = 'Okonawa'
!
@data = Todo.all
!
NSNotificationCenter.defaultCenter.addObserver(self, selector: 'todoChanged:',
name: 'MotionModelDataDidChangeNotification',
object: nil) unless RUBYMOTION_ENV == 'test'
end
app/controllers/todos_controller.rb
108. save button
• The todoChanged method received the notification object which has an
action property under userInfo
• On ‘update’ we will retrieve the passed Todo, find the row for the it (hacky),
create a NSIndexPath and tell the cell at that path to refresh itself:
def todoChanged(notification)
case notification.userInfo[:action]
when 'add'
when 'update'
todo = notification.object
row = todo.id - 1
path = NSIndexPath.indexPathForRow(row, inSection:0)
tableView.reloadRowsAtIndexPaths([path], withRowAnimation:UITableViewRowAnimationAutomatic)
when 'delete'
end
end app/controllers/todos_controller.rb
111. cocoapods
• Until recently there was only one way to maintain 3rd-party dependencies:
the vendoring procedure.
• The procedure entails downloading, unzipping/un-tarring and manually
copying dependencies (upgrades also following the same manual
procedure)
• Luckily CocoaPods liberates us from this 1990’s dependency management
hell!
• CocoaPods manages library dependencies for your Xcode/RubyMotion
projects. The dependencies for your projects are specified in a single text
file called a Podfile
112. cocoapods
• To use CocoaPods within a RubyMotion application you use the motion-cocoapods
gem located at https://github.com/HipByte/motion-cocoapods
• It can be installed with Bundler like any other Ruby Gem. Let’s add it to our
project’s Gemfile and bundle the application:
source 'https://rubygems.org'
!
gem 'rake'
gem 'motion_model'
gem 'formotion'
gem 'motion-cocoapods'
Gemfile
114. PIXATE FREESTYLE
• Pixate Freestyle is a native iOS (and Android) library that styles native
controls with CSS
• Replace many complicated lines of Objective-C with a few lines of CSS
• Pixate Freestyle allows you to add IDs, Class’es, and inline styles to your
native components, and style them with CSS
• Pixate also offers themes (CSS stylesheets offering a based style for most
iOS components) http://pixate.github.io/pixate-freestyle-ios/themes/
115. INSTALLING W/ COCOAPODS
• In order to add the Pixate Freestyle library to our RubyMotion project we
first need to find out if the library is maintained under CocoaPods
• The easiest way to do this to search directly on the CocoaPods site (paste
http://cocoapods.org/?q=pixatef in your browser):
116. INSTALLING W/ COCOAPODS
• Using motion-cocoapods we can declare our Pods directly in the Rakefile:
Motion::Project::App.setup do |app|
# Use `rake config' to see complete project settings.
app.name = 'okonawa'
!
app.pods do
pod 'PixateFreestyle', '~> 2.1'
end
end
Rakefile
117. INSTALLING W/ COCOAPODS
• CocoaPods configures a master repository locally (to avoid duplication)
• After bundling the application, run the pod setup command:
~/rm_workshop/code/okonawa
/> pod setup
Setting up CocoaPods master repo
Setup completed (read-only access)
!!!!!!!!!!
118. INSTALLING W/ COCOAPODS
• The next step is to install the dependencies into our RubyMotion project.
• Run motion-pods provided Rake task: rake pod:install:
~/rm_workshop/code/okonawa
/> rake pod:install
Updating spec repo `master`
Current branch master is up to date.
Analyzing dependencies
Downloading dependencies
Installing PixateFreestyle (2.1.4)
Generating Pods project
!!!!!!
119. INSTALLING W/ COCOAPODS
• Installing the pods create a Pods folder under your project’s vendor folder
• Since we do not want to check those artifacts into our Repo, add the following
lines to your .gitignore file
vendor/Pods/
vendor/Pods/.build/
vendor/Pods/build-iPhoneOS/
vendor/Pods/build-iPhoneSimulator/
vendor/Podfile.lock
.gitignore
120. motion-pixatefreestyle
• Our next step is to bring in the Ruby bridge to our Pixate Freestyle pod, the
motion-pixatefreestyle Gem:
source 'https://rubygems.org'
!
gem 'rake'
gem 'motion_model'
gem 'formotion'
gem 'motion-cocoapods'
gem 'motion-pixatefreestyle'
Gemfile
121. motion-pixatefreestyle
• The motion-pixatefreestyle Gem adds a couple of Rake tasks
• The init and sass tasks:
~/rm_workshop/code/okonawa
/> rake -T | grep pixate
rake pixatefreestyle:init # Create initial stylesheet files
rake pixatefreestyle:sass # Compile SASS/SCSS file
!
!
!
!
!
!
!
!
122. motion-pixatefreestyle
• Running rake pixatefreestyle:init will create our sass/scss
style file and the ‘compiled’ default.css under the resources folder:
~/rm_workshop/code/okonawa
/>rake pixatefreestyle:init
!
Create sass/default.scss
Create resources/default.css
!
!
!
!
123. Using A THEME
• Freestyle provides a theme (a set of SASS files) to quickly style an entire
iOS app
• Themes are available under the Pixate Freestyle repo, currently only the
pixate-blue is available
• To copy the theme clone the repo: git clone https://github.com/Pixate/
pixate-freestyle-ios.git OR download just the them from pixate-blue
• Copy the contents of the file to your /sass folder
124. • Running rake pixatefreestyle:sass with the them SASS files in
place will ‘compile’ a new default.css under the resources folder:
~/rm_workshop/code/okonawa
/>rake pixatefreestyle:sass
!
Compile sass/default.scss
!
!
!
!
Using A THEME
125. Using A THEME
• To work with Pixate Freestyle, we add CSS classes and ids to the app
elements
• To illustrate the power of CSS-based styling. Let’s add a CSS class to the cells
in our UITableView (in UITableViewController in
todos_controller.rb)
def tableView(tableView, cellForRowAtIndexPath: indexPath)
cell = UITableViewCell.alloc.initWithStyle(UITableViewCellStyleDefault,
reuseIdentifier:nil)
cell.textLabel.text = @data[indexPath.row].name
cell.styleClass = 'table-cell'
cell
end
app/controllers/todos_controller.rb
127. Using A THEME
• Styling the Formotion artifacts proved to be a little more involved since they
are not directly exposed
• After a few hours of GooglingTM I found that I needed to monkey-patch the
Formotion::Form class as shown below:
module Formotion
class Form < Formotion::Base
def tableView(tableView, willDisplayCell: cell, forRowAtIndexPath: indexPath)
cell.styleClass = 'table-cell'
cell.updateStyles
end
end
end app/formotion_pixate.rb
130. CONTINUING YOUR LEARNING
• I’ve created a series of articles that chronicle the development of the Okonawa
application and I will continue enhancing the series in 2014 and 2015
• http://integrallis.com/2013/03/tdd_in_ios_w_ruby_motion_part_i
• http://integrallis.com/2013/04/tdd_in_ios_w_ruby_motion_part_ii
• http://integrallis.com/2014/06/ios-development-in-ruby-with-rubymotion-part-iii
• We also offer a 3 day RubyMotion course! http://integrallis.com/courses/rubymotion