← Accueil IFM025921 · Module 10 · Notes de cours

Core Data & SwiftUI

Module 10 Notes de cours · Persistance des données avec Core Data

01 Introduction

Dans presque toutes les applications iOS, on a besoin de sauvegarder des données pour qu'elles survivent à la fermeture de l'app. Par exemple : une liste de tâches, un carnet de contacts, un historique d'achats.

Une première approche consiste à utiliser directement SQLite, la base de données embarquée sur iOS. Ça fonctionne, mais ça oblige à écrire du SQL un langage qui n'est pas orienté objet et qui ajoute de la complexité.

Analogie : SQLite, c'est comme construire une maison en manipulant directement les briques et le ciment. Core Data, c'est comme utiliser des panneaux préfabriqués le résultat final (la maison / la base de données SQLite) est le même, mais le travail est beaucoup plus simple.

Core Data est un framework d'Apple qui sert d'intermédiaire entre votre code Swift et la base de données. Il vous permet de travailler avec vos données comme des objets Swift normaux sans écrire une seule ligne de SQL.

SQLite direct
Vous écrivez des requêtes SQL. Les données reviennent sous forme de tableaux de valeurs brutes. Vous devez tout convertir en objets Swift manuellement.
Core Data
Vous créez et manipulez des objets Swift. Core Data s'occupe de la traduction vers SQLite de façon transparente.

02 La Stack Core Data

Core Data n'est pas un composant unique c'est un ensemble de couches qui travaillent ensemble. On appelle cet ensemble la Core Data Stack. Votre code n'interagit qu'avec la couche du haut ; les couches inférieures fonctionnent de façon transparente.

Diagramme de la Core Data Stack

Application SwiftUIVotre code
Persistent ContainerPoint d'entrée unique
Managed Object ContextZone de travail (brouillon)
Managed Object ModelSchéma / structure
Persistent Store CoordinatorCoordinateur (invisible)
Persistent Store ObjectCouche de stockage
Base de données SQLiteFichier sur disque

Votre code n'interagit qu'avec le Persistent Container et le Managed Object Context les couches inférieures sont transparentes.

Couche d'entrée

Persistent Container

C'est la porte d'entrée de Core Data. Il crée et configure toute la Stack en une seule opération.

  • Crée le modèle de données depuis le fichier .xcdatamodeld
  • Charge la base de données SQLite sur disque
  • Expose le viewContext pour travailler avec les données
C'est comme le gérant d'un entrepôt il ne stocke rien lui-même, mais il organise tout et vous donne accès à ce dont vous avez besoin.
Zone de travail

Managed Object Context (MOC)

C'est votre espace de travail. Toutes vos opérations sur les données (créer, lire, modifier, supprimer) passent par ici.

  • Contient les objets en mémoire vive (RAM)
  • Accumule les changements sans les sauvegarder immédiatement
  • Appel à .save() = écriture vers le store
C'est comme un brouillon dans Word vous pouvez modifier le texte librement, mais rien n'est enregistré tant que vous n'appuyez pas sur Sauvegarder.
Schéma des données

Managed Object Model

Définit la structure de vos données les entités, leurs attributs et les relations entre elles.

  • Contenu dans le fichier .xcdatamodeld
  • Une entité ≈ une table SQL / une classe Swift
  • Un attribut ≈ une colonne SQL / une propriété
  • Une relation ≈ une clé étrangère SQL
C'est le plan d'architecte il décrit comment les données doivent être organisées, sans contenir de données lui-même.
Coordinateur invisible

Persistent Store Coordinator

Fait le lien entre le MOC et le fichier de stockage. En tant que développeur, vous n'interagissez presque jamais directement avec lui.

  • Gère potentiellement plusieurs stores à la fois
  • Gère les migrations lors des changements de modèle
  • Présente plusieurs stores comme un seul au MOC
Stockage réel

Persistent Store Object

C'est le support de stockage physique. Par défaut sur iOS, c'est un fichier SQLite. Core Data supporte aussi XML, binaire et mémoire vive (en RAM, non persistant).

Le choix du type de store est transparent votre code Swift appelle toujours les mêmes méthodes Core Data, quelle que soit la technologie de stockage sous-jacente.

03 Cycle de vie des données

Comprendre le cycle de vie des données est essentiel pour ne pas se perdre. Voici les quatre étapes que suit chaque donnée :

Créer
On instancie un objet Swift dans le MOC
Modifier
On assigne les valeurs des attributs
Sauvegarder
On appelle .save() sur le MOC
Persister
Les données sont écrites dans SQLite sur disque
Attention : Tant que viewContext.save() n'est pas appelé, les changements n'existent qu'en mémoire RAM. Si l'app se ferme avant la sauvegarde, les données sont perdues.

04 Modèle d'entités (.xcdatamodeld)

Le fichier .xcdatamodeld est l'éditeur visuel où vous définissez la structure de vos données. Xcode le génère automatiquement si vous cochez l'option Core Data à la création du projet, mais on peut aussi le créer manuellement (ce qu'on fait dans le laboratoire).

Comment le créer manuellement : ⌘N → Core Data → Data Model. Donnez-lui le même nom que vous utiliserez dans NSPersistentContainer(name:).

Définir une entité

Une entité dans Core Data est l'équivalent d'une classe en Swift ou d'une table dans une base de données. Xcode génère automatiquement la classe Swift correspondante.

  1. Ouvrez le fichier .xcdatamodeld
  2. Cliquez sur Add Entity en bas du panneau
  3. Double-cliquez sur le nom pour le changer (ex. : Product)
  4. Ajoutez des attributs en cliquant sur + dans la section Attributes
  5. Choisissez le type de chaque attribut dans le menu déroulant

Types d'attributs disponibles

Type Usage typique Exemple
String Texte libre Nom, description, email
Integer 16/32/64 Nombres entiers
16 = petit (-32 768 à 32 767) · 32 = standard · 64 = très grand
Quantité, âge, score
Double / Float Nombres décimaux Prix, coordonnées GPS
Boolean Vrai / Faux Tâche complétée, en stock
Date Date et heure Date de création, échéance
Binary Data Données brutes Image, fichier PDF
UUID Identifiant unique Clé primaire unique
Transformable Tout objet Swift conforme à Codable Structures complexes personnalisées

Relations entre entités

Les relations permettent de lier deux entités entre elles, comme des clés étrangères en SQL. Il existe trois types de relations :

One-to-One (1:1)
Un objet est lié à exactement un autre. Ex. : un utilisateur a un seul profil.
One-to-Many (1:N)
Un objet est lié à plusieurs autres. Ex. : une catégorie contient plusieurs produits. C'est le type le plus courant.
Many-to-Many (N:N)
Plusieurs objets liés à plusieurs autres. Ex. : un étudiant peut s'inscrire à plusieurs cours, et un cours peut avoir plusieurs étudiants.
Relation inverse : Core Data recommande toujours de définir la relation dans les deux sens (ex. : Category → products et Product → category). Ça permet à Core Data de maintenir la cohérence des données automatiquement.

05 Utilisation en Swift

Voici les trois éléments Swift que vous utiliserez le plus souvent avec Core Data.

1 Initialiser la Stack : PersistenceController

On crée une structure PersistenceController (généralement dans un fichier Persistence.swift) qui initialise le NSPersistentContainer et expose le viewContext au reste de l'application.

Persistence.swift
import CoreData

struct PersistenceController {

    // Singleton : une seule instance dans toute l'app
    static let shared = PersistenceController()

    let container: NSPersistentContainer

    init() {
        // Nom du fichier .xcdatamodeld (sans l'extension)
        container = NSPersistentContainer(name: "Products")

        container.loadPersistentStores { _, error in
            if let error { fatalError("Erreur : \(error)") }
        }

        // Synchronise le contexte si des données changent en arrière-plan
        container.viewContext.automaticallyMergesChangesFromParent = true
    }
}

// Dans l'App : on injecte le viewContext dans l'environnement SwiftUI
// → Toutes les vues peuvent y accéder via @Environment(\.managedObjectContext)

2 Lire des données : @FetchRequest

@FetchRequest est un property wrapper SwiftUI qui lit automatiquement les données depuis Core Data et met à jour la vue dès qu'un changement est détecté. Pas besoin de recharger manuellement.

Analogie : C'est comme un abonnement en direct au lieu de demander les données une seule fois, @FetchRequest vous notifie chaque fois que les données changent. La vue se re-dessine automatiquement.
ContentView.swift
// Récupère tous les produits, sans tri
@FetchRequest(sortDescriptors: [])
private var products: FetchedResults<Product>

// Récupère tous les produits, triés alphabétiquement par nom
//  SortDescriptor(\.name) est vérifié à la compilation (plus sûr que "name" en String)
@FetchRequest(sortDescriptors: [SortDescriptor(\.name)])
private var products: FetchedResults<Product>

// Récupère les produits par quantité, ordre décroissant
@FetchRequest(sortDescriptors: [SortDescriptor(\.quantity, order: .reverse)])
private var products: FetchedResults<Product>
sortDescriptors: []
Aucun tri l'ordre est celui de l'insertion dans la base.
SortDescriptor(\.name)
Tri alphabétique ascendant (A → Z). Utilise un keypath Swift, vérifié à la compilation.
FetchedResults<Product>
Un tableau réactif qui se met à jour automatiquement quand la base de données change.

3 Filtrer les données : NSPredicate

Un NSPredicate définit des critères de filtrage l'équivalent de la clause WHERE en SQL. On peut l'utiliser avec @FetchRequest ou dans une requête ponctuelle.

Exemples de NSPredicate
// Égalité exacte produits dont le nom est exactement "Pomme"
NSPredicate(format: "name == %@", "Pomme")

// Contient (insensible à la casse et aux accents grâce à [cd])
NSPredicate(format: "name CONTAINS[cd] %@", searchText)

// Commence par
NSPredicate(format: "name BEGINSWITH[cd] %@", "A")

// Comparaison numérique quantité supérieure à 10
NSPredicate(format: "quantity > %d", 10)

// Combinaison de critères avec AND / OR
NSPredicate(format: "name CONTAINS[cd] %@ AND quantity > %d", searchText, 5)
%@
Placeholder pour une valeur de type objet (String, Date, etc.).
%d
Placeholder pour un entier (Int).
%f
Placeholder pour un décimal (Double / Float).
[cd]
c = case-insensitive (ignore majuscules/minuscules) · d = diacritic-insensitive (ignore accents). Toujours recommandé pour les recherches texte.
Requête ponctuelle (one-time fetch) : Si vous n'avez pas besoin de la mise à jour automatique de @FetchRequest, vous pouvez faire une requête unique dans un .task ou une fonction, en construisant un NSFetchRequest et en appelant viewContext.fetch() directement.
Requête ponctuelle dans une vue
@Environment(\.managedObjectContext) private var viewContext
@State private var results: [Product] = []

// Exécutée une seule fois à l'apparition de la vue
.task {
    let request: NSFetchRequest<Product> = Product.fetchRequest()
    request.predicate = NSPredicate(
        format: "name CONTAINS[cd] %@", searchText
    )
    request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
    results = (try? viewContext.fetch(request)) ?? []
}

Passer au laboratoire

Module 10 Créer un projet SwiftUI avec Core Data de A à Z