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
? DesSpending
? 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.
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.
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 typeNSFetchRequest
.On exécute la requête avec la méthode
fetch
deNSManagedObjectContext
.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 !