Quand on travaille avec SwiftData, il n'est pas nécessaire de créer physiquement la base de données ni d'y insérer manuellement les données initiales.
SwiftData se chargera de tout cela en fonction des modèles définis et des opérations qui y seront faites.
Dans cette fiche :
Une technique intéressante pour insérer des données initiales dans une base de données SwiftData consiste à définir un nouveau conteneur qui contiendra le code à exécuter.
Il faut alors apporter une modification importante au fichier de l'application dont le nom est au format MonProjetApp.swift.
Le modifieur .modelContainer() recevra le nouveau conteneur plutôt que le modèle de données.
import SwiftUI
import SwiftData
@main
struct MonProjetApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(preloadAppContainer)
}
}
Voici un extrait de code qui permet d'insérer des données initiales dans une table de la base de données.
Cet extrait sera placé dans le nouveau conteneur, que j'ai choisi d'appeler PreloadAppContainer.swift, placé au même endroit que ContentView.swift.
// Inspiré de https://www.andrewcbancroft.com/blog/ios-development/data-persistence/pre-populate-swiftdata-persistent-store/
import SwiftData
@MainActor
let preloadAppContainer: ModelContainer = {
do {
let container = try ModelContainer(for: Item.self)
// Vérifier s'il y a déjà des données
let descriptor = FetchDescriptor<Item>()
let nombre = try container.mainContext.fetchCount(descriptor)
// Si le nombre n'est pas 0, arrêter le traitement
guard nombre == 0 else { return container }
print("Insertion des données initiales.")
let donneesInitiales = [
Item(code: "A", titre: "Item A"),
Item(code: "B", titre: "Item B"),
Item(code: "C", titre: "Item C")
]
donneesInitiales.forEach { donnee in
container.mainContext.insert(donnee)
}
try? container.mainContext.save()
if let nombreAjouts = try? container.mainContext.fetchCount(descriptor) {
print("\(nombreAjouts) enregistrements ajoutés.")
}
return container
} catch {
fatalError("Impossible de créer le conteneur.")
}
}() // Les parenthèses forcent le code à s'exécuter immédiatement
Prenons comme exemple une application dans laquelle un item appartient à une catégorie et une catégorie peut avoir plusieurs items. On a donc une relation de un à plusieurs entre les catégories et les items.
Le modèle de la table enfant (ici : Item) doit avoir un constructeur qui ne requiert pas de paramètre pour la clé étrangère (ici : la catégorie).
Le modèle de la table parent (ici : Categorie) doit avoir un constructeur qui permet de définir les données liées dans la table enfant (ici : le tableau d'items). Au besoin, le constructeur pourrait définir un tableau vide s'il n'est lié à aucune donnée de la table enfant.
Avec cette structure en place, les données de la table parent sont le point de départ. Pour chacune, on pourra définir un tableau de données de la table enfant.
Le plus intéressant, c'est que SwiftData saura enregistrer l'identifiant de la catégorie dans la table des items même si on a plutôt défini les items dans l'objet Categorie.
// Inspiré de https://www.andrewcbancroft.com/blog/ios-development/data-persistence/pre-populate-swiftdata-persistent-store/
import SwiftData
@MainActor
let preloadAppContainer: ModelContainer = {
do {
let container = try ModelContainer(for: Categorie.self) // retrouvera l'item tout seul car les tables sont liées
// Vérifier s'il y a déjà des données dans la table parent
let descriptor = FetchDescriptor<Categorie>()
let nombre = try container.mainContext.fetchCount(descriptor)
// Si le nombre n'est pas 0, arrêter le traitement
guard nombre == 0 else { return container }
print("Insertion des données initiales.")
// on par de la table parent (Categorie) et, pour chaque enregistrement, on définit la liste des enfants (Item)
let donneesInitiales = [
Categorie(
titre: "Catégorie 1",
items: [
Item(code: "A", titre: "Item A"),
Item(code: "C", titre: "Item C")
]
),
Categorie(
titre: "Catégorie 2"
),
Categorie(
titre: "Catégorie 3",
items: [
Item(code: "B", titre: "Item B")
]
)
]
donneesInitiales.forEach { donnee in
container.mainContext.insert(donnee)
}
try? container.mainContext.save()
if let nombreAjouts = try? container.mainContext.fetchCount(descriptor) {
print("\(nombreAjouts) enregistrements ajoutés dans la table parent.")
}
return container
} catch {
fatalError("Impossible de créer le conteneur.")
}
}() // Les parenthèses forcent le code à s'exécuter immédiatement
Dans le cas où l'application a besoin de travailler avec des tables qui ne sont pas liées entre elles, par exemple Item et Configuration, l'approche est légèrement différente.
// Inspiré de https://www.andrewcbancroft.com/blog/ios-development/data-persistence/pre-populate-swiftdata-persistent-store/ import Foundation
import SwiftData
@MainActor
let preloadAppContainer: ModelContainer = {
do {
let schema = Schema([Item.self, Configuration.self])
let container = try ModelContainer(for: schema, configurations: [])
// Vérifier s'il y a déjà des données
let descriptor = FetchDescriptor<Item>()
let nombre = try container.mainContext.fetchCount(descriptor)
// Si le nombre n'est pas 0, arrêter le traitement
guard nombre == 0 else { return container }
print("Insertion des données initiales.")
let donneesInitialesItems = [
Item(code: "A", titre: "Item A"),
Item(code: "B", titre: "Item B"),
Item(code: "C", titre: "Item C")
]
donneesInitialesItems.forEach { donnee in
container.mainContext.insert(donnee)
}
try? container.mainContext.save()
if let nombreAjoutsItems = try? container.mainContext.fetchCount(descriptor) {
print("\(nombreAjoutsItems) enregistrements ajoutés dans la table Item.")
}
// on fait une boucle distincte pour la seconde table puisque les tables ne sont pas liées let donneesInitialesConfigurations = [
Configuration(cle: "courriel", valeur: "admin@mondomaine.com"),
Configuration(cle: "delaicapteur", valeur: "0.01")
]
donneesInitialesConfigurations.forEach { donnee in
container.mainContext.insert(donnee)
}
try? container.mainContext.save()
let descriptorConfiguration = FetchDescriptor<Configuration>()
if let nombreAjoutsConfigurations = try? container.mainContext.fetchCount(descriptorConfiguration) {
print("\(nombreAjoutsConfigurations) enregistrements ajoutés dans la table Configuration.")
}
return container
} catch {
fatalError("Impossible de créer le conteneur.")
}
}() // Les parenthèses forcent le code à s'exécuter immédiatement
Il pourrait être tentant d'utiliser le onAppear() de la vue principale pour insérer des données initiales.
struct ContentView: View {
@Query(sort: \Item.code) var items: [Item]
@Environment(\.modelContext) var modelContext
var body: some View {
VStack {
...
}
}
}
Il est cependant préférable d'insérer les données initiales à l'aide du ModelContext car le code sera exécuté une seule fois au début de l'application alors qu'avec le onAppear(), le code peut être exécuté plusieurs fois, ce qui forcera l'application à vérifier la présence de données à chaque fois.
De plus, avec l'approche du preloadAppContainer, le code pourra être utilisé pour voir les données initiales dans le canevas.
« Pre-populate a SwiftData Persistent Store ». Andrew Bankroft. https://www.andrewcbancroft.com/blog/ios-development/data-persistence/pre-populate-swiftdata-persistent-store/
« How to pre-load an app with JSON ». Hacking with Swift. https://www.hackingwithswift.com/quick-start/swiftdata/how-to-pre-load-an-app-with-json
« How to pre-populate an app with an existing SwiftData database ». Hacking with Swift. https://www.hackingwithswift.com/quick-start/swiftdata/how-to-pre-populate-an-app-with-an-existing-swiftdata-database
▼Publicité