Swift frente a Objective-C. 10 diferencias

El 2 de junio de 2014, Apple presentó un nuevo lenguaje de programación orientado a objetos: Swift, un sustituto de Objective-C, que hasta ese momento había sido el lenguaje de programación estándar para el desarrollo de aplicaciones para OS X e iOS.

Fue un movimiento audaz cambiar el lenguaje de desarrollo principal para una plataforma tan popular; aun así, nuestro equipo respondió a esta noticia con gran entusiasmo y decidió cambiarse a él en poco tiempo. Y no somos los únicos: Swift ha recibido elogios por su sencillez y flexibilidad, y su tasa de adopción se ha disparado.

Después de desarrollar un par de aplicaciones iOS con Swift, hemos decidido compartir nuestras opiniones sobre algunas de las cosas que creemos que hacen que sea más fácil y agradable programar con él. Así que aquí está nuestra lista de las diez diferencias más importantes entre Swift y Objective-C.

(No dudes en utilizar una zona de juegos para practicar con los fragmentos de código de este artículo. Siempre es una buena idea explorar nuevos lenguajes y frameworks).

1. Opcionales

Los opcionales son un concepto que no existe en C ni en Objective-C. Permiten que las funciones que no siempre pueden devolver un valor significativo (por ejemplo, en caso de entrada no válida) devuelvan un valor encapsulado en un opcional o nil. En C y Objective-C, ya podemos devolver nil desde una función que normalmente devolvería un objeto, pero no tenemos esta opción para funciones que se espera que devuelvan un tipo básico como int, float o double. Observa:

let a = "512"
let b = Int(a)
print(b) // Optional(512)

La función Int en este caso devuelve un opcional que contiene el valor int 512, como era de esperar. Pero, ¿qué ocurre si en su lugar intentamos convertir algo como “Hola”? En ese caso, la función devolverá nil, como se ha explicado anteriormente:

let a = "Hello"
let b = Int(a)
print(b) // nil

Como ya sabrás, en Objective-C, si llamas a un método sobre un puntero nil, no pasa nada. Ni errores de compilación, ni errores de ejecución, nada. Esta es una fuente bien conocida de errores y comportamientos inesperados, y de frustración en general: la falta de información obliga a los desarrolladores a buscar el origen de estos errores por su cuenta.

Con Swift, no tiene que preocuparse por esto:

var x = Int("555")
print(x.successor()) // error

El propio compilador lanzará un error, ya que x ahora mismo es un opcional y potencialmente podría ser nil. Antes de utilizar un valor opcional, es necesario desenvolverlo así:

var x = Int("555")
if x != nil {
    print(x!.successor()) // 556
}

Alternativamente, podemos utilizar la encuadernación opcional:

var x = Int("555")
if var y = x {
    y += 445
    print(y) // 1000
}

2. Flujo de control

Cualquiera que haya programado en C o en un lenguaje similar a C está familiarizado con el uso de llaves ({}) para delimitar bloques de código. En Swift, sin embargo, no son solo una buena idea: ¡son la ley!

if a < 5 {
    print("Something")
}

Esto provocará un error de compilación en Swift.

if a < 5
print(“Something")

A diferencia de Objective-C, Swift trata las llaves como obligatorias para las sentencias si, para, mientras y repita. Puede que esto no suene como una mejora sobre Objective-C per se, pero considera que incluso los propios desarrolladores de Apple han tenido problemas sin llaves para mantenerlos en línea:

if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
goto fail;
goto fail;

Así es: el infame error “goto fail” de iOS podría haberse evitado si Objective-C fuera tan estricto como Swift a la hora de aplicar las llaves.

3. Inferencia de tipo

Swift introduce la seguridad de tipos en el desarrollo de iOS. Una vez que una variable se declara con un tipo determinado, su tipo es estático y no puede cambiarse. El compilador también es lo suficientemente inteligente como para averiguar (o inferir) qué tipo deben tener tus variables en función de los valores que les asignes:

var str = "Some string"
// OR
var str2:String
str2 = "Other string"

Esto también significa que el compilador emitirá un error si intentas asignar un valor numérico, como 10, a nuestra variable str2:

str2 = 10 // error: Cannot assign value of type 'Int' to type 'String'

Compárese con Objective-C, donde siempre hay que indicar explícitamente el tipo de una variable:

NSString str = @"There is no type inference in Objective-C :("

4. Tuplas

Swift admite tuplas, valores que almacenan grupos de otros valores. A diferencia de las matrices, los valores de una tupla no tienen por qué ser todos del mismo tipo. Por ejemplo, puedes tener una tupla de Strings e Ints:

var t:(String, Int) = ("John", 33)
print(t.0, t.1)
var j:(name:String, Int) = ("Morgan", 52)
print(j.name, j.1)

El uso más obvio de las tuplas es devolver múltiples valores desde una función:

var arr = [23, 5, 7, 33, 9]
 
func findPosition(el:Int, arr:[Int]) -> (found:Bool, position:Int)? {
    if arr.isEmpty {
        return nil
    }
 
    for (i, value) in arr.enumerate() {
        if value == el {
            return (true, i)
        }
    }
    return (false, -1)
}
 
if let (isFound, elPosition) = findPosition(5, arr: arr) {
    print(isFound, elPosition)
    // true 1
}

En Objective-C, podemos utilizar bloques de forma similar, pero no es tan sencillo ni elegante.

5. Manipulación de cadenas

Swift ofrece enormes mejoras sobre Objective-C en términos de manipulación de cadenas. Para empezar, ya no tienes que preocuparte por las cadenas mutables frente a las inmutables: basta con declarar tu cadena con var si quieres cambiarla en el futuro, o con let si necesitas que permanezca constante.

La concatenación de cadenas es tan fácil como 1+1=2:

// Swift:
var str = "My string"
str += " and another string”

Compárese con Objective-C, donde la concatenación de dos cadenas inmutables requiere la creación de un objeto NSString completamente nuevo. He aquí un ejemplo que utiliza el método “stringWithFormat”:

// Obj-C:
NSString *myString = @"My string";
myString = [NSString stringWithFormat:@"%@ and another string", myString];

Como puedes ver aquí, el formateo de cadenas en Objective-C implica insertar marcadores de posición especiales para cada tipo de dato diferente que queramos interpolar en nuestra cadena:

NSString *str = [NSString stringWithFormat:@"String: %@ | Signed 32-bit integer: %d | 64-bit floating-point number: %f", @"My String", myInt, myFloat];

En Swift, en cambio, podemos interpolar valores insertando los nombres de las variables directamente en nuestra cadena:

var str = "String: (myString) | Signed 32-bit integer: (myInt) | 64-bit floating-point number: (myFloat)"

6. Vigilar y aplazar

¿Ha oído hablar alguna vez de la “pirámide de la perdición”? Si no es así, aquí tienes un fragmento de código Objective-C que te refrescará la memoria:

enum TriangleAreaCalcError: ErrorType {
    case AngleNotSpecified
    case InvalidAngle
    case SideANotSpecified
    case SideBNotSpecified
}
 
func calcTriangleArea(a: Double ? , b : Double ? , alpha : Double ? ) throws - > Double {
    if let a = a {
        if let b = b {
            if let alpha = alpha {
                if alpha < 180 && alpha >= 0 {
                    if alpha == 180 {
                        return 0
                    }
                    return 0.5 * a * b * sin(alpha * M_PI / 180.0)
                } else {
                    throw TriangleAreaCalcError.InvalidAngle
                }
            } else {
                throw TriangleAreaCalcError.AngleNotSpecified
            }
        } else {
            throw TriangleAreaCalcError.SideBNotSpecified
        }
    } else {
        throw TriangleAreaCalcError.SideANotSpecified
    }
}

¡Eso sí que es una pirámide que haría parpadear incluso a Indiana Jones! Afortunadamente, en Swift tenemos guard, una nueva sentencia condicional que puede hacer este código mucho más legible. Guard detiene el flujo del programa si no se cumple una condición:

func calcTriangleArea(a: Double ? , b : Double ? , alpha : Double ? ) throws - > Double {
 
    guard
    let a = a
    else {
        throw TriangleAreaCalcError.SideANotSpecified
    }
 
    guard
    let b = b
    else {
        throw TriangleAreaCalcError.SideBNotSpecified
    }
 
    guard
    let alpha = alpha
    else {
        throw TriangleAreaCalcError.AngleNotSpecified
    }
 
    if alpha == 180 {
        return Double(0)
    }
 
    guard alpha < 180 && alpha >= 0
    else {
        throw TriangleAreaCalcError.InvalidAngle
    }
 
    return 0.5 * a * b * sin(alpha * M_PI / 180.0)
}

Swift también nos da la palabra clave defer, que proporciona una manera segura y fácil de manejar el código que queremos que se ejecute sólo cuando el programa salga del ámbito actual:

func someImageFunc() - > UIImage ? {
 
    let dataSize: Int = ...
    let destData = UnsafeMutablePointer < UInt8 > .alloc(dataSize)
 
    // ...
 
    guard error
    else {
        destData.dealloc(dataSize) // #1
        return nil
    }
 
    guard error2
    else {
        destData.dealloc(dataSize) // #2
        return nil
    }
 
    guard error3
    else {
        destData.dealloc(dataSize) // #3
        return nil
    }
 
    destData.dealloc(dataSize) // #4
 
    // ...
}

Como puedes ver, necesitamos escribir cuatro llamadas separadas a destData.dealloc(dataSize) para asegurarnos de que nuestro puntero es liberado incluso si guard hace que la función salga en caso de error. Con defer, tenemos una forma sencilla de limpiar tanto nuestro puntero como nuestro código:

func someImageFunc() - > UIImage ? {
 
    let dataSize: Int = ...
    let destData = UnsafeMutablePointer < UInt8 > .alloc(dataSize)
 
    // ...
 
    defer {
        destData.dealloc(dataSize)
    }
 
    guard error
    else {
        return nil
    }
 
    guard error2
    else {
        return nil
    }
 
    guard error3
    else {
        return nil
    }
 
    // ...
}

7. Patrones de programación funcional

Swift incorpora una serie de funciones de programación funcional, como map y filter, que pueden utilizarse en cualquier colección que implemente el protocolo CollectionType:

let a = [4, 8, 16]
print(a.map{$0 / 2})
// [2, 4, 8]
 
let a:[(name:String, area:String)] = [("John", "iOS"), ("Sam", "Android"), ("Paul", "Web")]
 
let b = a.map({"Developer ($0.name) (($0.area))"})
// ["Developer John (iOS)", "Developer Sam (Android)", "Developer Paul (Web)”]
 
let a = [23, 5, 7, 12, 10]
let b = a.filter{$0 > 10}
print(b) // [23, 12]
 
let sum = (20...30)
.filter { $0 % 2 != 0 }
.map { $0 * 2 }
.reduce(0) { $0 + $1 }
 
print(sum) // 250

Objective-C no tiene soporte integrado para la programación funcional. Para utilizar este mismo tipo de funciones, tendría que recurrir a una biblioteca de terceros.

8. Enumeraciones

En Swift, las enumeraciones son más potentes que en Objective-C: ahora pueden contener métodos y ser pasadas por valor. He aquí un pequeño fragmento de código que ilustra muy bien cómo funcionan las enumeraciones en Swift:

enum Location {
    case Address(city: String, street: String)
    case Coordinates(lat: Float, lon: Float)
 
    func printOut() {
        switch self {
            case let.Address(city, street):
            print("Address: " + street + ", " + city)
            case let.Coordinates(lat, lon):
            print("Coordiantes: ((lat), (lon))")
        }
    }
}
 
let loc1 = Location.Address(city: "Boston", street: "33 Court St")
let loc2 = Location.Coordinates(lat: 42.3586, lon: -71.0590)
 
loc1.printOut() // Address: 33 Court St, Boston
loc2.printOut() // Coordiantes: (42.3586, -71.059)

Los enums también pueden ser recursivos, por lo que podemos construir una lista enlazada utilizando la sentencia indirect, que indica al compilador que añada la capa de indirección necesaria:

enum List {
    case Empty
    indirect
    case Cell(value: Int, next: List)
}
 
let list0 = List.Cell(value: 1, next: List.Empty)
let list1 = List.Cell(value: 4, next: list0)
let list2 = List.Cell(value: 2, next: list1)
let list3 = List.Cell(value: 6, next: list2)
let headL = List.Cell(value: 3, next: list3)
 
func evaluateList(list: List) - > Int {
    switch list {
        case let.Cell(value, next):
        return value + evaluateList(next)
        case .Empty:
        return 0
    }
}
 
print(evaluateList(headL)) // 16

9. Funciones

La sintaxis de función de Swift es lo suficientemente flexible como para definir cualquier cosa, desde una simple función de estilo C hasta un complejo método de estilo Objective-C con nombres de parámetros locales y externos.

Cada función en Swift tiene un tipo, que consiste en los tipos de parámetros de la función y el tipo de retorno. Esto significa que puedes asignar funciones a variables o pasarlas como parámetros a otras funciones:

func stringCharactersCount(s: String) - > Int {
    return s.characters.count
}

func stringToInt(s: String) - > Int {
    if let x = Int(s) {
        return x
    }
    return 0
}

func executeSuccessor(f: String - > Int, s: String) - > Int {
    return f(s).successor()
}

let f1 = stringCharactersCount
let f2 = stringToInt

executeSuccessor(f1, s: "5555") // 5
executeSuccessor(f2, s: "5555") // 5556

Swift también permite definir valores por defecto para los parámetros de las funciones:

func myFunction(someInt: Int = 5) {
    // If no arguments are passed, the value of someInt is 5
}
myFunction(6) // someInt is 6
myFunction() // someInt is 5

10. Declaración Do

La sentencia do en Swift permite introducir un nuevo ámbito:

let a = "Yes"
 
do {
    let a = "No"
    print(a) // No
}
 
print(a) // Yes

Las sentencias Do también pueden contener una o más cláusulas catch:

do {
    try expression
    statements
} catch pattern 1 {
    statements
} catch pattern 2 where condition {
    statements
}

Para más información, consulte la documentación oficial de Swift sobre la declaración do.

Conclusión

Creemos firmemente que Swift es el futuro del desarrollo para iOS, y con su naturaleza de código abierto y su creciente popularidad, podemos esperar verlo utilizado en otras plataformas en el futuro.

Una de sus características más prometedoras es lo que Apple denomina programación orientada a protocolos, un paradigma que puede tener un enorme impacto en el desarrollo de software en el futuro. En Swift, los protocolos (o “Interfaces” en lenguajes como C#) pueden ser implementados no sólo por clases, sino también por enums y structs; además, proporcionan una alternativa elegante y libre de confusiones a la herencia múltiple, ya que un único objeto puede implementar múltiples protocolos.

Con su sintaxis ligera y flexible y sus numerosas funciones para evitar errores de programación comunes, Swift es sencillamente un placer para programar. Pruébalo tú mismo, seguro que estás de acuerdo. Un buen punto de partida es la documentación oficial (y bastante completa) de Apple sobre el lenguaje.

Acerca de Redwerk

Al haber sido fundada en 2005, Redwerk tuvo tiempo suficiente para practicar en el desarrollo de juegos, aplicaciones, soluciones de administración electrónica y medios de comunicación locales para iOS, etc.

Entre los casos más desafiantes que nos ayudaron a obtener y poner a prueba la experiencia en el desarrollo de iOS se encuentran los siguientes:

Aplicación de observación escolar. Una solución perfecta para los supervisores escolares. El desarrollo de esta aplicación fue encargado por The Education Partners, ya que necesitaban registrar sus notas sobre profesores, alumnos y la propia escuela de forma rápida y sencilla, tanto dentro como fuera de línea. Gracias a nuestra experiencia previa, Redwerk pudo entregar un prototipo sencillo y fácil de usar en muy poco tiempo y pasar rápidamente a la implementación de la app. Desde el principio, el rendimiento fue un objetivo clave, ya que la aplicación debía gestionar grandes cantidades de archivos multimedia. Solo tardamos tres meses en desarrollar y poner en marcha una aplicación de observación escolar que esperamos ayude a The Education Partners a mantener el nivel educativo al más alto nivel.

Aplicaciones móviles para medios locales. Redwerk se encargó del mantenimiento duradero de la mayoría de las aplicaciones iOS de noticias locales de Worldnow. Nuestra rutina diaria consistía en una serie de problemas que debíamos resolver con regularidad, como problemas de carga del feed de noticias, contenido duplicado y fallos en la actualización de los partes meteorológicos. También trabajamos para añadir nuevos bloques y funciones a la aplicación y modificar los existentes según las necesidades actuales de los usuarios.

YouTown. Nuestro equipo fue contratado para diseñar y desarrollar desde cero una aplicación móvil que conectara a los ciudadanos estadounidenses con sus autoridades locales y les proporcionara información actualizada sobre la ciudad en cualquier momento que la necesitaran. Redwerk se enorgullece de poder afirmar que sólo tardamos tres meses en entregar las primeras betas totalmente funcionales de una interfaz web para los representantes del gobierno y una aplicación móvil para los usuarios finales. Con el tiempo, la aplicación se extendió por más de 20 ciudades dentro y fuera de Estados Unidos y finalmente fue adquirida por una tercera empresa.