code - lucatironi Code snippets and tutorials

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