Module 10 - Exercice

← Retour au module principal

Créer un projet SwiftUI avec Core Data

Créer le projet « CoreDataDemo »

  1. Lancez Xcode et cliquez sur «Create a new Xcode project».
  2. Sélectionnez «IOS» et «App».
  3. Donnez un nom à votre projet, par exemple «CoreDataDemo».
  4. Ne cochez pas l’option «Core Data» dans la partie Storage. Laissez-la sur «None».
  5. Sélectionnez l’emplacement de sauvegarde, puis cliquez sur «Finish».

Objectif : Nous allons ajouter manuellement tous les éléments nécessaires à l’utilisation de Core Data.

Ajouter le modèle de données (.xcdatamodeld)

Comme nous n’avons pas coché l’option Core Data, Xcode n’a pas créé de fichier .xcdatamodeld par défaut. Nous allons le créer nous-mêmes.

  1. Appuyez sur Cmd+N pour créer un nouveau fichier.
  2. Dans la section iOS, cherchez la rubrique Core Data.
  3. Sélectionnez « Data Model » puis cliquez sur « Next ».
  4. Donnez un nom au fichier, par exemple « Products ».
  5. Cliquez sur « Create ».
  6. Vous verrez apparaître un fichier Products.xcdatamodeld dans votre arborescence.

Définir une entité (Entity)

  1. Ouvrez Products.xcdatamodeld.
  2. Dans la fenêtre principale, cliquez sur « Add Entity » en bas.
  3. Renommez l’entité « Product » (double-clic sur le nom par défaut).
  4. Dans la section Attributes, cliquez sur + pour ajouter un attribut :
    • name (String) : pour le nom du produit.
    • quantity (String) : pour stocker la quantité du produit (vous pourriez utiliser un entier, mais on reste simple).

Votre modèle de données contient désormais une entité Product avec deux attributs : name et quantity.

Créer la structure PersistenceController

Maintenant, nous allons créer un fichier Swift pour configurer la stack Core Data

  1. Appuyez sur Cmd+N pour créer un nouveau fichier Swift.
  2. Nommez ce fichier Persistence.swift et enregistrez-le à la racine de votre projet.
  3. Ajoutez le code suivant :
    import CoreData
    
    struct PersistenceController {
    static let shared = PersistenceController()
    let container: NSPersistentContainer
    
    init() {
      // « Products » correspond au nom du fichier .xcdatamodeld (sans l’extension)
      container = NSPersistentContainer(name: "Products")
    
      container.loadPersistentStores { (storeDescription, error) in
          if let error = error as NSError? {
              // En cas d’erreur, on arrête l’application (fatalError)
              // Pour une app de production, on gérerait l’erreur différemment
              fatalError("Échec du chargement du store: \(error)")
          }
      }
    }
    }
    

Explications :

Injecter le Managed Object Context dans SwiftUI

Dans votre fichier CoreDataDemoApp.swift, on va utiliser le PersistenceController pour fournir le viewContext aux vues SwiftUI.

import SwiftUI

@main
struct CoreDataDemoApp: App {
let persistenceController = PersistenceController.shared

var body: some Scene {
  WindowGroup {
      ContentView()
          // On injecte le viewContext dans l’environnement
          .environment(\.managedObjectContext, persistenceController.container.viewContext)
  }
}
}

Avantage : Toutes les vues pourront accéder à @Environment(\.managedObjectContext) pour créer, lire ou supprimer des objets.

Mettre en place l’interface (ContentView.swift)

import SwiftUI
import CoreData

struct ContentView: View {
// Champs de saisie pour le nom et la quantité
@State private var name = ""
@State private var quantity = ""

// Récupération du contexte Core Data via l’environnement
@Environment(\.managedObjectContext) private var viewContext

// FetchRequest pour récupérer tous les Product
@FetchRequest(
  entity: Product.entity(),
  sortDescriptors: [] // On pourra ajouter un tri plus tard
)
private var products: FetchedResults< Product >

var body: some View {
  NavigationStack {
      VStack {
          TextField("Nom du produit", text: $name)
          TextField("Quantité", text: $quantity)
              .keyboardType(.numberPad)

          HStack {
              Spacer()
              Button("Add") {
                  addProduct()
              }
              Spacer()
              Button("Clear") {
                  name = ""
                  quantity = ""
              }
              Spacer()
          }
          .padding()

          // Affichage de la liste des produits
          List {
              ForEach(products) { product in
                  HStack {
                      Text(product.name ?? "Inconnu")
                      Spacer()
                      Text(product.quantity ?? "0")
                  }
              }
              .onDelete(perform: deleteProducts)
          }
          .listStyle(.plain)
      }
      .padding()
      .textFieldStyle(RoundedBorderTextFieldStyle())
      .navigationTitle("Inventaire")
  }
}
}

Explications :

Ajouter les fonctions Core Data (ajout/suppression)

Toujours dans ContentView :

extension ContentView {
private func addProduct() {
  withAnimation {
      let newProduct = Product(context: viewContext)
      newProduct.name = name
      newProduct.quantity = quantity

      saveContext()
  }
}

private func deleteProducts(offsets: IndexSet) {
  withAnimation {
      offsets.map { products[$0] }.forEach(viewContext.delete)
      saveContext()
  }
}

private func saveContext() {
  do {
      try viewContext.save()
  } catch {
      // Arrêt brutal si erreur (fatalError)
      // En production, on afficherait un message d’erreur plus propre
      fatalError("Erreur lors de la sauvegarde : \(error)")
  }
}
}

Explications :

Améliorations possibles

Trier les produits

Pour trier par ordre alphabétique, on peut ajouter un NSSortDescriptor :

@FetchRequest(
entity: Product.entity(),
sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)]
)
private var products: FetchedResults< Product >

Rechercher un produit

On peut créer une vue de recherche (ResultsView) qui utilise un NSPredicate :

struct ResultsView: View {
var name: String
var viewContext: NSManagedObjectContext

@State var matches: [Product]?

var body: some View {
  VStack {
      List {
          ForEach(matches ?? []) { match in
              HStack {
                  Text(match.name ?? "Not found")
                  Spacer()
                  Text(match.quantity ?? "Not found")
              }
          }
      }
      .navigationTitle("Results")
  }
  .task {
      let fetchRequest: NSFetchRequest = Product.fetchRequest()
      
      fetchRequest.entity = Product.entity()
      // CONTAINS[cd] = insensible à la casse et aux accents
      fetchRequest.predicate = NSPredicate(format: "name CONTAINS[cd] %@", name)
      matches = try? viewContext.fetch(fetchRequest)
  }
}
}

Cela permet d’afficher uniquement les produits selon le nom.

ContentView

On peut ajouter des boutons pour accéder à la vue de recherche :

...

HStack {
Spacer()

Button("Add") {
  addProduct()
}

Spacer()

NavigationLink(destination: ResultsView(name: name, viewContext: viewContext)) {
  Text("Find")
}

Spacer()

Button("Clear") {
  name = ""
  quantity = ""
}
Spacer()
}

...

Création de la vue de modification (EditProductView)

Lorsqu’un utilisateur sélectionne un produit dans la liste, il doit pouvoir modifier ses informations (nom et quantité) et sauvegarder ces modifications. Nous allons créer une vue EditProductView qui affichera un formulaire pré-rempli avec les données existantes du produit.

struct EditProductView: View {
@ObservedObject var product: Product
var viewContext: NSManagedObjectContext

@State private var name: String = ""
@State private var quantity: String = ""
@State private var showConfirmation = false

var body: some View {
  Form {
      Section(header: Text("Informations du produit")) {
          TextField("Nom du produit", text: $name)
          TextField("Quantité", text: $quantity)
              .keyboardType(.numberPad)
      }

      Section {
          Button("Enregistrer") {
              saveChanges()
          }
          .buttonStyle(.borderedProminent)
          .disabled(name.isEmpty || quantity.isEmpty) // Empêche l'enregistrement si les champs sont vides
      }
  }
  .navigationTitle("Modifier Produit")
  .onAppear {
      loadData()
  }
  .alert("Modification enregistrée", isPresented: $showConfirmation) {
      Button("OK", role: .cancel) { }
  }
}

/// Charge les données actuelles du produit dans les champs de saisie.
private func loadData() {
  name = product.name ?? ""
  quantity = product.quantity ?? ""
}

/// Sauvegarde les modifications dans Core Data.
private func saveChanges() {
  product.name = name
  product.quantity = quantity

  do {
      try viewContext.save()
      showConfirmation = true
  } catch {
      print("Erreur lors de la sauvegarde : \(error.localizedDescription)")
  }
}
}

Explication :

Navigation vers la vue de modification

Nous devons maintenant permettre à l’utilisateur d’accéder à EditProductView depuis la liste des produits affichée dans ContentView. Pour cela, nous allons utiliser NavigationLink.

...

NavigationView {
List {
  ForEach(products) { product in
      NavigationLink(destination: EditProductView(product: product, viewContext: viewContext)) {
          HStack {
              Text(product.name ?? "Inconnu")
                  .font(.headline)

              Spacer()

              Text(product.quantity ?? "0")
                  .foregroundColor(.secondary)
          }
      }
  }
  .onDelete(perform: deleteProducts)
}
.navigationTitle("Produits")
}

...

Explication :

Gérer les erreurs plus finement

Au lieu d’utiliser fatalError, on peut afficher une alerte, ou logger l’erreur dans la console, etc.