Использование NSOutlineView и Core Data

Создадим новый проект:

Почитать о базовой шаблонной структуре Core Data проекта можно в посте "Из чего состоит Cocoa-приложение работающее с Core Data".

В этом посте мы рассмотрим как соединить NSOutlineView с Core Data моделью. Для этого есть несколько способов в том числе Cocoa Bindings.

1. Создание сущности в модели данных

Откроем нашу модель CoreData_NSOutlineView.xcdatamodeld и создадим новую сущность Group.

Добавим обязательный атрибут name. По умолчанию он будет принимать значение New Group.


Добавим опциональные отношения parent и subGroups.


Проведем небольшой ликбез по отношениям в реляционных базах данных. Отношение - это связь между двумя сущностями. Например, есть сущности Сотрудник и Подразделение.
Видите, одной записи гараж соответствует много записей людей. Это соотношение называется один-ко-многим (1:M).

Идея реализации данной связи заключается в следующем. В таблице подразделения есть ключевое поле (ID) которое в данной таблице первичный ключ. На рисунке это запись с номером два. Этой записи может соответствовать много записей в таблице сотрудники. Значит в таблицу сотрудники нужно добавить поле в котором будет находиться первичный ключ таблицы подразделения. Подразделение, в котором работает человек - это просто атрибут этого человека. А список подразделений - это обычная таблица классификатор.
Давайте посмотрим пример связи многие-ко-многим. На предприятии есть работники, но и один работник может работать на двух предприятиях.
Вот это и есть связь многие-ко-многим (M-N). Как реализовать? Данная связь должна быть устранена путем замены двумя связями один-ко-многим (M-1, N-1). Это реализуется путем введения новой таблицы.
Можно сказать, что новая таблица является слабой сущностью, ее существования обусловлено только существованием двух других таблиц. Слабый тип то есть сущность зависит от другой. И сильный - это когда она не от кого не зависит.


Любое отношение (realationship) устанавливает связь между двумя сущностями.

У отношения parent установим свойству Destination значение Group, а свойству Inverse установим значение subGroups.

Аналогичным образом у отношения subGroups установим свойству Destination значение Group, а свойству Inverse установим значение parent. И ещё свойству Type нужно установить значение To Many.

2. Добавление и начальная настройка NSOutlineView

Переходим к интерфейсу. Добавьте на форму компонент NSOutlineView. Нам нужен только один столбец.

У меня долго не получалось добиться отображения имени объекта в ячейке. Оказалось, что надо поставить свойству Content Mode значение Cell Based.

Дополнительно в целях эстетики я убрал Focus Ring для следующие объектов:

3. Добавление и настройка NSArrayController

Далее добавим NSArrayController. Нужно сделать следующее:
  • установить режим Mode на значение Entity;
  • отметить пункт Prepares Content;
  • указать наименование требуемой сущность Entity Name, в нашем случае это Group;
  • указать предикат для извлечения экземпляров сущности: parent == nil (для начала нам нужен только корневой объект, а у него нет родителя).

Managed Object Model представляет собой описание сущностей базы данных.
Managed Object Context - это контекст посредством которого осуществляется доступ к Core Data хранилищу, которое описано с помощью Managed Object Model.
Можно провести такую аналогию, например, мультфильм Симпсоны это хранилище данных. Вы можете посмотреть этот мультфильм по телеканалу, интернету или на DVD. При такой аналогии Managed Object Context - это метод с помощью которого вы будете смотреть мультфильм.

Для того чтобы добавленный Array Controller мог "смотреть" Groups, нам нужно указать контекст.

По шаблону проекта делегат приложения AppDelegate уже содержит свойство managedObjectContext. Остается только связать добавленный Array Controller с этой переменной. Это можно сделать в инспекторе Bindings. Надо выбрать Bind to Application и указать значение delegate.managedObjectContext для свойства Model Key Path.

4. Добавление и настройка NSTreeController

Теперь добавим NSTreeController. В дальнейшем мы привяжем добавленный ранее NSOutlineView к данному контроллеру. Также как раньше нужно сделать следующее:
  • установить режим Mode на значение Entity;
  • отметить пункт Prepares Content;
  • указать наименование требуемой сущность Entity Name, в нашем случае это Group;
  • установить для свойства Children значение subGroups (контроллер будет знать как обходить дерево).

Теперь для добавленного только что NSTreeController откроем Bindings Inspector. Привяжем его к добавленному ранее NSArrayController, по ключу arrangedObjects. Смотрите не напутайте, привязывать надо в секции Content Array.

У нас будет следующая схема. NSTreeController базируется на NSArrayController, который предоставляет управляемые объекты (managed objects), а NSTreeController отдает иерархическую структуру в NSOutlineView.

5. Заключительная настройка NSOutlineView

Вернемся к NSOutlineView. Надо привязать его единственный столбец к NSTreeController.

6. Кнопка для добавления новых элементов

Добавим кнопку для добавления новых элементов. Свяжем её с NSArrayController.

Во всплывающем окне надо выбрать пункт add:

7. Drag-and-drop

Создадим новый класс OutlineViewController.

OutlineViewController.h

#import <Cocoa/Cocoa.h>

@interface OutlineViewController : NSObject <NSOutlineViewDataSource> {
    IBOutlet NSTreeController *treeController;
    IBOutlet NSOutlineView    *myOutlineView;
    NSArray     *dragType;
    NSTreeNode  *draggedNode;
}
@end


OutlineViewController.m

#import "OutlineViewController.h"

@implementation OutlineViewController

- (void)awakeFromNib {
    dragType = [NSArray arrayWithObjects: @"factorialDragType", nil];
    [ myOutlineView registerForDraggedTypes:dragType ];
    NSSortDescriptor* sortDesc = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES];
    [treeController setSortDescriptors:[NSArray arrayWithObject: sortDesc]];
}

- (BOOL) outlineView : (NSOutlineView *) outlineView
          writeItems : (NSArray*) items
        toPasteboard : (NSPasteboard*) pboard {
    [ pboard declareTypes:dragType owner:self ];
    draggedNode = [ items objectAtIndex:0 ];
    return YES;
}

- (BOOL)outlineView:(NSOutlineView *)outlineView
         acceptDrop:(id <NSDraggingInfo>)info
               item:(id)item
         childIndex:(NSInteger)index {
    NSManagedObject* draggedTreeNode = [ draggedNode representedObject ];
    [ draggedTreeNode setValue:[item representedObject ] forKey:@"parent" ];
    return YES;
}

- (BOOL) category:(NSManagedObject* )cat
  isSubCategoryOf:(NSManagedObject* )possibleSub {
    // Depends on your interpretation of subCategory ....
    if ( cat == possibleSub ) {
        return YES;
    }
    NSManagedObject* possSubParent = [possibleSub valueForKey:@"parent"];
    if ( possSubParent == NULL ) {
        return NO;
    }
    
    while ( possSubParent != NULL ) {
        if ( possSubParent == cat ) {
            return YES;
        }
        // move up the tree
        possSubParent = [possSubParent valueForKey:@"parent"];
    }
    
    return NO;
}

// This method gets called by the framework but
// the values from bindings are used instead
- (id)outlineView:(NSOutlineView *)outlineView
objectValueForTableColumn:(NSTableColumn *)tableColumn
           byItem:(id)item {
    return NULL;
}

- (NSDragOperation)outlineView:(NSOutlineView *)outlineView
                  validateDrop:(id <NSDraggingInfo>)info
                  proposedItem:(id)item
            proposedChildIndex:(NSInteger)index {
    // drags to the root are always acceptable
    if ( [item representedObject] == NULL ) {
        return NSDragOperationGeneric;
    }
    // Verify that we are not dragging a parent to one of it's ancestors
    // causes a parent loop where a group of nodes point to each other
    // and disappear from the control
    NSManagedObject* dragged = [ draggedNode representedObject ];
    NSManagedObject* newP = [ item representedObject ];
    if ( [ self category:dragged isSubCategoryOf:newP ] ) {
        return NO;
    }
    return NSDragOperationGeneric;
}

/* The following are implemented as stubs because they are
 required when implementing an NSOutlineViewDataSource.
 Because we use bindings on the table column these methods are never called.
 The NSLog statements have been included to prove that these methods are not called. */
- (NSInteger)outlineView:(NSOutlineView *)outlineView
  numberOfChildrenOfItem:(id)item {
    NSLog(@"numberOfChildrenOfItem");
    return 1;
}

- (BOOL)outlineView:(NSOutlineView *)outlineView
   isItemExpandable:(id)item {
    NSLog(@"isItemExpandable");
    return YES;
}

- (id)outlineView:(NSOutlineView *)outlineView
            child:(NSInteger)index
           ofItem:(id)item {
    NSLog(@"child of Item");
    return NULL;
}

@end


В интерфейсе создадим экземпляр класса OutlineViewController. Свяжем свойства treeController и myOutlineView с соответствующими элементами в интерфейсе.

Для NSOutlineView установим источником данных наш экземпляр OutlineViewController.

Результат:


источникмои исходники