코어데이터와 UITableView와의 연결
지금까지 몇 개의 예제를 통해 코어데이터를 사용해서 일련의 데이터를 디스크에 읽고 쓰며, 이를 관리하는 방법에 대해 살펴보았는데, 이런 작업을 보다 쉽게 만들어주는 컨트롤러가 있었으니, 바로 NSFetchedResultsController
이다.
이 클래스는 코어데이터의 컨텍스트를 기반으로 저장소로부터 조건에 맞는 객체를 읽어들여서, UITableView의 데이터소스 메소드에서 쉽게 사용할 수 있는 형태로 제공한다. 또한 읽어온 객체에 대해 추적 기능을 가지고 있어서 managed object에 어떤 변경이 발생할 때 이를 감지하여 적절하게 테이블 뷰에서의 변경을 만들어낼 수 있다.
단, NSFetchedResultsController는 저장소나 컨텍스트를 생성하지는 못한다. 코어데이터 프로그램에서 수동으로 배열을 만들어 불러온 데이터를 관리하는 부분에서의 코드의 양을 줄이는 용도로 사용할 수 있다는 것이 이 클래스의 의의랄까.
NSFetchedResultsController는 이름에서처럼 불러온 데이터들을 제어한다. 따라서 데이터를 불러오는 데 필요한 managed object context, fetch request, predicate, sort descriptor 등은 일반적인 코어데이터 프로그램에서와 같이 우리가 일일이 생성해 주어야 한다.
대신, 이 컨트롤러는 UITableView의 indexPath에 대한 접근 방식에 일치하는 메소드들을 가지고 있으므로, 데이터소스를 생성하는데 매우 편리하게 활용된다.
생성방법
컨트롤러의 생성을 위해서는 initWithFetchRequest:… 메소드를 사용하면 된다. 이 때 필요한 파라미터로는 다음의 것들이 있다.
(NSFetchRequest*)fetchRequest
(NSManagedObjectContext*)managedObjectContext
(NSString *)sectionNameKeyPath
(NSString*)cacheName
기본적으로 fetchRequest
는 별도로 생성해야 한다. 섹션네임키패스는 섹션이 여러 개인 경우 각 섹션의 이름을 제공하는 객체를 지정한다. 보통 섹션을 하나만 사용하는 경우에는 nil을 넘기면 된다. 또한 캐시 이름은 캐시를 저장할 파일을 생성한다. nil을 사용하면 메모리내에서만 추적이 일어나는데, 이는 약간의 오버헤드를 유발할 수 있다. 대신 서로 다른 엔트리에 대해서 컨트롤러를 반복해서 재사용해야 하거나 할 때는 일일이 캐시를 삭제해 주어야 하므로, 상황에 맞게 사용하면 된다. 또한 캐시는 단순히 이름만 주면 된다.
다음은 컨트롤러를 생성하는 예제이다. 해당 클래스에는 컨텍스트, 패치 리퀘스트, 컨트롤러가 각각 프로퍼티로 설정되어 있다고 가정한다.
<# managedObjectContext를 구함 #>
...
-(NSFetchRequest*)fetchRequest
{
// 일반적인 코어데이터의 fetch 구문과 동일하다.
if(!_fetchRequest) {
_fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entityDescription = [NSEntityDescription EntityForName:@"Memo" inManagedObjectContext:self.managedObjectContext];
[_fetchRequest setEntity:entityDescription];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"isAvailable == YES"];
[_fetchRequest setPredicate:predicate];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastModifiedDate" ascending:NO];
[_fetchRequest setSortDescriptors:[NSArray arrayWithObject:sortDescriptor]];
}
return _fetchRequest;
}
-(NSFetchedResultsController*)controller {
if (!_controller) {
_controller = [[NSFetchedResultsController alloc] initWithFetchRequest:self.fetchRequest
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:nil
cacheName:nil];
}
return _controller;
}
performFetch
컨트롤러에 performFetch 메시지를 보내면 코어데이터 저장소로부터 지정된 조건에 맞게 결과를 가져와 그 결과값을 컨트롤러 내부에서 관리하기 시작한다. 만약 델리게이트가 지정되어 있다면, 컨트롤러는 불러온 결과에 대한 변경을 추적하고 변경이 생기면 이를 델리게이트에게 알려준다.
테이블 뷰와의 결합
컨트롤러는 sections
, sectionIndexTitles
의 프로퍼티와 objectAtIndexPath:
, sectionForSectionIndexTitle:
, sectionIndexTitleForSectionName:
등의 메소드를 가지고 있고, 이를 사용하여 테이블 뷰에 데이터소스로 연결될 수 있다.
다음은 테이블뷰의 데이터소스를 구현한 예이다. 테이블 뷰 컨트롤러에서 구현해야 하는 내용은 대부분 동일하거나 유사한 구문으로 페치 컨트롤러에서 제공하고 있다.
-(NSInteger)numberOfSectionsInTableView:(UITableView*)tableView
{
return [self.controller.sections count];
}
-(NSArray*)sectionIndexTitlesForTableView:(UITableView*)tableView
{
return self.controller.sectionIndexTitles;
}
-(UITableView*)sectionForSectionIndexTitle:(NSString*)title atIndex:(NSInteger)index
{
return [self.controller sectionForSectionIndexTitle:title atIndex:index];
}
-(UITableView*)tableView titleForHeaderInSection:(NSInteger)section
{
id<NSFetchedResultSectionInfo> sectionInfo = [[self.controller sections] objectAtIndex:section];
return [sectionInfo name];
}
-(NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section
{
id<NSFetchedResultsSectionInfo> sectionInfo = [[self.controller sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
-(UITableViewCell)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath
{
UITableViewCell *cell = [tableView dequeReusableCellWithIdentifier:@"cellIdentifier"];
NSManagedObject *managedObject= [self.controller objectAtIndexPath:indexPath];
<# cell에 표시될 내용을 memo로 부터 지정 #>
cell.textLabel.text = [managedObject valueForKey:@"title"];
...
return cell;
}
각각의 섹션은 NSFetchedResultsSectionInfo
프로토콜을 따르는데, 이 프로토콜에는 indexTitle, name, numberOfObjects, objects이 정의되어 있고, 이를 통해 섹션과 관련한 정보를 테이블 뷰에 제공할 수 있다.
데이터의 변경
데이터가 추가/삭제/변경될 때 컨트롤러의 델리게이트가 있다면 (그리고 그 델리게이트가 최소 1개의 델리게이트 메소드를 구현했다면) 컨트롤러는 로드된 데이터를 추적하게 된다. 그리고 변화가 발생할 때 델리게이트에게 적절한 메시지를 보내게 된다.
-controllerWillChangeContent:
-controller: didChangeObject: atIndexPath: forChangeType: newIndexPath:
-controller: didChangeSection: atIndex: forChangeType:
-controllerDidChangeContent:
이 때 가장 많이 사용하는 것은 두 번째의 것으로 각 객체의 추가/삭제에 대한 내용으로 이는 다음과 같이 구현된다. 특히 이 메소드는 여러 개의 데이터가 한 번에 변경되는 경우에, 여러 차례 반복해서 호출되므로 이에 신경을 써야 한다.
- (void)controller:(NSFetchedResultsController *)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
if (!self.suspendAutomaticTrackingOfChangesInManagedObjectContext)
{
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:
[self.tableView
reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeMove:
[self.tableView
deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
[self.tableView
insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
}
또한 섹션 자체의 변경에 대해서는 다음과 같이 구현할 수 있다. 테이블 뷰 컨트롤러의 변경처리방식과 거의 1대 1로 대응된다고 보면 된다.
- (void)controller:(NSFetchedResultsController *)controller
didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex
forChangeType:(NSFetchedResultsChangeType)type
{
if (!self.suspendAutomaticTrackingOfChangesInManagedObjectContext)
{
switch(type)
{
case NSFetchedResultsChangeInsert:
[self.tableView
insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView
deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
}
또한, 컨텐츠의 변경 시작과 끝을 알리는 두 메소드는 tableView의 beginUpdates, endUpdates를 각각 호출하여 변경 사항이 UI에 한 번에 적용되는 것으로 보이도록 할 수 있다.