Ce chapitre est un petit chapitre bonus pour vous parler d'un concept très pratique de Swift : les extensions ! On en a très rapidement parlé pour ajouter la conformance à un protocole. Mais nous allons aller plus loin avec ce chapitre.
Oui je sais : c'est la fin du cours, et vous être probablement KO. Mais courage, prenez un petit remontant parce que ça vaut vraiment le coup !
Présentation du concept
Les extensions permettent d'ajouter 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 :
extensionInt {
}
Maintenant vous pouvez ajouter des méthodes ; par exemple :
extensionInt {
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 d'ajouter des fonctionnalités très pratiques à des classes auxquelles vous n'avez pas accès.
Par exemple, il existe une méthode pour obtenir un nombre aléatoire, qui n’est pas très agréable à utiliser :
Int(arc4random_uniform(UInt32(10)))
On va créer une méthode de classe qui soit plus simple à utiliser que le code ci-dessus :
extensionInt {
// (...)
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 d'ajouter 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
Eh 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 utiliserez 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'autocomplé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 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 {
guard let name = name, name.isEmpty == false else {
return .rejected("Vous n'avez pas indiqué votre nom !")
}
guard let phone = phone, phone.isEmpty == false else {
return .rejected("Vous n'avez pas indiqué votre téléphone !")
}
guard let race = race, race.isEmpty == false else {
return .rejected("Quel est votre race ?")
}
guard hasMajority else {
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
Nous l’avons vu précédemment, les extensions sont aussi un bon moyen de séparer le code de la classe même du code qui ajoute la conformance à un protocole. C’est même une bonne pratique à respecter !
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() {
let pet = createPetObject()
checkPetStatus(pet)
}
private func createPetObject() -> Pet {
let name = nameTextField.text
let phone = phoneTextField.text
let hasMajority = majoritySwitch.isOn
let gender = genderSegmentedControl.selectedSegmentIndex == 0 ? Pet.Gender.male : Pet.Gender.female
let race = dogRaces[racePickerView.selectedRow(inComponent: 0)]
return Pet(name: name, hasMajority: hasMajority, phone: phone, race: race, gender: gender)
}
private func checkPetStatus(_ pet: Pet) {
switch pet.status {
case .accepted: performSegue(withIdentifier: "segueToSuccess", sender: pet)
case .rejected(let reason):
presentAlert(with: reason)
}
}
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
let pet = sender as? Pet
successVC?.dog = pet
}
}
}
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'utilisation majeurs :
Ajouter une nouvelle fonctionnalité.
Ajouter des constantes.
Organiser le code.
Bravo ! Vous êtes venu à bout de ce gros chapitre bonus ! Je savais bien que je n'avais pas encore pressé tout le jus de votre cerveau !