Experiencias desenvolvendo um aplicativo para iOS: Eventbee

Experiencias desenvolvendo um aplicativo para iOS: Eventbee

Aqui relatarei as experiências que tive no desenvolvimento de um projeto com meus colegas: Renata Faria e Marcus Reuber. O projeto tratava-se em criar um aplicativo nativo para iOS da qual um usuário poderá cadastrar um evento e também se inscrever neles. Uma funcionalidade a mais seriam chats nos eventos, para os inscritos no evento poderem se interagir pelo aplicativo.
Além do mais, nós tínhamos cerca de 3 semanas para desenvolver todo o projeto. Ademais, todos nós 3 somos estudantes (eu e Marcus do curso de Engenharia da Computação do IFCE, enquanto a Renata é do curso de Sistemas e Mídias Digitais da UFC). Nós somos estagiários do BEPiD.
Por fim, decidimos chamar o aplicativo de Eventbee.

Algumas fotos de como ficou o aplicativo:

Essa tipo de postagem é muito útil para servir de embassamento para outras pessoas que vão começar a desenvolver um projeto como esse. E é útil para eu para poder refletir e rever onde acertei e onde preciso melhorar =P

A postagem é longa, então recomendo que escute essa playlist enquanto lê tudo isso aí.

Primeiras escolhas

Pelo escopo do projeto, é óbvio que precisaríamos de algumas coisa server side, para gerenciar a database, armazenar os assets, gerenciar o servidor, e toda essa parafernalha. Então, que escolhas fazer?

      Eu decidi optar pelo Rails5 para o servidor. Eu já tive experiências boas com ele, e decidi continuar a usá-lo.

      Antes de realmente confirmar essa escolha, cheguei a analisar a integração do websocket dele com o iOS, pois é necessário para o chat. Estudei como funciona e eu fiz um simples app conectando o websocket do Rails no Swift, e funcionou. Passado esse teste, confirmei a escolha do Rails.

      Assim como na escolha do Rails, optei em usar o Heroku por já ter tido boas experiências com ele. A documentação do Heroku é linda, curva de aprendizado muito baixa... Enfim, é ótimo.

      Por fim, como não faria muito sentido armazenar imagens no Heroku, por recomendação de um amigo, decidi que usaria a AWS S3 para armazenar arquivos. Até então eu nunca havia usado algum serviço da Amazon.

No decorrer desta postagem vou relatar como foram as experiências com esses serviços.

Já na metade do curso do projeto, descobri que a Amazon também fornece um "Heroku" deles, o Beanstalk. Pelo o que ouvi falar e pesquisei, o serviço é bem mais barato que o Heroku, porém, em contrapartida, a curva de aprendizado é maior. Certamente, se eu tivesse optado pelo Beanstalk, teria me ajudar quando eu fui começar a usar o S3. Na próxima oportunidade testarei o Beanstalk no lugar o Heroku, para saber como funciona.

Após alguns dias do começo do projeto, percebi que todos os meus colegas do estágio estavam usando o Firebase no desenvolvimento do aplicativo para iOS. Eu nunca cheguei a usá-lo, mas é consenso entre os meus colegas que ele é muito mais fácil de se usar do que criar um servidor do zero com algo como o Rails.
Eu até cheguei a estudar a possibilidade em experimentar o Firebase, porém, decidi ser conservador e continuar no Rails. Uma das razões que mais pesou nessa escolha foi o fato do Firebase me prender nele. Fazendo uma aplicação no Rails eu terei a possibilidade de migrar do Heroku para o Beanstalk, visando o melhor custo x benefício, por exemplo.
O Firebase parece ser bem interessante e a comunidade é bem ativa. Se pesquisar no Stack Overflow por "swift" verá várias perguntas que também usam a tag do "firebase". Porém, como nunca cheguei a experimentá-lo, não posso dizer se teria sido melhor ter usado ele ou se o Rails foi uma boa escolha .

Arquitetura & Organização

Já tendo escolhida as plataformas para o projeto, falta agora pensar no código dele.
O que mais desejo explanar aqui é como foi evoluindo a forma de se escrever as chamadas ao servidor. Além disso, também abordarei como foi implementada a sessão do usuário ao logar no app, a conversão dos dados trazidos do servidor em objetos e, por fim, como realizar os tratamentos de erros.

Chamadas ao servidor

É óbvio que num projeto sério nunca deve-se usar o Massive View Controller, portando, as chamas ao servidor precisam ficar em algum outro canto que não seja a sua querida ViewController.
Então, decidi criar uma pasta chamada Request. Nela, criei uma classe pai chamada de Request e todas as demais classes que fazem requisições ao servidor herdam dela; assim, criei classes como RequestUser para lidar com requisições a respeito do usuário (logar, criar conta, alterar foto de perfil...) e a RequestEvent (para criar evento, se inscrever num evento...).

Tanto a Request como as herdeiras delas são singletons. Ok, eu reconheço que singletons são odiadas por muita gente, porém, lancei mão de usá-la porque os valores delas serão sempre constantes por toda a vida da aplicação, evitando o problema gerado por variáveis globais, porém, ainda assim mantenho o problema de dependências escondidas. Pensando e revisando o código agora, talvez uma alternativa melhor fosse usar dependency injection.

A razão de criar a Request é para poder compartilhar com as filhas a URL ao servidor e também compartilhar o método chamado de requestBase. Esse método abstrai toda a parte básica de uma requisição ao servidor e devolve a resposta em um JSON. Explicarei o requestBase mais para frente.

Diagrama simplificado das classes Request

Eu poderia jogar todos os métodos apenas em Request. Desse modo, eu não precisaria criar várias classes... porém, ficaria uma completa bagunça! A divisão em sub-classes como RequestUser e RequestEvent proporciona uma divisão lógica. Além do mais, isso ajuda a nomear os métodos, já que eu posso ter um método chamado index em RequestUser e RequestEvent, por exemplo.

Adotando essa organização, em qualquer momento da aplicação posso usar RequestEvent.shared.index() para receber a lista dos eventos. Simples e efetivo.

Assincronismo

Se reparar bem o código citado agora a pouco, RequestEvent.shared.index(), nada é dito a respeito do assincronismo. Quando o usuário apertar o botão para listar os eventos, a aplicação toda vai parar? Vai aguardar a resposta do servidor? E para onde vai a resposta? E o tratamento de erros? AAH!

Calma...... A requisição é feita no método requestBase, da classe de Request. A requisição é feita usando o Alamofire. A princípio, no começo do projeto estávamos usando callbacks:

// ** repare os parâmetros erroHandle e success **
func requestBase(url: String, ...
                 errorHandle: @escaping (JSON, Int) -> (Void),
                 success: @escaping (JSON) -> (Void)) {
    ...

    Alamofire.request(
        self.urlBase + url,
        ...
    ).responseJSON(completionHandler: {
        response in
        let statusCode = (response.response?.statusCode)!
        var json: JSON?

        if let resultValue = response.result.value {
            json = JSON(resultValue)
        }

        if (statusCode != 200) && (statusCode != 201) {
            errorHandle(json, statusCode) // ** chamando o callback em caso de erro **
            return
        }

        success(json) // ** chamando o callback em caso de sucesso **
    })
}

Ou seja, na ViewController, quando for usar o método index, era preciso passar dois callbacks: um que seria chamado em caso de sucesso, e outro para caso de fracasso. Dentro do index, por sua vez, repassava os callbacks para o requestBase, que então executaria a closure errorHandle ou a success.
A função request do Alamofire, como já deve-se saber, é assincrona. Logo, a aplicação é interrompida. E quando um dos callbacks forem chamados, a execução ocorreria em uma outra thread.

Divino, não? Eu conseguia manter o assincronismo da aplicação por meio dos callbacks, um para caso de sucesso e outro para caso de fracasso. Poréeeem, após alguns dias usando essa solução, caí no mesmo problema que a humanidade inteira já passou...

O código começou a virar um verdadeiro inferno. Imagine a seguinte situação no cadastro: o usuário vai criar a sua conta (um request), após isso deve-se logar na conta (outro request).
Nesse simples cenário já teríamos 4 callbacks, sendo que 2 estão um dentro de outro! E situações similares se repetiam em vários momentos da aplicação!!
O código até então era algo mais ou menos assim:

RequestUser.shared.createAccount(...,
  erroHandle: { error in
    ...
  },
  success: { result in
    RequestUser.shared.login(...,
      errorHandle: { error in
        ...
      },
      success: { result in
        ...
      }
    )
  }
)

Felizmente, a humanidade já resolveu esse problema, desenvolvendo o conceito de promise. Eu já a conhecia no JS, então fui pesquisar como seria em Swift, e encontrei o divino pacote PromiseKit!
A curva de aprendizado foi muito baixa e em um domingo deu para refatorar todos os infernais callbacks em lindas e imaculadas promises.

O método requestBase foi refatorado para

func requestBase(url: String, ...) -> Promise<JSON> {
    ...

    return Promise { fulfill, reject in
        Alamofire.request(
            self.urlBase + url,
            ...
        ).responseJSON(completionHandler: {
            response in
            let statusCode = (response.response?.statusCode)!
            var json: JSON?

            if let resultValue = response.result.value {
                json = JSON(resultValue)
            }

            if (statusCode != 200) && (statusCode != 201) {
                reject(NSError(json: json, code: statusCode))
                return
            }

            fulfill(json!)
        })
    }
}

Desse modo, ficou bem mais legível códigos como esse:

firstly {
    RequestUser.sharedInstance.createUserWithEmail(...)
}.then { _ -> Promise<Void> in
    RequestUser.sharedInstance.loguinWithEmail(...)
}.then { _ -> Void in
    self.performSegue(withIdentifier: "GoToApp", sender: sender)
}.catch { error in
    if error is RequestCreateUserWithEmailErro {
        switch error as! RequestCreateUserWithEmailErro {
        case .repeatedEmail:
            self.msgAlertOK(title: "Erro!", msg: "E-mail já em uso!")
            break
        case .failedProcessEntity:
            self.msgAlertOK(title: "Erro!", msg: "Falha ao tentar criar o usuário!")
            break
        case .unknown:
            self.msgAlertOK(title: "Erro!", msg: "Erro desconhecido")
        }
    } else {
        self.msgAlertOK(title: "Erro!", msg: "Foi possível criar a conta, mas houve alguma falha ao tentar efetuar o loguin")
    }
}

A leitura do código fica bem mais linear e o tratamento de erros se concentra em um só callback, no catch. Nesse caso em particular, como desejo exibir mensagens diferentes para cada falha, há a necessidade do if e switch, porém, em várias outras situações uma ou duas linha no catch já é o suficiente.
Abordarei o tratamento de erro mais detalhadamente em outra seção.

O uso de promises é bem mais poderoso e permite que eu faça coisas interessantes como essa: considere que em uma determinada tela o usuário possa editar suas informações básicas de perfil (nome, about), e também a sua foto.
Desse modo, dois requests precisam ser feito: um para enviar as informações básicas de perfil, e outro para enviar a sua foto. Porém, nem sempre o usuário vai ter modificado a sua foto, então há situações que desejamos chamar apenas um dos dois requests. Como proceder nesse cenário? Assim:

@IBAction func btSaveProfileChanges(_ sender: UIButton) {
    // recolher valores inseridos pelo usuário
    let newName = self.txtFieldName.text!
    let newAbout = self.txtFieldAbout.text!
    let newImage = self.userImage.image!

    // pré-definir os valores para as promises que talvez chamaremos
    func promiseToSaveProfile() -> Promise<Void> {
        return RequestCurrentUser.shared.updateMyProfile(
            newName: newName,
            newAbout: newAbout
        )
    }

    func promiseToUpdatePhoto() -> Promise<Void> {
        return RequestCurrentUser.shared.updateProfilePhoto(photo: newImage)
    }

    // colocar na array prommisesToCall as promises que realmente precisamos chamar
    var promisesToCall = [promiseToSaveProfile]

    if self.sendNewPhoto {
        promisesToCall.append(promiseToUpdatePhoto)
    }

    // executar promises
    when(fulfilled:
        promisesToCall.map { promise in promise() }
    ).then { _ -> Void in
        self.dismiss(animated: true, completion: nil)
    }.catch { _ in
        print("Falha ao editar o perfil!")
    }
}

A função when(fulfilled:) vem do PromiseKit. Nele, podemos pasar uma array de promises que desejamos chamar, e só após todas terem sido concluídas que irá ao then. Se alguma falhar, irá ao catch.
Foram criadas funções aninhadas para poder definir os parâmetros das chamadas dos requests sem executá-los. Por fim, colocamos na array promisesToCall as promises que realmente precisamos chamar.
Desse modo, só chamaremos as promises necessárias e aguadará todas terminarem para então chamar o then.
Fazer isso usando callback hell seria uma aberração.

Sessão do usuário

Mais uma vez... singleton. Criei uma classe chamada de UserCurrent para armazenar as informações do usuário logado, como o seu id, name, email e, uma das coisas mais importantes, que é o token da sesão de login.
Além dos atributos armazenados, o UserCurrent também encapsula métodos do RequestUser para já efetuar a requisição passando o id do usuário atual. Por exemplo, em RequestUser há o método getPhotoProfile(userId:), para pegar a foto do usuário do id fornecido. Já em UserCurrent tem o método getPhotoProfile(); ele chama o RequestUser.shared.getPhotoProfile(userId:) já passando o id do usuário logado no parâmetro userId.

Diferente das classes de Request, os atributos de UserCurrent não é sempre constante, pois, sempre que houver um login ou logoff, os valores dos atributos obviamente serão alterados. Porém, ainda achei válida essa estratégiia, pois isso vai acontecer poucas vezes, e seria completamente inviável ficar arrastando entre as ViewController uma variável com os dados do usuário logado.

Seguindo a estratégia de usar a singleton UserCurrent, no método requestBase de Request fiz o seguinte:

// repare o parâmetro needToken e seus usos no método
func requestBase(url: String, needToken: Bool ...) -> Promise<JSON> {
    ...

    if needToken {
        headers = ["Content-Type": "application/json", "Token": UserCurrent.shared.token!]
    }

    return Promise { fulfill, reject in
        Alamofire.request(
            self.urlBase + url,
            method: method,
            ...
        ).responseJSON(completionHandler: {
            ...

Assim, se um determinado request precisar do token do usuário, basta passar true para o parâmetro needToken.

Os valores do usuário logado em UserCurrent são atualizados nos métodos logout, loguinWithEmail e loginWithFacebook, todos da classe RequestUser. Veja um breve exemplo,

func logout() -> Promise<Void> {
    return Promise { fulfill, reject in
        firstly {
            self.requestBase(
                url: "users/logoff",
                method: .get,
                needToken: true,
                parameters: nil
            )
        }.then { json -> Void in
            // ** atualizar os valores do UserCurrent para quando está deslogado **
            UserCurrent.sharedInstance.logged = false // não está mais logado
            UserCurrent.sharedInstance.token = nil // para evitar erros, apagar o token
            if AccessToken.current != nil { // isso é coisa do SDK do Facebook
                let loginManager = LoginManager()
                loginManager.logOut()
            }
            fulfill()
        }.catch { error in
            let statusCode = (error as NSError).code
            let json = (error as NSError).json!

            ...
        }
    }

Apesar das facilidades na implementação dessa abordagem, eu particulamente não gostei do resultado final, pois acabou gerando um grande acoplamento entre a classe UserCurrent com as Request e RequestUser, algo que no mundo ideal não deve ocorrer. Porém, não estamos no mundo ideal e eu realmente não tive ideias de uma solução melhor que essa.

Montar os objetos trazidos do servidor

Ok. As requisições estão funcionando. Estou puxando os dados do servidor. Mas como efetuar a conversão do JSON em um objeto?

Por exemplo, na classe RequestEvent tenho o método index(). Ele retorna todos os eventos cadastrado no servidor.

func index() -> Promise<[Event]> {
    return Promise { fulfill, reject in
        firstly {
            self.requestBase(
                url: "events",
                method: .get,
                needToken: false,
                parameters: nil
            )
        }.then { json -> Void in
            let events = json.arrayValue.map {
                Event(
                    id: $0["id"].intValue,
                    name: $0["name"].stringValue,
                    description: $0["description"].stringValue,
                    permission: EventPermission(rawValue: $0["permission"].stringValue)!,
                    datetimeStart: NSDate(timeIntervalSince1970: TimeInterval($0["datetime_start"].intValue)),
                    datetimeEnd: NSDate(timeIntervalSince1970: TimeInterval($0["datetime_end"].intValue))
                )
            }

            fulfill(events)
        }.catch { error in
            reject(self.handleCommonErrosWithoutToken(error: error))
        }
    }
}

Como pode-se ver na closure do then, para cada elemento retornado do JSON, é construído um objeto de Event. Então, de forma similar à classe UserCurrent, em Event eu crio métodos para encapsular algumas chamadas de RequestEvent. Por exemplo, em Event eu tenho um método para se inscrever, já passando o ID do evento do objeto.

Tratamento de erros

No código acima, a closure do catch tem apenas uma única linha. Interessante, não?
Explicarei aqui como foi implementado o tratamento de erros.

Antes de tudo, eu precisei fazer uma extension em NSError:

// Error.swift
import ObjectiveC
import SwiftyJSON

private var xoAssociationKey: UInt8 = 0

extension NSError {
    var json: JSON! {
        get {
            return objc_getAssociatedObject(self, &xoAssociationKey) as? JSON
        }
        set(newValue) {
            objc_setAssociatedObject(self, &xoAssociationKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
        }
    }

    convenience init(json: JSON?, code: Int) {
        self.init(domain: "", code: code, userInfo: nil)
        self.json = json
    }
}

Essa extension MUITO bizarra serve para, basicamente, adicionar o atributo JSON aos objetos de NSError, e também adicionar o construtor init(json: JSON?, code: Int).

Mais uma vez, veja o requestBase, pretando atenção para a chamada de reject:

func requestBase(url: String, method: HTTPMethod, needToken: Bool, parameters: Parameters?) -> Promise<JSON> {
    var headers = ["Content-Type": "application/json"]

    if needToken {
        headers = ["Content-Type": "application/json", "Token": UserCurrent.sharedInstance.token!]
    }

    return Promise { fulfill, reject in
        Alamofire.request(
            self.urlBase + url,
            method: method,
            parameters: parameters,
            encoding: JSONEncoding.default,
            headers: headers
        ).responseJSON(completionHandler: {
            response in
            let statusCode = (response.response?.statusCode)!
            var json: JSON?

            if let resultValue = response.result.value {
                json = JSON(resultValue)
            }

            // ** preste antenção nesse if **
            if (statusCode != 200) && (statusCode != 201) {
                reject(NSError(json: json, code: statusCode))
                return
            }

            fulfill(json!)
        })
    }
}

Para a minha aplicação em especial, apenas os HTTP Status Code 200 e 201 são válidos. Se vier algum diferente desses, algum erro aconteceu. Então chamo o reject e passo o NSError contendo o json. Eu precisava fazer aquela extension porque a biblioteca PromiseKit obriga que o parâmetro de reject seja um Error, e eu precisava passar o json retornado.

Na classe Request, além do requestBase, também há enum de erros e dois métodos que o retornam:

enum RequestCommonErro: Error {
    case withoutAuthorization
    case failedProcessEntity
    case unknown
}

enum RequestCommonErroWithoutToken: Error {
    case failedProcessEntity
    case unknown
}

class Request {
...
    // Usa-se essa função em erroHandle para requests que retornam erros típicos,
    // tais como falha ao tentar processar a entidade e falta de autorização
    func handleCommonErros(error: Error) -> RequestCommonErro {
        let statusCode = (error as NSError).code

        if statusCode == 422 {
            return .failedProcessEntity
        } else if statusCode == 401 {
            return .withoutAuthorization
        } else {
            return .unknown
        }
    }

    // Similar à função acima, mas para requests que não precisam do token
    func handleCommonErrosWithoutToken(error: Error) -> RequestCommonErroWithoutToken {
        let statusCode = (error as NSError).code

        if statusCode == 422 {
            return .failedProcessEntity
        } else {
            return .unknown
        }
    }
 }

Fiz isso porque, em muitas vezes, os erros são sempre esses. Não preciso complicar o tratamento dos erros básicos.
Desse modo, eu posso fazer como foi feito no método index de RequestEvent: apenas chamar o método handleCommonErrosWithoutToken passando como parâmetro o error. Algo muito prático!

Porém, há algumas situações mais delicadas. Considere esse exemplo: pode ser que em determinado evento só o admininstrador possa cadastrar novos participantes. Como lidar com o erro de um usuário não-autorizado tentar adicionar um participante? Assim:

enum RequestEventAddUserError: Error {
    case userPermissionDenied
    case failedProcessEntity
    case unknow
}

class RequestEvent: Request {
    ...
    func addParticipantToEvent(eventId: Int, userId: Int) -> Promise<Void> {
        return Promise { fulfill, reject in
            firstly {
                self.requestBase(
                    url: "events/\(eventId)/add_user/\(userId)",
                    method: .post,
                    needToken: true,
                    parameters: nil
                )
            }.then { json -> Void in
                fulfill()
            }.catch { error in
                let statusCode = (error as NSError).code
                let json = (error as NSError).json!

                if statusCode == 401 && json.dictionary?["result"]?.stringValue == "Erro: Usuário não tem permissão para fazer isso nesse evento" {
                    reject(RequestEventAddUserError.userPermissionDenied)
                } else if statusCode == 422 {
                    reject(RequestEventAddUserError.failedProcessEntity)
                } else {
                    reject(RequestEventAddUserError.unknow)
                }
            }
        }
    }
}

Completamente trivial tratar os erros nas classes de request.
E como fica na ViewController?

firstly {
    RequestEvent.shared.addParticipantToEvent(eventId: event.id, userId: user.id)
}.then {
    ...
}.catch { error in
    switch error as! RequestEventAddUserError {
    case .userPermissionDenied:
        self.msgAlertOK(title: "Error", msg: "Você não tem permissão para adicionar participantes ao evento!")
    case .failedProcessEntity:
        self.msgAlertOK(title: "Error", msg: "Erro enquanto tentava adicionar o participante")
    case .unknow:
        self.msgAlertOK(title: "Error", msg: "Erro desconhecido")
    }
}

Igualmente trivial.

Finalizando

Após a leitura dessa seção, creio que tenha entendido como foi implementada toda a abordagem a respeito das requisições no servidor, desde o uso de promise, a organização das classes e o tratamento de erros.

Implementando permissões no Rails

Saindo um pouco do client side e indo para o server. Pela características do nosso app, precisávamos implementar permissões, fazendo com que algumas APIs sejam acessíveis somente a determinado nível de usuário.

Temos os seguintes níveis: usuário não logado e usuário logado. Eu também preciso restringir para somente quem criou o evento possa modificá-lo.
Então, como implementar isso?

Em outro projeto eu usei a gem CanCan, e como já tive boas experiências com ela, decidi usá-la novamente.
A forma mais simples que pensei foi: "a cada requisição verificará o token do usuário; se nenhum for fornecido ou se o token for inválido, será tratado como usuário não logado; se o token for válido, será tratado como usuário logado". E assim implementei:

# app/controllers/applicatio_controller.rb
class ApplicationController < ActionController::API
  include CanCan::ControllerAdditions
  before_action :check_permission

  ###
  # Fazer a verificação da permissão do usuário
  def current_user
    @token = Token.get_token_if_valid(request.headers['token'])

    if @token
      @current_user = @token.user

      :logged
    else
      :not_logged
    end
  end

  def check_permission
    permission = can?(action_name.to_sym, self.class)
    if permission
      true
    else
      warn "Erro de permissão: '#{current_user}' tentou acessar '#{action_name.to_sym}' de '#{self.class}'! Talvez falte adicionar a permissão no arquivo /app/models/ability.rb"
      render json: {result: 'Erro: Sem permissão para o acesso'}.to_json, status: :unauthorized
      false
    end
  end
end
# app/models/ability.rb
class Ability
  include CanCan::Ability

  def initialize(user)
    if user == :not_logged || user == :logged
      can(:show, UsersController)

      can(:index, EventsController)
      can(:show, EventsController)

      ...
    end

    if user == :not_logged
      can(:create, UsersController)
      can(:login, UsersController)

      ...
    end

    if user == :logged
      can(:logoff, UsersController)
      can(:index, UsersController)
      can(:update, UsersController)

      ...
    end
  end
end

Então, eu uso o par "nome do método que será chamado e a classe dele" para verificar se o usuário tem permissão ou não.
Desse modo, fica fácil escalonar e de crescer o projeto. Se um novo tipo de permissão sugir, por exemplo, "admin", basta mudar o valor retornado em current_user.
Além do mais, se houver falta de permissão, já forneço uma mensagem avisando do possível erro.
Essa mesma verificação da permissão me fornece a variável global @current_user com a referência do usuário que fez a requisição. Ok, é variável global, compensa por ela simplificar muito o código e também por ela ser mantida constante durante o acesso do usuário.

E como fazer as permissões a respeito dos eventos, para só o criador do evento poder modificá-lo? A princípio eu pensei em também tratar isso no CanCan, porém, depois vi que não valia a pena. Como isso só afetará o EventsController, fica melhor fazer as verificações e restrições lá mesmo.

# app/controller/events_controller.rb
class EventsController < ApplicationController
  before_action :check_permission_as_organizer, only: [:update]

  ...

  def check_permission_as_organizer
    event = Event.find(params[:id])

    unless event.user_is_organizer?(@current_user)
      render json: {result: 'Erro: Usuário não tem permissão para fazer isso nesse evento'}.to_json, status: :unauthorized
      false
    end
  end
end

Amazon

Agora, vem outro assunto que desejo explanar. A AWS S3 da Amazon.
Como seria viável jogar as fotos dos usuários no Heroku, eu precisava recorrer a um lugar onde eu poderia armazenar essas informações, então, por recomendação, decidi apostar na AWS S3.

Criar uma conta na AWS e um bucket (o "cesto" onde você jogará seus arquivos) no S3 foi a parte mais fácil... e a facilidade acabou aí. A documentação da AWS é horrível, em vários pontos ela não cobre de forma clara o assunto. Eles sumpões que você já tenha uma boa experiência quando vão explicar qualquer coisa, o que não é bem o meu caso.

Para usar a AWS S3 no aplicativo, pensei em duas alternativas:

A primeira alternativa seria mais ineficiente, porém, me parecia ser mais fácil de se implementar, pois eu já sabia como fazer upload do iOS para o Rails e havia mais documentação de Rails + AWS S3. Porém, após pesquisar e ler a documentação do Heroku, percebi que não deve-se fazer isso:

A more stable alternative is to upload the image to S3 directly from the client side, and when the image is fully uploaded save the reference URL in the database. This way it does not matter if our dyno is restarted while the image is uploaded, your dyno does not need to handle the extra load of image uploads, and you don’t have to wait for the file to upload twice, once to your dyno and once to S3.
The only downside is that all the logic must be performed on the client side. This article shows how to accomplish this.

Desse modo, restou apenas a segunda alternativa: fazer a comunicação direta entre o iOS e a AWS S3.

A Amazon até fornece um SDK para usar no iOS, porém, não explica muito bem como usá-lo. Além disso, demorou e sofri para encontrar uma boa documentação escrita por outras pessoas. Somente após muita procura finalmente encontrei uma pessoa de bom coração que explanou bem o assunto.
Levei praticamente um dia inteiro para finalmente conseguir fazer um upload e download de um arquivo do meu bucket.

O código até que funcionava, porém, se os requests ao Heroku eram feitos usando promises... Por qual razão não usar promises também para os requests ao S3? Assim como eu, várias pessoas devem ter tido bastante dificuldade para usar o SDK da Amazon, então, porque nao fazer um pod para simplificar o processo?
Desse modo, escrevi uma classe (também singleton :3) para abstrair e simplificar os acessos à AWS S3, ficando bem mais fácil o upload e download de arquivos, e usando promise para lidar com o assincronismo, e ainda por cima faz cache! Ela já encontra-se disponível no GitHub e no CocoaPods.

Swift e o websocket do Rails 5

Uma das novidades do Rails 5 são melhorias no websocket, que chamaram de ActionCable.

Até então eu nunca havia mexido com websocket. Porém, como a documentação do Rails é muito boa, foi trivial aprender a usar websocket.
Após implementar websocket no Rails, precisava fazer funcionar no Heroku. Como a documentação do Heroku é muito boa, e eles ensinaram muito bem as configurações necessárias para poder usar websocket em produção, foi bem trivial toda essa parte.

Porém, faltava que o cliente se comunicasse. Eu não havia encontrado algo pronto para Swift se comunidade com o ActionCable. Eu conseguia instanciar um websocket no Rails, mas como conectá-lo no iOS? Eu pensei "o Rails 5 ainda é relativamente novo, então não deve existir ainda algo pronto para integrar o ActionCable no iOS".
Então comecei a desenvolver uma solução para mim. Nesse ponto específico não havia uma documentação clara, de como implementar a conexão do ActionCable em algo que não fosse JavaScript. Então, praticamente tive que fazer engenharia reversa até finalmente conseguir... e isso levou um certo tempo.

Assim como foi o caso da AWS S3, eu pretendia fazer um pods para facilitar a conexão do iOS com o ActionCable. Desenvolvi um pacote simples especificamente para a minha aplicação e pretendia terminá-lo nas férias, para então divulgá-lo.
Porém, quando chegou as férias e fui pesquisar mais sobre "Swift", "Rails" e "ActionCable", descubro que já havia a tempos uma solução pronta. E muito boa, por sinal.
Basicamente faltou pesquisa por minha parte. Pesquisas que se eu tivesse feito direito teriam enconomizado um bom tempo no projeto.

Postman

Algo interessante que desejo explanar é o Postman. Caso você ainda não o conheça, o Postman é uma plataforma gráfica para criar requisições de API, documentação e, incusive, testes.
Eu já o conhecia e usáva-o bastante a alguns anos. Porém, sabe quando só após anos usando uma ferramenta você conhece uma feature muito bacana e útil? Pois é. Aconteceu isso comigo. E com o Postman.

Eu sempre estava usando o Postman para testar as API enquanto desenvolvia no Rails. Com o tempo, passou a usuá-lo também para popular o banco, pois após alguns testes era necessário resetar todo o banco.
E por qual razão não usar os seeds do próprio Rails? Pois seria incompleto. Ele não executa os controllers nem verifica as routes. Ele injeta dados diretamente no banco, algo de péssima manutenção e que não reproduz o que de fato acontece na aplicação.
Então, eu ficava re-executando as minhas requests já salvas no histórico do Postman.

Mas aí a aplicação foi crescendo, e era um saco ter que sempre executar manualmente uns 10 requests! Então... o que será que aquela aba "collections" faz?

Após ver mais um pouco ela e ir futricando, descobri essa excelente feature do Postman! E tudo bem intuitivo e fácil de se usar!!

Na aba collections eu posso criar uma lista de requests para executar em um só click. Inclusive da para eu pegar o token de login retornado e salvá-lo numa variável de ambiente, para então usr em outros requests.
Eu também posso usar variáveis de ambiente para definir a URL. Isso facilita caso eu queria testar localhost ou no Heroku.
Outra feature excelente é o de testes. Em poucas linhas eu conseguia escrever testes para verificar se o valor retornado pela API está correto ou não.

Desse modo, em um único click eu conseguia popular o meu banco e também executar os testes de integração, ambos fornecidos pelo Postman.

Conclusão

Ao final dessa postagem, espero que você tenha entendido toda a arquitetura adotada nessa aplicação. Certamente muita coisa poderia ter sido abordada de uma outra forma, talvez até melhor, mas essas foram as soluções encontradas e adotadas por nós, considerando o tempo e requisitos do projeto.
Espero que as decisões tomada nesse projeto sirva de inspiração e guia para os seus próximos projetos.

A propósito, eu estou escrevendo isso nos últimos dias do ano de 2016. Isso é um claro sinal que você precisa compartilhar a postagem. Eu agradeço õ/
Sinta-se livre para opiniar e sugerir melhorias nas abordagens apresentadas. Eu ficaria bem feliz =]