lucatironi.net Code snippets, tutorials and stuff

Ruby on Rails and RubyMotion Authentication Part Two

A Complete iOS App with a Rails API backend


Welcome back to the second part of the tutorial on how to create a mobile ToDo list app for the iPhone with RubyMotion. In the first part of this tutorial we created the app delegate and the view controllers to allow the users to register and login with the Ruby on Rails backend and made a stub of the task lists view controller.

In this second part we will complete the app with the missing features like displaying the tasks retrieved from the API, allowing the user to create new tasks and let him mark them as completed.

Displaying User’s Tasks

Last time we left the TasksListController with just a blank view and nothing to show besides a title bar. In order to retrieve, create and update the user’s tasks from the backend, we need something that will manage this kind of activities.

To do so we create a simple Ruby model/class called Task and we use some Ruby meta-programming to create some accessors method (getters and setters). It seems complicated but it’s not: for each of the the Task’s properties (id, title and completed flag) we create a getter and a setter through attr_accessor and we override the initialize method to pass an Hash of values to the Task.new call to set them to the correct property.

To learn more about this topic, I suggest to have a look to this tutorial.

# file app/models/Task.rb
class Task
  API_TASKS_ENDPOINT = "http://localhost:3000/api/v1/tasks"

  PROPERTIES = [:id, :title, :completed]

  PROPERTIES.each do |prop|
    attr_accessor prop
  end

  def initialize(hash = {})
    hash.each do |key, value|
      if PROPERTIES.member? key.to_sym
        self.send((key.to_s + "=").to_s, value)
      end
    end
  end

  def self.all(&block)
    BW::HTTP.get("#{API_TASKS_ENDPOINT}.json", { headers: Task.headers }) do |response|
      if response.status_description.nil?
        App.alert(response.error_message)
      else
        if response.ok?
          json = BW::JSON.parse(response.body.to_str)
          tasksData = json[:data][:tasks] || []
          tasks = tasksData.map { |task| Task.new(task) }
          block.call(tasks)
        elsif response.status_code.to_s =~ /40\d/
          App.alert("Not authorized")
        else
          App.alert("Something went wrong")
        end
      end
    end
  end

  def self.headers
    {
      'Content-Type' => 'application/json',
      'Authorization' => "Token token=\"#{App::Persistence['authToken']}\""
    }
  end
end

The Task.all is a similar to an ActiveRecord “find all” method. It will call the API and retrieve the user’s tasks and call the block, passing the tasks to it to do something with them (ie: populating a list).

To put this code in good use, we’ll add some lines to the TasksListController and transform it in a TableView: in the viewDidLoad method a @tasksTableView instance variable is created with UITableView.alloc.initWithFrame.

The delegate and datasource is set to the controller itself, so it is necessary to add some method to it to populate and manage the tableview’s cells: tableView(tableView, numberOfRowsInSection:section) to return the cell’s count and tableView(tableView, cellForRowAtIndexPath:indexPath) to return the cell at the given position.

Don’t forget to add the attr_accessor :tasks at the beginning of the file: it sets up an instance variable to store the tasks as an array.

Finally we add the actual code for the refresh method that is called clicking on the refresh button and at the end of the viewDidLoad method in order to populate the table list with the task retrieved from the API.

# file app/controllers/TasksController.rb
class TasksListController < UIViewController
  attr_accessor :tasks

  def self.controller
    @controller ||= TasksListController.alloc.initWithNibName(nil, bundle:nil)
  end

  def viewDidLoad
    super

    self.tasks = []

    self.title = "Tasks"
    self.view.backgroundColor = UIColor.whiteColor

    logoutButton = UIBarButtonItem.alloc.initWithTitle("Logout",
                                                       style:UIBarButtonItemStylePlain,
                                                       target:self,
                                                       action:'logout')
    self.navigationItem.leftBarButtonItem = logoutButton

    refreshButton = UIBarButtonItem.alloc.initWithBarButtonSystemItem(UIBarButtonSystemItemRefresh,
                                                                      target:self,
                                                                      action:'refresh')
    self.navigationItem.rightBarButtonItems = [refreshButton]

    @tasksTableView = UITableView.alloc.initWithFrame([[0, 0],
                                                      [self.view.bounds.size.width, self.view.bounds.size.height]],
                                                      style:UITableViewStylePlain)
    @tasksTableView.dataSource = self
    @tasksTableView.delegate = self
    @tasksTableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight

    self.view.addSubview(@tasksTableView)

    refresh if App::Persistence['authToken']
  end

  # UITableView delegate methods
  def tableView(tableView, numberOfRowsInSection:section)
    self.tasks.count
  end

  def tableView(tableView, cellForRowAtIndexPath:indexPath)
    @reuseIdentifier ||= "CELL_IDENTIFIER"

    cell = tableView.dequeueReusableCellWithIdentifier(@reuseIdentifier) || begin
      UITableViewCell.alloc.initWithStyle(UITableViewCellStyleDefault, reuseIdentifier:@reuseIdentifier)
    end

    task = self.tasks[indexPath.row]

    cell.textLabel.text = task.title

    cell
  end

  # Controller methods
  def refresh
    SVProgressHUD.showWithStatus("Loading", maskType:SVProgressHUDMaskTypeGradient)
    Task.all do |jsonTasks|
      self.tasks.clear
      self.tasks = jsonTasks
      @tasksTableView.reloadData
      SVProgressHUD.dismiss
    end
  end

  def logout
    UIApplication.sharedApplication.delegate.logout
  end
end

In the refresh method we first clear the controller instance variable tasks and we use the Task.all method to retrieve the user’s tasks and set them to the same variable. We then reload the data inside the @tasksTableView.

The actual cells with the correct task’s title will be set by the tableView(tableView, cellForRowAtIndexPath:indexPath) with the updated tasks list.

TasksListController

The TasksController populated with the tasks read from the backend

Create a new Task

Unless you want to send curl command in order to create new tasks, we need to add the most important feature of the app: the “new task” view.

First thing first, modify the viewDidLoad method inside the TasksListController adding the “+” button to the right of the navigation bar, besides the refresh button.

# file app/controllers/TasksListController.rb
class TasksListController < UIViewController

  def viewDidLoad
    # Other code

    newTaskButton = UIBarButtonItem.alloc.initWithBarButtonSystemItem(UIBarButtonSystemItemAdd,
                                                                      target:self,
                                                                      action:'addNewTask')
    self.navigationItem.rightBarButtonItems = [refreshButton, newTaskButton]

    # Other code
  end
end

Then add another instance method - addNewTask - at the end of the file that will be called when the user click on the “+” button we just added.

# file app/controllers/TasksListController.rb
class TasksListController < UIViewController
  # Other code

  def addNewTask
    @newTaskController = NewTaskController.alloc.init
    @newTaskNavigationController = UINavigationController.alloc.init
    @newTaskNavigationController.pushViewController(@newTaskController, animated:false)

    self.presentModalViewController(@newTaskNavigationController, animated:true)
  end
end

As you can see, what this method does is similar to the way we launch the WelcomeController in the app delegate: we create a new instance of this new view controller - NewTaskController - we are about to code, we push it in the stack of a UINavigationController and we present the whole thing as a modal, sliding up from the bottom.

The actual NewTaskController is a simple Formotion::FormContoller similar to the login and register one: just a text field and a button to create the new task.

# file app/controllers/NewTaskController.rb
class NewTaskController < Formotion::FormController
  def init
    form = Formotion::Form.new({
      sections: [{
        rows: [{
          title: "Title",
          key: :title,
          placeholder: "Task title",
          type: :string,
          auto_correction: :yes,
          auto_capitalization: :none
        }],
      }, {
        rows: [{
          title: "Save",
          type: :submit,
        }]
      }]
    })
    form.on_submit do
      self.createTask
    end
    super.initWithForm(form)
  end

  def viewDidLoad
    super

    self.title = "New Task"

    cancelButton = UIBarButtonItem.alloc.initWithTitle("Cancel",
                                                       style:UIBarButtonItemStylePlain,
                                                       target:self,
                                                       action:'cancel')
    self.navigationItem.rightBarButtonItem = cancelButton
  end

  def cancel
    self.navigationController.dismissModalViewControllerAnimated(true)
  end
end

Take a look to the bare foundation of this controller: it sets the text field and the button using the init method, it sets the “cancel” button in the navigation bar and defines the method to actually cancel the create action and dismiss the controller.

Before going deeper, we have to go back to the Task model to add a new class method that we’ll use to create new tasks and send them to the backend through the API.

The Task.create method does this thing in a simple way, leveraring on the code we already used in other places like the login action: it sends a POST request to the API endpoint dedicated to the creation of new tasks, with the task’s parameters in the payload. Hopefully, if everything is correct, it sends back the request’s response and calls the provided block.

# file app/models/Task.rb
class Task
  # Other code

  def self.create(params = {}, &block)
    data = BW::JSON.generate(params)

    BW::HTTP.post("#{API_TASKS_ENDPOINT}.json", { headers: Task.headers, payload: data } ) do |response|
      if response.status_description.nil?
        App.alert(response.error_message)
      else
        if response.ok?
          json = BW::JSON.parse(response.body.to_str)
          block.call(json)
        elsif response.status_code.to_s =~ /40\d/
          App.alert("Task creation failed")
        else
          App.alert(response.to_str)
        end
      end
    end
  end
end

To use this new feature in the NewTaskController we need to add a new method called createTask to it:

# file app/controllers/NewTaskController.rb
class NewTaskController < Formotion::FormController
  # Other code

  def createTask
    title = form.render[:title]
    if title.strip == ""
      App.alert("Please enter a title for the task.")
    else
      taskParams = { task: { title: title } }

      SVProgressHUD.showWithStatus("Loading", maskType:SVProgressHUDMaskTypeGradient)
      Task.create(taskParams) do |json|
        App.alert(json['info'])
        self.navigationController.dismissModalViewControllerAnimated(true)
        TasksListController.controller.refresh
        SVProgressHUD.dismiss
      end
    end
  end
end

As you can see, before passing the user’s input to the method, we checks if the submitted title isn’t blank and providing an alert if it is.

If the task’s title isn’t blank, we use the Task.create with a block that provides feedback to the user and dismiss the controller view, asking to the TasksListController to refresh the table view with the newly created task.

NewTaskController

The NewTaskController with the input field

Mark the task as completed

We almost done, we just miss the second most important feature in a ToDo list application: the ability to mark an item as “completed” (and reopening it if it isn’t done yet).

To do so we need to add one more method to the TasksListController tableview delegate and modify the tableView(tableView, cellForRowAtIndexPath:indexPath) method to show a UITableViewCellAccessoryCheckmark (a small “v” checkmark on the right) and change the their title’s font color to a light grey if they are completed.

The last method to add is tableView(tableView, didSelectRowAtIndexPath:indexPath) and as its name suggests, it’s called whenever the user click on a cell in the table, passing the position of the item in the list.

# file app/controllers/TasksListController.rb
class TasksListController < UIViewController
  # Other code

  def tableView(tableView, cellForRowAtIndexPath:indexPath)
    @reuseIdentifier ||= "CELL_IDENTIFIER"

    cell = tableView.dequeueReusableCellWithIdentifier(@reuseIdentifier) || begin
      UITableViewCell.alloc.initWithStyle(UITableViewCellStyleDefault, reuseIdentifier:@reuseIdentifier)
    end

    task = self.tasks[indexPath.row]

    cell.textLabel.text = task.title

    if task.completed
      cell.textLabel.color = '#aaaaaa'.to_color
      cell.accessoryType = UITableViewCellAccessoryCheckmark
    else
      cell.textLabel.color = '#222222'.to_color
      cell.accessoryType = UITableViewCellAccessoryNone
    end

    cell
  end

  def tableView(tableView, didSelectRowAtIndexPath:indexPath)
    tableView.deselectRowAtIndexPath(indexPath, animated:true)
    task = self.tasks[indexPath.row]

    task.toggle_completed do
      refresh
    end
  end
end

What the app does when the task is clicked is simple: first it deselect the cell because we don’t want to leaving it highlighted; then we find the task inside the tasks array and finally we call a new method - toggle_completed - on it, passing it a block where we just refresh the updated list.

The last piece of code we are going to write to complete the application has to be added to the Task model and it’s an instance method this time.

It accepts a block and it just makes an HTTP PUT request to the usual API endpoint, checking if the task is already completed or not and using the correct url then.

The rest of the code should be familiar at this point, it checks for error and calls the block.

# file app/models/Task.rb
class Task
  # Other code

  def toggle_completed(&block)
    url = "#{API_TASKS_ENDPOINT}/#{self.id}/#{self.completed ? 'open' : 'complete'}.json"
    BW::HTTP.put(url, { headers: Task.headers }) do |response|
      if response.status_description.nil?
        App.alert(response.error_message)
      else
        if response.ok?
          json = BW::JSON.parse(response.body.to_str)
          taskData = json[:data][:task]
          task = Task.new(taskData)
          block.call(task)
        elsif response.status_code.to_s =~ /40\d/
          App.alert("Not authorized")
        else
          App.alert("Something went wrong")
        end
      end
    end
  end
end

TasksListController

The final TasksListController with some tasks marked as completed

Conclusions

We made it!

As always I hope you could find this tutorial helpful and useful for your projects. If you have any question or request, just drop me a line to luca.tironi@gmail.com.

You can find the complete code of this tutorial and the previous one as well in this repository on GitHub. Check out the other tutorials for the Ruby on Rails backend if you need to.

Bye, Luca

^Back to top