Ruby & Cocoa

Who?

Who?

Who?

Who?

Who?

Who?

Who?

Who?

Who?

Floehopping…

What?

What?

What?

What?

What?

What?

What?

Choices

Choices – RubyCocoa

Choices – RubyCocoa

Choices – RubyCocoa

Choices – RubyCocoa

Choices – RubyCocoa

Choices – RubyCocoa

Choices – RubyCocoa

Choices – RubyCocoa

Choices – MacRuby

Choices – MacRuby

Choices – MacRuby

Choices – MacRuby

Choices – MacRuby

Choices – MacRuby

Choices – MacRuby

Choices – MacRuby

Choices – HotCocoa

Choices – HotCocoa

Choices – HotCocoa

Choices – HotCocoa

Choices – HotCocoa

Choices – HotCocoa

RubyCocoa irb

>> require 'osx/cocoa'
=> true
 

RubyCocoa irb

>> require 'osx/cocoa'
=> true
 
>> 'hello'.class
=> String
 

RubyCocoa irb

>> require 'osx/cocoa'
=> true
 
>> 'hello'.class
=> String
 
>> hello = OSX::NSString.stringWithString('hello')
=> #<NSCFString "hello">
 

RubyCocoa irb

>> require 'osx/cocoa'
=> true
 
>> 'hello'.class
=> String
 
>> hello = OSX::NSString.stringWithString('hello')
=> #<NSCFString "hello">
 
>> hello.objc_methods.select { |m| m =~ /caseString$/ }
=> ["lowercaseString", "uppercaseString"]

MacRuby macirb

>> 'hello'.class
=> NSMutableString
 

MacRuby macirb

>> 'hello'.class
=> NSMutableString
 
>> 'hello'.methods(true, true).select { |m| m =~ /caseString$/ }
=> [:lowercaseString, :uppercaseString]

Reading Objective-C

Reading Objective-C

Reading Objective-C

Reading Objective-C

Reading Objective-C

Method Calls (without parameters)

Objective-C

 
 [sound play];
 

Method Calls (without parameters)

Objective-C

 
 [sound play];
 

RubyCocoa

 
 sound.play
 

Method Calls (without parameters)

Objective-C

 
 [sound play];
 

RubyCocoa

 
 sound.play
 

MacRuby

 
 sound.play
 

Method Calls (with parameters)

Objective-C

 
 NSSound* sound;
 sound = [sound initWithContentsOfFile: @"my.mp3"
                byReference: NO
         ];

Method Calls (with parameters)

Objective-C

 
 NSSound* sound;
 sound = [sound initWithContentsOfFile: @"my.mp3"
                byReference: NO
         ];

RubyCocoa

 
 sound = NSSound.initWithContentsOfFile_byReference(
   'my.mp3',
   false
 )

Method Calls (with parameters)

Objective-C

 
 NSSound* sound;
 sound = [sound initWithContentsOfFile: @"my.mp3"
                byReference: NO
         ];

RubyCocoa

 
 sound = NSSound.initWithContentsOfFile_byReference(
   'my.mp3',
   false
 )

MacRuby

 
 sound = NSSound.initWithContentsOfFile(
   'my.mp3',
   byReference: false
 )

Objective-C “Hello World”

#import <Cocoa/Cocoa.h>

NSApplication *app = [NSApplication sharedApplication]

NSWindow *window = [[NSWindow alloc] initWithContentRect: NSMakeRect(0, 0, 200, 200)
  styleMask: NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask | NSResizableWindowMask
  backing: NSBackingStoreBuffered
  defer: NO
)
[window setTitle: @"Hello World"]

NSButton* button = [[NSButton alloc] initWithFrame: NSZeroRect]
[[window contentView] addSubview: button]
[button setBezelStyle: NSRoundedBezelStyle]
[button setTitle: @"Say Hello"]
[button sizeToFit]
[button setFrameOrigin: NSMakePoint(
  ([[[window contentView] frameSize] width] / 2.0) - ([[button frameSize] width] / 2.0),
  ([[[window contentView] frameSize] height] / 2.0) - ([[button frameSize] height] / 2.0)
)

@interface HelloWorldController : NSObject {
}
- (IBAction)sayHello:(id)sender;
@end

#import "HelloWorldController.h"

@implementation HelloWorldController
- (IBAction)sayHello:(id)sender{
  NSLog(@"Hello World!");
}
@end

HelloWorldController* controller = [[HelloWorldController alloc] init]
[button setTarget: controller]
[button setAction: @selector(sayHello:)]

[window display]
[window orderFrontRegardless]

[app run]

RubyCocoa “Hello World”

require 'osx/cocoa'; include OSX

app = NSApplication.sharedApplication

window = NSWindow.alloc.initWithContentRect_styleMask_backing_defer(
  [0, 0, 200, 60],
  NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask | NSResizableWindowMask,
  NSBackingStoreBuffered,
  false
)
window.title = 'Hello World'

button = NSButton.alloc.initWithFrame(NSZeroRect)
window.contentView.addSubview(button)
button.bezelStyle = NSRoundedBezelStyle
button.title = 'Say Hello'
button.sizeToFit
button.frameOrigin = NSMakePoint(
  (window.contentView.frameSize.width / 2.0) - (button.frameSize.width / 2.0),
  (window.contentView.frameSize.height / 2.0) - (button.frameSize.height / 2.0)
)

button_controller = Object.new
def button_controller.sayHello(sender)
  NSLog('Hello World!')
end
button.target = button_controller
button.action = 'sayHello:'

window.display
window.orderFrontRegardless

app.run

MacRuby “Hello World”

framework 'Cocoa'

app = NSApplication.sharedApplication

window = NSWindow.alloc.initWithContentRect(
  [0, 0, 200, 60],
  styleMask:NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask | NSResizableWindowMask,
  backing:NSBackingStoreBuffered,
  defer:false
)
window.title = 'Hello World'

button = NSButton.alloc.initWithFrame(NSZeroRect)
window.contentView.addSubview(button)
button.bezelStyle = NSRoundedBezelStyle
button.title = 'Say Hello'
button.sizeToFit
button.frameOrigin = NSMakePoint(
  (window.contentView.frameSize.width / 2.0) - (button.frameSize.width / 2.0),
  (window.contentView.frameSize.height / 2.0) - (button.frameSize.height / 2.0)
)

button_controller = Object.new
def button_controller.sayHello(sender)
  NSLog('Hello World!')
end
button.target = button_controller
button.action = 'sayHello:'

window.display
window.orderFrontRegardless

app.run

HotCocoa “Hello World”

require 'hotcocoa'; include HotCocoa

application do
  window = window(:title => 'Hello World', :frame => [0, 0, 200, 60])
  button = button(:title => 'Say Hello', :layout => { :align => :centre })
  button.on_action { NSLog('Hello World!') }
  window << button
end

HotCocoa “Hello World”

require 'hotcocoa'; include HotCocoa

application do
  window = window(
    :title => 'Hello World',
    :frame => [0, 0, 200, 60]
  )
  button = button(
    :title => 'Say Hello',
    :layout => { :align => :centre }
  )
  button.on_action { NSLog('Hello World!') }
  window << button
end

HotCocoa “Hello World”

+-helloworld/
  +-config/
  | +-build.yml
  +-lib/
  | +-application.rb
  | +-menu.rb
  +-Rakefile
  +-resources/
    +-smiley.icns

HotCocoa “Hello World” – build.yml

+-helloworld/
  +-config/
  | +-build.yml      <-----
  +-lib/
  | +-application.rb
  | +-menu.rb
  +-Rakefile
  +-resources/
    +-smiley.icns
 
name: HelloWorld
load: lib/application.rb
secure: true
version: "1.0"
icon: resources/smiley.icns
sources: 
  - lib/**/*.rb
 

HotCocoa “Hello World” – application.rb

+-helloworld/
  +-config/
  | +-build.yml
  +-lib/
  | +-application.rb <-----
  | +-menu.rb
  +-Rakefile
  +-resources/
    +-smiley.icns
 
require 'hotcocoa'; include HotCocoa
application do
  window = window(
    :title => 'Hello World',
    :frame => [0, 0, 200, 60]
  )
  button = button(
    :title => 'Say Hello',
    :layout => { :align => :centre }
  )
  button.on_action { NSLog('Hello World!') }
  window << button
end
 

HotCocoa “Hello World” – menu.rb

+-helloworld/
  +-config/
  | +-build.yml
  +-lib/
  | +-application.rb
  | +-menu.rb        <-----
  +-Rakefile
  +-resources/
    +-smiley.icns
 
module HotCocoa
  def application_menu
    menu do |main|
      main.submenu :apple do |apple|
        apple.item(:quit, {
          :title => "Quit #{NSApp.name}",
          :key => "q"
        })
      end
    end
  end
end
 

HotCocoa “Hello World” – Rakefile

+-helloworld/
  +-config/
  | +-build.yml
  +-lib/
  | +-application.rb
  | +-menu.rb
  +-Rakefile         <-----
  +-resources/
    +-smiley.icns
 
require 'hotcocoa/application_builder'
require 'hotcocoa/standard_rake_tasks'

task :default => [:run]
 
 
macrake clean   # Delete app bundle
macrake build   # Build app bundle
macrake run     # Build & run app bundle
macrake deploy  # Build app bundle and embed MacRuby.framework
 

HotCocoa “Hello World” – smiley.icns

+-helloworld/
  +-config/
  | +-build.yml
  +-lib/
  | +-application.rb
  | +-menu.rb
  +-Rakefile
  +-resources/
    +-smiley.icns    <-----

HotCocoa “Hello World” Demo

 
# DO THE DEMO
 
$ macrake deploy

$ macrake run
 

Anatomy of “Hello World” App Bundle

+-helloworld
  +-HelloWorld.app/
    +-Contents
      +-Frameworks
      | +-MacRuby.framework # Embedded
      +-Info.plist          # Metadata
      +-MacOS
      | +-HelloWorld        # Binary
      +-PkgInfo             # Metadata
      +-Resources
        +-HelloWorld.icns   # Icons
        +-rb_main.rb        # MacRuby app loader
        +-vfs.db            # Virtual FS database

installd.com – Website

installd.com – PreferencePane

Universal Access

HotCocoa Calculator

CalculatorDriver – Setup

require 'appscript'

module CalculatorDriver
  
  include Appscript
  
  def activate_calculator_app
    app_path = File.expand_path(...)
    app(app_path).activate
  end
  
  def calculator_process
    system_events = app('System Events')
    system_events.processes['Calculator']
  end
  
  def setup_calculator_driver
    activate_calculator_app
    window = calculator_process.windows['Calculator']
    @buttons = window.buttons
    @text_fields = window.text_fields
  end
  
  #...

CalculatorDriver – Clicks

 
  def input_with_buttons(buttons_text)
    buttons_text.each_byte do |byte|
      click_button(byte.chr)
    end
  end

  def click_button(character)
    buttons = %w(C 0 1 2 3 4 5 6 7 8 9 . + - * / = √)
    if buttons.include?(character)
      @buttons[character].click
    else
      raise "Button does not exist for: #{character}"
    end
  end
  
  #...
  

CalculatorDriver – Assertion

require 'test/unit/assertions'

  #...
  
  include Test::Unit::Assertions
  
  def displays_number(expected_result)
    result = @text_fields[1].value.get
    assert_equal expected_result, result
  end

Calculator – UIElementInspector

Calculator – Acceptance Test

require 'test/unit'
require 'calculator_driver'

class AdditionTest < Test::Unit::TestCase

  include CalculatorDriver
  
  def setup
    setup_calculator_driver
    click_button('C')
  end
  
  def test_adding_two_single_digit_numbers
    input_with_buttons('1')
    click_button('+')
    input_with_buttons('2')
    click_button('=')
    
    displays_number('3')
  end
  
  #...

Calculator – Run Acceptance Tests

 
# DO THE DEMO
 
$ macrake build

$ rake test:acceptance
 

Calculator – application.rb

require 'hotcocoa'

include HotCocoa

class Calculator

  def self.on
    self.new.show
  end

  attr_accessor :accumulator, :value, :operand_pressed, :button_view

  def initialize
    @accumulator = []
  end

  def show
    application :name => "Calculator" do |app|
      main_window << value
      main_window << button_view
      main_window.will_close { exit }
    end
  end

  private

    def main_window(&block)
      @main_window ||= window(:frame => [100, 800, 220, 280], :title => "Calculator", :view => :nolayout, :style => [:titled, :closable, :miniaturizable], &block)
    end

    def value
      @value ||= text_field(:frame => [10, 230, 200, 40], :text => "0", :font => font(:name => "Tahoma", :size => 22), :text_align => :right)
    end

    def button_view
      @button_view || create_button_view
    end

    def create_button_view
      @button_view = view(:frame => [10, 10, 200, 240])
      add_buttons
      @button_view
    end

    def add_buttons
      calc_button("CL",  0, 4)         { clear }
      calc_button("SQR", 1, 4)         { sqrt }
      calc_button("/",   2, 4)         { operand :/ }
      calc_button("*",   3, 4)         { operand :* }
      calc_button("7",   0, 3)         { press 7 }
      calc_button("8",   1, 3)         { press 8 }
      calc_button("9",   2, 3)         { press 9 }
      calc_button("-",   3, 3)         { operand :- }
      calc_button("4",   0, 2)         { press 4 }
      calc_button("5",   1, 2)         { press 5 }
      calc_button("6",   2, 2)         { press 6 }
      calc_button("+",   3, 2)         { operand :+ }
      calc_button("1",   0, 1)         { press 1 }
      calc_button("2",   1, 1)         { press 2 }
      calc_button("3",   2, 1)         { press 3 }
      calc_button("=",   3, 1, 0, 1)   { evaluate }
      calc_button("0",   0, 0, 1, 0)   { press 0 }
      calc_button(".",   2, 0)         { press '.' }
    end

    def calc_button(name, x, y, w=0, h=0, &block)
      button_view << button(:title => name, :bezel => :regular_square, :frame => [x*50, y*43-h*43, 47+w*50, 40+h*43]).on_action(&block)
    end

    def evaluate
      accumulator << float_value
      result = eval(accumulator.join(" "))
      value.text = (result.to_i == result ? result.to_i : result.to_s)
      accumulator.clear
    end

    def press(key)
      if operand_pressed
        value.text = key.to_s
      else
        value.text = (value.to_s == "0" ? key.to_s : (value.to_s + key.to_s))
      end
      @operand_pressed = false
    end

    def operand(key)
      accumulator << float_value
      accumulator << key
      @operand_pressed = true
    end

    def float_value
      (value.to_s[0,1] == "." ? "0#{value.to_s}" : value.to_s).to_f
    end

    def sqrt
      value.text = Math.sqrt(float_value).to_s
    end

    def clear
      value.text = "0"
    end

end

Calculator.on

Calculator – display.rb

class Display
  
  def initialize
    clear
  end
  
  def clear
    @value = '0'
  end
  
  def set(value)
    @value = value.to_s
  end
  
  def push(character)
    if @value == '0'
      @value = character
    else
      @value += character
    end
  end
  
  #...

Calculator – display_test.rb

class DisplayTest < Test::Unit::TestCase
  
  def test_initial_value
    assert_equal '0', @display.to_s
    assert_in_delta 0.0, @display.to_f, 1e-8
  end
  
  def test_clear_value
    @display.push('1')
    @display.clear
    assert_equal '0', @display.to_s
    assert_in_delta 0.0, @display.to_f, 1e-8
  end
  
  def test_entering_single_digit_number
    @display.push('1')
    assert_equal '1', @display.to_s
    assert_in_delta 1.0, @display.to_f, 1e-8
  end
  
  #...

Calculator – accumulator.rb

class Accumulator
  
  def initialize
    clear
  end
  
  def push(value)
    @values << value
  end
  
  def evaluate
    result = eval(@values.join(' ')).to_f
    clear
    result
  end
  
  def clear
    @values = []
  end
  
  #...

Calculator – view.rb

class View
  
  attr_reader :buttons
  
  def initialize(controller)
    main_window = window(
      :frame => [100, 800, 220, 280],
      :title => "Calculator",
      :view => :nolayout,
      :style => [:titled, :closable, :miniaturizable]
    )
    
    @value = text_field(
      :frame => [10, 230, 200, 40],
      :text => controller.display.to_s,
      :font => font(:name => "Tahoma", :size => 22),
      :text_align => :right
    )
    main_window << @value
    
    #...

Calculator – view.rb

    #...
    
    @buttons = {}
    @buttons['C'] = calc_button("C", 0, 4)
    @buttons[''] = calc_button("", 1, 4)
    @buttons['/'] = calc_button("/", 2, 4)
    
    #...
    
    @buttons['='] = calc_button("=", 3, 1, 0, 1)
    @buttons['0'] = calc_button("0", 0, 0, 1, 0)
    @buttons['.'] = calc_button(".", 2, 0)
    
    buttons_view = view(:frame => [10, 10, 200, 240])
    @buttons.values.each do |b|
      buttons_view << b
    end
    main_window << buttons_view
    main_window.will_close { exit }
  end

Calculator – accumulator_test.rb

class AccumulatorTest < Test::Unit::TestCase

  def test_initial_evaluate
    result = @accumulator.evaluate
    assert_in_delta 0.0, result, 1e-8
  end
  
  def test_evaluate_after_no_operations
    @accumulator.push(123.0)
    result = @accumulator.evaluate
    assert_in_delta 123.0, result, 1e-8
  end
  
  def test_evaluate_addition_operation
    @accumulator.push(123.0)
    @accumulator.push('+')
    @accumulator.push(456.0)
    result = @accumulator.evaluate
    assert_in_delta 579.0, result, 1e-8
  end
  
  #...

Calculator – application.rb (refactored)

class Calculator
  
  def initialize
    @model = Model.new
  end
  
  def show
    application :name => "Calculator" do |app|
      @view = View.new(self)
      %w(0 1 2 3 4 5 6 7 8 9 .).each do |digit|
        @view.buttons[digit].on_action { press(digit) }
      end
      %w(+ - * /).each do |operation|
        @view.buttons[operation].on_action { operand(operation) }
      end
      @view.buttons['C'].on_action { clear }
      @view.buttons[''].on_action { sqrt }
      @view.buttons['='].on_action { evaluate }
    end
  end
  
  #...

Calculator – application.rb (refactored)

  def evaluate
    @model.evaluate
    @view.update_value(@model.current_value)
  end
  
  def press(key)
    @model.press(key)
    @view.update_value(@model.current_value)
  end
  
  def operand(key)
    @model.operand(key)
  end
  
  def sqrt
    @model.sqrt
    @view.update_value(@model.current_value)
  end
  
  def clear
    @model.clear
    @view.update_value(@model.current_value)
  end

Calculator – Run Unit Tests

 
# DO THE DEMO
 
$ TESTOPTS="-v" rake test:units
 

Summary

Questions?

Resources / Links