Из чего состоит Cocoa-приложение работающее с Core Data


Базовый шаблон Cocoa Application проекта с поддержкой Core Data состоит из следующих файлов:
  • AppDelegate.h
  • AppDelegate.m
  • ViewController.h
  • ViewController.m
  • Assets.xcassets
  • Main.storyboard
  • Info.plist
  • Core_Data_Template.xcdatamodeld
  • main.m

Рассмотрим все файлы и код в них чтобы понять какую роль они играют в проекте.



AppDelegate.h

Класс AppDelegate представляет собой делегат приложения. Это означает что приложение делегирует этому классу часть своих рабочих вопросов. Программист в процессе реализации делегата определяет как конкретно приложение должно поступать в доступных делегату ситуациях. В заголовочном файле AppDelegate.h заданы объявления трех свойств:

#import <Cocoa/Cocoa.h>

@interface AppDelegate : NSObject <NSApplicationDelegate>

@property (readonly, strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator;
@property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel;
@property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;


@end

Эти три свойства составляют основу Core Data стека:
  • persistent store coordinator (координатор постоянных хранилищ);
  • managed object model (управляемая объектная модель);
  • managed object context (управляемый контекст объектов).

В качестве постоянного хранилища может выступать легковесная база данных SQLite. Для Mac OS X также доступен вариант использования XML в качестве постоянного хранилища.

В runtime-режиме координатор постоянных хранилищ отвечает за отображение (mapping) объектов на базу данных. Один NSPersistentStoreCoordinator может управлять множеством постоянных хранилищ. Но в нашем простейшем случае координатор постоянного хранилищ "координирует" работу одиночного постоянного хранилища.

Вдумайтесь в название класса NSManagedObjectContext, оно говорит само за себя, это контекст для управляемого объекта (NSManagedObject). Каждый управляемый объект NSManagedObject существует в рамках определенного контекста NSManagedObjectContext. Можно создать любое количество контекстов. Управляемый контекст объектов связывает воедино постоянное хранилище и модель данных, т.е. является промежуточным звеном между хранилищем и управляемыми объектами. Контекст следит за тем, что должно быть считано или записано.

AppDelegate.m

Рассмотрим по порядку весь кода файла AppDelegate.m. В этом файле реализуется делегат Cocoa-приложения. Смотрим:

#import "AppDelegate.h"

@interface AppDelegate ()

- (IBAction)saveAction:(id)sender;

@end

@implementation AppDelegate

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    // Insert code here to initialize your application
}

- (void)applicationWillTerminate:(NSNotification *)aNotification {
    // Insert code here to tear down your application
}

Тут у нас объявление метода saveAction:, назначение которого мы рассмотрим ниже. А также два метода applicationDidFinishLaunching: и applicationWillTerminate: с говорящими названиями. Первый выполняется после запуска приложения, так что если вам нужно сделать какую-нибудь инициализацию, то тут ей самое место. Второй выполняется при завершении работы приложения. Смотрим дальше:

#pragma mark - Core Data stack

@synthesize persistentStoreCoordinator = _persistentStoreCoordinator;
@synthesize managedObjectModel = _managedObjectModel;
@synthesize managedObjectContext = _managedObjectContext;

- (NSURL *)applicationDocumentsDirectory {
    // The directory the application uses to store the Core Data store file. This code uses a directory named "com.blogspot.devtype.Core_Data_Template" in the user's Application Support directory.
    NSURL *appSupportURL = [[[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask] lastObject];
    return [appSupportURL URLByAppendingPathComponent:@"com.blogspot.devtype.Core_Data_Template"];
}

Команда сокращенного вызова @synthesize предписывает компилятору сгенерировать код, необходимый для доступа к экземплярным переменным. Вроде бы начиная с Xcode 4.5 использование @synthesize в файле реализации стало необязательным. Понятия не имею почему эта директива в данном случае добавляется в Xcode 7. Если вы знаете, то поясните пожалуйста в комментариях к посту.

Метод applicationDocumentsDirectory возвращает путь к папке в которой приложение хранит свои документы. В данном коде метод вернет URL соответствующий пути ~/Library/Application Support/com.blogspot.devtype.Core_Data_Template. Последний элемент пути com.blogspot.devtype.Core_Data_Template соответствует bundle-идентификатору приложения (см. самый первый скриншот с параметрами проекта). В папке по этому пути будет хранится база данных приложения. Смотрим дальше:

- (NSManagedObjectModel *)managedObjectModel {
    // The managed object model for the application. It is a fatal error for the application not to be able to find and load its model.
    if (_managedObjectModel) {
        return _managedObjectModel;
    }
 
    NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"Core_Data_Template" withExtension:@"momd"];
    _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
    return _managedObjectModel;
}

Тут создается управляемая объектная модель. Создается она единожды, а дальше просто в место вызова данного метода возвращается уже созданная модель. Обратите внимание на то что модель данных подгружается из файла с именем Core_Data_Template.momd. После сборки проекта исходная модель данных, которая изначально хранится в файле с расширением xcdatamodeld компилируется в momd-пакет.


Соответственно данный пакет в runtime-режиме надо подгрузить. Смотрим дальше:

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
    // The persistent store coordinator for the application. This implementation creates and returns a coordinator, having added the store for the application to it. (The directory for the store is created, if necessary.)
    if (_persistentStoreCoordinator) {
        return _persistentStoreCoordinator;
    }
    
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSURL *applicationDocumentsDirectory = [self applicationDocumentsDirectory];
    BOOL shouldFail = NO;
    NSError *error = nil;
    NSString *failureReason = @"There was an error creating or loading the application's saved data.";
    
    // Make sure the application files directory is there
    NSDictionary *properties = [applicationDocumentsDirectory resourceValuesForKeys:@[NSURLIsDirectoryKey] error:&error];
    if (properties) {
        if (![properties[NSURLIsDirectoryKey] boolValue]) {
            failureReason = [NSString stringWithFormat:@"Expected a folder to store application data, found a file (%@).", [applicationDocumentsDirectory path]];
            shouldFail = YES;
        }
    } else if ([error code] == NSFileReadNoSuchFileError) {
        error = nil;
        [fileManager createDirectoryAtPath:[applicationDocumentsDirectory path] withIntermediateDirectories:YES attributes:nil error:&error];
    }
    
    if (!shouldFail && !error) {
        NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
        NSURL *url = [applicationDocumentsDirectory URLByAppendingPathComponent:@"OSXCoreDataObjC.storedata"];
        if (![coordinator addPersistentStoreWithType:NSXMLStoreType configuration:nil URL:url options:nil error:&error]) {
            coordinator = nil;
        }
        _persistentStoreCoordinator = coordinator;
    }
    
    if (shouldFail || error) {
        // Report any error we got.
        NSMutableDictionary *dict = [NSMutableDictionary dictionary];
        dict[NSLocalizedDescriptionKey] = @"Failed to initialize the application's saved data";
        dict[NSLocalizedFailureReasonErrorKey] = failureReason;
        if (error) {
            dict[NSUnderlyingErrorKey] = error;
        }
        error = [NSError errorWithDomain:@"YOUR_ERROR_DOMAIN" code:9999 userInfo:dict];
        [[NSApplication sharedApplication] presentError:error];
    }
    return _persistentStoreCoordinator;
}

В этом методе создается координатор постоянных хранилищ. В начале делается проверка, что существует папка ~/Library/Application Support/com.blogspot.devtype.Core_Data_Template, если она не существует, то она создается. Если вместо с папки с таким именем уже существует файл с таким именем, то это приведет к ошибке.

Далее создается координатор. Обратите внимание, что при создании координатора через параметр конструктора ему передается управляемая объектная модель (managed object model). Это значит, что имея экземпляр NSPersistentStoreCoordinator можно через его метод managedObjectModel получить управляемую объектную модель NSManagedObjectModel.

К координатору добавляется постоянное хранилище URL которого указывает на файл по адресу  ~/Library/Application Support/com.blogspot.devtype.Core_Data_Template/OSXCoreDataObjC.storedata. Если файл не существует, то он будет создан.


Смотрим дальше:

- (NSManagedObjectContext *)managedObjectContext {
    // Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.)
    if (_managedObjectContext) {
        return _managedObjectContext;
    }
    
    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (!coordinator) {
        return nil;
    }
    _managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    [_managedObjectContext setPersistentStoreCoordinator:coordinator];

    return _managedObjectContext;
}

Тут создается контекст. Имея экземпляр NSManagedObjectContext можно через его свойство persistentStoreCoordinator получить координатор постоянных хранилищ NSPersistentStoreCoordinator. Соответственно тут при создании контекста ему назначается координатор постоянных хранилищ. Смотрим дальше:

#pragma mark - Core Data Saving and Undo support

- (IBAction)saveAction:(id)sender {
    // Performs the save action for the application, which is to send the save: message to the application's managed object context. Any encountered errors are presented to the user.
    if (![[self managedObjectContext] commitEditing]) {
        NSLog(@"%@:%@ unable to commit editing before saving", [self class], NSStringFromSelector(_cmd));
    }
    
    NSError *error = nil;
    if ([[self managedObjectContext] hasChanges] && ![[self managedObjectContext] save:&error]) {
        [[NSApplication sharedApplication] presentError:error];
    }
}

В вышеприведенном методе выполняется сохранение. При сохранении изменений сохраняется контекст. Данные сохраняются в постоянное хранилище. Смотрим дальше:

- (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)window {
    // Returns the NSUndoManager for the application. In this case, the manager returned is that of the managed object context for the application.
    return [[self managedObjectContext] undoManager];
}

Тут возвращается менеджер undo/redo операций. С помощью него можно отменять и вновь применять проведенные изменения. И последний в этом файле метод:

- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
    // Save changes in the application's managed object context before the application terminates.
    
    if (!_managedObjectContext) {
        return NSTerminateNow;
    }
    
    if (![[self managedObjectContext] commitEditing]) {
        NSLog(@"%@:%@ unable to commit editing to terminate", [self class], NSStringFromSelector(_cmd));
        return NSTerminateCancel;
    }
    
    if (![[self managedObjectContext] hasChanges]) {
        return NSTerminateNow;
    }
    
    NSError *error = nil;
    if (![[self managedObjectContext] save:&error]) {

        // Customize this code block to include application-specific recovery steps.              
        BOOL result = [sender presentError:error];
        if (result) {
            return NSTerminateCancel;
        }

        NSString *question = NSLocalizedString(@"Could not save changes while quitting. Quit anyway?", @"Quit without saves error question message");
        NSString *info = NSLocalizedString(@"Quitting now will lose any changes you have made since the last successful save", @"Quit without saves error question info");
        NSString *quitButton = NSLocalizedString(@"Quit anyway", @"Quit anyway button title");
        NSString *cancelButton = NSLocalizedString(@"Cancel", @"Cancel button title");
        NSAlert *alert = [[NSAlert alloc] init];
        [alert setMessageText:question];
        [alert setInformativeText:info];
        [alert addButtonWithTitle:quitButton];
        [alert addButtonWithTitle:cancelButton];

        NSInteger answer = [alert runModal];
        
        if (answer == NSAlertFirstButtonReturn) {
            return NSTerminateCancel;
        }
    }

    return NSTerminateNow;
}

Тут перед закрытием приложения сохраняются сделанные изменения.

исходники