Apprendre à utiliser Core Data en programmation Swift

Partie 2

Jameson Quave

Traducteur

Oppenheimer

Ce tutoriel est compatible avec Xcode 6.3, mise à jour le 16 février 2015.

Ce tutoriel est la deuxième partie d'une série de cours pour vous apprendre à utiliser le Core Data en programmation Swift pour mettre en place des applications iOS persistantes. Si vous n'avez pas encore lu la première partie, lisez-la d'abord.

Vous pouvez également avoir un premier accès à mon livre Swift disponible ici

Commentez

- Titre : Apprendre à utiliser Core Data en programmation Swift

- Auteur : Jameson Quave

- Traducteur : Oppenheimer

- Parution : 25 novembre 2016

- Licence : Copyright ® 2016 Jameson Quave. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Voir également :

- Apprendre à utiliser Core Data en programmation Swift - Partie 1 - Notre première application

Sommaire

- I. Créer plus d'enregistrements

I - Créer plus d'enregistrements

Avant tout, pour faire quelque chose d'aussi simple qu'afficher une fenêtre présentant une alerte, nous aurons besoin de générer quelques enregistrements de plus pour faire des manipulations. Ouvrons notre fichier LogItem.swift et ajoutons une fonction d'aide pour ajouter de nouveaux enregistrements.

De manière basique, nous voulons être capables d' appeler aisément une méthode dans la classe LogItem, lui passer quelques paramètres, et obtenir un nouveau LogItem en retour.

let newItem = LogItem.createInManagedObjectContext(managedObjectContext, "Item Title", "Item text")

La classe LogItem n'est pas liée à un quelconque NSManagementObjectContext, donc nous voulons être sûrs que nous ne sommes pas en train de stocker la référence au contexte d'objet géré où que ce soit dans le modèle ; il faut passer par là quand nous voulons créer un objet semblable.

D'accord ! Réalisons donc une méthode dans LogItem.swift :

LogItem.swift

1. class LogItem: NSManagedObject {

3. @NSManaged var itemText: String

4. @NSManaged var title: String

6. class func createInManagedObjectContext(moc: NSManagedObjectContext, title: String, text: String) -> LogItem {

7. let newItem = NSEntityDescription.insertNewObjectForEntityForName("LogItem", inManagedObjectContext: moc) as! LogItem

8. newItem.title = title

9. newItem.itemText = text

11. return newItem

12. }

14. }

La première ligne est la définition de la fonction. Il s'agit d'une fonction de classe, appelée createInModelObjectContext qui prend en argument un objet de type NSManagedObjectContext nommé moc, une chaîne de caractères appelée title, et une autre chaîne de caractères appelée text. La fonction retourne un objet LogItem qui a été inséré dans le context spécifié d'objet géré.

Ensuite, elle exécute un code quasiment identique au précédent, pour créer un nouvel objet LogItem, sauf que maintenant, elle utilise les arguments qui lui sont passés afin de paramétrer le nouvel objet LogItem.

Nous pouvons maintenant remplacer notre code original dans viewController.swift par la nouvelle méthode. Ajoutons un lot de nouveaux objets…

ViewController.swift

1. override func viewDidLoad() {

2. super.viewDidLoad()

3. //  Effectuer une configuration supplémentaire après le chargement de la vue, généralement à partir d'un nib.

5. //  Utiliser option obligatoire pour confirmer le managedObjectContext

6. if let moc = self.managedObjectContext {

8. // Créer des données fictives pour travailler avec

9. var items = [

10. ("Best Animal", "Dog"),

11. ("Best Language","Swift"),

12. ("Worst Animal","Cthulu"),

13. ("Worst Language","LOLCODE")

14. ]

16. //  Boucle pour créer des items

17. for (itemTitle, itemText) in items {

18. // Créer un item

19. LogItem.createInManagedObjectContext(moc,

20. title: itemTitle, text: itemText)

21. }

22. }

23. }

Pour garder la simplicité du code, nous utilisons quelques raccourcis afin de fournir nos données. Ce que vous voyez lorsque j'organise les objets dans un tableau (en utilisant des crochets []), c'est que chaque enregistrement est un doublet de chaînes de caractères [(String,String)].

Ensuite, je les redécompose en deux variables, titleArticle et textArticle pour chacun des doublets du tableau.

Finalement, j'appelle la méthode creat, que nous avions créée plus tôt, en lui passant les nouveaux itemTitle et itemText.

Si vous savez déjà comment mettre en place un UITableView et que vous voulez passer au Core Data, cliquez ici.

Maintenant que nous avons une paire d'enregistrements, enlevons presentItemInfo et à la place, optons pour une vue tabulaire ici. Nous allons l'ajouter complètement à droite, sous viewDidLoad et par programmation nous allons créer UITableView. Dans mon didacticiel iTunes, nous le faisons en utilisant des tableaux de bord historiques. Si vous êtes intéressés à travailler avec ces composants, je vous recommande de prendre un délai pour y lire comment obtenir ce réglage.

Nous allons paramétrer la vue en ajoutant une logTableView à la classe ViewController, et mettre le tout dans la fonction viewDidLoad().

1. // Créer une vue dès que cette classe est chargée

2. var logTableView = UITableView(frame: CGRectZero, style: .Plain)

4. override func viewDidLoad() {

5. super.viewDidLoad()

6. // Effectuer une configuration supplémentaire après le chargement de la vue, généralement à partir d'un nib.

8. // Utiliser l'option obligatoire pour confirmer le managedObjectContext

10. if let moc = self.managedObjectContext {

12. //  Créer des données fictives pour travailler avec

13. var items = [

14. ("Best Animal", "Dog"),

15. ("Best Language","Swift"),

16. ("Worst Animal","Cthulu"),

17. ("Worst Language","LOLCODE")

18. ]

20. // Boucle pour créer des items

21. for (itemTitle, itemText) in items {

22. // Créer un item

23. LogItem.createInManagedObjectContext(moc,

24. title: itemTitle, text: itemText)

25. }

28. // Maintenant que la vue est chargée, nous avons un cadre pour la vue, qui sera (0,0, largeur de l'écran, la hauteur de l'écran)

29. // C'est une bonne taille pour l'affichage du tableau, donc on l'utilisera

30. //Le seul réglage que nous allons faire est de le déplacer de 20 pixels vers le bas et de le réduire de 20 pixels

31. //  afin d'apercevoir la barre d'état

33. // Stocker le cadre dans une variable temporaire

34. var viewFrame = self.view.frame

36. // Le régler à 20 points

37. viewFrame.origin.y += 20

39. // Réduire la hauteur totale de 20 points

40. viewFrame.size.height -= 20

42. // Régler le cadre de logTableview pour égaler notre variable temporaire avec la taille de la vue

43. // Ajuster en tenant compte de la taille de la barre d'états

44. logTableView.frame = viewFrame

46. // Ajouter la vue de la table à ce controlleur de vue

47. self.view.addSubview(logTableView)

49. // Ici nous disons à la vue que nous comptions utiliser une cellule que nous appellerons « LogCell »

50. // Pour l'instant, elle sera associée à la classe UITableViewCell

51. logTableView.registerClass(UITableViewCell.classForCoder(), forCellReuseIdentifier: "LogCell")

53. // Ceci indique à la vue qu'il devrait obtenir ses données de cette classe, ViewController

54. logTableView.dataSource = self

56. }

57. }

Depuis que nous avions paramétré le dataSource pour être notre classe viewController, nous avons alors besoin d'adhérer au protocole UITableViewDataSource, donc d'ajouter cela à la définition de la classe de ViewController :

1. class ViewController: UIViewController, UITableViewDataSource {

…et d'ajouter les méthodes actuelles de dataSource…

1. // MARK: UITableViewDataSource

2. func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

3. return 5

4. }

6. func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

7. let cell = tableView.dequeueReusableCellWithIdentifier("LogCell") as! UITableViewCell

8. cell.textLabel?.text = "\(indexPath.row)"

9. return cell

10. }

Toujours avec moi ?

Je l'espère… sinon, voici le code complet de ViewController.swift jusqu'à maintenant. Notez que nous avons aussi enlevé la fonction viewDidAppear puisque nous l'utilisions uniquement pour un test auparavant.

1. import UIKit

2. import CoreData

4. class ViewController: UIViewController, UITableViewDataSource {

6. // Récupérer le managedObjectContext de AppDelegate

7. let managedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext

9. // Créer la vue dès que cette classe est chargée

10. var logTableView = UITableView(frame: CGRectZero, style: .Plain)

12. override func viewDidLoad() {

13. super.viewDidLoad()

15. //   Effectuer une configuration supplémentaireaprès le chargement de la vue, généralement à partir de nib

17. // Utiliser l'option obligatoire pour confirmer le managedObjectContext

18. if let moc = self.managedObjectContext {

20. // Créer des données virtuelles pour utilisation

21. var items = [

22. ("Best Animal", "Dog"),

23. ("Best Language","Swift"),

24. ("Worst Animal","Cthulu"),

25. ("Worst Language","LOLCODE")

26. ]

28. // Boucle pour créer les items

29. for (itemTitle, itemText) in items {

30. // Créer un item

31. LogItem.createInManagedObjectContext(moc,

32. title: itemTitle, text: itemText)

33. }

36. // Maintenant que la vue est chargée, nous avons un cadre pour la vue, qui sera (0,0, largeur de l'écran, la hauteur de l'écran)

37. //  C'est une bonne taille pour l'affichage du tableau, donc nous l'utilisons

38. // Le seul réglage que nous allons faire est de le déplacer de 20 pixels vers le bas et de le réduire de 20 pixels

39. // afin d'apercevoir la barre d'état

40. // Stocker le cadre dans une variable temporaire

41. var viewFrame = self.view.frame

43. // Ajuster à 20 points

44. viewFrame.origin.y += 20

46. // Réduire la hauteur totale à 20 points

47. viewFrame.size.height -= 20

49. // Régler le cadre logTableview pour égaler  la taille notre variable temporaire

50. // Ajuster en tenant compte de la hauteur de la barre d 'états

51. logTableView.frame = viewFrame

53. // Ajouter la vue à ce controlleur de vue

54. self.view.addSubview(logTableView)

56. // Ici nous disons à la vue que nous utiliserons une cellule que nous appellerons « LogCell »

57. // Elle sera associée pour l'instant à la classe standard UITableViewCell

58. logTableView.registerClass(UITableViewCell.classForCoder(), forCellReuseIdentifier: "LogCell")

60. // Ceci indique  à la vue qu'elle devrait obtenir ses données de cette classe, ViewController

61. logTableView.dataSource = self

63. }

64. }

66. // MARK: UITableViewDataSource

67. func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

68. return 5

69. }

71. func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

72. let cell = tableView.dequeueReusableCellWithIdentifier("LogCell") as! UITableViewCell

73. cell.textLabel?.text = "\(indexPath.row)"

74. return cell

75. }

77. override func didReceiveMemoryWarning() {

78. super.didReceiveMemoryWarning()

79. // Éliminer toutes les ressources qui peuvent être recréées

80. }

82. }

Cela va nous donner une liste numérotée si nous exécutons l'application. Cela confirme seulement que la vue est correctement paramétrée.

Si jamais vous obtenez une erreur semblable à : «  The model used to open the store is incompatible with the one used to create the store » cela signifie que le modèle avec lequel le Core Data travaille, ne correspond pas assez avec la base de données.

Résoudre ce problème avec une application en ligne signifierait migrer entre les versions de votre modèle. Mais pour le fait d'apprendre et de faire le développement initial, habituellement la manière la plus rapide de le faire est de supprimer l'application du simulateur ; en effaçant les données, et résolvant ainsi le conflit.

Maintenant que tout est paramétré et prêt à être exécuté, nous pouvons commencer à travailler afin d'obtenir un affichage de nos données de journalisation dans la vue.

Veuillez, je vous prie, noter qu'à ce stade du tutoriel j'évite intentionnellement la classe NSFetchResultsController. J'agis ainsi parce que je crois donner de cette façon plus de sens, à la perspective du core data, dans cette première approche que vous voyez, faite à la vieille mode. Une fois que vous aurez achevé ce tutoriel, je vous encouragerai à jeter un coup d'œil sur la classe afin de voir comment vous auriez pu, à la place, l'utiliser pour réaliser quelques-unes de ces choses. Je pense que c'est important d'apprendre d'abord comment réaliser une vue renforcée par le Core data, sans utiliser la fonction d'assistance. La fonction d'assistance n'est pas applicable dans toutes les situations, et l'utiliser en premier ne vous aurait rendu aucun service. Vous verrez qu'elle ne fonctionne pas dans tous les cas, et quand elle fonctionne, elle cache certaines des choses.

.Maintenant, nous allons exécuter un « fetch » sur tous les résultats du Core Data dans viewDidLoad(), et les stocker dans un tableau de LogItems, « logItems ».:

Premièrement, nous allons ajouter la variable à la classe ViewController :

1. // Créer un tableau vide de LogItem

2. var logItems = [LogItem]()

Ensuite, nous allons la peupler à partir de viewDidLoad(), dans une certaine fonction, appelée fetchLog().

1. override func viewDidLoad() {

2. ...

3. fetchLog()

4. }

6. func fetchLog() {

7. let fetchRequest = NSFetchRequest(entityName: "LogItem")

8. if let fetchResults = managedObjectContext!.executeFetchRequest(fetchRequest, error: nil) as? [LogItem] {

9. logItems = fetchResults

10. }

11. }

Maintenant, nous pouvons modifier les méthodes tableView de dataSource, pour nous référer à ce tableau, au lieu de valeurs codées.

1. // MARK: UITableViewDataSource

2. func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

3. // Combien de lignes sont dans cette section

4. //  Il y a seulement une section et il y a un certain nombre de lignes

5. //  égal au nombre de LogItems, donc retourne le count

6. return logItems.count

7. }

9. func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

10. let cell = tableView.dequeueReusableCellWithIdentifier("LogCell") as! UITableViewCell

12. // Obtenir le LogItem pour cet index

13. let logItem = logItems[indexPath.row]

15. //  Définir le titre de la cellule pour être le titre du LogItem

16. cell.textLabel?.text = logItem.title

17. return cell

18. }

Cela devrait nous permettre de voir les éléments énumérés dans la vue, mais nous voulons montrer le texte d'un élément quand il est cliqué. Ainsi nous avons besoin de paramétrer la vue afin d'utiliser le contrôleur de vue en tant que délégué de ladite vue ; ainsi nous pourrons recevoir en retour la méthode didSelectRowAtIndexPath.

Comme précédemment, nous allons ajouter le protocole UITableViewDelegate à la classe.

1. class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

Et pour paramétrer le délégué en mode autonome, vous pouvez régler la source de données dans viewDidLoad…

1. // Ceci indique  à la vue  qu'il devrait obtenir ses données de cette classe, ViewController

2. logTableView.dataSource = self

3. logTableView.delegate = self

Finalement, nous pouvons créer la méthode, en sachant que la vue appelle cette méthode lorsqu'une cellule est cliquée.

1. // MARK: UITableViewDelegate

2. func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {

3. let logItem = logItems[indexPath.row]

4. println(logItem.itemText)

5. }

Ainsi lorsque le bouton est cliqué, un message affiche le itemText dans la console (fenêtre Xcode), pour l'élément sélectionné.

Le but de ce tutoriel n'est pas réellement d'expliquer comment régler une vue manuellement, mais c'est en quelque sorte nécessaire afin d'obtenir un bon aperçu des données. Pour cette raison, je fournis le code source depuis le début de cette partie 2 jusqu'ici.

L' organisation des résultats peut être différente chaque fois que vous exécutez l'application, car cette dernière n'effectue aucun tri lorsqu'elle exécute une requête. Dans certains entrepôts de données, vous pourriez recevoir en retour ces données dans le même ordre que celui dans lequel elles ont été insérées ; mais avec Core Data, cela finit par être joliment aléatoire. Nous allons corriger cela en ajoutant un tri à la requête fetch.

1. func fetchLog() {

2. let fetchRequest = NSFetchRequest(entityName: "LogItem")

4. // Créer un objet descripteur qui trie selon le « title »

5. // propriété de l'objet Core Data

6. let sortDescriptor = NSSortDescriptor(key: "title", ascending: true)

8. // Définir la liste des descripteurs de tri dans la requête fetch,

9. // donc il contient le descripteur de tri

10. fetchRequest.sortDescriptors = [sortDescriptor]

12. if let fetchResults = managedObjectContext!.executeFetchRequest(fetchRequest, error: nil) as? [LogItem] {

13. logItems = fetchResults

14. }

15. }

Maintenant, la requête fetch a son sortDescriptor bien paramétré. Notez qu'il s'agit d'un tableau, c'est pourquoi nous avons besoin de parenthèses seulement autour de sortDescriptor que nous avons créé en utilisant title comme clé. En lançant l'application, vous devriez maintenant voir la liste des éléments triée (alphabétiquement), beaucoup mieux ! Notez que vos données sont escomptées à être différentes.

Pensons aussi à filtrer certains éléments. Premièrement, essayons seulement d'obtenir les éléments nommés « Best Language ». Nous allons créer un NSPredicate qui utilise une chaîne de caractères pour représenter les prérequis que tout objet doit vérifier afin de passer dans la requête.

1. func fetchLog() {

2. let fetchRequest = NSFetchRequest(entityName: "LogItem")

4. // Créer un objet descripteur qui trie selon le « title »

5. // propriété de l'objet Core Data

6. let sortDescriptor = NSSortDescriptor(key: "title", ascending: true)

8. // Définir la liste des descripteurs de tri dans la requête fetch,

9. // donc il contient le descripteur de tri

10. fetchRequest.sortDescriptors = [sortDescriptor]

12. // Créer un nouveau prédicat qui filtre tout objet qui n'a pas exactement le titre de "Best Language"

13. // doesn't have a title of "Best Language" exactly.

14. let predicate = NSPredicate(format: "title == %@", "Best Language")

16. //Définir le prédicat dans la requête fetch

17. fetchRequest.predicate = predicate

19. if let fetchResults = managedObjectContext!.executeFetchRequest(fetchRequest, error: nil) as? [LogItem] {

20. logItems = fetchResults

21. }

22. }

Si vous n'avez pas encore vu le format de la syntaxe, ou que vous ne l'avez plus vu depuis un moment, c'est assez simple de dire que chaque fois que vous voyez un format en tant qu'un paramètre nommé, cela provient des méthodes en objective-C (langage C avec objets) « predicateWithFormat », ou « stringWithFormat », et ainsi de suite.

Cela remplace toute instance de symbole %@ par une description d'objet (la valeur d'une chaîne de caractères, ou une représentation autrement utile d'autres types d'objets). Pour les types primitifs tels que Int, vous opterez à la place pour %i, pour les Float, vous opterez pour %f, et ainsi de suite.

Donc lorsque vous voyez

1. (format: "title == %@", "Best Language")

Ce que le compilateur prend en compte est semblable à :

1. ("title == 'Best Language'")

Donc nous spécifions que nous voulons que le title soit égal à cette chaîne de caractères exacte.

Nous pourrions également faire une comparaison de chaînes de caractères en utilisant les mots-clés relatifs aux contenus ; si nous regardons la sous-chaîne de caractères «Worst», nos recevrons seulement les articles qui contiennent cette chaîne…

1. // Rechercher uniquement les items utilisant la sous-chaîne « Worst »

2. let predicate = NSPredicate(format: "title contains %@", "Worst")

4. // Définir le prédicat dans la requête fetch

5. fetchRequest.predicate = predicate

Qu'en serait-il si nous combinions cependant les deux ? Nous voulons à la fois les éléments contenant la chaîne de caractères « Worst » et le titre « Best Language » ?

Premièrement, créons les deux prédicats séparément :

1. // Créer un nouveau prédicat qui filtre tout objet qui n'a pas exactement le titre de "Best Language"

3. let firstPredicate = NSPredicate(format: "title == %@", "Best Language")

5. // Rechercher uniquement les items utilisant la sous-chaîne « Worst »

6. let thPredicate = NSPredicate(format: "title contains %@", "Worst")

Ensuite, combinons-les en utilisant le constructeur NSCompoundPredicate :

1. // Combiner les deux prédicats ci-dessus en un seul prédicat composé

2. let predicate = NSCompoundPredicate(type: NSCompoundPredicateType.OrPredicateType, subpredicates: [firstPredicate, thPredicate])

4. // Définir le prédicat dans la requête fetch

5. fetchRequest.predicate = predicate

Du moment que nous voulons à la fois les cas de « Best Language » et n'importe quel élément contenant « Worst », nous utilisons un prédicat composé de type NSCompoundPredicateType.OrPredicateType.

Tout cela n'est qu'une manière confuse de dire « donne-moi n'importe lequel des éléments où le firstPredicate (premier prédicat) ou le thPredicate est vrai ».

Ce que nous avons fait là est assez puissant en pratique. Nous pouvons utiliser une comparaison de chaînes de caractères comme prédicats, et filtrer ou trier de grandes listes de données à travers le Core Data.

L'API Core Data va alors se connecter au magasin de soutien (SQLite), et produire une requête efficiente pour rapidement regrouper l'information nécessaire. C'est un schéma très commun en développement iOS, et le comprendre est essentiel. Alors, si vous avez été coincé sur ce didacticiel ou que vous avez une quelconque question, n'hésitez pas à demander de l'aide sur les forums.

Cela conclut la partie 2 pour le moment ; dans la partie suivante, nous changerons pour un scénario plus appliqué, en ajoutant pour les utilisateurs une façon de créer des articles de journaux, de les éditer, de les sauver, et de les effacer.

Le code source complet de cette partie.