Module 3 - Note de cours

← Retour au module principal

Introduction au langage Swift (Partie 2)

Référence officielle

Accessibilité

En Swift, le contrôle d’accès est un mécanisme essentiel qui détermine la visibilité et l’accessibilité des différentes parties de votre code, telles que les classes, les structures, les fonctions et les propriétés. Il vous permet de restreindre l’accès à certaines parties de votre code afin de protéger son intégrité et d’encapsuler les détails d’implémentation.

Niveaux d’accès en Swift

Exemples :


  public class PublicClass {
    open func openMethod() {
        // Peut être surchargée en dehors du module
    }
    
    public func publicMethod() {
        // Peut être appelée en dehors du module, mais ne peut être surchargée qu'à l'intérieur
    }
    
    internal func internalMethod() {
        // Accessible uniquement au sein du module
    }
    
    fileprivate func fileprivateMethod() {
        // Accessible uniquement dans ce fichier source
    }
    
    private func privateMethod() {
        // Accessible uniquement dans cette classe
    }
  }
      

Bonnes pratiques :

En comprenant et en appliquant correctement ces niveaux d’accès, vous pouvez créer des applications iOS plus robustes et mieux structurées, tout en protégeant les parties sensibles de votre code.

Getters et Setters

En Swift, les propriétés peuvent utiliser des accesseurs (get) et des mutateurs (set) pour contrôler l’accès en lecture ou en écriture.


  class Person {
    private var _age: Int = 0

    var age: Int {
        get { return _age }
        set { _age = newValue }
    }
  }

  let person = Person()
  person.age = 25
  print(person.age) // Affiche : 25
      

Méthodes de type

En Swift, les méthodes de type sont des fonctions qui appartiennent directement à une classe ou une structure, et non à une instance spécifique. Pour les définir, on utilise les mots-clés static ou class.

Méthode de type avec static

Ici, additionner est une méthode de type définie avec static. Elle peut être appelée directement sur le type Math sans créer d’instance.


  struct Math {
    static func additionner(a: Int, b: Int) -> Int {
        return a + b
    }
  }
      
  let resultat = Math.additionner(a: 5, b: 3)
  print(resultat) // Affiche : 8
      

Méthode de type avec class

Dans cet exemple, la méthode de type description est définie avec class, permettant aux sous-classes de la surcharger. Ainsi, Chien fournit sa propre implémentation de description.


  class Animal {
    class func description() -> String {
        return "Ceci est un animal."
    }
  }
  
  class Chien: Animal {
    override class func description() -> String {
        return "Ceci est un chien."
    }
  }
  
  print(Animal.description()) // Affiche : Ceci est un animal.
  print(Chien.description())  // Affiche : Ceci est un chien.
      

Quand utiliser static ou class ?

Propriétés paresseuses (Lazy property)

En Swift, une propriété paresseuse (lazy property) est une propriété dont la valeur n’est calculée et initialisée qu’au moment où elle est utilisée pour la première fois. Cela permet d’optimiser les performances en retardant des calculs coûteux ou en différant l’initialisation d’objets jusqu’à ce qu’ils soient réellement nécessaires.

Déclaration d’une propriété paresseuse

Pour déclarer une propriété paresseuse, on utilise le mot-clé lazy avant la déclaration de la propriété. Les propriétés paresseuses doivent toujours être des variables (var), car leur valeur peut ne pas être initialisée au moment de la création de l’instance.

Exemple


  class DataLoader {
    lazy var data: [String] = loadData()

    func loadData() -> [String] {
        // Simuler un chargement de données
        print("Chargement des données...")
        return ["Donnée1", "Donnée2", "Donnée3"]
    }
  }
  
  let loader = DataLoader()
  // À ce stade, la propriété 'data' n'est pas encore initialisée
  print(loader.data) // Déclenche l'appel à 'loadData' et initialise 'data'
    

Explications

  1. Dans cet exemple, la classe DataLoader possède une propriété paresseuse data.
  2. La méthode loadData() simule le chargement de données et est utilisée pour initialiser data.
  3. Lorsque l’instance loader est créée, la propriété data n’est pas immédiatement initialisée.
  4. L’accès à loader.data pour la première fois déclenche l’appel à loadData(), initialise data et affiche “Chargement des données…”.

Avantages des propriétés paresseuses

Override

En Swift, le mot-clé override est utilisé pour indiquer qu’une méthode, une propriété ou un sous-script d’une classe dérivée redéfinit l’implémentation héritée de sa classe parente. Cela permet à une sous-classe de fournir une version spécifique d’une fonctionnalité définie dans sa superclasse.

Pourquoi utiliser override ?

L’utilisation du mot-clé override garantit que vous redéfinissez intentionnellement une méthode ou une propriété existante dans la superclasse. Swift vérifie alors que la méthode ou la propriété que vous tentez de redéfinir existe bien dans la superclasse, évitant ainsi les erreurs potentielles dues à des fautes de frappe ou à des incohérences.

Exemple de redéfinition de méthode


  class Animal {
    func makeSound() {
        print("Animal sound")
    }
  }

  class Dog: Animal {
    override func makeSound() {
        print("Bark")
    }
  }

  let dog = Dog()
  dog.makeSound() // Affiche : Bark
      

Accéder à l’implémentation de la superclasse

Il est possible, dans une méthode redéfinie, d’appeler l’implémentation de la superclasse en utilisant le mot-clé super. Cela peut être utile si vous souhaitez étendre le comportement hérité plutôt que de le remplacer complètement.


  class Animal {
    func makeSound() {
        print("Son d'animal")
    }
  }
  
  class Chat: Animal {
    override func makeSound() {
        super.makeSound()
        print("Miauler")
    }
  }
  
  let monChat = Chat()
  monChat.makeSound()
  // Affiche :
  // Son d'animal
  // Miauler
      

Remarques importantes :

  1. Le mot-clé override est obligatoire en Swift lorsque vous redéfinissez une méthode, une propriété ou un sous-script hérité. Cela permet au compilateur de vérifier que la redéfinition correspond bien à une déclaration existante dans la superclasse.
  2. Si vous tentez de redéfinir une méthode ou une propriété qui n’existe pas dans la superclasse, le compilateur générera une erreur, vous aidant ainsi à détecter les incohérences dans votre code.
  3. Il est possible d’empêcher une méthode ou une propriété d’être redéfinie dans une sous-classe en la déclarant avec le mot-clé final. Par exemple :

  class Animal {
    final func makeSound() {
    print("Son d'animal")
    }
  }
      

Toute tentative de redéfinir makeSound() dans une sous-classe entraînera une erreur de compilation.

Wrappers de propriété

En Swift, les wrappers de propriété (property wrappers) sont une fonctionnalité qui permet d’ajouter une couche de logique autour de la gestion des propriétés. Ils facilitent la réutilisation de comportements courants, comme la validation des données ou la gestion du stockage, sans répéter le même code pour chaque propriété.

Qu’est-ce qu’un wrapper de propriété?

Un wrapper de propriété est une structure ou une classe qui encapsule une propriété, en ajoutant une logique personnalisée lors de l’accès en lecture ou en écriture. Cela permet de séparer la logique de gestion des données de la définition de la propriété elle-même.

Comment définir un wrapper de propriété?

Pour créer un wrapper de propriété, on utilise l’attribut @propertyWrapper et on définit une propriété nommée wrappedValue qui représente la valeur encapsulée. Par exemple, voici un wrapper qui garantit que toutes les chaînes de caractères assignées sont automatiquement mises en majuscules :


  @propertyWrapper
  struct Majuscule {
    private var valeur: String = ""

    var wrappedValue: String {
        get { valeur }
        set { valeur = newValue.uppercased() }
    }
  }
      

Dans cet exemple, chaque fois qu’une nouvelle valeur est assignée à wrappedValue, elle est convertie en majuscules avant d’être stockée.

Utilisation d’un wrapper de propriété

Une fois le wrapper défini, on peut l’appliquer à une propriété en utilisant l’annotation correspondante. Par exemple, si nous avons une structure Utilisateur et que nous voulons que le nom soit toujours en majuscules :


  struct Utilisateur {
    @Majuscule var nom: String
  }
  
  var utilisateur = Utilisateur()
  utilisateur.nom = "Jean Dupont"
  print(utilisateur.nom) // Affiche : JEAN DUPONT
      

Ici, la propriété nom utilise le wrapper Majuscule, garantissant que toute valeur assignée est automatiquement convertie en majuscules.

Avantages des wrappers de propriété

Limitations

Il est important de noter que les wrappers de propriété ne peuvent pas participer à la gestion des erreurs et qu’il n’est pas possible d’appliquer plusieurs wrappers à une même propriété.

Gestion des erreurs

En Swift, la gestion des erreurs permet de répondre aux situations inattendues qui peuvent survenir lors de l’exécution d’un programme, comme l’échec de la lecture d’un fichier ou une connexion réseau interrompue. Swift offre des mécanismes robustes pour détecter, propager et gérer ces erreurs de manière contrôlée.

Définition des erreurs

Les erreurs en Swift sont représentées par des types qui adoptent le protocole Error. Généralement, on utilise des énumérations pour définir les différentes erreurs possibles :


  enum ErreurFichier: Error {
    case fichierIntrouvable
    case lectureImpossible
  }
      

Propagation des erreurs

Une fonction susceptible de générer une erreur doit être marquée avec le mot-clé throws. Pour signaler une erreur, on utilise l’instruction throw :


  func lireFichier(chemin: String) throws -> String {
    // Vérifier si le fichier existe
    guard fichierExiste(chemin) else {
        throw ErreurFichier.fichierIntrouvable
    }
    // Tentative de lecture du fichier
    // ...
  }
      

Appel des fonctions pouvant générer des erreurs

Lorsqu’on appelle une fonction qui peut générer une erreur, on utilise le mot-clé try. Cet appel doit être effectué dans un bloc do-catch pour gérer les éventuelles erreurs:


  do {
    let contenu = try lireFichier(chemin: "chemin/vers/fichier.txt")
    print(contenu)
  } catch ErreurFichier.fichierIntrouvable {
      print("Erreur : Fichier introuvable.")
  } catch ErreurFichier.lectureImpossible {
      print("Erreur : Lecture du fichier impossible.")
  } catch {
      print("Erreur inattendue : \(error).")
  }
      

Autres moyens de gérer les erreurs

Quand une fonction peut provoquer une erreur (comme lire un fichier), on peut utiliser try de plusieurs manières :

try?


if let contenu = try? lireFichier(chemin: "chemin/vers/fichier.txt") {
    print(contenu)
} else {
    print("Erreur lors de la lecture du fichier.")
}

Ici, si le fichier est trouvé, on affiche son contenu. Sinon, on affiche un message d’erreur. C’est simple et sécurisé, parfait quand tu n’as pas besoin de savoir quelle est l’erreur exactement.

try!


let contenu = try! lireFichier(chemin: "chemin/vers/fichier.txt")
print(contenu)

Ici, si le fichier n’existe pas ou ne peut pas être lu, l’app crashe immédiatement. Ce n’est pas recommandé, sauf dans des cas très précis (comme pour des fichiers intégrés dans l’app et sûrs à 100 %).

Résumé

La gestion des erreurs en Swift est essentielle pour créer des applications robustes et fiables. En définissant clairement les erreurs possibles, en les propageant de manière appropriée et en les gérant avec des blocs do-catch, vous pouvez anticiper et réagir efficacement aux situations inattendues qui pourraient survenir lors de l’exécution de votre programme.

Any et AnyObject

En Swift, les types spéciaux Any et AnyObject offrent une flexibilité accrue lors de la manipulation de données de types variés.


  let collectionMixte: [Any] = [42, "Bonjour", 3.14, true]
      

  class MaClasse {}
  let tableauObjets: [AnyObject] = [MaClasse(), NSString(string: "Bonjour")]
      

Différences clés

Conversion de type

Lorsque vous récupérez des valeurs à partir d’une collection de Any ou AnyObject, il est souvent nécessaire de les convertir vers leur type d’origine


  let collectionMixte: [Any] = [42, "Bonjour", 3.14]
  if let premierÉlément = collectionMixte[0] as? Int {
      print("Le premier élément est un entier : \(premierÉlément)")
  }
      

Bonnes pratiques

Extensions

En Swift, les extensions permettent d’ajouter de nouvelles fonctionnalités à des classes, structures, énumérations ou protocoles existants, même si vous n’avez pas accès à leur code source original. Cette capacité est particulièrement utile pour enrichir des types prédéfinis ou provenant de bibliothèques tierces, sans modifier leur implémentation initiale.

Utilisations courantes des extensions

Ajouter des méthodes d’instance et de type : Vous pouvez introduire de nouvelles méthodes qui étendent les capacités d’un type existant.


  extension Int {
    func estPair() -> Bool {
        return self % 2 == 0
    }
  }

  let nombre = 4
  print(nombre.estPair()) // Affiche : true
      

Ajouter des propriétés calculées : Vous pouvez définir des propriétés qui calculent leur valeur à partir d’autres propriétés existantes.


  extension Double {
    var enKilomètres: Double {
        return self * 1_000.0
    }
  }
  
  let distance = 5.0
  print("\(distance.enKilomètres) mètres") // Affiche : 5000.0 mètres
      

Fournir des implémentations par défaut pour des protocoles : En utilisant des extensions, vous pouvez offrir des implémentations par défaut pour les méthodes d’un protocole, réduisant ainsi le besoin d’implémentations répétitives.


  protocol Identifiable {
    var id: String { get }
    func afficherID()
  }

  extension Identifiable {
    func afficherID() {
        print("Mon identifiant est \(id).")
    }
  }

  struct Utilisateur: Identifiable {
    var id: String
  }

  let utilisateur = Utilisateur(id: "12345")
  utilisateur.afficherID() // Affiche : Mon identifiant est 12345.
      

Conformer un type existant à un protocole : Les extensions permettent de déclarer qu’un type existant adopte un protocole, et de fournir les implémentations nécessaires.


  struct Point {
    var x: Double
    var y: Double
  }
  
  extension Point: CustomStringConvertible {
    var description: String {
        return "Point(x: \(x), y: \(y))"
    }
  }
  
  let point = Point(x: 3.0, y: 4.0)
  print(point) // Affiche : Point(x: 3.0, y: 4.0)
      

Limitations des extensions

  1. Les extensions ne peuvent pas ajouter de propriétés stockées ou de variables d’instance.
  2. Elles ne peuvent pas redéfinir les méthodes existantes ou les propriétés d’un type.
  3. Si une méthode ajoutée par une extension entre en conflit avec une méthode existante, le comportement peut être imprévisible.

Bonnes pratiques

  1. Utilisez les extensions pour regrouper des fonctionnalités similaires, améliorant ainsi la lisibilité et la maintenance du code.
  2. Évitez d’utiliser les extensions pour remplacer des sous-classes lorsque l’héritage est plus approprié.
  3. Documentez clairement les extensions pour indiquer qu’elles ajoutent des fonctionnalités à des types existants.

Les extensions sont un outil puissant en Swift, offrant une flexibilité accrue pour adapter et enrichir les types existants selon les besoins spécifiques de votre application.