Swift : les premiers pas

Le 2 juin 2014, lors de la keynote de la WWDC d’Apple (le jour de mon arrivée chez SQLI Enterprise), a été présenté un nouveau langage, Swift, pour créer des applications iOS, OSX (Cocoa et Console). Ce langage m’a tout de suite semblé beaucoup moins rébarbatif qu’Objective-C (mais c’est personnel) et très intuitif (plus de pointeurs! ;)).

Premiers pas avec Switf by Apple

Il m’a fait pensé par beaucoup d’aspect à Groovy (d’ailleurs Guillaume Laforge a écrit un comparatif Groovy versus Swift). Avant de vous lancer « bille en tête » dans le développement d’application iOS, je vous conseille de jeter un petit coup d’œil au langage lui-même.

Remarque : pour ceux qui ont un compte développeur Apple, il n’est pas obligatoire d’installer la version beta du futur OSX Yosemite pour pouvoir faire du Swift. Il est tout à fait possible d’installer XCode Beta 6 (qui contient Swift) sur un OSX Mavericks.

Télécharger XCode Beta 6 pour OSX Mavericks

 

Débuter avec Swift

Pour découvrir un langage, les classes sont généralement mon point de départ. Cette fois ci, j’utiliserais plutôt les fonctions et les « closures » car Swift a une orientation fonctionnelle. Ce post est illustré d’exemples de code qui je l’espère seront le plus explicites possible.

Découvrir les fonctions de Swift

Swift est un langage typé, il faut donc déclarer le type de retour (s’il existe) et le type des paramètres.

Une fonction qui ne retourne rien (void)

func printHello(message: String) {
  println("hello \(message)")
}

printHello("world")

L’appel de printHello(« world ») (donc avec un paramètre de type String) affichera hello world. Jusqu’ici, rien de bien méchant, notez cependant au passage la possibilité de faire de l’interpolation de chaîne avec la notation \(variable).

Une fonction qui retourne quelque chose

Pour exprimer le type de retour de la fonction, il faut utiliser la notation -> Type entre la signature de la fonction et le début du bloc de fonction ({}) :

func hello(message: String) -> String {
  return "hello \(message)"
}

hello("world")

L’appel de hello(« world ») retournera une chaîne de caractères hello world.

C’est maintenant que cela devient amusant.

Utiliser les Nested Functions avec Swift

Les « nested functions » sont des définitions de fonctions à l’intérieur de fonctions, ce qui signifie qu’elles ne sont accessibles qu’à l’intérieur de la fonction qui les contient.

Exemple simple

func salut(message: String) -> String {
  func nestedHello(message: String) -> String {
    return "Hello \(message)"
  }
  return nestedHello(message)
}

salut("World !!!")

L’appel salut(« World !!! ») retournera la chaîne de caractères Hello World !!!

Mais une fonction peut aussi retourner une fonction.

Exemple d’une fonction qui retourne une fonction

func hola(startMessage: String) -> String -> String { 

  func nestedHello(endMessage: String) -> String {
    return "\(startMessage) \(endMessage)"
  }
  return nestedHello
}

Notez que pour définir que le type de retour de la fonction « contenante » et une fonction qui prend en paramètre une String et qui retourne une String, nous utilisons la notation : String -> String.
Cette fois-ci l’appel de notre méthode hola se fera de la façon suivante :

hola("Hello")("World !!!")

… qui retournera bien sûr Hello World !!!

Remarque : la composition de fonction peut être intéressante pour de l’écriture de DSL.

Une fonction comme paramètre

Si les fonctions dans Swift peuvent retourner des fonctions, elles peuvent aussi prendre des fonctions en paramètres. Comme par exemple :

func morgen(startMessage:String, endMessage: String -> String) -> String {
  let param = "World"
  return "\(startMessage) \(endMessage(param))"
}

func message(msg: String) -> String {
  return "\(msg) !!!"
}

morgen("Hello", message)

… qui retournera encore et toujours Hello World !!!

Simplifier le code grâce aux « Closures Expressions »

Selon Apple, une « Closure Expression » est un moyen « synthétique » pour écrire/définir une closure. C’est plus simplement ce qui va nous permettre de « simplifier » notre exemple précédent en définissant « inline » le paramètre endMessage sans passer par la fonction intermédiaire message de la façon suivante:

morgen("Hello", { (msg: String) -> String in
  return "\(msg) !!!"
})

Augmenter les capacités des Types avec les extensions

Avant de m’attarder sur les classes, je voudrais faire une brève incursion dans ce que l’on appelle les « extensions ».

Les « extensions » dans Swift permettent d’ajouter des fonctionnalités (méthodes) à des types (Classes, Structures …), ce qui permet « d’augmenter » le langage lui même en alliant les « extensions » aux « closures expressions » et aux « generics ».

Nous allons par exemple « augmenter » le type Array :

Méthode first()

Il n’y a pas de méthode first sur le type Array. Qu’à cela ne tienne, il suffit d’écrire ceci :

extension Array{

  func first() -> (T) {
    return self[0]
  }
}

var villes = ["Lyon", "Valence", "Chambéry"]

villes.first()

Et villes.first() retournera Lyon, et c’est plus « joli » (question de point de vue) que villes[0].

Méthode each()

Je viens du monde Javascript où l’on peut parcourir un tableau avec la méthode forEach et effectuer un traitement sur chacun des items de tableau parcouru :

//ceci est du javascript
["Lyon", "Valence", "Chambéry"].forEach(function (ville) {
  console.log(ville)
});

En Swift pour parcourir un tableau, il faut écrire ceci:

for ville: String in villes {
  println(ville)
}

Je souhaite avoir une méthode each pour le type Array, je vais donc modifier mon extension Array :

extension Array{

  func each(each: (T) -> ()){
    for object: T in self {
      each(object)
    }
  }
  
  func first() -> (T) {
    return self[0]
  }
}

Et maintenant, je peux écrire ceci :

villes.each({ (item: String) in
  println(item)
})

L’inférence de Swift me permet même d’écrire ceci :

villes.each({ item in
  println(item)
})

Nul doute que cette possibilité d’extension permettra de voir fleurir des frameworks intéressants dans le même esprit que Underscore par exemple.

Les classes (et les structures)

Définir les classes avec Swift

La définition d’une classe reste somme toute très classique :

class Human {
  
  var _firstName: String
  var _lastName: String
  
  init(firstName: String, lastName: String) {
    _firstName = firstName
    _lastName = lastName
  }
  
  init() {
    _firstName = "John"
    _lastName = "Doe"
  }
  
  func hello() -> String {
    return "Hello, i'm \(_firstName) \(_lastName)"
  }

}

let John = Human()

John.hello()

let Bob = Human(firstName:"Bob", lastName:"Morane")

Bob.hello()

init() est la méthode qui sert de constructeur et vous pouvez noter que la surcharge est possible.

Les interfaces ou protocol

Comme dans tout langage « orienté objet » qui « se respecte », la notion d’interface existe aussi, mais sous le nom de protocol. Le nom est différent, mais le principe est identique. Par exemple, je définis HumanProtocol de cette manière :

protocol HumanProtocol {
  func firstName() -> String
  func firstName(value: String) -> HumanProtocol
  func lastName() -> String
  func lastName(value: String) -> HumanProtocol
  func hello() -> String
}

Si je veux « dire » que ma classe Human implémente HumanProtocol j’utiliserai la notation suivante :

classe Human: HumanProtocol { 
  //...
}

L’implémentation définitive de ma classe sera la suivante :

class Human: HumanProtocol {
 
 var _firstName: String
 var _lastName: String
 
 init(firstName: String, lastName: String) {
    _firstName = firstName
    _lastName = lastName
 }
 
 init() {
    _firstName = "John"
    _lastName = "Doe"
 }
 
 func firstName() -> String {
    return _firstName
 }
 
 func firstName(value: String) -> HumanProtocol {
    _firstName = value
    return self
 }
 
 func lastName() -> String {
    return _lastName
 }
 
 func lastName(value: String) -> HumanProtocol {
    _lastName = value
    return self
 }
 
 func hello() -> String {
    return"Hello, i'm \(_firstName) \(_lastName)"
 }
 }

Notez l’utilisation de return self qui va me permettre de chaîner mes méthodes. Je vais donc pouvoir utiliser ma classe de la façon suivante :

let Bob = Human()
Bob.firstName("Bob").lastName("Morane")
Bob.hello() // retournera "Hello, i'm Bob Morane"

let Jane = Human()
Jane.firstName("Jane").hello() // retournera `Hello, i'm Jane Doe`

Comment créer des membres privés avec Swift

Il y a une chose qui peut surprendre avec Swift, c’est que la notion de privé ou public n’existe pas de manière explicite, donc les variables _firstName et _lastName sont accessibles directement de cette façon :

Bob._firstName = "Bob"
Bob._lastName = "Morane"

Voici une astuce pour contourner cette problématique, un peu de la même façon qu’en Javascript, en encapsulant notre classe dans une fonction, comme ceci :

func getHuman() -> HumanProtocol {

  class Human: HumanProtocol {
    
    var _firstName: String
    var _lastName: String
    
    init(firstName: String, lastName: String) {
      _firstName = firstName
      _lastName = lastName
    }
    
    init() {
      _firstName = "John"
      _lastName = "Doe"
    }
    
    func firstName() -> String {
      return _firstName
    }
    
    func firstName(value: String) -> HumanProtocol {
      _firstName = value
      return self
    }
    
    func lastName() -> String {
      return _lastName
    }
    
    func lastName(value: String) -> HumanProtocol {
      _lastName = value
      return self
    }
    
    func hello() -> String {
      return "Hello, i'm \(_firstName) \(_lastName)"
    }
  }
  
  return Human()
}

Ainsi la classe Human est inaccessible en dehors du corps de la fonction getHuman (qui retourne un type HumanProtocol), seuls les membres exposés par le protocole HumanProtocol sont donc accessibles :

let Bob = getHuman()
Bob.firstName("Bob").lastName("Morane")
Bob.hello()

let Jane = getHuman()
Jane.firstName("Jane").hello()

Computed Properties ou comment remplacer les getters et les setters

Comme en Objective-C, le concept de propriétés existe et notamment celui de Computed Properties (pour remplacer les getters et les setters) :

var firstName: String {
  get {return _firstName}
  set(value) {
    _firstName = value
  }
}

var lastName: String {
  get {return _lastName}
  set(value) {
    _lastName = value
  }
}

On perd cependant l’avantage du chaînage.

Définir l’héritage avec Swift

En ce qui concerne l’héritage, il suffit de déclarer juste après le nom de la classe le type (la classe) dont elle hérite :

class SuperHero: Human {
  var power = "walking"
  var nickName = "Kick-Ass"
  
  override func hello() -> String {
    return super.hello() + ", my Hero name is \(nickName) and i'm \(power)"
  }
}

let Clark = SuperHero(firstName: "Clark", lastName: "Kent")
Clark.nickName = "SuperMan"
Clark.power = "flying"
Clark.hello() // retourne "Hello, i'm Clark Kent, my Hero name is SuperMan and i'm flying"

Ensuite en jouant avec les protocoles, extensions et classes vous pouvez réglez vos problématiques d’héritage de différentes façons :

protocol SuperHero {
  var power: String { get set }
  var nickName: String { get set }
}

class FlyingSuperHero:Human, SuperHero {
  var power = "flying"
  var nickName = "Kick-Ass"
}

extension FlyingSuperHero {
  func fly() -> String {
    return self.hello() + ", my Hero name is \(self.nickName) and i'm \(self.power)"
  }
}

let Clark = FlyingSuperHero(firstName: "Clark", lastName: "Kent")
Clark.nickName = "SuperMan"

Clark.fly() // retourne "Hello, i'm Clark Kent, my Hero name is SuperMan and i'm flying"

Remarque : à mon avis, la seule chose qui manque (à moins que je sois passé à côté) est la notion de « trait » comme en Groovy ou de méthode par défaut dans les interfaces en Java 8 (ou même de « mixin »).

Swift et le fonctionnement des structures

En plus des classes, Swift propose les structures. La déclaration se fait de la même manière que les classes, une structure peut implémenter un protocole, être étendue par une extension, par contre elle ne peut hériter. Mais le plus important c’est que les classes sont des types par référence et les structures des types par valeur :

class Human {
  
  var firstName: String = "John"
  var lastName: String = "Doe"
  
  func hello() -> String {
    return "Hello, i'm \(firstName) \(lastName)"
  }
}

struct Duck {
  var firstName: String = ""
  var lastName: String = ""
  
  func hello() -> String {
    return "Hello, i'm \(firstName) \(lastName)"
  }
}

let John = Human()
let Bob = John
Bob.firstName = "Bob"
Bob.lastName = "Morane"

John.hello() // retourne Hello, i'm Bob Morane

Dans ce cas là John.hello() retournera Hello, i’m Bob Morane et non pas Hello, i’m John Doe car Bob est une référence à John.

Alors qu’avec la structure :

let Donald = Duck(firstName: "Donald", lastName: "Duck")
var Daffy = Donald

Daffy.firstName = "Daffy"

Donald.hello() // retourne Hello, i'm Donald Duck
Daffy.hello() // retourne Hello, i'm Daffy Duck

Donald.hello() retournera toujours Hello, i’m Donald Duck. Daffy est une copie de Donald et donc devient indépendant de Donald. Vous pouvez noter que les structures disposent d’un constructeur par défaut que nous ne sommes pas obligés de définir.

Conclusion

Je suis loin d’avoir fait le tour de Swift, mais j’espère que cet article vous donne déjà un aperçu suffisant pour commencer à vous y mettre.

Depuis la présentation au monde de Swift, une communauté très active est apparue « en génération spontanée » et l’on commence à trouver déjà un certain nombre de tutoriaux (pour beaucoup en vidéo). La documentation d’Apple est complète avec un e-book déjà disponible sur l’iTunes Store.

Les autres ressources intéressantes à suivre sont :

 

Enterprise advocate

2 commentaires

Les commentaires sont fermés.

Inscription newsletter

Ne manquez plus nos derniers articles !