• 12 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 26/02/2020

Allez plus loin avec les extensions

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Ce chapitre est un petit chapitre bonus pour vous parler d'un concept très pratique de Swift : les extensions !

Oui je sais ! Mais courage, prenez un petit remontant parce que ça vaut vraiment le coup !

Présentation du concept

Les extensions permettent de rajouter des fonctionnalités à un type, que ce soit une classe, une structure, une énumération ou même un protocole.

Le principe est vraiment simple et ce sera limpide avec un exemple. Je vais vous montrer comment étendre le type Int.

Pour créer une extension, on utilise le mot-clé extension suivi du nom du type et on ouvre des accolades :

extension Int {
}

Maintenant vous pouvez rajouter des méthodes, par exemple :

extension Int {
func addTwo() -> Int {
return self + 2
}
func square() -> Int {
return self * self
}
}

Désormais dans votre projet, tous les entiers pourront utiliser ces méthodes. C'est comme si elles avaient toujours existé dans le code original de la classe.

Donc vous pouvez écrire :

var x = 3
x.addTwo() // Renvoie 5
x.square() // Renvoie 9

Et vous pouvez même écrire directement :

3.addTwo() // Renvoie 5
3.square() // Renvoie 9

Bien sûr, ici ce n'est pas particulièrement utile. Mais les extensions permettent de rajouter des fonctionnalités très pratiques à des classes auxquelles vous n'avez pas accès.

Par exemple, si vous avez suivi le cours d'introduction à iOS, vous vous souvenez que le code pour obtenir un entier aléatoire n'est pas très agréable :

Int(arc4random_uniform(UInt32(10)))

On va créer une méthode de classe qui soit plus simple à utiliser que le code ci-dessus :

extension Int {
// (...)
static func random(max: Int) -> Int {
return Int(arc4random_uniform(UInt32(max)))
}
}

La partie illisible et compliquée se retrouve cachée dans notre méthode. Maintenant, pour obtenir un entier aléatoire entre 0 et 9, on n'a plus qu'à faire :

Int.random(max: 10)

C'est quand même beaucoup plus lisible et plus clair !

Philosophie de l'extension

Lorsque vous travaillez dans une extension, c'est comme si vous étiez dans la classe, structure ou autre que vous étendez !

Du coup, vous pouvez rajouter :

  • des méthodes

  • des méthodes de classe

  • des propriétés calculées de classe ou d'instance

  • des initialisations

  • des sous-types (déclaration d'un type à l'intérieur de l'extension)

Les deux seules choses que vous ne pouvez pas faire dans une extension sont :

  • ajouter des propriétés stockées ou modifier les observateurs d'une propriété stockée existante.

  • modifier une méthode existante.

Ajouter des constantes

En plus de rajouter des fonctionnalités à une classe, les extensions peuvent être utilisées pour rajouter des constantes. Laissez-moi vous donner 3 exemples.

UIColor

Le designer avec qui vous travaillez utilise sans doute une palette de couleurs bien précise pour l'application. Cette palette, vous pouvez la reproduire dans le storyboard et cela évite de devoir recréer la couleur à chaque fois que vous voulez l'utiliser !

Mais quand vous voulez utiliser vos couleurs dans le code, c'est nettement moins simple et vous devez constamment répéter du code qui ressemble à :

UIColor(red: 205/255, green: 240/255, blue: 255/255, alpha: 1.0)

Avouons-le, ce n'est pas merveilleux de répéter ça partout. Non seulement c'est difficile à écrire — qui se souviendra des valeurs ? — mais en plus, c'est difficile à lire, on ne sait pas quelle couleur ça représente.

Pourtant, pour les couleurs par défaut comme le blanc, on peut écrire plus simplement :

UIColor.white

Et bien avec les extensions, nous allons essayer d'obtenir ce résultat, mais pour les couleurs de notre choix. Et voilà comment nous allons faire :

extension UIColor {
public class var lightBlue: UIColor {
return UIColor(red: 205/255, green: 240/255, blue: 255/255, alpha: 1.0)
}
public class var deepBlue: UIColor {
return UIColor(red: 41/255, green: 180/255, blue: 206/255, alpha: 1.0)
}
public class var purple: UIColor {
return UIColor(red: 173/255, green: 79/255, blue: 139/255, alpha: 1.0)
}
public class var pink: UIColor {
return UIColor(red: 219/255, green: 167/255, blue: 201/255, alpha: 1.0)
}
}

Nous créons une extension de  UIColor. Dans l'extension on crée simplement des propriétés calculées de classe de type  UIColor. Ces propriétés calculées renvoient les couleurs de notre choix.

Et maintenant on peut utiliser nos couleurs partout dans l'application comme ceci :

UIColor.lightBlue
UIColor.purple
UIColor.deepBlue
UIColor.pink

C'est pas magnifique ? Je vous suggère de créer un fichier  Colors.swift  dans tous vos projets dans lequel vous utilisez cette technique.

UIFont

On peut faire la même chose pour la police ! Pour utiliser les polices dans le code, on doit écrire :

UIFont(name: "MyCustomFont", size: 12)

Je n'aime pas garder une chaîne de caractères comme ça au milieu de mon code, c'est la porte ouverte à des fautes de frappe, car l'auto-complétion ne fonctionne pas pour les chaînes de caractères.

À la place, j'aimerais obtenir quelque chose comme ce qui existe pour la police par défaut d'iOS :

UIFont.systemFont(ofSize: 12)

Et pour cela, nous allons créer une extension :

extension UIFont {
public class func myCustomFont(ofSize size: CGFloat) -> UIFont {
return UIFont(name: "MyCustomFont", size: 12)!
}
}

Et on peut maintenant utiliser notre police aisément dans l'application :

UIFont.myCustomFont(ofSize: 12)

On peut même aller plus loin en créant des propriétés calculées pour les différentes tailles de polices utilisées dans l'application :

extension UIFont {
public class func myCustomFont(ofSize size: CGFloat) -> UIFont {
return UIFont(name: "MyCustomFont", size: 12)!
}
public class var textFont: UIFont {
return myCustomFont(ofSize: 12)
}
public class var titleFont: UIFont {
return myCustomFont(ofSize: 20)
}
}

Et vous pouvez utiliser vos polices comme cela :

UIFont.textFont
UIFont.titleFont

C'est pas beau franchement ? Je vous suggère de faire cela à chaque fois que vous devrez gérer des polices !

Notification

Autre exemple : les notifications. Lorsque vous voulez envoyer une notification, vous devez écrire :

let name = Notification.Name(rawValue: "LeNomDeMaNotification")
let notification = Notification(name: name)
NotificationCenter.default.post(notification)

Et pour recevoir la notification, vous devez écrire :

let name = Notification.Name(rawValue: "LeNomDeMaNotification")
NotificationCenter.default.addObserver(
self, selector: #selector(unMéthode), name: name, object: nil)

Il y a plusieurs choses que je n'aime pas. Déjà, on se trimballe encore une chaîne de caractères partout dans le code comme pour les polices ! Ensuite, on doit répéter la ligne de déclaration du nom de la notification à chaque fois.

Pour éviter tout cela et rendre notre code plus propre, nous allons étendre le type Notification.Name.

Et ça donne ça :

extension Notification.Name {
static let leNomDeMaNotification = Notification.Name("LeNomDeMaNotification")
}

Je crée une propriété de classe constante qui contient le nom de ma notification. Et maintenant je peux envoyer ma notification comme ceci :

let notification = Notification(name: .leNomDeMaNotification)
NotificationCenter.default.post(notification)

Et je peux même gagner encore une ligne en utilisant une variante de la méthode 'post' :

NotificationCenter.default.post(name: .leNomDeMaNotification, object: nil)

Et je peux recevoir la notification sans avoir à créer une nouvelle instance de Notification.Name :

NotificationCenter.default.addObserver(
self, selector: #selector(unMéthode), name: .leNomDeMaNotification, object: nil)

Et voilà ! C'est quand même bien plus propre. Maintenant je veux que vous n'utilisiez les notifications que comme ça !

Avec seulement trois exemples, vous pouvez améliorer de beaucoup la qualité de votre code en évitant les chaînes de caractères qui se baladent et la répétition de code compliqué ou illisible. Les extensions sont une particularité de Swift qui contribue beaucoup à la qualité du langage.

Mais il y a plus !

Organiser son code

Les extensions sont aussi utiles pour organiser son code. En effet, on peut créer autant d'extensions qu'on veut d'un même type. Donc on va pouvoir faire des choses de ce genre-là :

class MaClasse {
// Une partie du contenu de ma classe
}
extension MaClasse {
// Une autre partie du contenu de ma classe
}
extension MaClasse {
// Une troisième partie du contenu de ma classe
}

Voyons ce qu'on peut faire avec ça :

Séparer le contenu et le comportement

C'est une bonne pratique de séparer ses classes ou structures en deux :
- le contenu (la structure de données): les propriétés stockées
- le comportement (ce que fait la classe) : les initialisations, méthodes, propriétés calculées, etc.

Par exemple, faisons cela avec notre structure  Pet, cela donne :

struct Pet {
enum Gender {
case male, female
}
var name: String?
var hasMajority: Bool
var phone: String?
var race: String?
var gender: Gender
}
extension Pet {
enum Status {
case accepted
case rejected(String)
}
var status: Status {
if name == nil || name == "" {
return .rejected("Vous n'avez pas indiqué votre nom !")
}
if phone == nil || phone == "" {
return .rejected("Vous n'avez pas indiqué votre téléphone !")
}
if race == nil || race == "" {
return .rejected("Quelle est votre race ?")
}
if !hasMajority {
return .rejected("Les mineurs ne sont pas admis.")
}
return .accepted
}
}

On voit tout de suite quelles sont les données que détient la structure, et ensuite on peut s'intéresser à ce qu'elle fait. C'est bien propre ! :)

Se conformer à des protocoles

Ce découpage se fait aussi souvent lorsque le type se conforme à des protocoles. Par exemple, pour notre  FormViewController, on peut créer une extension pour traiter tout ce qui concerne les protocoles  UIPickerViewDataSource  et  UIPickerViewDelegate à part.

extension FormViewController: UIPickerViewDataSource, UIPickerViewDelegate {
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return dogRaces.count
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return dogRaces[row]
}
}

Organisation du code par groupe de méthodes

Dans la même logique, on peut organiser le code d'une classe complexe en regroupant des méthodes qui se rapportent à la même fonctionnalité. Ainsi, notre code du  FormViewController devient :

class FormViewController: UIViewController {
// MARK: - Properties
var dog: Pet!
// MARK: - Outlets
@IBOutlet weak var racePickerView: UIPickerView!
@IBOutlet weak var majoritySwitch: UISwitch!
@IBOutlet weak var nameTextField: UITextField!
@IBOutlet weak var phoneTextField: UITextField!
@IBOutlet weak var genderSegmentedControl: UISegmentedControl!
}
// MARK: - Keyboard
extension FormViewController: UITextFieldDelegate {
@IBAction func dismissKeyboard(_ sender: UITapGestureRecognizer) {
nameTextField.resignFirstResponder()
phoneTextField.resignFirstResponder()
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
}
// MARK: - PickerView
extension FormViewController: UIPickerViewDataSource, UIPickerViewDelegate {
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return dogRaces.count
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return dogRaces[row]
}
}
// MARK: - Validate
extension FormViewController {
@IBAction func validate() {
createPetObject()
checkPetStatus()
}
private func createPetObject() {
let name = nameTextField.text
let phone = phoneTextField.text
let hasMajority = majoritySwitch.isOn
let gender: Pet.Gender = (genderSegmentedControl.selectedSegmentIndex == 0) ? .male : .female
let raceIndex = racePickerView.selectedRow(inComponent: 0)
let race = dogRaces[raceIndex]
dog = Pet(name: name, hasMajority: hasMajority, phone: phone, race: race, gender: gender)
}
private func checkPetStatus() {
switch dog.status {
case .accepted:
performSegue(withIdentifier: "segueToSuccess", sender: nil)
case .rejected(let error):
presentAlert(with: error)
}
}
private func presentAlert(with error: String) {
let alert = UIAlertController(title: "Erreur", message: error, preferredStyle: .alert)
let action = UIAlertAction(title: "OK", style: .cancel, handler: nil)
alert.addAction(action)
present(alert, animated: true, completion: nil)
}
}
// MARK: - Navigation
extension FormViewController {
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "segueToSuccess" {
let successVC = segue.destination as! SuccessViewController
successVC.dog = dog
}
}
}

Le code de FormViewController est bien plus facile à lire. On voit au premier coup d'œil quelles sont les propriétés de cette classe et quelles en sont les fonctionnalités principales.

En résumé

  • Les extensions permettent d'étendre les fonctionnalités d'un type.

  • Il y a trois cas d'utilisations majeures :

    • Rajouter une nouvelle fonctionnalité

    • Rajouter des constantes

    • Organiser le code

Bravo d'être venu à bout de ce gros chapitre bonus ! Je savais bien que je n'avais pas encore pressé tout le jus de votre cerveau !

Exemple de certificat de réussite
Exemple de certificat de réussite