Skip to content

Blog


Fonctionnement du cycle de vie des vues et de l'identité SwiftUI

31 mai 2022

|
Mike Zaslavskiy

Mike Zaslavskiy

Anthony Williams

Anthony Williams

Ryan Zhang

Ryan Zhang

Alors qu'UIKit a été le framework de référence pour les ingénieurs iOS pour construire des interfaces utilisateur dans leurs applications au fil des ans, SwiftUI a gagné régulièrement du terrain en tant que framework alternatif qui corrige de nombreux inconvénients d'UIKit. Par exemple, SwiftUI nécessite beaucoup moins de code pour construire la même interface utilisateur, et produit toujours une mise en page valide. Les développeurs n'ont plus besoin de passer des heures à déboguer les problèmes de mise en page automatique. Dans cet article, nous allons d'abord comparer l'approche événementielle d'UIKit à l'approche orientée données de SwiftUI, puis nous plongerons dans le cycle de vue, l'identité et le processus de rendu de SwiftUI afin de mieux comprendre comment écrire du code performant dans SwiftUI.

Fonctionnement d'un cadre événementiel

UIKit fournit une interface utilisateur événementielle par nature, où les vues sont créées par une séquence d'événements qui effectuent des opérations et finissent par se rassembler pour former ce qui est vu à l'écran. Dans un cadre événementiel, il doit y avoir un contrôleur qui colle la vue et les événements. Cette colle est appelée contrôleur de vue.

Fonctionnement du contrôleur de vue

Le contrôleur de vue est essentiellement un centre de contrôle qui décide de ce qui se passe en fonction d'événements particuliers. Par exemple, si un contenu doit être affiché à l'écran lors du chargement d'une page, le contrôleur de vue écoute l'événement de chargement de la page et exécute la logique métier nécessaire pour charger et afficher le contenu. Examinons un exemple plus spécifique :

Supposons qu'il y ait un bouton qui, lorsqu'on clique dessus, affiche une image d'un type de fruit aléatoire sur l'écran. Après chaque nouveau clic sur le bouton, une nouvelle sorte de fruit est affichée. Examinons une représentation du flux si cela était construit avec UIKit dans la figure 1 ci-dessous.

Figure 1 : Flux d'un cadre événementiel
Figure 1 : Flux d'un cadre événementiel

Dans ce flux, le contrôleur de vue conserve une référence au bouton et à la vue. Lorsqu'un utilisateur clique sur le bouton, le contrôleur de vue prend cela comme un signal pour calculer un nouveau type de fruit. Une fois que le nouveau fruit est renvoyé, le contrôleur de vue demande à la vue de mettre à jour l'interface utilisateur. Dans ce cas, l'événement de clic sur le bouton commande la logique qui modifie l'interface utilisateur.

Restez informé grâce aux mises à jour hebdomadaires

Abonnez-vous à notre blog d'ingénierie pour recevoir régulièrement des informations sur les projets les plus intéressants sur lesquels notre équipe travaille.

Les défis de l'utilisation d'UIKit et des contrôleurs de vue

Même s'il s'agit d'un exemple très simple, nous pouvons voir que le contrôleur de vue a plusieurs responsabilités. Avec des vues plus complexes dans une application de production, ces responsabilités signifient que le contrôleur de vue peut devenir massif et difficile à gérer. Nous devons écrire le code et dicter la logique de l'interaction entre le contrôleur de vue, la vue et chaque événement, ce qui peut être source d'erreurs et difficile à lire.

Bien sûr, une grande partie de la douleur liée au traitement du contrôleur de vue peut être atténuée par une bonne architecture de code et une séparation des préoccupations. L'architecture VIP que notre application iOS DoorDash utilise permet d'extraire la logique métier et la logique de présentation, de sorte que le contrôleur de vue n'a pas besoin de connaître cette logique et peut simplement se concentrer sur l'affichage de la vue à l'écran en fonction des données. 

Mais toute architecture ne peut éviter le contrôleur de vue, car son rôle de ciment entre les événements et la vue est irremplaçable dans un cadre axé sur les événements.

Fonctionnement d'un cadre axé sur les données 

Alors qu'UIKit utilise un framework piloté par les événements, SwiftUI est basé sur un framework piloté par les données. Dans SwiftUI, les vues sont une fonction de l'état, et non une séquence d'événements(WWDC 2019). Une vue est liée à certaines données (ou état) en tant que source de vérité, et se met automatiquement à jour chaque fois que l'état change. Ceci est réalisé en définissant les vues comme des fonctions qui prennent la liaison de données comme argument.

Ce cadre axé sur les données élimine complètement le contrôleur de vue en tant qu'intermédiaire. Ce que l'utilisateur voit à l'écran est directement contrôlé par un état, qui peut être n'importe quel type de données. En utilisant le même exemple d'application de fruits que nous avons utilisé ci-dessus avec UIKit, nous pouvons voir une illustration de ce concept ci-dessous dans la Figure 2.

Figure 2 : Flux d'un cadre axé sur les données

Le type de fruit est un état lié à la vue, ce qui signifie que chaque fois que le fruit est mis à jour, cela se répercute automatiquement sur la vue. Cela signifie que lorsqu'un utilisateur clique sur le bouton, il suffit de mettre à jour l'état pour que la vue se mette à afficher le nouveau fruit, sans avoir besoin d'un contrôleur pour le lui demander. D'où le terme "piloté par les données" : l'interface utilisateur est une représentation directe des données.

Les avantages d'un cadre fondé sur les données 

Travailler avec un cadre axé sur les données signifie qu'il n'y a plus de contrôleurs de vue massifs et qu'il n'est plus nécessaire de définir la logique des événements pour effectuer les mises à jour de la vue. L'interface est couplée aux données, ce qui réduit le nombre de lignes de code et améliore la lisibilité. Nous pouvons facilement comprendre que le fruit que la vue affiche est piloté par l'état du fruit, contrairement à UIKit, où nous devrions fouiller dans le code pour voir comment le fruit est contrôlé.

Les défis de l'utilisation de SwiftUI

Tout nouveau framework ou technologie présente des compromis. En se basant uniquement sur les comparaisons de frameworks basés sur les événements et les données mentionnées ci-dessus, SwiftUI peut toujours apparaître comme l'option supérieure, mais ce n'est pas tout à fait vrai. 

Les inconvénients de SwiftUI sont principalement liés au fait qu'il n'a été publié qu'il y a trois ans. SwiftUI est un nouveau framework, il faudra donc du temps pour que les développeurs l'adoptent et l'apprennent. Compte tenu de l'adoption en cours, il y a moins d'architectures de code établies basées sur SwiftUI. Nous avons également rencontré des problèmes de rétrocompatibilité, lorsque le même code SwiftUI fonctionne différemment dans iOS 14 et 15, ce qui rend le débogage très difficile. 

Maintenant que nous avons une compréhension de base des avantages et des inconvénients des deux types de frameworks, nous allons nous plonger dans les défis spécifiques que nous avons rencontrés avec SwiftUI et son processus de rendu de vue, et comment écrire un code efficace pour préserver l'identité d'une vue afin de créer une interface utilisateur fluide et optimale.

Voir dans SwiftUI

Certains concepts principaux méritent d'être mentionnés lorsque l'on travaille avec SwiftUI :

  • Vue en fonction de l'état
  • Identité de la vue
  • Durée de vie de la vue

Tout d'abord, les données sont la source de vérité pour la vue. Lorsque les données changent, nous recevons les mises à jour sur une vue. Nous savons donc déjà que les vues dans SwiftUI sont fonction d'un état (Figure 3). Mais quel est cet état dans le monde de SwiftUI ?

Figure 3 : La vue est une fonction de l'état et non une séquence d'événements

Lorsque l'on passe d'une architecture événementielle à un framework déclaratif, on peut se poser quelques questions. Il n'est pas difficile de comprendre les bases de SwiftUI, mais ce qui se passe sous le capot n'est pas très clair. Nous savons que lorsque l'état de la vue change, la vue est mise à jour, mais certaines questions se posent naturellement :

  • Comment les données sont-elles mises à jour ?
  • Comment le point de vue comprend-il ce qui doit exactement changer ?
  • Crée-t-il une nouvelle vue à chaque fois qu'un petit élément de données est modifié ?
  • Quelle est l'efficacité et le coût des mises à jour des données ?

Il est essentiel de comprendre le fonctionnement interne du framework. Les réponses à ces questions et à d'autres peuvent aider à résoudre certains comportements indésirables dans nos applications, comme des performances médiocres, des bogues aléatoires ou des animations inattendues. Cela permettra de développer des applications bien optimisées et exemptes de bogues. 

A propos de la hiérarchie des vues de SwiftUI. 

L'élément principal de l'interface utilisateur dans SwiftUI est une vue. Les performances et la qualité de la partie visuelle de l'application dépendent de l'efficacité de sa définition et de ses manipulations d'état. Jetons un coup d'œil à la vue par défaut qui a été créée pour un modèle SwiftUI dans Xcode :

struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
            .padding()
    }
}

Il existe une structure ContentView conforme au protocole View:

public protocol View {
    associatedtype Body : View
 
    @ViewBuilder var body: Self.Body { get }
}

Une propriété de corps calculée définit le contenu de la vue. La composition des vues SwiftUI forme une hiérarchie de vues. Le protocole View a un type associé, qui est également une View. À un moment donné, SwiftUI essaiera de rendre le ContentView, et demandera simplement le corps du ContentView. Cependant, si la vue de contenu ne contient pas une vue Text primitive, mais une autre vue personnalisée, SwiftUI devra demander à toutes les vues personnalisées imbriquées leur corps afin de les afficher. Jetons un coup d'œil à cet exemple :

struct FruitsView: View {
    var body: some View {
        BananaView()
    }
}
 
struct BananaView: View {
    var body: some View {
        Text("I am banana!")
            .padding()
    }
}

Dans ce cas, FruitsView demandera son corps à BananaView, car il doit savoir ce qu'il doit afficher. BananaView demande son corps au Text. Il s'agit d'une série d'appels récursifs, comme le montre la figure 4, car chaque vue a un corps, et le corps renvoie une vue.

Figure 4 : Séquence d'appels au corps de View
Figure 4 : Séquence d'appels au corps de View

SwiftUI, afin d'avoir de bonnes performances, doit couper court et briser la récursion d'une manière ou d'une autre. Dans notre cas, la récursion se terminera lorsque SwiftUI tentera de demander au texte son corps, car le texte, ainsi que d'autres composants de SwiftUI, est un type primitif. Il peut être dessiné sans demander son corps. Ceci est réalisé avec un type Never:

extension Text : View {
    public typealias Body = Never
}
 
extension Never : View {
    public typealias Body = Never
    public var body: Never { get }
}

De plus, Never est conforme au protocole View. Ainsi, notre récursivité se terminera lorsque nous atteindrons le type primitif, comme le montre la figure 5, car SwiftUI traitera les types primitifs d'une manière spéciale.

Figure 5 : Fin de la récursion lorsque le type Jamais est atteint
Figure 5 : Fin de la récursion lorsque le type Jamais est atteint

Les types primitifs constituent la base de toute hiérarchie de vues. Le texte est l'un des types de vues primitives, mais il en existe d'autres :

  • Texte
  • Image
  • Entretoise
  • ZStack
  • VStack
  • HStack
  • Liste
  • Etc.

Système de gestion de l'État

Chaque vue a un état, qui peut être modifié au cours de l'exécution de notre application. L'état est une source unique de vérité pour cette vue. La vue et son état ont des mécanismes qui pilotent les mises à jour du corps, donc à chaque fois que l'état de la vue change, le corps est demandé. Dans SwiftUI, l'état peut être créé de différentes manières, par exemple :

  •  @State
  •  @StateObject
  •  @Binding
  •  @ObservedObject
  •  @EnvironmentObject.

@State

L'état est une source de vérité pour la vue et il est utilisé lorsque la portée des changements est limitée à la vue uniquement. En enveloppant les types de valeurs dans des propriétés d'état transitoires, le cadre alloue un stockage persistant pour ce type de valeur et en fait une dépendance, de sorte que les modifications de l'état seront automatiquement reflétées dans la vue. C'est une bonne pratique d'utiliser un mot-clé private lors de la déclaration de State, parce qu'il est conçu pour être utilisé par la vue en interne.

@StateObject

Ce property wrapper doit être appliqué au type qui se conforme au protocole ObservedObject et permet de suivre les changements de cet objet et de le traiter comme un état. SwiftUI crée une nouvelle instance de l'objet une seule fois pour chaque instance de la structure qui déclare l'objet. Lorsque les propriétés publiées de l'objet observable changent, SwiftUI met à jour les parties de la vue qui dépendent de ces propriétés.

@ObservedObject

Il s'agit d'un type d'enveloppeur de propriété qui s'abonne à un objet observable et invalide une vue chaque fois que l'objet observable change. Cette enveloppe de propriété est très similaire à @StateObject ; la principale différence est que @StateObject est utilisé pour créer initialement la valeur et que nous pouvons ensuite la transmettre en tant que dépendance aux autres vues à l'aide de @ObservedObject. 

@ObservedObject est utilisé pour garder la trace d'un objet qui a déjà été créé. 

@Binding

Ce wrapper de propriété est utile dans presque toutes les vues SwiftUI. Le binding est un wrapper de propriété qui peut lire et écrire une valeur appartenant à une source de vérité, par exemple un @State ou l'une des propriétés de @StateObject. Le signe du dollar ($) est utilisé pour préfixer la variable de propriété @State afin d'obtenir la valeur projetée, et cette valeur projetée est un binding. Vous pouvez ensuite transmettre une liaison à un niveau plus bas dans la hiérarchie de la vue et la modifier. Les modifications seront répercutées sur toutes les vues qui l'utilisent comme source de vérité.

struct BananaView: View {
    @State private var isPeeled: Bool = false
    
    var body: some View {
        Text(isPeeled ? "Peeled banana!" : "Banana!")
            .background(.yellow)
        
        PeelBananaButton(isPeeled: $isPeeled)
    }
}
 
struct PeelBananaButton: View {
    @Binding var isPeeled: Bool
    
    var body: some View {
        Button("Peel Banana") {
            isPeeled = true
        }
    }
}

@EnvironmentObject

Ce wrapper de propriété ne crée ni n'alloue l'objet lui-même. Au lieu de cela, il fournit un mécanisme pour surveiller l'environnement de la hiérarchie des vues. Par exemple, la vue parentale, qui contient la source de vérité (par exemple, StateObject), comporte plusieurs couches de vues secondaires (figure 6).

Figure 6 : Exemple de hiérarchie de vues
Figure 6 : Exemple de hiérarchie de vues

Les vues C et D dépendent des données. La transmission des données peut être réalisée en injectant continuellement l'objet observé à plusieurs reprises, jusqu'à ce que ces vues disposent d'une référence à cet objet. Les vues A et B n'ont pas vraiment besoin de connaître cet objet, puisque seules les vues C et D ont besoin des données. Cette approche peut créer du code de type "boilerplate" et apporter des dépendances supplémentaires aux vues qui n'en ont pas besoin. 

Un objet d'environnement est très utile dans ce cas. Il est défini dans une vue de niveau supérieur et toute vue enfant dans une hiérarchie de vues peut accéder à l'objet et obtenir les bonnes mises à jour de données, comme le montre la figure 7 ci-dessous. Il est possible d'accéder à l'objet observé sur une vue ancêtre à condition que l'un de ses ancêtres l'ajoute à la hiérarchie à l'aide du modificateur environmentObject(_ :):

Figure 7 : L'utilisation de l'objet environnement à quelques couches de profondeur
Figure 7 : L'utilisation de l'objet environnement à quelques couches de profondeur  

Ce sont les instruments que nous pouvons utiliser pour mettre à jour les données et faire en sorte que la vue reflète les mises à jour. Chaque petite modification du flux de données peut entraîner de multiples calculs dans le corps de la vue. Ces calculs peuvent potentiellement affecter les performances, par exemple dans le cas de l'utilisation de variables calculées non optimisées. SwiftUI est suffisamment intelligent pour détecter les changements et ne peut redessiner que les parties de la vue qui ont été réellement affectées par une mise à jour des données. Ce redécoupage est effectué à l'aide de l'outil AttributeGraph - un composant interne utilisé par SwiftUI pour construire et analyser le graphe de dépendance des données et des vues associées.

Identité d'une vue

Dans UIKit, les vues sont des classes et les classes ont des pointeurs qui identifient leurs vues. En revanche, dans SwiftUI, les vues sont des structs et n'ont pas de pointeurs. Pour être efficace et optimisée, SwiftUI doit savoir si les vues sont identiques ou distinctes. Il est également important pour le framework d'identifier les vues afin d'effectuer une transition correcte et de rendre la vue correctement une fois que certaines valeurs de la vue ont changé.

L'identité d'une vue est un concept qui apporte un peu de lumière à la magie du rendu de SwiftUI. Il peut y avoir des milliers de mises à jour dans votre application, et certaines propriétés du corps sont recalculées encore et encore. Cependant, cela ne conduit pas toujours à un nouveau rendu complet de la vue concernée. L'identité de la vue est un élément clé pour comprendre cela. Il y a deux façons d'identifier la vue dans SwiftUI, par l'identité explicite ou l'identité structurelle. Nous allons nous pencher sur ces deux types d'identité.

Identité explicite

Les vues peuvent être identifiées à l'aide d'identifiants personnalisés ou pilotés par les données. L'identité des pointeurs utilisée dans UIKit est un exemple d'identité explicite, puisque les pointeurs sont utilisés pour identifier la vue. Vous en avez probablement vu des exemples en itérant sur vos vues dans une boucle for each. L'identité explicite peut être fournie en utilisant directement l'identifiant : .id(...) . Cela lie l'identité d'une vue à la valeur donnée, qui doit être hachable :

extension View {
        @inlinable public func id<ID>(_ id: ID) -> some View where ID : Hashable
}

Supposons que nous ayons un ensemble de fruits. Chaque fruit a un nom unique et une couleur :

struct Fruit {
    let name: String
    let color: Color
}

Pour afficher une liste déroulante de fruits, la structure ForEach peut être utilisée :

struct FruitListView: View {
    let fruits = [Fruit(name: "Banana", color: .yellow),
                      Fruit(name: "Cherry", color: .red)]
    
    var body: some View {
        ScrollView {
            ForEach(fruits) { fruit in
                FruitView(fruit: fruit)
            }
        }
    }
}
 
struct FruitView: View {
    let fruit: Fruit
    
    var body: some View {
        Text("\(fruit.name)!")
            .foregroundColor(fruit.color)
            .padding()
    }
}

Cependant, cela ne se compilera pas et il y aura une erreur : Le référencement de l'initialisateur 'init(_:content :)' sur 'ForEach' nécessite que 'Fruit' soit conforme à 'Identifiable'

Ce problème peut être résolu soit en implémentant le protocole Identifiable dans la structure Fruit, soit en fournissant un keypath. Dans les deux cas, cela permettra à l'interface SwiftUI de savoir quelle identité explicite le FruitView devrait avoir :

struct FruitListView: View {
    let fruits = [Fruit(name: "Banana", color: .yellow),
                      Fruit(name: "Cherry", color: .red)]
    
    var body: some View {
        ScrollView {
            ForEach(fruits, id: \.name) { fruit in
                FruitView(fruit: fruit)
            }
        }
    }
}

Ce nouveau code sera compilé et FruitView sera identifié par son nom, puisque le nom du fruit est conçu pour être unique. 

Un autre cas d'utilisation où l'identité explicite est régulièrement utilisée est la possibilité d'effectuer un défilement manuel vers l'une des sections de la vue de défilement.

struct ContentView: View {
    let headerID = "header"
    
    let fruits = [Fruit(name: "Banana", color: .yellow),
                      Fruit(name: "Cherry", color: .red)]
    
    var body: some View {
        ScrollView {
            ScrollViewReader { proxy in
                Text("Fruits")
                    .id(headerID)
 
                ForEach(fruits, id: \.name) { fruit in
                    FruitView(fruit: fruit)
                }
 
                Button("Scroll to top") {
                    proxy.scrollTo(headerID)
                }
            }
        }
    }
}

Dans cet exemple, le fait d'appuyer sur un bouton fait défiler la vue jusqu'en haut. L'extension .id() est utilisée pour fournir des identifiants personnalisés à nos vues, en leur donnant une identité explicite. 

Identité structurelle

Chaque vue SwiftUI doit avoir une identité. Si la vue n'a pas d'identité explicite, elle a une identité structurelle. On parle d'identité structurelle lorsque la vue est identifiée à l'aide de son type et de sa position dans une hiérarchie de vues. SwiftUI utilise la hiérarchie des vues pour générer l'identité implicite des vues.

Prenons l'exemple suivant : 

struct ContentView: View {
    @State var isRounded: Bool = false
    
    var body: some View {
        if isRounded {
            PizzaView()
                .cornerRadius(25)
        } else {
            PizzaView()
                .cornerRadius(0)
        }
 
        PizzaView()
            .cornerRadius(isRounded ? 25 : 0)
        
        Toggle("Round", isOn: $isRounded.animation())
            .fixedSize()
    }
}

Comme le montre l'exemple ci-dessus, il existe deux approches différentes pour mettre en œuvre le changement de rayon d'angle animé pour le PizzaView.

La première approche crée deux vues complètement distinctes, en fonction de l'état booléen. En fait, SwiftUI crée une instance de la vue ConditionalContent dans les coulisses. Cette vue ConditionalContent est chargée de présenter l'une ou l'autre vue en fonction de la condition. Et ces vues de pizzas ont des identités de vues différentes, en raison de la condition utilisée. Dans ce cas, SwiftUI redessinera la vue une fois que la bascule aura changé, et appliquera la transition de fondu entrant/sortant pour le changement, comme on peut le voir dans la Figure 8 ci-dessous. Il est important de comprendre qu'il ne s'agit pas de la même PizzaView, ce sont deux vues différentes et elles ont leurs propres identités structurelles. Il peut également être mis en œuvre à l'aide du modificateur de vue :

PizzaView()
            .cornerRadius(isRounded ? 25 : 0)

L'identité structurelle de la vue reste ainsi inchangée et SwiftUI n'applique pas la transition de fondu. Elle animera le changement du rayon de l'angle, comme le montre la figure 8 ci-dessous, car pour le cadre de travail, il s'agit de la même vue, mais avec des valeurs de propriétés différentes.

Figure 8 : La différence dans les transitions d'état - la première vue a un effet de fondu enchaîné et la deuxième vue n'anime que le changement de rayon de l'angle, l'identité structurelle de la vue restant inchangée.
Figure 8 : La différence dans les transitions d'état - la première vue a un effet de fondu enchaîné et la deuxième vue n'anime que le changement de rayon de l'angle, l'identité structurelle de la vue restant inchangée.

Dans ce cas, l'identité structurelle de la vue ne change pas. Apple recommande de préserver l'identité de la vue en plaçant des conditionnelles dans le modificateur de vue plutôt que d'utiliser des instructions if/else.

L'identité structurelle et sa compréhension sont la clé d'une application mieux optimisée avec moins de bogues. Il explique également pourquoi l'utilisation d'un modificateur de vue conditionnel peut être une mauvaise idée.  

Il y a quelques points à garder à l'esprit pour obtenir de meilleures performances :

  • Maintenir l'identité de la vue. Si vous le pouvez, n'utilisez pas d'instructions conditionnelles pour préserver l'identité. 
  • Utilisez des identifiants stables pour votre vue s'ils sont explicitement fournis.
  • Évitez d'utiliser AnyView si possible

Un exemple concret d'identité de vue chez DoorDash

Prenons un exemple dans l'application DoorDash iOS. La vue des contacts affiche la liste des contacts et permet à l'utilisateur de choisir un ou plusieurs contacts, comme le montre la figure 9 ci-dessous. Le composant liste de contacts est utilisé dans DoorDash aujourd'hui lors de l'envoi d'un cadeau.

Figure 9 : Composant de la liste de contacts
Figure 9 : Composant de la liste de contacts

Cette vue utilise le framework Contacts pour récupérer les contacts sur l'appareil et les transformer en sections avec des titres à afficher dans le composant `List` de SwiftUI.

Pour ce faire, nous itérons sur notre liste de sections à l'aide d'un `ForEach` et nous les affichons dans la liste avec pour clé l'identifiant unique de la section.

```
List {
    ForEach(listSections, id: \.id) { contactsSection in
        // Display the contact section header & rows
    }
}
```

La section `ContactSection` est responsable de l'encapsulation des propriétés nécessaires à l'affichage de la liste des contacts dans la vue. Elle contient 3 propriétés :

  1. Un identifiant unique pour la section
  2. Le titre de la section
  3. La liste des contacts de la section
```
struct ContactSection {
    let id: String = UUID().uuidString
    let title: String
    let contacts: [Contacts]
        
    init(title: String, contacts: [Contacts]) {
        self.title = title
        self.contacts = contacts
    }
}

Nos contacts s'affichent désormais dans la liste, mais nous rencontrons un problème : lorsqu'un numéro de téléphone non valide est sélectionné dans la liste, un message de toast s'anime dans la vue pour alerter le client. Lorsque le toast apparaît, la liste entière se modifie (Figure 10) comme s'il y avait de nouvelles données à présenter - ce qui n'est pas une expérience idéale pour l'utilisateur.

Figure 10 : Problème de mise à jour du corps de la liste de contacts
Figure 10 : Problème de mise à jour du corps de la liste de contacts

Au fur et à mesure que la vue est animée, Swift redessine la vue et donc notre liste. Chaque fois que nous accédons à la variable calculée qui génère les sections, la structure `ContactSection` est initialisée avec un nouvel identifiant différent pour la même section.

Dans ce cas, le titre de nos sections est la première initiale du nom du contact, ce qui rend chaque titre unique. Nous pouvons donc supprimer la propriété `id` de notre structure `ContactSection` et classer la liste par le titre au lieu de l'identifiant incohérent.

List {
    ForEach(listSections, id: \.title) { contactsSection in
        // Display the contact section header & rows
    }
}

Maintenant, comme le montre la figure 11, l'animation est superbe !

Figure 11 : Liste de contacts améliorée
Figure 11 : Liste de contacts améliorée

Lorsque l'on utilise le composant `List` dans SwiftUI, il faut se souvenir d'utiliser un identifiant persistant pour donner une clé à la liste ; cela améliore nos animations et nos performances.

Conclusion

D'après ce qui précède, nous pouvons clairement voir les avantages en termes d'expérience utilisateur et de performance lorsque nous préservons l'identité d'une vue et que nous gérons les dépendances correctement et efficacement. Ces concepts sont essentiels pour écrire des applications iOS mieux optimisées, plus fluides et plus efficaces avec SwiftUI. Le framework utilise un algorithme de diffing basé sur les types pour déterminer quelles vues doivent être redessinées à chaque changement d'état, et il fait de son mieux pour s'assurer que notre interface utilisateur reste performante et optimisée.Cependant, il est important de comprendre que ce n'est pas de la pure magie. Nous devons toujours écrire un code efficace et comprendre comment les invocations de corps fonctionnent, comment les dépendances sont gérées et comment préserver l'identité de la vue.

About the Authors

Emplois connexes

Localisation
Seattle, WA; Sunnyvale, CA; San francisco, CA
Département
Ingénierie
Localisation
San Francisco, CA ; Sunnyvale, CA ; Los Angeles, CA ; Seattle, WA ; New York, NY
Département
Ingénierie
Localisation
San Francisco, CA ; Sunnyvale, CA ; Los Angeles, CA ; Seattle, WA ; New York, NY
Département
Ingénierie
Localisation
New York, NY; San Francisco, CA; Los Angeles, CA; Seattle, WA; Sunnyvale, CA
Département
Ingénierie
Localisation
Toronto, ON
Département
Ingénierie