niedziela, 11 stycznia 2015

iOS, a co to jest - odcinek 12: Core Data

Tym razem nieco informacji o Core Data, który jest takim uniwersalnym ORM mogącym współpracować z różnymi źródłami danych. Przy okazji dodam, że takie są założenia budowanego Entity Framework w wersji 7. Muszę przyznać, że API w Core Data wygląda całkiem przyjemnie, dodatkowo przewidziano współpracę np. z UITableView w zakresie grupowania (no, tu już API nie jest takie przyjemne, ale idea słuszna) czy stronicowania. Należy zauważyć możliwość szczegółowego profilowania operacji na danych. Brawo!

 

Intro

Core Data

  • framework
  • zarządzanie cyklem życia obiektów
  • zarządzanie grafem obiektów
  • persystencja

Stos

  • NSManagedObject, NSManagedObjectContext
  • NSPersistentStoreCoordinator (+ NSManagedObjectModel)
  • NSPersistentStore (SQLite, XML, pamięć, iCloud, …)

 

Model

Elementy

  • encje (odpowiednik tabel)
  • atrybuty (odpowiednik kolumn; typ, walidacja: min, max, default, właściwości)
  • relacje (to-one, to-many, opcje: m.in typ, opcjonalność, reguła usuwania)
  • dziedziczenie
  • designer (plik *.xcdatamodelId)

Zaawansowane opcje

  • szablony zapytań
  • wersjonowanie
  • dodatkowe informacje w postaci słowników (user info dictionaries)
  • konfiguracje

 

Prosty odczyt i zapis

XCode: projekt z opcją Core Data z wygenerowanym kodem

AppDelegate

@interface AppDelegate: UIResponder<UIApplicationDelegate>

@property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;

@property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel;

@property (readonly, strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator;

- (void) saveContext;

- (NSURL *) applicationDocumentsDirectory;

@end

@implementation AppDelegate

- (NSManagedObjectModel *)managedObjectModel {
    if (_managedObjectModel != nil) {
        return _managedObjectModel;
    }
    NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"CoreDataDemo" withExtension:@"momd"];
    _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];

   //inna możliwość:   [NSManagedObjectModel mergedModelFromBundles: nil]
    return _managedObjectModel;
}

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
    if (_persistentStoreCoordinator != nil) {
        return _persistentStoreCoordinator;
    }    
    _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"CoreDataDemo.sqlite"];
    NSError *error = nil;
    if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
    }
   
    return _persistentStoreCoordinator;
}

- (NSManagedObjectContext *)managedObjectContext {
    if (_managedObjectContext != nil) {
        return _managedObjectContext;
    }    
    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (!coordinator) {
        return nil;
    }
    _managedObjectContext = [[NSManagedObjectContext alloc] init];
    [_managedObjectContext setPersistentStoreCoordinator:coordinator];
    return _managedObjectContext;
}

- (void)saveContext {  
    NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
    if (managedObjectContext != nil) {
        NSError *error = nil;
        if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
        }
    }
}

@end

Opis encji:

NSEntityDescription *desc;

desc = [NSEntityDescription entityForName: @”Album”  inManagedObjectContext: context];

Wstawianie obiektu:

NSManagedObject *obj;

obj = [NSEntityDescription insertNewObjectForEntityForName: @”Album”  inManagedObjectContext: context];

 

[obj setValue: @”Kazik”  forKey: @”artist”];

[obj setValue: @”Melassa”  forKey: @”title”];

 

NSString *title = [obj valueForKey: @”title”];

Pobieranie obiektów:

NSFetchRequest *req;

req = [NSFetchRequest fetchRequestWithEntityName: @”Album”];

NSError *error;

NSArray *allAlbums = [context executeFetchRequest: req  error: &error];

 

Odczytywanie i zapisywanie - bardziej złożone scenariusze

Własne klasy encji:

dziedziczymy po NSManagedObject

@interface Album: NSManagedObject

@property (nonatomic, retain) NSString* artist;

@property (nonatomic, retain) NSString* title;

@end

 

@interface Album (CoreDataGeneratedAccessors)

@end

 

@implementation Album

@dynamic artist;  //implementacja dostarczana w runtime

@dynamic title;

- (void) awakeFromInsert {

        [self setTitle: @”No title”];   //w przypadku niedostarczenia danych

}

@end

nie należy nadpisywać:

  • primitiveValueForKey:
  • setPrimitiveValue: forKey:
  • isEqual:
  • hash
  • superclass
  • class
  • self
  • zone
  • isProxy
  • managedObjectContext
  • entity
  • description

implementujemy:

  • awakeFromInsert
  • awakeFromFetch

kreator w XCode: nowy plik –> Core Data –> NSManagedObject subclass

używanie wygenerowanych klas:

Album *album;

album = [NSEntityDescription insertNewObjectForEntityForName: @”Album”  inManagedObjectContext: context];

[album setTitle: @”Hurra”];

NSString *title = [album title];

Relacje:

NSManagedObject *category = [item valueForKey: @”category”];

MusicCategory *category = [item category];  //przy własnych klasach encji

 

NSMutableSet *songs = [item mutableSetValueForKey: @”songs”];

[songs addObject: song];

[item addSongsObject: song];  //wygenerowana metoda we własnej klasie encji

Zapytania:

Zwracają obiekty NSManagedObject albo nasze encje. Encje powiązane są ładowane.

NSPredicate:

NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName: @”Album”];

NSPredicate *pred = [NSPredicate predicateWithFormat: @”title CONTAINS %@”, searchString];

NSPredicate *pred = [NSPredicate predicateWithFormat: @”%K CONTAINS %@”, attr, searchString];

[request setPredicate: pred];

porównanie napisu:

  • CONTAINS
  • BEGINSWITH
  • ENDSWITH
  • LIKE
  • MATCHES

proste porównanie:

  • =, ==
  • >=, =>
  • <=, =<
  • <
  • >
  • !=, <>
  • BETWEEN

agregacje:

  • ANY, SOME
  • ALL
  • NONE
  • IN

NSSortDescriptor:

NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName: @”Album”];

NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey: @”title” ascending: YES];

NSArray *sortArray = [NSArray arrayWithObject: sort];

[request setSortDescriptors: sortArray];

Szablony zapytań

możliwość definiowania i trzymania w diagramie

NSFetchRequest *request = [model fetchRequestTemplateForName: @”allRockAlbums”];

 

NSFetchedResultsController

Prezentowanie danych w sekcjach UITableView:

sekcje: pierwszy deskryptor w kolekcji z obiektami NSSortDescriptor w NSFetchRequest

sortowanie w sekcjach: drugi deskryptor

 

@interface ViewController: UITableViewController <NSFetchedResultsControllerDelegate>

@property (nonatomic, retain) NSFetchedResultsController *fetchedResultsController;

@end

@implementation ViewController

- (void) viewDidLoad  {

     …

     NSError *error;

      if (![[self fetchedResultsController]  performFetch: &error]) {

      }

}

- (NSFetchedResultsController *) fetchedResultsController  {

       …

       //przygotowanie requestu z deskryptorami do sortowania, pierwszy z “category.name”

       NSFetchedResultsController *frc = [[NSFetchedResultsController alloc] initWithFetchRequest: request

                             managedObjectContext: context  sectionNameKeyPath: @”category.name”  cacheName: nil ];

       [self setFetchedResultsController: frc];

       [[self  fetchedResultsController] setDelegate: self];

       return _fetchedResultsController;

}

- (NSInteger) numberOfSectionsInTableView: (UITableView *) tableView  {

       NSArray *sections = [[self fetchedResultsController] sections];

       return [sections count];

}

- (NSInteger) tableView: (UITableView *) tableView

numberOfRowsInSection: (NSInteger) section {

         NSArray *sections = [[self fetchedResultsController] sections];

         id<NSFetchedResultsSectionInfo> curSec;

         curSec = [sections objectAtIndex: section];

         return [curSec numberOfObjects];

}

- (UITableViewCell *) tableView: (UITableView *) tableView cellForRowAtIndexPath: (NSIndexPath *) indexPath  {

          …

          Album *item = [[self fetchedResultsController] objectAtIndexPath: indexPath];

         …

}

- (NSString *) tableView: (UITableView *) tableView titleForHeaderInSection: (NSInteger) section {

         NSArray *sections = [[self fetchedResultsController] sections];

         id<NSFetchedResultsSectionInfo> curSec = [sections objectAtIndex: section];

         return [curSec name];

}

//nasłuchiwanie zmian danych

- (void) controllerWillChangeContent: (NSFetchedResultsController *) controller {

        [[self tableView] beginUpdates];

}

- (void) controller: (NSFetchedResultsController *) controller

    didChangeSection: (id<NSFetchedResultsSectionInfo>) sectionInfo

                        atIndex:  (NSUInteger) sectionIndex

         forChangeType:  (NSFetchedResultsChangeType) type {

         switch (type) {

                  case NSFetchedResultsChangeInsert:

                           [[self tableView] insertSections: [NSIndexSet indexSetWithIndex: sectionIndex]

                                                              withRowAnimation: UITableViewRowAnimationFade];

                           break;

                  case NSFetchedResultsChangeDelete:

                           [[self tableView] deleteSections: [NSIndexSet indexSetWithIndex: sectionIndex]

                                                              withRowAnimation: UITableViewRowAnimationFade];

                           break;

         }

}

- (void) controller: (NSFetchedResultsController *) controller

    didChangeObject: (id) anObject

    atIndexPath: (NSIndexPath *) indexPath

    forChangeType: (NSFetchedResultsChangeType) type

    newIndexPath: (NSIndexPath *) newIndexPath {

     switch (type) {

              case NSFetchedResultsChangeInsert:

                      [[self tableView] insertRowsAtIndexPaths: [NSArray arrayWithObject: newIndexPath]

                                                         withRowAnimation: UITableViewRowAnimationFade];

                      break;

              case NSFetchedResultsChangeDelete:

                      [[self tableView] deleteRowsAtIndexPaths: [NSArray arrayWithObject: indexPath]

                                                         withRowAnimation: UITableViewRowAnimationFade];

                      break;

              case NSFetchedResultsChangeUpdate:

              {

                       UITableViewCell *cell = [[self tableView] cellForRowAtIndexPath: indexPath];

                       Album *item = [[self fetchedResultsController]  objectAtIndexPath: indexPath];

                       [[cell textLabel] setText: [item title];

                       break;

              }

              case NSFetchedResultsChangeMove:

                       [[self tableView] deleteRowsAtIndexPaths: [NSArray arrayWithObject: indexPath]

                                                         withRowAnimation: UITableViewRowAnimationFade];

                       [[self tableView] insertRowsAtIndexPaths: [NSArray arrayWithObject: newIndexPath]

                                                         withRowAnimation: UITableViewRowAnimationFade];

                       break;

      }

}

- (NSString *) controller: (NSFetchedResultsController *) controller

    sectionIndexTitleForSectionName: (NSString *) sectionName {

       return sectionName;

}

- (void) controllerDidChangeContent: (NSFetchedResultsController *) controller {

       [[self tableView] endUpdates]; 

}

@end

 

Wersjonowanie modelu

po zmianie modelu trzeba zmigrować dane w repozytorium

migracje

  • dodawanie, usuwanie, zmiana nazw atrybutów, encji, relacji
  • tworzenie encji rodziców i dzieci
  • przesuwanie atrybutów w górę i w dół hierarchii encji

konfiguracja automatycznych migracji:

NSMutableDictionary *options = [NSMutableDictionary dictionary];

[options setValue: [NSNumber numberWithBool: YES]

                      forKey: NSMigratePersistentStoresAutomaticallyOption];

[options setValue: [NSNumber numberWithBool: YES]

                      forKey: NSInferMappingModelAutomaticallyOption];

NSError *error = nil;

[persistentStoreCoordinator addPersistentStoreWithType: NSSQLiteStoreType

                                                                                 configuration: nil

                                                                                                   URL: dataStoreURL

                                                                                           options: options

                                                                                                error: &error];

tworzymy nową wersję modelu:  Editor -> Add Model Version…

ustawiamy nowy model jako bieżący: wybieramy w ustawieniach katalogu z modelami

wykonujemy zmiany na nowej wersji, w ustawieniach elementu uzupełniamy w sekcji Versioning np. poprzednią nazwę

poprzednio wygenerowane klasy encji albo ręcznie edytujemy albo generujemy jeszcze raz

bardziej zaawansowane scenariusze: tworzenie modeli mapujących pomiędzy wskazanymi wersjami modelu

 

Wydajność

Model

  • denormalizacja (zamiast relacji atrybut z obiektem tego rodzaju)
  • dodanie indeksu na atrybucie (opcja Indexed)
  • tworzenie indeksu na wielu kolumnach (w encji sekcja Indexes)
  • przechowywanie w postaci binarnej atrybutu (np. zdjęcia, typ: Binary Data, opcja: Allows External Storage)

Refaulting:

[context refreshObject: item mergesChanges: NO];

[context reset];

Prefetching:

NSEntityDescription *entity = [NSEntityDescription entityForName: @”Album”  inManagedObjectContext: context];

NSFetchRequest *request = [[NSFetchRequest alloc] init];

[request setEntity: entity];

 

NSMutableArray *prefetchKeys = [NSMutableArray array];

[prefetchKeys addObject: @”category”];

 

[request setRelationshipKeyPathsForPrefetching: prefetchKeys];  //ładowanie od razu encji we wskazanych miejscach

Ograniczenie pobieranych danych:

NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName: @”Album”];

[request setFetchBatchSize: 10]; //doładowywanie na żądanie, jeśli będzie używane, współpraca z UITableView

Wydajne tekstowe zapytania:

np. < przed CONTAINS w koniunkcji

Rodzaje współbieżności:

  • NSConfinementConcurrencyType
  • NSMainQueueConcurrencyType
  • NSPrivateQueueConcurrencyType

NSUInteger type = NSPrivateQueueConcurrencyType;

_context = [[NSManagedObjectContext alloc] initWithConcurrencyType: type];

[_context performBlock: ^{

}];

Debugowanie:

w ustawieniach projektu w sekcji Run na zakładce Arguments dodajemy argument:  -com.apple.CoreData.SQLDebug 1

Profilowanie:

XCode: Product –> Profile –> Core Data

 

Uzupełnienia

UIPickerView - odpowiednik comba

@interface ViewController: UIViewController <UIPickerViewDelegate, UIPickerViewDataSource>

@property (weak, nonatomic) IBOutlet UIPickerView *picker;

@property (nonatomic, strong) NSArray *allCategories;

@property (nonatomic, strong) MusicCategory *selectedCategory;

@end

 

@implementation ViewController

- (void) viewDidLoad {

         …

         [[self picker] setDelegate: self];

         [[self picker] setDataSource: self];

 

        [[self picker]  selectRow: 0 inComponent: 0 animated: NO];

        [self setSelectedCategory: [[self allCategories]  objectAtIndex: 0]];

}

//UIPickerViewDataSource

- (NSInteger) numberOfComponentsInPickerView: (UIPickerView *) pickerView {

        return 1;

}

- (NSInteger) pickerView: (UIPickerView *) pickerView numberOfRowsInComponent: (NSInteger) component {

       return [[self allCategories] count];

}

//UIPickerViewDelegate

- (NSString *) pickerView: (UIPickerView *) pickerView

                           titleForRow: (NSInteger) row

                       forComponent: (NSInteger) component  {

         MusicCategory *category = [[self allCategories] objectAtIndex: row];

         return [category categoryName];

}

- (void) pickerView: (UIPickerView *) pickerView

            didSelectRow:  (NSInteger) row

            inComponent:  (NSInteger) component  {

         MusicCategory *selected = [[self allCategories] objectAtIndex: row];

         [self setSelectedCategory: selected];

}

@end

Programowe wyłączenie klawiatury na polu tekstowym

[textField resignFirstResponder];

Programowa zmiana zakładki w UITabBarController z poziomu jednej z zakładek

[[self tabBarController]  setSelectedIndex: 0];

Brak komentarzy: