• 12 hours
  • Hard

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 3/22/24

Récupérez vos données

Nous savons maintenant sauvegarder nos objets Person . Les participants sont donc sauvegardés. Mais si on quitte l'interface d'ajout de personnes et qu'on y retourne, la liste a disparu !

C'est logique, on sauvegarde les données, mais on ne les récupère pas. Dans ce chapitre, on va récupérer la liste de tous les participants dans Core Data au moment du chargement de la page.

Découvrez NSFetchRequest

Pour récupérer des données, on va utiliser une classe dont on a à peine parlé : NSFetchRequest  . Cette classe permet de créer une requête.

Une requête va contenir un certain nombre d'informations :

  • Quel type de données est-ce que je cherche à récupérer ? Des Person  ? Des Spending  ? Quelle entité ?

  • Quels objets est-ce que je veux récupérer ? Tous ? Seulement ceux dont le nom commence par A ?

  • Dans quel ordre est-ce que je veux obtenir les objets ? Par ordre croissant ? Décroissant ? Rangés selon quel attribut ?

Dans ce chapitre, nous allons faire la requête la plus simple : récupérer tous les objets d'une même entité sans plus de précision. Mais dans la prochaine partie, nous allons faire des requêtes plus fines en répondant aux questions citées ci-dessus.

Créez une requête

On veut récupérer les données lors du chargement de notre PeopleViewController  pour pouvoir afficher tous les participants dans notre Text View.

On va donc faire cela dans la méthode viewDidLoad  que je vous propose de rajouter :

final class PeopleViewController: UIViewController {

  // MARK: - Outlets

  @IBOutlet private weak var peopleTextView: UITextView!
  @IBOutlet private weak var peopleTextField: UITextField!

  // MARK: - View life cycle

  override func viewDidLoad() {
    super.viewDidLoad()
    // Récupérer les données dans CoreData
  }

  // MARK: - Actions
  @IBAction private func dismiss() {
    dismiss(animated: true, completion: nil)
  }
}

Maintenant, nous allons créer notre requête. Pour cela, souvenez-vous, je vous ai montré que dans l'extension de Person  générée automatiquement par Core Data se trouvait une méthode statique, fetchRequest  , qui permet de créer un objet NSFetchRequest  :

// Person+CoreDataProperties.swift
import CoreData
extension Person {
   @nonobjcpublic class func fetchRequest() -> NSFetchRequest<Person> {
      return NSFetchRequest<Person>(entityName: "Person")
   }
   @NSManagedpublic var name: String?
}

Nous allons donc utiliser cette méthode pour créer notre requête :

let request: NSFetchRequest<Person> = Person.fetchRequest()

Je crée simplement ma requête avec la méthode statique fetchRequest  de Person  .

OK, mais il est bizarre le type de la variable : NSFetchRequest<Person>  !

Eh oui en effet ! La raison, c'est que NSFetchRequest est une classe générique, et je vais vous expliquer ce que c'est.

Créez une classe générique

Les génériques sont un concept Swift dont on n’a pas encore parlé. Je vais rapidement vous expliquer comment cela fonctionne avec un exemple.

Array  est également une classe générique. Pour utiliser cette classe, on peut faire ce que vous avez l'habitude de faire :

let myArray: [Int]

Mais en fait, ceci est seulement un raccourci pour :

let myArray: Array<Int>

On précise entre les chevrons le type que va contenir le tableau. C'est exactement la même chose pour NSFetchRequest  :

let request: NSFetchRequest<Person>

On précise entre les chevrons le type de résultat que va retourner notre requête. Comme pour un tableau, le type est incomplet si on ne fournit pas cette information.

Exécutez la requête

Notre requête est prête ! Il n'y a plus qu'à la lancer. Comme à chaque fois qu'on interagit avec les données, on va passer par notre contexte.

On va utiliser la méthode fetch  de NSManagedObjectContext  pour exécuter notre requête :

CoreDataStack.sharedInstance.viewContext.fetch(request)

Cette méthode retourne un tableau de Person  . Ce tableau contient tous les objets récupérés dans la base de données.

Cette méthode peut renvoyer une erreur, donc je vais devoir utiliser  try?  et déballer l'optionnel ainsi créé. Je fais ça avec  guard let    :

guard let persons = try? CoreDataStack.sharedInstance.viewContext.fetch(request) else {
   return
}

Et voilà ! En à peine 4 lignes, on a récupéré nos objets dans la base de données.

Maintenant, je peux utiliser mes objets pour afficher la liste des participants dans ma Text View.

var peopleText = ""
for person in persons {
   if let name = person.name {
      peopleText += name + "\n"
   }
}
peopleTextView.text = peopleText

Désormais, quand vous ouvrez cette page, la liste des participants est chargée dans la base de données et affichée dans la Text View. Et bien sûr, cela fonctionne aussi entre deux lancements de l'application. Vous goûtez à la persistance des données avec Core Data.

Gérez les requêtes dans le modèle : une question de réflexe

De nouveau, il doit y avoir quelque chose qui vous donne envie de bondir !

Je n’ai pas voulu faire la remarque, mais on fait une requête à la base dans le contrôleur. On n’avait pas dit que les données, ça devait être géré dans le modèle ?

Eh oui ! Bien sûr ! Du coup, je vous propose de refactoriser ça pour créer et exécuter notre requête dans le modèle, et plus précisément dans une classe PeopleRepository  (je vous laisse créer cette nouvelle classe dans votre dossier Model) :

import Foundation
import CoreData

final class PeopleRepository {
}

Notre classe PeopleRepository  ayant pour rôle de traiter la récupération et l’écriture des données, je vais y créer deux fonctions : getPersons  et savePerson  :

final class PeopleRepository {

  // MARK: - Repository

  func getPersons(completion: ([Person]) -> Void) {

  }

  func savePerson(named name: String, completion:  () -> Void) {

  }
}

À présent, cette classe va nous permettre d’encapsuler correctement l’utilisation de CoreData, et nous allons y injecter justement notre super CoreDataStack  :

import Foundation
import CoreData

final class PeopleRepository {

  // MARK: - Properties

  private let coreDataStack: CoreDataStack

  // MARK: - Init

  init(coreDataStack: CoreDataStack = CoreDataStack.sharedInstance) {
    self.coreDataStack = coreDataStack
  }

  // MARK: - Repository

  func getPersons(completion: ([Person]) -> Void) {

  }

  func savePerson(named name: String, completion: () -> Void) {

  }
}

Le fait d’avoir créé un paramètre privé, et ajouté un initialiseur pour injecter une instance de ce même paramètre, est une technique de programmation avancée : l’injection de dépendance. Ceci vous permettra par la suite d'écrire des tests sur cette classe.

Pour le moment, gardez en tête une seule chose : il faut toujours injecter vos dépendances, même des singletons  ! Et c’est d’ailleurs ce que j’ai fait dans notre initialiseur :

init(coreDataStack: CoreDataStack = CoreDataStack.sharedInstance)

Vous avez sûrement remarqué le fait que ces deux méthodes ne retournent pas de résultat, mais mettent à disposition des closures  à la place. Pour rappel, une closure  est une variable dont le type est une fonction. Lorsque l’on va appeler une méthode qui en contient une, au moment d’injecter les paramètres attendus, nous injecterons un scope d'exécution de fonction à la place d’une valeur. Par exemple, dans le cas de getPerson  , le paramètre que nous injectons est donc completion  .

Les closures  vont nous permettre de travailler avec les fonctions fetch  et save  , tout en exécutant un scope de fonction contextualisé avec la réussite (ou non) d’une méthode qui pourra (ou non) throw  une erreur.

//question Hein ? Quoi ?

Je m’explique : vous vous souvenez de ce que l’on a mis dans le PeopleViewController  pour la fonction addPerson  ? Nous avons utilisé un try?  car la méthode peut throw  une potentielle erreur, qui peut être traitée dans un bloc  do - catch   si on le souhaite. Eh bien… voici la solution ci-dessous :

func savePerson(named name: String, completion: () -> Void) {
   let person = Person(context: coreDataStack.viewContext)
   person.name = name
   do {
     try coreDataStack.viewContext.save()
     completion()
   } catch {
     print("We were unable to save \(name)")
   }
}

Je vous propose donc ici d’appeler la closure completion()  si le bloc do  fonctionne, et de n’exécuter un print que si le bloc catch  est appelé.

Voici à présent l’implémentation de la méthode getPersons  avec le même format :

func getPersons(completion: ([Person]) -> Void) {
   let request: NSFetchRequest<Person> = Person.fetchRequest()
   do {
     let persons = try coreDataStack.viewContext.fetch(request)
     completion(persons)
   } catch {
     completion([])
   }
}

On remarque alors que notre completion  retourne un tableau de Person  , qui sera vide si une erreur est catch  . Encore une fois, cette architecture est amenée à être améliorée sur une vraie application, car ici nous ne traitons pas le contenu d’une éventuelle erreur.

Voici donc notre repository final :

import Foundation
import CoreData

final class PeopleRepository {

   // MARK: - Properties

   private let coreDataStack: CoreDataStack

   // MARK: - Init

   init(coreDataStack: CoreDataStack = CoreDataStack.sharedInstance) {
      self.coreDataStack = coreDataStack
   }

   // MARK: - Repository

   func getPersons(completion: ([Person]) -> Void) {
      let request: NSFetchRequest<Person> = Person.fetchRequest()
      do {
        let persons = try coreDataStack.viewContext.fetch(request)
        completion(persons)
      } catch {
        completion([])
      }
   }

   func savePerson(named name: String, completion: () -> Void) {
      let person = Person(context: coreDataStack.viewContext)
      person.name = name
      do {
        try coreDataStack.viewContext.save()
        completion()
      } catch {
        print("We were unable to save \(name)")
      }
   }
}

À présent, je peux mettre à jour notre peopleViewController  en :

  • y ajoutant une instance de notre repository ; 

  • mettant à jour la fonction addPerson  ;

  • ajoutant une fonction getPeople  pour récupérer les données depuis CoreData.

Commençons par ajouter une instance de notre repository :

final class PeopleViewController: UIViewController {

   // MARK: - Properties

   private let repository = PeopleRepository()

   (...)
}

Puis mettons à jour notre méthode addPerson  en utilisant notre repository :

private func addPerson() {
    guard
      let personName = peopleTextField.text,
      var people = peopleTextView.text
    else { return }

    repository.savePerson(named: personName, completion: { [weak self] in
      people += personName + "\n"
      self?.peopleTextView.text = people
      self?.peopleTextField.text = ""
    })
  }

Puis, récupérons les données de CoreData  via la fonction getPersons  de notre repository. Je me permet donc d’encapsuler ça dans une méthode privée.

// MARK: - Private

   private func getPeople() {
     repository.getPersons(completion: { [weak self] persons in
       var peopleText = ""
       for person in persons {
         if let name = person.name {
            peopleText += name + "\n"
         }
       }
       self?.peopleTextView.text = peopleText
     })
   }

N’oubliez pas d’appeler la fonction getPeople  au chargement de la vue, dans la méthode viewDidLoad  :

// MARK: - View life cycle

   override func viewDidLoad() {
     super.viewDidLoad()
     getPeople()
   }

N’oubliez pas le Picker View

Il y a un autre endroit où ces données nous intéressent ! Dans l'interface de création d'une dépense ( AddSpendingViewController  ), on va maintenant pouvoir remplir le Picker View que j'ai préparé pour vous, pour afficher la liste des participants.

Ainsi, l'utilisateur va pouvoir indiquer à quel participant appartient la dépense qu'il est en train de créer.

Pour commencer, il faut afficher le Picker View. Dans le storyboard, j'ai caché, avec la propriété isHidden  , le Picker View et son label de titre. Je vous invite à décocher la case Hidden.

Sélectionnez les views Cekikapeye et Person Picker View dans la stack view de la view d’Add Spending View Controller. Puis, sous Drawing dans le menu de la View, décochez Hidden.
Affichons le Picker View

Le label et le Picker View doivent apparaître.

Maintenant, nous allons ajouter une instance de notre PeopleRepository  pour ajouter/récupérer des personnes, qui seront stockées localement dans une variable people  .

// MARK: - Properties

   private let spendingRepository = SpendingRepository()
   private let peopleRepository = PeopleRepository()
   private var people: [Person] = []

J'initialise ma propriété people  avec un tableau vide pour le moment, mais nous allons le remplir au moment du viewWillAppear  grâce à notre repository dédié :

// MARK: - View life cycle

   override func viewWillAppear(_ animated: Bool) {
     super.viewWillAppear(animated)
     getPersons()
   }

   private func getPersons() {
     peopleRepository.getPersons(completion: { [weak self] people in
       self?.people = people
       self?.personPickerView.reloadAllComponents()
     })
   }

Maintenant, je n'ai plus qu'à remplir mon Picker View avec mon tableau people  :

extension AddSpendingViewController: UIPickerViewDataSource, UIPickerViewDelegate {
  func numberOfComponents(in pickerView: UIPickerView) -> Int {
    return 1
  }

   func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
     return people.count
  }

      func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
     return people[row].name
  }
}

Et voilà ! Vous pouvez lancer l'application, et maintenant le Picker View affiche les différents participants.

Le Picker View affiche les différentes personnes que nous pouvons sélectionner.
Notre Picker View !

En résumé

Pour récupérer des données :

  • On crée une requête avec la méthode fetchRequest  qui renvoie un objet de type NSFetchRequest  .

  • On exécute la requête avec la méthode fetch  de NSManagedObjectContext  .

  • On traite l'exécution de cette méthode dans un bloc do-catch afin de traiter une éventuelle erreur.

Dans la prochaine partie, nous allons ajouter une deuxième entité dans notre modèle de données : Spending  . Cela va nous permettre de sauvegarder les dépenses dans Core Data. On va voir comment gérer les relations entre les entités Spending  et Person  , et vous allez approfondir vos connaissances de Core Data !

À tout de suite !

Example of certificate of achievement
Example of certificate of achievement