DEV Community

Fernando Martín Ortiz
Fernando Martín Ortiz

Posted on • Updated on

Sobrevivir en Swift: Parte 2

Esta es la parte 2 de un post en dos partes.
La parte 1 aquí

Programación orientada a objetos

En un programa tenemos dos conceptos perfectamente definidos: Datos y Comportamiento.
Una tupla con cinco campos que define una dirección es Dato. Una función que toma esa tupla y devuelve un String con su descripción, es Comportamiento.

Todo lo que hemos hecho hasta el momento en la primera parte de la introducción a Swift ha sido definir los Datos por un lado y el Comportamiento por el otro, lo cual nos ha funcionado bien. Sin embargo, la forma más frecuente de organizar el código en los lenguajes modernos, tales como Swift es integrando Datos y Comportamiento en estructuras como las que veremos hoy, cuando estudiemos Programación orientada a objetos (POO).

Esto nos dará algunas ventajas. Principalmente, la capacidad de abstraer. La clase tendrá comportamiento que se podrá utilizar desde afuera de ella. Es decir, sabremos qué hace la clase. Sin embargo, el cómo se lo reserva la clase para sí. Esto se llama encapsulamiento.

Class

Una clase es una de las estructuras que integran datos y comportamiento en una unidad. Los datos en una clase se llaman Atributos. El comportamiento en una clase se modela por medio de funciones llamadas Métodos.
En Swift, una clase se define con la palabra clave class seguida por su nombre. Los atributos serán variables, y los métodos, funciones.
Veamos un ejemplo de una jerarquía de clases, y luego explicaremos en detalle qué sucede:

class Animal {
    var nombre: String
    var paseos: [String]

    var tipo: String { return "" }

    init(nombre: String) {
        self.nombre = nombre
        self.paseos = []
    }

    func emitirSonido() {}

    func pasear(a lugar: String) {
        self.paseos.append(lugar)
    }
}

class Perro: Animal {
    override var tipo: String { return "Perro" }
    override func emitirSonido() {
        print("\(nombre): Guau!")
    }
}

class Gato: Animal {
    override var tipo: String { return "Gato" }
    override func emitirSonido() {
        print("\(nombre): Miau!")
    }
}

class Persona {
    private var nombre: String
    private var mascotas: [Animal]

    init(nombre: String, mascotas: [Animal]) {
        self.nombre = nombre
        self.mascotas = mascotas
    }

    func llegarACasa() {
        print("\(nombre): llega a casa")
        for mascota in mascotas {
            mascota.emitirSonido()
        }
    }
}

let romina = Persona(
    nombre: "Romina",
    mascotas: [
        Gato(nombre: "Fausto"),
        Perro(nombre: "Lupi")
    ]
)

romina.llegarACasa()
Enter fullscreen mode Exit fullscreen mode

Imprimirá:

Romina: llega a casa
Fausto: Miau!
Lupi: Guau!
Enter fullscreen mode Exit fullscreen mode

Herencia

Bien, ¿qué sucedió aquí? Primero, definimos una clase base, llamada Animal. Se denomina clase base porque es la clase que define la base de la jerarquía de clases. Animal define la funcionalidad común a todos los animales.

¿Qué puede hacer un animal? (comportamiento). En este caso puede salir a pasear y puede emitir un sonido. ¿Qué sonido? No lo sabemos. El sonido que hará el animal será definido en las subclases. Esto se denomina herencia. Un animal puede ser un Gato o un Perro en este ejemplo. Entonces un Gato puede decir "Miau" y definir su tipo como "Gato", pero Hereda todo el comportamiento definido en su clase base.

¿Qué podemos saber de un animal? (datos). En este caso, su nombre y los paseos que ha realizado.

¿Y tipo? Es una variable computada, o getter. Un getter es una función que devuelve un valor. Desde fuera de la clase se ve y se usa como una variable a la cual no se le puede cambiar el valor. Forma parte del comportamiento de la clase.

Encapsulamiento

Una clase solo expone la parte de su comportamiento y sus datos que debemos utilizar desde afuera. Lo que es interno de la clase se define con la palabra clave private. Todo lo que sea private solo puede ser llamado desde dentro de la clase. Lo que no se marca como private es accesible desde cualquier parte de la aplicación y se denomina interfaz pública de la clase.

Init

El init es un método especial de la clase que se encarga de definir cómo se creará un objeto de esa clase. En este ejemplo, la clase Persona tiene un init definido así:

 init(nombre: String, mascotas: [Animal]) {
     self.nombre = nombre
     self.mascotas = mascotas
 }
Enter fullscreen mode Exit fullscreen mode

self en este caso se refiere al propio objeto en el que estamos operando.

El init no necesita ser escrito para utilizarlo. Vean el ejemplo:

 let romina = Persona(
     nombre: "Romina",
     mascotas: [
         Gato(nombre: "Fausto"),
         Perro(nombre: "Lupi")
     ]
 )
Enter fullscreen mode Exit fullscreen mode

Polimorfismo

La funcionalidad descrita en la clase base puede ser redefinida con la palabra clave override en las clases hijas. Por ejemplo, emitirSonido no tiene funcionalidad real en la clase base o clase padre. Pero en las clases hijas sí. Las clases hijas "overridean" el comportamiento de la clase padre. Por ejemplo:

 override func emitirSonido() {
     print("\(nombre): Guau!")
 }
Enter fullscreen mode Exit fullscreen mode

Lo que es interesante es que nos podemos referir tanto a perros como a gatos en el Array de mascotas. Cuando llamamos a mascota.emitirSonido() no sabemos si la mascota es un perro o un gato, eso se define en tiempo de ejecución, donde cada objeto ejecuta su funcionalidad según su clase específica.

Protocol

Una class responde a la pregunta "¿Qué es?".

  • ¿Qué es? Un Gato

  • ¿Qué es? Un Perro

  • ¿Qué es? Una Persona

Un protocol responde a la pregunta "¿Qué puede hacer?". En su forma más simple, un protocolo (también llamado interface en otros lenguajes de programación como Java), es un conjunto de firmas de métodos bajo un nombre. Cualquier clase puede implementar un protocolo. Si una clase implementa un protocolo, entonces se compromete a implementar todos los métodos definidos en él. De lo contrario, el código no compilará.

Veamos un ejemplo

protocol EmiteSonido {
    func emitirSonido()
}

class Animal {}

class Perro: Animal, EmiteSonido {
    func emitirSonido() {
        print("Guau!")
    }
}

class Gato: Animal, EmiteSonido {
    func emitirSonido() {
        print("Miau!")
    }
}

class Timbre: EmiteSonido {
    func emitirSonido() {
        print("Ring!")
    }
}
Enter fullscreen mode Exit fullscreen mode

Noten aquí que:

  1. Una clase puede heredar de una sola clase. Sin embargo, puede implementar cualquier cantidad de protocolos como necesitemos. Si una clase hereda y también implementa protocolos, lo que vaya a la derecha del : debe ser primero la clase padre, y luego los protocolos a implementar.
  2. Un Perro, un Gato y un Timbre ahora son del mismo tipo. Todos son del tipo EmiteSonido. Por lo tanto, podemos usarlos en un Array, por ejemplo.
class EfectosDeSonido {
    let sonidos: [EmiteSonido]

    init(sonidos: [EmiteSonido]) {
        self.sonidos = sonidos
    }

    func reproducir() {
        for sonido in sonidos {
            sonido.emitirSonido()
        }
    }
}

let sonidosLlegadaACasa = EfectosDeSonido(sonidos: [Timbre(), Gato(), Perro()])
sonidosLlegadaACasa.reproducir()
// Ring!
// Miau!
// Guau!
Enter fullscreen mode Exit fullscreen mode

Al implementar EmiteSonido, todos los objetos, sin importar su clase, pueden servir al propósito de emitir un sonido. En este ejemplo, esto nos sirve para implementar efectos de sonido. En la práctica, este mismo principio se usa muchísimo. Sobre todo cuando lo que necesitamos de otro objeto es lo que puede hacer, sin importar qué sea en verdad.

Struct

Una struct o es estructura es muy similar a una clase. Veamos algunas características:

Una clase:

  • Tiene atributos y métodos
  • Los atributos y métodos que contiene pueden ser privados
  • Puede implementar protocolos.
  • Debe implementar un método init para inicializar sus atributos al instanciarse.
  • Puede heredar de otras clases.

Una estructura:

  • Tiene atributos y métodos
  • Los atributos y métodos que contiene pueden ser privados
  • Puede implementar protocolos.
  • NO ES NECESARIO que implemente un método init para inicializar sus atributos por lo general.
  • NO PUEDE HEREDAR de ninguna clase o estructura.

Una clase tampoco puede heredar de una estructura.

protocol Describible {
    func obtenerDescripcion() -> String
}

struct Direccion: Describible {
    let calle: String
    let numero: String
    let departamento: String
    let piso: String
    let ciudad: Ciudad

    func obtenerDescripcion() -> String {
        return "\(calle) \(numero) - Dto. \(piso)-\(departamento) - \(ciudad.obtenerDescripcion())"
    }
}

struct Ciudad: Describible {
    let nombre: String
    let provincia: Provincia

    func obtenerDescripcion() -> String {
        return "\(nombre), \(provincia.obtenerDescripcion())"
    }
}

struct Provincia: Describible {
    let nombre: String
    let pais: String

    func obtenerDescripcion() -> String {
        return "\(nombre), \(pais)"
    }
}

let direccionOficina = Direccion(
    calle: "Avenida Rivadavia",
    numero: "18451",
    departamento: "1",
    piso: "PB",
    ciudad: Ciudad(
        nombre: "Morón",
        provincia: Provincia(
            nombre: "Buenos Aires",
            pais: "Argentina"
        )
    )
)

print(direccionOficina.obtenerDescripcion()) // Avenida Rivadavia 18451 - Dto. PB-1 - Morón, Buenos Aires, Argentina
Enter fullscreen mode Exit fullscreen mode

En la práctica, las struct se utilizan para diseñar trozos pequeños de información. Pueden servirnos para describir una dirección, los datos de una tarjeta de crédito, etc.

Enum

Una struct o una class nos permiten diseñar a partir del "y". Por ejemplo, una struct Persona tiene nombre y edad y email como sus miembros.
Una enum, en cambio, nos permite diseñar a partir del "o". Por ejemplo, una enum Provincia tiene buenosAires o entreRios o catamarca como sus miembros.

Al contrario de las enum en lenguajes como C, las enum de Swift pueden llegar a resultar MUY complejas. De todos modos, aquí usaremos las funcionalidades básicas.

Veamos un ejemplo simple:

class Usuario {
    var nombre: String
    var tipoCredenciales: TipoCredenciales

    init(
        nombre: String,
        tipoCredenciales: TipoCredenciales
    ) {
        self.nombre = nombre
        self.tipoCredenciales = tipoCredenciales
    }
}

// La enum TipoCredenciales define con qué medio el usuario creó su cuenta.
enum TipoCredenciales {
    case email
    case facebook
    case apple
    case google
}
Enter fullscreen mode Exit fullscreen mode

Con TipoCredenciales podemos decir por ejemplo que un usuario se registró por email, por facebook, por apple o por google. Es imposible que un usuario se haya registrado por más de una vía (al menos en el contexto de este dominio ficticio), y el mismo código lo hace imposible.

Switch en enum

Bien, intentemos ahora utilizar TipoCredenciales para nuestra lógica. La forma más típica de utilizar una enum en una lógica, por ejemplo dentro de una función, es mediante la cláusula switch.

func esUsuarioDeRedesSociales(_ usuario: Usuario) -> Bool {
    switch usuario.tipoCredenciales {
    case .email:
        return false
    case .facebook, .apple, .google:
        return true
    }
}
Enter fullscreen mode Exit fullscreen mode

Nótese que también podríamos haber escrito esta función como un método dentro del usuario. También, que aquí combinamos varias opciones dentro de un mismo case, pero que bien podríamos haber escrito un case por cada uno de los métodos (facebook, apple, google).

Métodos y atributos en enum

Una enum puede tener métodos y atributos, tal como las clases. El patrón más común para ello es incluir un switch self dentro del mismo. Veamos un ejemplo:

enum Pais {
    case argentina, alemania, inglaterra, mexico, camerun

    // Crearemos una propiedad computada. Esto es un `getter`.
    // Noten que esto es lo mismo que decir
    //
    // func esEuropeo() -> Bool { ... }
    //
    // Salvo que al llamarse no usaremos paréntesis, tal y como si se tratase de una
    // propiedad común y corriente del objeto.
    var esEuropeo: Bool {
        // switch self es MUY común en estos casos.
        switch self {
        // Si estamos llamando esta propiedad computada desde los cases 'alemania' y 'inglaterra'
        // entonces devolveremos true
        case .alemania, .inglaterra:
            return true
        // En cambio, si lo hacemos desde cualquier otro case de la enum, entonces
        // devolveremos false.
        default:
            return false
        }
    }

    // Similar al caso anterior pero con un String.
    var nombre: String {
        switch self {
        case .argentina:
            return "Argentina"
        case .alemania:
            return "Alemania"
        case .inglaterra:
            return "Inglaterra"
        case .mexico:
            return "México"
        case .camerun:
            return "Camerún"
        }
    }
}

func describir(pais: Pais) {
    if pais.esEuropeo {
        print("\(pais.nombre) es europeo")
    } else {
        print("\(pais.nombre) NO es europeo")
    }
}

describir(pais: Pais.alemania) // Alemania es europeo
Enter fullscreen mode Exit fullscreen mode

Otra curiosidad con respecto a las enum es que cuando requerimos el uso de una, como en este caso en la función describir, no es necesario especificar el tipo. Por ejemplo, Pais.alemania, podría haber sido tranquilamente solo .alemania.

describir(pais: .argentina) // Es igual que describir(pais: Pais.argentina), y se ve más natural.
Enter fullscreen mode Exit fullscreen mode

rawValue

Existe una forma alternativa de definir el nombre del país, como vimos en el ejemplo anterior, y es por medio de un rawValue. Cada enum puede tener un único rawValue para cada uno de sus case, y todos deben ser del mismo tipo.

El rawValue no nos permite únicamente obtener un valor asociado a cada uno de los case, sino también construir un case de la enum a partir de ese valor asociado.

Veamos un ejemplo:

enum Provincia: String {
    case cordoba = "Córdoba"
    case buenosAires = "Buenos Aires"
    case santaFe = "Santa Fe"
}

let provincia1 = Provincia.cordoba
print("La provincia 1 es \(provincia1.rawValue)")
// Usamos el rawValue para obtener el valor de la provincia
// esto devolverá "La provincia 1 es Córdoba"

let provincia2 = Provincia(rawValue: "Buenos Aires")
// Aquí estamos haciendo el proceso inverso.
// En vez de obtener el rawValue de una Provincia,
// creamos la provincia a partir de un rawValue.
//
// Como podemos confundirnos, no hay garantía de que exista
// una provincia con ese nombre, y el tipo de provincia2
// es `Provincia?`, una Provincia opcional.

if let provincia = provincia2 {
    print("Pudimos obtener la provincia2 y es \(provincia.rawValue)")
} else {
    print("No pudimos obtener la provincia2, y es nil")
}
Enter fullscreen mode Exit fullscreen mode

Valores asociados

Este es el último caso que veremos aquí sobre las enum. Lo quiero remarcar porque es algo muy utilizado en Swift, aunque no entra dentro del scope del curso. Quiero decir, no es necesario saberlo para poder desarrollar la aplicación que haremos aquí. Sin embargo, saber esto puede llegar a resultar muy útil.

Cada caso de una enum puede llegar a tener valores asociados que difieren unos de otros. Por ejemplo, imaginemos un paquete de galletitas surtidas. Las galletitas podrían estar representadas por una enum. Puedo estar confundiéndome entre galletitas, fanboys/fangirls de galletitas surtidas por favor tener paciencia.

enum Galletita {
    // Cada uno de los case puede tener cualquier cantidad de valores asociados. Cada uno puede opcionalmente
    // tener un nombre asociado, y puede ser del tipo que se desee.
    case sonrisas(saborDeMasa: SaborDeMasa)
    case rellena(relleno: Relleno, saborDeMasa: SaborDeMasa)
    case bocaDeDama

    var descripcion: String {
        switch self {
        // Al momento de realizar un switch sobre la enum, podemos
        // obtener los valores asociados por medio de `let`.
        case .sonrisas(let saborDeMasa):
            return "Sonrisa de \(saborDeMasa.descripcion)"
        case .rellena(let relleno, let saborDeMasa):
            return "Galletita rellena de \(relleno.descripcion), sabor \(saborDeMasa.descripcion)"
        case .bocaDeDama:
            return "Boca de dama"
        }
    }
}

enum Relleno {
    case vainilla, frambuesa

    var descripcion: String {
        switch self {
        case .vainilla: return "vainilla"
        case .frambuesa: return "frambuesa"
        }
    }
}

enum SaborDeMasa {
    case vainilla, chocolate

    var descripcion: String {
        switch self {
        case .vainilla: return "vainilla"
        case .chocolate: return "chocolate"
        }
    }
}

struct PaqueteDeGalletitas {
    let galletitas: [Galletita]

    func describir() {
        print("-")
        print("Paquete:")
        for galletita in galletitas {
            print(galletita.descripcion)
        }
    }
}

let surtido = PaqueteDeGalletitas(galletitas: [
    .bocaDeDama,
    .rellena(relleno: .frambuesa, saborDeMasa: .chocolate),
    .rellena(relleno: .frambuesa, saborDeMasa: .chocolate),
    .rellena(relleno: .frambuesa, saborDeMasa: .chocolate),
    .sonrisas(saborDeMasa: .chocolate),
    .sonrisas(saborDeMasa: .vainilla),
    .sonrisas(saborDeMasa: .vainilla),
    .bocaDeDama,
    .bocaDeDama,
    .sonrisas(saborDeMasa: .chocolate)
])

surtido.describir()
//    -
//    Paquete:
//    Boca de dama
//    Galletita rellena de frambuesa, sabor chocolate
//    Galletita rellena de frambuesa, sabor chocolate
//    Galletita rellena de frambuesa, sabor chocolate
//    Sonrisa de chocolate
//    Sonrisa de vainilla
//    Sonrisa de vainilla
//    Boca de dama
//    Boca de dama
//    Sonrisa de chocolate
Enter fullscreen mode Exit fullscreen mode

Closures

Los closure o clausuras son también llamadas funciones anónimas. Veamos el concepto primero de función como tipo de dato.

Funciones como tipo de dato

Las funciones son un tipo de dato, como Int, Double, Bool, una class, struct o enum. Esto implica que uno podría tomar una función y enviarla como argumento a otra función, o hacer que una función devuelva otra función como resultado, o tener una struct donde uno de sus atributos sea una función. Esto es en realidad algo extraño si venimos de un lenguaje como Java, C o C++, pero es en realidad bastante común en otros lenguajes.

Para convertir una función en su tipo de dato correspondiente, debemos ver los tipos de los argumentos que recibe, y el tipo de dato que devuelve. Así, una función sumar:

func sumar(x: Int, y: Int) -> Int { ... }
Enter fullscreen mode Exit fullscreen mode

Pasa a ser de tipo (Int, Int) -> Int, porque recibe dos enteros y devuelve un entero. Veamos otros ejemplos:

func sumar(x: Int, y: Int) -> Int { ... } // (Int, Int) -> Int
func describir(a persona: Persona) { ... } // (Persona) -> Void
func imprimirHoraActual() { ... } // () -> Void
func obtenerFechaActual() -> String { ... } // () -> String
Enter fullscreen mode Exit fullscreen mode

Y, como dijimos, un tipo de dato función puede usarse como argumento para otras funciones:

struct Persona {
    let id: Int
    let nombre: String
    let ocupacion: Ocupacion?
    let edad: Int
}

enum Ocupacion {
    case desarrollador, projectManager, contador, abogado
}

func imprimirPersonasMayoresDeEdad(_ personas: [Persona]) {
    for persona in personas {
        if persona.edad >= 18 {
            print("\(persona.id) - \(persona.nombre)")
        }
    }
}

let personas = [
    Persona(id: 1, nombre: "Franco", ocupacion: .abogado, edad: 34),
    Persona(id: 2, nombre: "Gimena", ocupacion: .projectManager, edad: 24),
    Persona(id: 3, nombre: "Gonzalo", ocupacion: .abogado, edad: 26),
    Persona(id: 4, nombre: "Noelia", ocupacion: .desarrollador, edad: 29),
    Persona(id: 5, nombre: "Pablo", ocupacion: nil, edad: 15),
    Persona(id: 6, nombre: "Lourdes", ocupacion: .contador, edad: 29),
]

imprimirPersonasMayoresDeEdad(personas)
// Esto funcionará correctamente
//
// Sin embargo, no tenemos forma de dar 'flexibilidad' al algoritmo. Quiero decir,
// dentro de la función imprimirPersonasMayoresDeEdad filtramos por edad, y luego imprimimos.
// Si quisiéramos variar la forma de filtrar, deberíamos escribir una función completamente nueva.
// Convirtamos esa función en algo más flexible:

// Estamos "inyectando" una función dentro de otra función como argumento
func imprimir(_ personas: [Persona], si cumpleCondicion: (Persona) -> Bool) {
    for persona in personas {
        if cumpleCondicion(persona) {
            print("\(persona.id) - \(persona.nombre)")
        }
    }
}

func esMayor(_ persona: Persona) -> Bool {
    return persona.edad >= 18
}

imprimir(personas, si: esMayor) // Exactamente el mismo resultado. Pasamos la función como argumento en este caso.
Enter fullscreen mode Exit fullscreen mode

Funciones anónimas

Ahora sí, ya con esta introducción, podemos hablar de funciones anónimas. Una función anónima o closure es una función que no tiene nombre. Tan simple como suena. Y el mejor contexto para usar funciones anónimas es para pasarlas a otras funciones. Por ejemplo, en este caso yo podría haber decidido que no tenía sentido definir una función solo para determinar si una persona es mayor.

Definámosla como función anónima:

print("Imprimiendo personas mayores de edad por closure (1):")
imprimir(
    personas,
    si: { (persona: Persona) -> Bool in
        return persona.edad >= 18
    }
)
Enter fullscreen mode Exit fullscreen mode

Eso es una closure. La sintaxis puede resultar extraña y hay muchas formas de definirlas, aquí hay más ejemplos: https://fuckingclosuresyntax.com/

Por lo pronto, veamos la transformación paso a paso

Tenemos esta función:

 func esMayor(_ persona: Persona) -> Bool {
     return persona.edad >= 18
 }
Enter fullscreen mode Exit fullscreen mode

Paso 1: Le quitamos el func y el nombre:

 (_ persona: Persona) -> Bool {
     return persona.edad >= 18
 }
Enter fullscreen mode Exit fullscreen mode

Paso 2: En caso de que sus parámetros tengan nombre interno y externo, nos quedaremos únicamente con el interno:

 (persona: Persona) -> Bool {
     return persona.edad >= 18
 }
Enter fullscreen mode Exit fullscreen mode

Paso 3: Pasamos la llave de inicio del cuerpo, al comienzo de la declaración , y en su lugar pondremos in:

 { (persona: Persona) -> Bool in
     return persona.edad >= 18
 }
Enter fullscreen mode Exit fullscreen mode

¡Perfecto! Ya con esto tendremos una closure definida correctamente. Podemos quedarnos aquí, pero voy a comentarles algunas otras formas de seguir acortando esa declaración. Repito, es completamente opcional:

Paso opcional 1: Quitamos el tipo del argumento, el compilador puede inferirlo en la mayoría de las situaciones.

 { (persona) in
     return persona.edad >= 18
 }
Enter fullscreen mode Exit fullscreen mode

Paso opcional 2: Quitamos los paréntesis que envuelven a los argumentos:

 { persona in
     return persona.edad >= 18
 }
Enter fullscreen mode Exit fullscreen mode

Paso opcional 3: Si la closure tiene una única sentencia, podemos simplemente obviar el return.

 { persona in persona.edad >= 18 }
Enter fullscreen mode Exit fullscreen mode

Paso opcional 4: En vez de usar los nombres de los argumentos (en este caso persona), podemos referirnos a los argumentos por orden de los mismos. Por ejemplo, en este caso persona es $0, si tuviéramos dos argumentos, el primero sería $0 y el segundo $1. Si tuviéramos cuatro, serían $0, $1, $2, $3, y así sucesivamente:

 { $0.edad >= 18 }
Enter fullscreen mode Exit fullscreen mode

Y más que eso no podemos acortarlo

print("Mayores de edad que son abogados")
imprimir(personas, si: { $0.edad >= 18 && $0.ocupacion == .abogado })
Enter fullscreen mode Exit fullscreen mode

Si tenemos la closure como último argumento de la función, entonces podemos sacarla por fuera de la llamada a la función. Quedará más claro con un ejemplo:

print("Mayores de edad que son abogados (2)")
imprimir(personas) { $0.edad >= 18 && $0.ocupacion == .abogado } // Exactamente igual al ejemplo anterior.
Enter fullscreen mode Exit fullscreen mode

Map, filter, sorted y forEach

Hay funciones dentro de la biblioteca de funciones estándar que provee Swift que nos permiten pasar funciones a otras funciones, especialmente en el caso de tratamiento de Array.

  • map es una función que nos permite transformar cada elemento del array en otro elemento pasándole una función que realiza la transformación para cada elemento.
  • filter es una función que nos permite filtrar un array pasándole una función que devuelve true en caso de que el elemento tenga que ir en el Array de detino, o false en caso contrario
  • sorted es una función que nos permite ordenar un array. Funciona de forma similar al filter, donde devolvemos true en caso de que un elemento deba ir primero que otro en el array de destino. La función de sort recibe dos argumentos para compararlos, y no uno solo.
  • forEach, por último nos permite iterar sobre un array, realizando algo en el array de origen. Esta función no devuelve nada, al contrario de map, filter y sorted.

Es importante destacar que cada una de estas funciones (salvo forEach) devuelven un array completamente nuevo. No realizan modificaciones sobre el mismo array.

let personasMayores = personas.filter { $0.edad >= 18 }
let nombres = personas.map { $0.nombre }
let personasOrdenadasPorEdad = personas.sorted { $0.edad < $1.edad }

// También podemos "encadenar" estas funciones, ya que cada una devolverá un `Array` nuevo.

print("Funciones encadenadas:")

personas
    .filter { $0.edad >= 18 } // Solo consideramos las personas mayores de edad
    .sorted { $0.edad < $1.edad } // Luego las ordenaremos por edad
    .map { $0.nombre } // Y solo nos importará el nombre
    .forEach { print($0) } // Finalmente, imprimiremos sus nombres
// Gimena
// Gonzalo
// Noelia
// Lourdes
// Franco
Enter fullscreen mode Exit fullscreen mode

Extensions

Las extensions nos permiten extender un tipo existente para agregarle funcionalidades nuevas. Estas funcionalidades pueden ser propiedades computadas o métodos.

Veamos un ejemplo sencillo:

struct Direccion {
    let calle: String
    let numero: String
    let ciudad: String
}

extension Direccion {
    var descripcion: String {
        return "\(calle) \(numero) - \(ciudad)"
    }
}

let direccion = Direccion(calle: "Rivadavia", numero: "18451", ciudad: "Morón")
print(direccion.descripcion) // Rivadavia 18451 - Morón
Enter fullscreen mode Exit fullscreen mode

Lo de recién es exactamente igual a esto:

 struct Direccion {
     let calle: String
     let numero: String
     let ciudad: String

     var descripcion: String {
         return "\(calle) \(numero) - \(ciudad)"
     }
 }
Enter fullscreen mode Exit fullscreen mode

Una extension solo brinda la posibilidad de extender la funcionalidad de un tipo, sea class, struct, enum, etc.

Una funcionalidad interesante de las extension es que podemos extender también tipos nativos, tales como Int, Double, o String.

extension Int {
    func esMayor(que otroNumero: Int) -> Bool {
        return self > otroNumero
    }
}

if 10.esMayor(que: 5) {
    print("10 es mayor que 5")
}
Enter fullscreen mode Exit fullscreen mode

Typealias

Los typealias son justamente alias para tipos. Esto quiere decir que podemos referirnos a un tipo con otro nombre. Por ejemplo, imaginemos que tenemos una aplicación con usuarios, donde cada usuario tiene un identificador. Este ID del usuario es un entero. Sin embargo, un typealias puede traer mayor claridad cuando necesitemos referirnos al id del usuario.

Recordemos que un buen código es fácil de extender, y fácil de entender.

typealias IDUsuario = Int

struct Usuario {
    let id: IDUsuario
    let name: String
}

typealias IDInmueble = Int
typealias Direccion = (calle: String, numero: String, ciudad: String, provincia: String, pais: String)

struct Inmueble {
    let id: IDInmueble
    let idPropietario: IDUsuario
    let direccion: Direccion
}

let oficina = Inmueble(
    id: 1,
    idPropietario: 10,
    direccion: (
        calle: "Rivadavia",
        numero: "18451 PB Torre 2",
        ciudad: "Moron",
        provincia: "Buenos Aires",
        pais: "Argentina"
    )
)
Enter fullscreen mode Exit fullscreen mode

Noten que podríamos haber hecho exactamente lo mismo sin typealias. Por lo general (salvo en casos mucho más avanzados que no veremos en este curso), los typealias brindan claridad al código, haciendo más evidente nuestra intención al diseñarlo.

Ejercicios Segunda Parte

Queremos desarrollar una aplicación de viajes. La idea de la aplicación es tener un listado de posibles destinos. El usuario puede elegir sus destinos favoritos, los que podrá ver en otra lista.
Este ejercicio consiste en desarrollar las estructuras de datos necesarias para dar soporte a la aplicación. Se requiere, al menos:

  • Usar clases, structs o enums para las entidades User, Address, Place (ciudades o destinos turísticos), Landmark (monumentos, atracciones turísticas, etc. dentro de las ciudades)
  • Escribir todas las entidades, atributos y métodos en inglés.
  • Permitir obtener los Place y Landmark favoritos de un usuario determinado.

Usar la imaginación y creatividad, y realizar el ejercicio más completo posible.

Top comments (0)