Pixel

Découverte de SwiftData

09/06/2023

Avec les nouvelles versions de leurs différents systèmes d’exploitation, Apple introduit Swift Data: Une combinaison de Core data avec les fonctionnalités modernes de Swift.

Mise en place d’un modèle dans SwiftData

La mise en place de SwiftData est très simple: Il suffit d’importer le framework SwiftData et d’annoter la class avec Model

import SwiftData

// Annotate new or existing model classes with the @Model macro.
@Model
class Trip {
    var name: String
    var destination: String
    var startDate: Date
    var endDate: Date
    var accommodation: Accommodation?
}

Par défaut, SwiftData inclut toutes les propriétés non calculées d’une classe, à condition qu’elles utilisent des types compatibles. Le framework prend en charge les types primitifs tels que Bool, Int et String, ainsi que les types de valeurs complexes tels que les structures, les énumérations et d’autres types de valeurs conformes au protocole Codable.

Personnaliser le comportement de persistance des attributs du modèle

Dans la plupart des cas, le comportement par défaut du framework est suffisant. Cependant, il peut arriver d’avoir besoin de modifier la façon dont SwiftData gère la persistance. Pour ce faire, il existe des annotations.

Par exemple, si vous souhaitez éviter des conflits en spécifiant un attribut comme étant unique dans toutes les instances du modèle, l’annotation Attribute(_:renamingIdentifier:hashModifier:) met en place cette règle d’unicité de valeur dans votre modèle de données.

import SwiftData

@Model
class Trip {
  // with Attribute(.unique) the value of an attribute is unique for all instances of this model.
    @Attribute(.unique) var name: String
    var destination: String
    var startDate: Date
    var endDate: Date
    var accommodation: Accommodation?
}

RelationShip

SwiftData gère implicitement la relation entre deux modèles (Ou collection de modèles)

import SwiftData

@Model
class Trip {
    ...
    var accommodation: Accommodation?
}

...

@Model 
class Accomodation {
  ...
}

Par défaut, le framework définit à nil les attributs de relation lorsque vous supprimez une instance de modèle liée.

L’annotation Relationship(_:_:renamingIdentifier:inverse:hashModifier:) permet de gérer ces relations. Par exemple, vous pouvez vouloir supprimer toutes les instances Accomodation liées lors de la suppression d’un Trip.

import SwiftData

@Model
class Trip {
    ...
    @Relationship(.cascade) var accommodation: Accommodation?
}

Pour plus d’informations sur les règles de suppression: RelationshipDeleteRule.

Dans certains cas, vous ne souhaitez pas stocker des attributs calculés ou temporaires.

Par exemple, si vous souhaitez stocker temporairement la météo d’un Trip. L’annotation Transient est là pour ça.

import SwiftData

@Model
class Trip {
    ...
  @Transient var destinationWeather = Weather.current()
}

Configurer le stockage des modèles

Avant que le framework puisse examiner les modèles et générer le schéma, il faut lui indiquer à l’exécution les modèles à persister et éventuellement la configuration à utiliser pour le stockage.

Par exemple, pour des tests, vous pouvez vouloir stocker les données uniquement en mémoire, ou bien utiliser un conteneur CloudKit spécifique pour la synchronisation entre appareils.

Pour ce faire, il faut utiliser le modificateur de vue modelContainer(for: inMemory: isAutosaveEnabled: isUndoEnabled: onSetup: ):

En ajoutant ce modificateur de vue tout en haut de la hiérarchie des vues, toutes les vues imbriquées héritent de l’environnement.

import SwiftUI
import SwiftData


@main
struct TripsApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .modelContainer(for: [
                    Trip.self,
                    Accommodation.self
                ])
        }
    }
}

Il est possible d’utiliser ModelConfiguration pour créer un stockage personnalisé. Le type fournit un certain nombre d’options à configurer:

  • le stockage n’existe qu’en mémoire.
  • le stockage est en lecture seule.
  • l’application utilise un App Group spécifique pour stocker les données de son modèle.
let configuration = ModelConfiguration(inMemory: true, readOnly: true)


let container = try ModelContainer(
    for: [Trip.self, Accommodation.self], 
    configurations: configuration
)

Sauvegarde, récupération des données

Pour gérer les instances de vos classes de modèles au moment de l’exécution, utilisez un contexte de modèle. Pour obtenir un contexte pour votre conteneur de modèle lié à l’acteur principal, utilisez la variable d’environnement modelContext

import SwiftUI
import SwiftData


struct ContentView: View {
    @Environment(\.modelContext) private var context
}

// Outside of SwiftUI Views

let context = container.mainContext

Dans les deux cas, le contexte renvoyé vérifie périodiquement s’il contient des modifications non enregistrées et, si c’est le cas, enregistre implicitement ces modifications en votre nom. Pour les contextes que vous créez manuellement, définissez la propriété autosaveEnabled à true pour obtenir le même comportement.

Pour permettre à SwiftData de persister une instance de modèle et de commencer à suivre les modifications qui lui sont apportées, insérez l’instance dans le contexte :

var trip = Trip(name: name, 
                destination: destination, 
                startDate: startDate, 
                endDate: endDate)


context.insert(trip)

Après l’insertion, vous pouvez sauvegarder immédiatement en invoquant la méthode #save() du contexte, ou vous fier plutôt au comportement de sauvegarde implicite du contexte. Les contextes suivent automatiquement les modifications apportées à leurs instances de modèle connues et les incluent dans les sauvegardes ultérieures. Outre la sauvegarde, vous pouvez utiliser un contexte pour récupérer, énumérer et supprimer des instances de modèle. Pour plus d’informations, voir ModelContext.

Récupération des modèles

Pour récupérer les instances de modèles, SwiftData fournit Queryet FetchDescriptor pour effectuer des récupérations.

L’annotation @Query dans une vue SwiftUI permet de récupérer le modèle avec éventuellement des critères de recherche ou un ordre de tri.

L’annotation @Model ajoute la conformité Observable aux modèles ce qui permet à SwiftUI de rafraîchir l’affichage des données à chaque modification d’une instance récupérée.

import SwiftUI
import SwiftData


struct ContentView: View {
    @Query(sort: \.startDate, order: .reverse) var allTrips: [Trip]
    
    var body: some View {
        List {
            ForEach(allTrips) {
                TripView(for: $0)
            }
        }
    }
}

En-dehors d’une vue SwiftUI, chaque méthode de ModelContext attend une instance deFetchDescriptor contenant un prédicat et un ordre de tri.

let context = container.mainContext


let upcomingTrips = FetchDescriptor<Trip>(
    predicate: #Predicate { $0.startDate > Date.now },
    sort: \.startDate
)
upcomingTrips.fetchLimit = 50
upcomingTrips.includePendingChanges = true


let results = context.fetch(upcomingTrips)

Conclusion

Ici s’arrête notre découverte de SwiftData qui offre une solution simple et efficace pour la persistance des données dans des applications Swift à partir des nouvelles versions des systèmes Apple.

Ne manquez pas nos autres articles sur le blog de Frianbiz !

#Swift
#SwiftData
#iOS17
Avatar Lucas Colomer Lucas Colomer
red pixel blue pixel