RubyMotion on Rails (Part 1)

Bereits seit über einem Jahr code ich mit RubyMotion. Und ich muss sagen, ich bin immer noch sehr davon begeistert. Speziell die Möglichkeit der Blöcke und der asynchronen Programmierung (Dispatch) finde ich sehr angenehm. Ich finde durch die zwei Möglichkeiten kommt Ruby erst so richtig zur Geltung. Außerdem ist ein Ruby Block einfach viel besser als ein Objective-C Block...

Ein kleines Beispiel:

def do_it
  # Some really useful things...

  after 1.0 do
    puts "A block was called!"
  end

  # Some really useful things...
end

...

def after(time, &block)
  # block.weak!
  queue  = Dispatch::Queue.current
  timer = Dispatch::Source.timer(time, Dispatch::TIME_FOREVER, 0.0, queue) do |src|
    begin
      block.call
    ensure
      src.cancel!
    end
  end
end

Auch bin ich ein großer Fan von MotionModel. Es ist zwar noch nicht komplett fertig entwickelt, aber die flexible Möglichkeit der Datenablage finde ich super. Allerdings fehlte mir hierbei die Anbindung an eine API. Deshalb habe ich für das Gem eine Erweiterung geschrieben: MotionModelResource. Folgende Ziele hatte ich dabei:

  • so "leicht" wie möglich
  • RESTful
  • ActiveResource ähnlich

Aber nun zum Thema. Bei meinen Apps habe ich sehr häufig eine API dahinter. Rails hat uns vorgemacht, wie einfach und strukturiert wir APIs bauen können. Also sollte der Client (RubyMotion) auch so einfach wie möglich an die Daten ran kommen können. Durch die genannte Asynchronität und die Blöcke ist das auch sehr einfach.

# RubyMotion
Task.fetch do |tasks|
  tasks.each do |task|
    puts task.name
  end
end

So habe ich mir das vorgestellt und in meinem Gem umgesetzt. Somit hat man 100% Flexibilität und kann zB. ein Loading Grafik anzeigen, während geladen wird.

Relations

Auch wichtig ist es Relationen abrufen zu können. ZB. hat ein User mehrere Tasks:

# Rails
class User < ActiveRecord::Base
  has_many :projects
end
# RubyMotion
user = User.find(1)
user.fetch do
  user.tasks.present? # Yes!
end

Die URL wird im Model hinterlegt. Es gibt eine Klassen- und Instanzmethode „url“.

# RubyMotion
class Task
  belongs_to :user
  ...
  def self.url
    "#{App.info_plist['host']}/tasks"
  end

  def url
    "#{App.info_plist['host']}/tasks/#{id}"
  end
end

Die Felder von der API und dem lokalen System können über eine Mapping Tabelle zusammengeführt werden. Somit kann man auch Felder verbinden, die zum Beispiel im CamelCase oder im Underscore sind.

# RubyMotion
class Task
  ...
  def self.wrapper
    @wrapper ||= {
      fields: {
        id:     :id,
        name:   :name,
        due_at: :due_at
     },
     relations: [:user]
    }
  end
end

Die wrapper Methode ist so aufgebaut, dass man in „fields“ die Felder verknüpft. Der Hash-Key ist der Remote-Name und die Hash-Value ist der lokale Name. In einer nächsten Version werde ich das optional machen. Der zweite Wert ist die Relations-Hash. Hier kann man alle verknüpften Models angeben. Wenn diese in der JSON Antwort enthalten sind, werden die Models auch angelegt und aktualisiert.

# JSON
{
  "id": 1,
  "name": "Peter",
  "tasks": [{
    "name": "Do it!"
    "due_date": null
  }]
}
…

"Aaaand Action"

Ok soweit so gut. Schauen wir uns ein Beispiel an. Wir haben eine ToDo App und sind auf einem Eingabeformular, das beim abschicken einen neuen „Task“ anlegen soll. Diese soll auf dem Device und auf dem Server gespeichert werden. Der Server hat eine Authentifizierung über einen „API-Key“.

# RubyMotion
def create_task(sender) # Called from UIButton down
  # TODO: Show loading indicator

  task = Task.new(name: input_field.text) # input_field is your UITextField
  task.save(params: params[:payload]) do |t|
    App.alert t.present? ? "Success!" : "Fail"
    # TODO: handle UI after save
  end
end

def params
  {
    payload: {
      api_key: current_user.try(:api_key) # User model
    }
  }
end

def current_user
  @current_user ||= User.find(App::Persistence["current_user_id"])
end

Super, wir haben einen ersten Eintrag. Auf der Rails Seite wird ein POST Request erstellt und der Task wird wie gewohnt als "task[name]" übergeben. Wichtig für den Wrapper ist, dass alle Actions von der Rails Seite, ein JSON Objekt mit den gewünschten Models zurückgeben. Wenn das nicht der Fall ist, wird es wie ein Fehler behandelt.
Nun wollen wir alle Tasks in einer TableView anzeigen. Dafür habe ich ein ganz simples Beispiel gebaut, wie wir auch hier unsere Remote-Daten verwenden können:

# RubyMotion
class TasksViewController < UITableViewController
  def viewDidLoad
    super

    tableView.registerClass(UITableViewCell, forCellReuseIdentifier:"Cell")

    fetch_tasks
  end

  def fetch_tasks
    # TODO: Show loading indicator
    Task.fetch(nil, params) do |tasks| # Loads the remote tasks
      @tasks = nil # emtpy to get a fresh result of tasks
      tableView.reloadData # reloads the table view
    end
  end

  def tasks
    @tasks ||= Task.all # First time it will be a []
  end

  def numberOfSectionsInTableView(tableView)
    1
  end

  def tableView(tableView, numberOfRowsInSection:section)
    tasks.length
  end

  def tableView(tableView, cellForRowAtIndexPath:indexPath)
    task = tasks[indexPath.row] # gets the current task

    cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath:indexPath)
    cell.textLabel.text = task.name

    cell
  end

  def params
  {
    payload: {
      api_key: current_user.try(:api_key) # User model
    }
  }
end

Installation

Die Installation und Konfiguration ist auf der github Seite beschrieben.

Der nächste Teil wird sein, die Models mit einem Observer auszustatten, der unsere Views bei Änderungen automatisch updated.

Torben small

Your Contact

Torben Toepper