Tutorial VIPER en Swift

8
20566

Tutorial VIPER en Swift

0. Índice de contenidos.

1. Introducción

Bien hoy vamos a ver como implementer un nuevo patrón de diseño que está pegando fuerte en la comunidad. Se trata de un patrón estructural para facilitar el bajo acoplamiento entre nuestras clases y su correcto testeo, su nombre es VIPER y voy a hacer una breve descripción del mismo.
Se trata de separar nuestras clases en algo más complejo que el típico MVC donde acababa siendo el “C”(controlador) el que se acaba convirtiendo en una clase gigante a medida que crecía la funcionalidad. VIPER son las siglas de la separación conceptual que vamos a hacer con nuestras clases.

Dame un “V”!!!!
La “V” significa View, es un básicamente un protocolo que van a implementar nuestros ViewControllers. Las vistas en VIPER son pasivas, esperan a que les lleguen los datos por parte del Presenter e informan al mismo de los eventos que genera el usuario.

Dame un “I”!!!!
La “I” es de Interactor, el interactor es la clase que va a llevar la lógica de negocio, el que va a tratar con las clases “Entity”, manejar su lógica y es el que va a pasarle al “Presenter ” la información que necesita para la “Vista.”

Dame una “P”!!!!
El Presenter es el intermediario entre la Vista y el Interactor. Recuerda que la vista es tonta y que el interactor es el que carga con la lógica de negocio, así que el Presenter va a recibir eventos de la vista (cuando el usuario pulse un botón por ejemplo) y se va a limitar a pasarlo al Interactor que es el que va hacer la acción oportuna. Cuando dicha acción se complete informará al Presenter y le pasará la información que necesita. Ojo, nunca le vamos a pasar una Entity al Presenter, hay que pasarle todo mascado: estructuras de datos simples. Tener en cuenta que hay que diferenciar bien las responsabilidades para facilitar el testeo. También es posible que el evento que genera el usuario simplemente sea de navegación, en cuyo caso en vez de solicitar información al Interactor llamaremos a nuestra clase Routing que como vamos a ver unas lineas más adelante es la que se encarga de la lógica de navegación.

Dame una “E”!!!
Las entidades son los objetos de negocio con los que tratará el Interactor. Hay que tener en cuenta que si las entidades nos llegan a través de un web service necesitaremos añadir una capa más tipo DAO, que nos haga las solicitudes y le pase las entidades construidas al interactor. Recuerda que el interactor solo lleva la lógica de negocio…SOLO.

Dame una “R”!!!
R de Routing. Va ser la clase encargada de la navegación de nuestra app, también es la responsable de instanciar nuestras Vistas, Interactors y Presenters. Son los Presenters los que notifican al Routing que se ha de navegar a X pantalla.

2. Ejemplo

Bien visto estos conceptos vamos a iniciar un nuevo proyecto en Xcode y como no tenemos miedo a nada vamos a hacerlo en Swift.

Vamos a crear nuestros ficheros necesarios para llevar a cabo una aplicación sencilla tipo ABM, presionamos Command-N para crear nuestro fichero swift al que llamaremos Interactor.

Otro para el Presenter, el Routing, la Entity y la View. Vamos a dejarlos vacíos de momento y nos vamos a nuestro StoryBoard.

Como veis los nuevos Storyboards han cambiado sustancialmente, ahora se centran en el autolayout, para poder tener un único Storyboard compatible con multiples dispositivos.
Vamos a arrastrar un objeto UITableViewController a nuestra vista y vamos a borrar el controlador que nos venía por defecto. También vamos a borrar nuestro fichero ViewController.swift y a crear una clase que herede de UITableViewController.

Después vamos al StoryBoard, seleccionamos nuestro TableViewController y en el inspector de identidad le decimos que somos una custom class del controlador que acabamos de crear (TableViewController). Es importante desmarcar la casilla de “is initial view controller” ya va ser nuestro “Routing” el que se encarga de la lógica de presentación de pantallas.

Bien ahora en nuestra clase TableViewController en nuestra función viewDidLoad vamos a añadir la siguiente línea:

	self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .Add, target: self, action: Selector("addNewObject"))

Ahora tendremos un botón en nuestra barra de navegación con la apariencia de un +.
Vamos a añadir en el controlador la función que ejecuta ese botón:

func addNewObject() {
        
}

Bien, ahora vamos a nuestro fichero Entity y vamos a escribir los siguiente:

class Persona {
    var nombre:String?
    var apellido:String?
    init() {
        
    }
}

Esta va ser la clase que representa nuestra entidad, ahora vamos a nuestro fichero View. Recordamos que la “View” en VIPER es básicamente un protocolo que va a adoptar nuestro controlador de vista para poder responder a los eventos generados por el usuario y vamos a escribir una función que recibe los objetos que nos pasa el Presenter:

protocol viewProtocol {
   func setListWithObjects(#objects:[String])
}

Ahora vamos al fichero Presenter y escribimos lo siguiente:

import UIKit

class Presenter {
    var view:TableViewController?
    var interactor:Interactor?
    var routing:Routing?

    init() {
        
    }
	
	func addNewObject() {
        
    }
}

El Presenter contiene una referencia a la vista para que esta pueda cargar los datos que le pasemos, otra a nuestro Interactor que es el que vamos a pedirle la información solicitada por los eventos del usuario y otra a nuestro Routing para cuando tengamos que navegar a otra pantalla.

Ahora vamos a nuestro TableViewController y creamos una variable que referencia a nuestro Presenter para pasarle los eventos del usuario:

    var presenter:Presenter?

Y en el método que responde a nuestro botón más vamos a llamar al Presenter y a su función llamada del mismo modo (addNewObject), que es la que en este caso va a solicitar a nuestro Routing que navegue a otra pantalla. También vamos a crear una variable de tipo Array para alimentar a nuestra tabla:

var objects:[String]?

func addNewObject() {
    presenter!.addNewObject()
}

También vamos a indicar en los métodos delegados de la tabla su comportamiento y vamos a crear la función que nos solicita la tabla para pintar las celdas:

override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    return 1
}

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return objects.count
}
    
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "cell")
    cell.textLabel.text = objects[indexPath.row];
    return cell
}

Hemos indicado que queremos una sola sección y que el numero de filas es igual al numero de elementos de nuestro array, también hemos dicho que el label de la celda escriba directamente el objeto de nuestro array que alimenta la tabla que, como habíamos definido, es un array de Strings.

Recordáis el protocolo que hemos definido antes llamado viewProtocol? Pues vamos a hacer que nuestro controller lo implemente. Únicamente hay que añadirlo después del nombre de la clase de esta forma:

class TableViewController: UITableViewController,viewProtocol {
	…
}

E implementamos su función indicando que los objetos que recibamos van a ser nuestro array:

func setListWithObjects(#objects: [String]) {

    self.objects = objects
    self.tableView.reloadData()
}

Vamos a nuestro fichero Interactor. Necesitamos una referencia a nuestro Presenter ya que es el que va a recibir la información para pasársela a la vista, escribimos lo siguiente:

class Interactor {

    var presenter:Presenter?
    
    init() {
        
    }
}

Vamos a crear otro fichero para definir otros dos protocolos más. Al fichero lo llamaremos InputOutput y vamos a definir en él un protocolo InteractorProtocolInput y un protocolo InteractorProtocolOutput. InteractorProtocolInput lo va adoptar el mismo Interactor y va a definir una función que es la que se llama desde el Presenter cuando quiera pasarle información introducida por el usuario. InteractorProtocolOutput lo va a adoptar nuestro Presenter, esta función se llama desde el Interactor cuando tengamos que actualizar la información que recibe el Presenter.

protocol InteractorProtocolInput {

    func addNewPersonWithData(#nombre:String, apellido:String)
}

protocol InteractorProtocolOutput {

    func updateObjects(#objects:[String])
}

Y ahora vamos al fichero Routing y escribimos lo siguiente:

import UIKit

class Routing {
    
    let vc:TableViewController = TableViewController()
    let presenter = Presenter()
    let interactor = Interactor()
    var navigationController: UINavigationController?
    
    init() {
        vc.presenter = presenter
        presenter.view = vc
        presenter.interactor = interactor
        presenter.routing = self
        interactor.presenter = presenter
        navigationController = UINavigationController(rootViewController: vc)
    }
}

Aquí lo que hemos hecho es inicializar nuestro controlador, nuestro Interactor y nuestro Presenter. También hemos dado valor a las variables de cada uno y hemos cargado un Navigation Controller con nuestro TableViewController.
Ahora si vamos a nuestro AppDelegate vamos a inicializar nuestra clase Routing para que empiece toda la magia:

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    // Override point for customization after application launch.
        
    let routing = Routing()
        
    self.window = UIWindow()
    var screen:UIScreen = UIScreen.mainScreen()
        
    self.window!.frame = screen.bounds
    self.window!.rootViewController = routing.navigationController
    self.window!.makeKeyAndVisible()
        
    return true
}

Si compilamos y ejecutamos deberíamos ver una tabla vacía con el botón de añadir en la barra de navegación, pero sin ningún tipo de contenido ni comportamiento de momento.

Lo siguiente que vamos hacer es crear una pantalla para añadir datos. En nuestro caso nuestra entidad era de tipo Persona con dos variables: nombre y apellido; así que vamos a arrastrar un ViewController a nuestro Storyboard y vamos a dejarlo de la siguiente forma:

Ahora vamos a crear una subclase UIViewController y vamos a conectar nuestros TextFields como outlets y los botones como acciones:

A continuación le damos su identidad:

Bien vamos a ver como quedaría nuestro controlador para añadir datos:

class ViewController: UIViewController,UITextFieldDelegate {

    @IBOutlet weak var nombre: UITextField!
    @IBOutlet weak var apellido: UITextField!
    var presenter:Presenter?
    
    @IBAction func add(sender: AnyObject) {

        presenter?.addNewObjectWithData(name: self.nombre.text, surname: self.apellido.text)

        self.presentingViewController?.dismissViewControllerAnimated(true, completion:nil)

    }
    
    @IBAction func cancel(sender: AnyObject) {
        
        self.presentingViewController?.dismissViewControllerAnimated(true, completion: nil)
        
    }
}

Como podeis ver, la función añadir le pasa la información introducida por el usuario al Presenter y cierra la pantalla modal.

Hemos añadido un nuevo método para introducción de datos en el Presenter que a su vez le pasa los datos al Interactor, queda de esta forma:

func addNewObjectWithData(name n:String, surname s:String) {
        
    interactor!.addNewPersonWithData(name: n, surname: s)
}

Y otro en el interactor:

func addNewPersonWithData(#name:String, surname:String) {
        
    if (countElements(name) > 0 && countElements(surname) > 0) {
            
    }
}

Como veis en el Interactor ya tenemos un nombre más descriptivo de la función por que aquí es donde se conoce la lógica de negocio. En la función también comprobamos que las cadenas que nos llegan sean de una longitud mayor que 0.

Ahora vamos a crear un nuevo fichero Swift que nos va a servir como una DataStore y nos va a quedar así:

import Foundation

class DataBase {
    
    var Personas:[Persona]?
    init() {
     
    }
}

Ahora volvemos al Interactor, completamos la función de añadir Personas y creamos una instancia de DataBase.

let dataBase:DataBase?
    
init() {
    dataBase = DataBase()
}
    
func addNewPersonWithData(#nombre:String, apellido:String) {
        
    if (countElements(nombre) > 0 && countElements(apellido) > 0) {
            
        let persona = Persona()
        persona.nombre = nombre
        persona.apellido = apellido

        if let personas = dataBase?.personas?{
            dataBase?.personas?.append(persona)
        }else{
            dataBase?.personas = [Persona]()
            dataBase?.personas?.append(persona)
        }     
    }
}

Lo que hemos hecho es crear una instancia de Persona en el método init de nuestro Interactor para disponer de una contante de tipo DataBase. En la función para añadir los datos creamos una instancia de persona y le asignamos los valores que ha introducido el usuario en los campos de texto. Finalmente en la variable de tipo array de nuestra DataBase añadimos este objeto Persona.

Ahora vamos a crear una función en nuestro Interactor que llame actualize la información a nuestro Presenter. Recordar que al Presenter únicamente hay que pasarle estructuras de datos simples por que no puede conocer los objetos de negocio, así que en dicha función transformamos nuestros objetos de negocio en Strings de la siguiente forma:

func updateList() {
        
    var arrayPersonas = [String]()
    for persona in dataBase!.personas! {
            
        arrayPersonas.append(persona.nombre! + " " + persona.apellido!)
    }
        
    presenter!.updateObjects(objects: arrayPersonas)
}

Y ahora llamamos a esta función al final de la función addNewPersonWithData:

self.updateList()

Como nuestro Presenter tenía que adoptar el protocolo InteractorProtocolOutput tiene que implementar su función:

func updateObjects(#objects: [String]) {
        
    view!.setListWithObjects(objects: objects)
}

Cuando el Interactor llama a esta función el Presenter se limita a actualizar la vista.
Para teminar con el Presenter vamos a añadir la función que se llama desde la vista para mostrar la pantalla modal de introducción de datos:

func addNewObject() {

    routing!.openAddView()
}

Y ahora vamos al routing e implementamos la función que nos abre dicha pantalla:

func openAddView() {
        
    let storyBoard:UIStoryboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
    let addVC:ViewController = storyBoard.instantiateViewControllerWithIdentifier("ViewController") as ViewController
    addVC.presenter = self.presenter
        
    vc.presentViewController(addVC, animated: true, completion: nil)
}

Hemos instanciado nuestro Storyboard y hemos solicitado presentar moralmente nuestro controlador “ViewController”.
Si compilamos y ejecutamos ya podemos añadir personas a nuestra tabla.

En resumen, con esta arquitectura podemos aislar de forma optima nuestras clases para que tengan una única responsabilidad. Para un aplicación de ejemplo como la que hemos visto carece de sentido hacer este patrón, pero cuando la aplicación tiene mucha funcionalidad y nuestro controladores de vista se ven sobrecargados esta es una solución muy práctica.

Podéis descargar el proyecto de github en este enlace.

8 COMENTARIOS

  1. Lo primero felicidades por la entrada, me ha hecho pensar mucho.

    Estoy pensando en probar este patron y me ha ayudado mucho tu articulo pero tengo un par de dudas.

    Dentro del patron en que casilla estas metiendo los ViewControllers y Database ? (Interactor o presenter)

    Otra pregunta es sobre el Routing, teniendo en cuenta que los Storyboard hacen principalmente este trabajo, no es mejor dejarles esa responsabilidad a ellos?

    Un saludo

  2. Hola Alvaro, el ViewController se limita a implementer el protocolo «View», y el Database se comunica únicamente con el «interactor», en relación al routing, es el que contiene la lógica de navegación y yo personalmente inyecto las dependencias desde ahí, cosa que el storyboard no puede hacer, espero haberte ayudado.
    Saludos.

  3. Hola Ignacio,

    He encontrado tu ejemplo de viper por internet y lo estoy estudiando (muy bueno por cierto). Tengo una duda con esta parte del código:

    if (countElements(nombre) > 0 && countElements(apellido) > 0) {
    }
    En este if que está en el interactor de donde sale countElements??
    Soy nuevo en esto cualquier duda será bien recibida.
    Muchas gracias!!

  4. Buenas Ignacio, estoy empezando a adoptar este patrón, todavía hay cosas que no me gustan, pero ayuda mucho a la hora de crear tests para unit testing. Quería hacerte un comentario, por lo que veo estas creando muchos retain cycles entre dependencias, por ejemplo tu vista tiene una propiedad que es el presenter y el presenter tiene una propiedad que es la vista, por lo que los dos tienen una referencia strong entre ellos y no permite liberar esos objetos cuando se deberían liberar. Está claro que alguna de las dos referencias debería ser weak, pero mi duda está en cual de ellas, porque las veo como en una estructura jerárquica plana, y no queda claro cual debería ser el padre y cual el hijo.

    Buen post tho!

  5. Hola Pau, en efecto se trata de un error, en el tutorial, el correcto es ella vista tiene un strong al presenter y el presenter tiene un strong al interactor, esto deja a las referencias restantes en weak, es decir la referencia al presenter desde el interactor y la de la vista desde el presenter, luego dependiendo de como tengas montado el routing puede contener una referencia fuerte o debil al interactor o al presenter. saludos.

  6. Buenas Ignacio.. No entendi esto: «Hola Pau, en efecto se trata de un error, en el tutorial, el correcto es ella vista tiene un strong al presenter y el presenter tiene un strong al interactor, esto deja a las referencias restantes en weak, es decir la referencia al presenter desde el interactor y la de la vista desde el presenter, luego dependiendo de como tengas montado el routing puede contener una referencia fuerte o debil al interactor o al presenter. saludos.» Puedes explicarlo mejor?

    • Hola Eduardo, se trata de un tema de evitar «retain cycles», nunca debes poner dos piezas de tu software con dependencias fuertes la una a la otra, me explico tienes tu clase A y tu clase B, si A tienes una referencia fuerte a B y B tiene referencia fuerte hacia A, ARC nunca podrá eliminarlas, han de ser una de las referencias de tipo débil (weak).
      Saludos.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad