빈 프로젝트에서 코어데이터를 시작하는 방법
일반적으로 코어데이터 기반의 앱을 만들 때는 프로젝트 생성시부터 코어데이터를 사용하도록 지정하여 코어데이터 사용에 필요한 기본적인 코드들이 미리 생성되도록 하여 사용한다. 하지만 불행히도 Xcode의 모든 템플릿이 코어데이터를 지원하지는 않는다. (예를 들면 현재 4.2 버전에서 싱글 뷰 기반 앱이 그러하다. )
이 번에는 예전에 만들어 본 적 있는 코어데이터 메모장 프로젝트를 완전히 새로 만들면서 (사실 그 당시 작성한 내용은 그저 “저장을 어떻게 하는가”를 보여주기 위한 내용이었으므로) 코어데이터 기반으로 시작하지 않은 프로젝트에서 코어데이터를 추가하는 방법과, 여러 화면 사이를 오갈 때 데이터를 전달하는 방법 (이전에는 무리하게 앱 델리게이트에 객체를 맡겨두었는데 상당히 좋지 않은 스타일이다)에 대해 알아보도록 하겠다.
UIDocument
iOS에서도 OSX처럼 문서 기반의 앱을 만드는 것이 수월하게 가능하도록 하는 UIDocument 객체가 있다. 이 객체의 서브클래스로 UIManagedDocument가 있는데, 이는 코어데이터와 통합된 문서 객체를 만드는 것을 지원한다. 즉 문서 파일 (정확히는 파일 래퍼[file wrapper]이다) 내에 코어데이터 데이터베이스 파일을 보관하는 것이다. 즉 코어데이터 템플릿이 아닌 프로젝트에서 코어데이터를 사용하고자 한다면 UIManagedDocument 객체를 생성하면 된다.
코어데이터 프레임워크에서 핵심이 되는 객체는 managedObjectContext 인데, 이 컨텍스트를획득하기 위해서는 로컬이든 iCloud이든 어딘가에 실제로 저장된 파일이 필요하다. 코어데이터 템플릿은 이 파일을 자동으로 생성하고 이 파일에 대해 영구저장소 코디네이터를 만들어 컨텍스트를 생성해주는 코드를 포함하고 있다. 같은 방식으로 NSManagedDocument 객체를 생성하면 파일에 대한 코디네이터 및 컨텍스트가 자동으로 생성되므로 손쉽게 코어데이터를 사용해 사용자가 작성한 내용을 영구적으로 저장할 수 있다.
더군다나 UIDocument 객체는 변경 사항을 자동으로 저장할 수 있고, 아이튠즈에서도 이 문서 객체를 데스크탑으로 옮길 수도 있다. 또한 iCloud와의 연동에 대한 부분도 쉽게 할 수 있어 이를 통해 정말 쿨하게 코어데이터를 쉽게 사용할 수 있다.
프로젝트 생성
적당한 이름을 주고 프로젝트를 생성한다. 시작은 비어있는 프로젝트 템플릿이다. 코어데이터 사용 여부에는 따로 체크하지 않는다. 대신 프로젝트를 생성한 다음 코어데이터 프레임워크를 추가해준다.
메모 모델
메모 데이터 모델은 간단히 다음 4개 속성을 가지게 될 것이다. 데이터 모델을 나중에 만들게 될 것이므로 우선 다음의 이름과 유형으로 만들어질 것이라고 알아둔다.
- 제목 (문자열) – title
- 내용 (문자열) – content
- 생성일 (날짜시간) – createdDate
- 최종수정일 (날짜시간) – lastModifiedDate
앱 델리게이트
메모장 앱은 실행되면서 저장된 내용을 읽어와야 한다. 이 작업은 앱 델리게이트에서 담당하도록 한다. 따라서 앱 델리게이트에서 문서 객체를 만들어 다른 객체가 이 문서의 managedObjectContext를 참조하도록 하면 된다.
앱 델리게이트의 인터페이스 파일에서는 코어데이터 프레임워크를 <CoreData/CoreData.h> 를 임포트하여 반입하고, 다음과 같은 프로퍼티를 지정해준다.
@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;
이제 구현부 파일에서 문서를 만들도록 하자. private 인터페이스에서 문서객체를 프로퍼티로 선언한다.
@interface AppDelegate()
@property (strong, nonatomic) NSManagedDocument *storedDocument
@end
앱이 실행되면 문서 객체를 생성한다.
@synthesize storedDocument = _storedDocument;
문서 객체의 생성과 문서 열기
이제 실행 완료시 호출되는 -applicationDidFinishLaunchingWithOptionos: 를 구현한다. 이 때 몇 가지 유의할 점이 있다.
- UIDocument는 파일 패스가 아닌 URL 기반으로 생성해야 한다.
- 문서 파일이 있다면 파일을 열어야 그 속에 있는 코어데이터 파일을 사용할 수 있고
- 문서 파일이 없다면 초기 저장을 통해 문서와 파일들을 생성해 주어야 한다.
- 또한 문서를 열거나 저장하는 작업은 비동기적으로 처리되므로 이에 주의해야 한다.
그럼 문서 객체를 생성해보자.
-(BOOL) applicationDidFinishLaunchingWithOptions:(NSDictionary *)options
{
NSString *docFilename = @"doc.data";
NSString *docPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES) objectAtIndex:0]
stringByAppendingPathComponent:docFilename];
NSURL *pathURL = [NSURL fileURLWithPath:docPath];
_storedDocument = [[UIManagedDocument alloc] initWithFileURL:urlPath];
// 파일이 있는지 검사
if ([[NSFileManage defaultManage] fileExistsAtPath:docPath]) {
// 파일이 있다면 열어야 한다.
[_storedDocument openWithCompletionHandler:^(BOOL success){
if(!success) {
NSLog(@"fail to open document");
} else {
self.managedObjectContext = _storedDocument.managedObjectContext;
// 추가적인 액션이 필요하다.
}
}];
}
else {
// 파일이 없다면 저장을 해야한다.
[_storedDocument saveToURL:pathURL forSaveOperation:UIDocumentSaveForCreating
completionHandler:^(BOOL success){
if(!success) {
NSLog(@"fail to create document");
} else {
self.managedObjectContext = _storedDocument.managedObjectContext;
// 추가적인 액션이 필요하다.
}
}];
}
return YES;
}
문서을 열거나 저장할 때는 -openWithCompletionHandler: 와 -saveToURL:forSaveOperation: 메소드를 사용하는데, 두 메소드는 모두 코드블럭을 인자로 넘겨주고 있다. 즉 문서를 열고 저장하는 작업은 로컬에서는 즉각적일 수 있으나, icloud와 연계된 경우라면 시간이 걸릴 수 있는 비동기적인 작업인 것이다. 따라서 외부의 객체 (이 경우에는 루트 뷰 컨트롤러가 될 것이다.)는 문서의 저장이나 로딩이 완료된 이후에 문서의 managedObjectContext를 획득해 가야 한다.따라서 추가적인 액션이 필요하다고한 부분에 대해서는 바로 이 부분에 대해 이야기한 것이다.
문서가 열리는 시점에 대한 고민
문서는 문서 객체를 만들고 나서 “열어야” 사용할 수 있다고 하였다. 그리고 이 문서를 여는 작업은 스토리지를 액세스해야 하므로 약간의 지연이 발생할 수 있다. 따라서 메모의 목록을 표시하는 루트 뷰 컨트롤러가 적절한 시점에 컨텍스트를 요청해야 유효한 컨텍스트를 얻어갈 수 있다는 부분이 요점이 된다.
이 부분을 어떻게 처리하면 좋을까? 바로 델리게이션이다. 즉 루트 뷰 컨트롤러가 이 앱델리게이트의 델리게이트가 되고, 앱 델리게이트에서는 문서 로딩이 완료되면 델리게이트(루트 뷰 컨트롤러)에게 managedContextObject를 전달해주면 되는 것이다.
이를 구현하기 위해서는 두 부분이 필요하다. 첫째로 델리게이트 객체를 만드는 것이고, 둘째로 이 델리게이트가 구현해야할 메소드를 지정하는 것이다. 구현은 델리게이트 측에서 하게 될 것이므로 여기서는 선언만 하면 된다. 그렇다 그게 바로 프로토콜이다.
앱델리게이트의 델리게이트 만들기
인터페이스 파일에서 프로토콜과 델리게이트를 선언해준다. 프로토콜은 @interface 블럭 밖에서 선언해야 한다.
@protocol ManagedDocumentDelegate <NSObject>
-(void)managedDocumentDidFinishOpening:(id)sender;
@end
이제 델리게이트를 선언해준다. 델리게이트가 될 객체의 클래스는 특정하기가 어려우므로 id를 사용한다. 특히 델리게이트는 위에서 선언한 프로토콜을 준수해야 하므로, 이를 명시적으로 표현하기 위해서는 다음과 같이 코딩한다.
@propery (strong, nonatomic) id<ManagedDocuementDelegate> delegate;
다시 구현부 파일에서는 synth한다.
@synthesize delegate = _delegate;
다시 문서 열기 및 저장과 관련한 부분에서 그 ‘추가적인 액션’은 델리게이트에게 자신을 넘겨줌으로써 델리게이트가 managedObjectContext를 획득하도록 하는 부분인 것이다.
[self.delegate managedDocumentDidFinishOpening:self];
이 코드를 추가적인 액션… 부분에 각각 추가해주도록 한다.
루트 뷰 컨트롤러
이제 앱델리게이트가 할 일은 모두 끝났다. 정말이다. 이게 전부이다. 오히려 코어데이터 기반 프로젝트로 시작할 때보다 훨씬 더 깔끔하게 끝낸 기분이 들지 않는가?
이제 UI를 생성하기 전에 먼저 루트 뷰 컨트롤러의 클래스를 작성하자. 루트 뷰 컨트롤러는 위에서 우리가 정한 ManagedDocumentDelegate 프로토콜을 따라야한다. 새 파일을 생성한다. UIViewController의 서브 클래스로 파일을 생성하고, 구현부 파일을 연다.
private interface에서는 ManagedDocumentDelegate 프로토콜을 따른다고 명시한다. 또한 NSManagedObjectContext와 NSMutableArray 하나를 각각 프로퍼티로 선언한다.
#import "AppDelegate.h"
@interface RootViewController() <MemoDocumentDelegate>
@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;
@property (strong, nonatomic) NSMutableArray *memoListArray;
@end
@implemtation RootViewController
@synthesize managedObjectContext = _managedObjectContext;
@synthesize memoListArray = _memoListArray;
...
특히, NSMutableArray의 경우에는 초기화하는 코드가 없으면 나중에 객체를 추가하거나 뺄 때 에러를 뿜으며 종료된다. -viewDidLoad 에서 초기화할 수도 있지만 실제로 필요한 시점에 초기화되도록하려면 다음과 같이 접근자 메서드를 구현한다.
-(NSMuatbleArray *)memoListArray
{
if(!_memoListArray) _memoListArray = [NSMutableArray array];
return _memoListArray;
}
대신 -viewDidLoad에서는 자신을 앱델리게이트 객체의 델리게이트로 지정해야 한다. 그래야 문서가 열릴 때 이 내용을 전달 받을 수 있다.
-(void)viewDidLoad
{
[super viewDidLoad];
[[[UIApplication sharedApplication] delegate] setDelegate:self];
}
이제 앱 델리게이트가 managedContext를 생성하고 포인터를 얻었을 때 호출해 줄 메서드를 구현한다.
-(void)managedDocumentDidFinishOpening:(id)sender
{
self.managedObjectContext = sender.managedObjectContext;
// 목록을 갱신해 준다.
}
문서가 열리고 나면 컨텍스트를 획득하게 되니, 그 이후에 목록을 갱신해주면 된다.
이 부분까지는 사실 앱이 시작되는 시점에 일어날 일들을 처리해준 것에 지나지 않는다. 이후 루트 뷰 컨트롤러는 다음의 기능을 처리해야 한다. (그렇다. 갈길이 멀다.)
- 코어데이터로부터 저장된 메모들을 불러오는 작업
- 불러온 데이터를 뷰 컨트롤러 내 테이블 뷰에 보여주는 작업
- (화면 어딘가에 있을) 신규 작성 버튼을 눌러 메모 작성 화면을 호출하는 기능
- 메모 작성이 완료될 때 이를 저장하는 기능
- 기존 메모를 선택할 때 그 상세 내용을 보여주는 화면을 호출하는 기능
- 메모 목록에서 메모를 삭제하는 기능
메모 작성 화면 생성
사실 이 시점에서는 스토리보드를 짓기 시작하면 좋지만, 귀찮은 기분이들어서 그냥 디테일 뷰 컨트롤러를 작성하는 것부터 시작하도록 한다. 또 하나의 뷰 컨트롤러 클래스를 하나 생성한다. 이름은 DetailViewController가 적당하겠다. 이 컨트롤러는 다음과 같은 기능을 수행한다.
- 신규 메모를 작성
- 기존 메모의 상세 내용을 표시
- 기존 메모를 편집
따라서 이 컨트롤러는 다음과 같은 속성들을 가지고 있어야 한다.
- 뷰가 호출될 때 신규메모를 작성하는지, 기존 메모를 표시할 것인지를 저장할 프로퍼티
- 편집이 완료되었을 때 상위 뷰 컨트롤러 (presentingViewController)에게 이를 알려주는 기능
그런데 우리는 두 번째 기능을 앞서 살펴본 델리게이션을 통해 구현해보고자 한다. 또한 신규 작성시에는 Modal 의 형태로 표시되고, 기존 메모를 선택한 경우에는 네비게이션 스택에 뷰가 추가되도록 할 것이다.(후자가 기존에 사용한 방법이다.)
인터페이스 파일
따라서 인터페이스 파일에서는 메모 작성이 완료되었을 때 델리게이트가 처리해줄 메서드를 정의하고, 두 개의 프로퍼티 (현재 편집할 메모 객체/ 델리게이트) 를 추가해준다.그 외 이 클래스가 사용하는 모든 프로퍼티는 외부에서 참조할 필요가 없으므로 private interface에 선언할 것이다.
@protocol MemoDetailDelegate
-(void)memoDetailDidFinishEditingMemo:(id)memo isNew:(BOOL)new;
@end
@interface DetailViewController : UIViewController
@property (strong, nonatomic) id currentMemo;
@property (weak, nonatomic) id<MemoDetailDelegae> delegate;
@end
프로토콜에서 선언한 메소드는 현재 편집하던 메모의 편집이 끝났다면서 그 메모를 델리게이트에게 전달해주고, isNew: 파라미터를 통해 신규 메모가 추가되었던 것인지, 기존 메모를 편집하던 것인지를 함께 알려준다. 또한 아직 코어데이터 모델 및 모델객체 클래스를 만들지 않아 이들을 id 타입으로 선언해주었다. (이는 나중에 추가해주어야 한다.)
내부 인터페이스
이제 구현부 파일에서 내부 인터페이스를 선언한다. 내부 인터페이스에서는 다음과 같은 프로퍼티를 선언해준다.
- 화면에 표시될 제목 (텍스트필드)
- 메모 내용을 담을 텍스트뷰 (텍스트뷰)
- 현재 편집하는 메모가 신규 메모인지 여부를 기억하는 플래그
@interface DetailViewController()
@property (weak, nonatomic) IBOutlet UITextField *titleField;
@property (weak, nonatomic) IBOutlet UITextView *contentView;
@property (assign) BOOL isAdding;
@end
외부 인터페이스 및 내부 인터페이스에서 선언한 프로퍼티를 synth 해준다.
@synthesize currentMemo = _currentMemo, delegate = _delegate;
@synthesize titleField = _titleField, contentView = _contentView, isAdding = _isAdding
디테일 뷰가 열리기 전에 루트 뷰에서는 디테일뷰에서 작성할 메모 객체를 전달해 줄 것이다. 그 때 신규 메모 여부를 판별하기로 한다.
-(void)setCurrentMemo:(id)currentMemo
{
_currentMemo = currentMemo;
if(![_currentMemo valueForKey:@"lastModifiedDate"]) {
// 최종 수정일이 nil 이라는 것은 신규 메모를 의미한다.
self.isAdding = YES;
}
}
또한 뷰가 화면에 표시되기 전에 기존 메모인 경우에는 제목과 내용을 미리 표시해주도록 한다.
-(void)viewWillAppear
{
if(!self.isAdding) {
self.titleField.text = [self.currentMemo valueForKey:@"title"];
self.contentView.text = [self.currentMemo valueForKey:@"content"];
}
}
또한 저장 버튼을 눌렀을 때는 델리게이트(아마도 루트 뷰 컨트롤러)에게 다음의 적절한 동작을 하도록 시킬 수 있을 것이다.
-(IBAction)savePressed
{
[self.currentMemo setValue:self.titleField.text forKey:@"title"];
[self.currentMemo setValue:self.contentView.text forKey:@"content"];
if(self.isAdding) [self.currentMemo setValue:[NSDate date] forKey:@"createdDate"];
[self.currentMemo setValue:[NSDate date] forKey:@"lastModifiedDate"];
[self.delegate memoDetailDidFinishEditingMemo:self.currentMemo isNew:self.isAdding];
}
이제 다시 남은 작업은 루트 뷰 컨트롤러의 몫이 되었다. 즉 디테일 뷰 컨트롤러의 델리게이트(루트 뷰 컨트롤러)가 최종적으로 남은 작업을 수행한다. 델리게이트는 신규 메모를 받아가므로 다음 작업을 처리해야 한다.
- 신규 메모인 경우 이 메모를 메모 리스트 배열에 추가한다.
- 테이블 뷰를 갱신한다.
다시 루트 뷰 컨트롤러
다시 RootViweController.m 에서 남은 작업을 처리하는 코드를 입력한다.
@interface RootViewController () <MemoDocumentDelegate, MemoDetailDelegate>
로 디테일 뷰 컨트롤러의 프로토콜을 따른다고 수정해준 다음, 프로토콜이 선언한 메소드를 구현한다.
-(void)memoDetailDidFinishEditingMemo:(id)Memo isNew:(BOOL)isNew {
if (isNew) [self.memoListArray insertObject:memo atIndex:0];
[self dissmissModalViewControllerAnimated:YES];
// 테이블 내용을 갱신해줌
}
이제는 실제 데이터 모델과 UI를 만들고 나서 테이블 뷰에 실제 메모 데이터를 남기도록 하는 부분의 작업을 이어나가도록 한다.
모델 만들기
신규 파일을 작성한다. 파일의 종류는 코어데이터 모델이다. 코어 데이터 튜토리얼 등에서 흔히 볼 수 있는 엔티티 편집기에서 새 엔티티를 만든다. 엔티티의 이름은 Memo로 한다. 엔티티는 DB의 테이블에 해당한다고 보면된다. 엔티티의 이름은 클래스의 이름이 되므로 대문자로 시작하는 것이 관례이다. Memo 엔티티를 추가했으면, 거기다가 4개의 속성(attribute)들을 추가한다. 그리고 글의 서두에서 말한 것처럼 각각의 속성의 이름과 타입을 지정한다. 속성은 DB에서는 각각의 필드이며, 클래스의 프로퍼티에 해당하므로 소문자로 시작하는 이름을 따른다.
속성 추가를 완료하면 메뉴에서 Editor > Create ManagedObjectModel Subclass… 를 선택해서 해당 엔티티의 모델 클래스(Memo)를 생성한다.
UI 만들기
이번에는 스토리보드 파일을 추가한다. (스토리보드 파일 추가 후에는 꼭 타겟 속성에서 스토리보드 이름을 기입해주도록 하자)
- 뷰 컨트롤러를 하나 추가한다. 클래스 이름은 RootViewController. editor 메뉴에서 embed > Navigatoin Controller를 선택해 네비게이션 컨트롤러를 추가한다.
- 루트 뷰 컨트롤러에 상단 네비게이션 막대가 생기는데, BarButtonItem을 선택해 오른쪽에 추가한다. 추가한 버튼의 유형은 Compose로 만든다.
- 아직 관련된 소스는 입력하지 않았지만 테이블 뷰도 하나 올려준다. (테이블 뷰 컨트롤러가 아님) 테이블 뷰를 올렸으면 테이블 뷰를 우클릭으로 끌어 하단의 검은 막대에 있는 뷰 컨트롤러 클래스 아이콘으로 연결한다. datasource와 delegate를 각각 연결해준다. 즉, 두 번 연결해야 한다.
- 새 뷰 컨트롤러를 하나 추가한다. 클래스 이름은 DetailViewController로 변경해준다.
- 디테일 뷰 컨트롤러에는 텍스트 필드 하나, 텍스트 뷰 하나, 버튼 하나를 올려준다. 그리고 두 입력 필드는 미리 지정해놓은 아울렛과 연결한다. 어시스턴트 에디터를 열고 소스코드를 구현부 파일로 선택해서 save 버튼을 작성해 놓은 액션에 우클릭으로 끌면 (컨트롤+드래드) 해당 메소드와 연결할 수도 있다.
- 루트뷰 컨트롤러의 상단 ‘작성’ 버튼을 우클릭하여 디테일 뷰로 끌어준다. segue가 생성되는데, 이 segue를 클릭하여 선택하고 속성창에서 identifier 란에 AddMemoSegue라고 입력해준다. 이는 나중에 사용될 값이므로 복사해두자.
남은 작업
실행해보기 전에 한가지 고려할 것이 있다. 앱을 실행하면 바로 리스트 화면 (테이블 뷰)가 표시된다. 동시에 앱 델리게이트는 문서 파일을 생성하고 이를 열어 컨텍스트를 생성한다. 이 작업이 완료되면 다시 루트 뷰 컨트롤러에 이 사실을 알려준다. (델리게이션) 이 시점 전까지는 루트 뷰 컨트롤러는 컨텍스트를 가지고 있지 않으므로 새 메모를 작성할 수 없어야 한다. 따라서 이 부분에 대한 수정이 필요하다.
테이블 뷰에 데이터 표시하기
또한 컨텍스트를 획득하고 나면 저장된 메모들을 불러와 화면에 표시하는 작업을 해 줘야 한다. 이 때 강제로 테이블 뷰를 갱신하므로 테이블뷰에 대한 아울렛도 하나 필요하다.
루트 뷰 컨트롤러에 아울렛을 하나 추가한다.
@property (weak, nonatomic) IBOutlet UITableView *listTableView;
를 내부 인터페이스에 추가한 다음, 스토리보드에서 테이블 뷰 아울렛을 컨트롤러에 연결해준다. (이 연결해주는 작업을 하지 않으면… 갱신이 안된다. 흔히 하는 실수이므로 꼭 습관을 들여야 한다.)
먼저 저장된 내용을 가져오는 메소드를 작성해보자. 이제 코어데이터의 기능을 본격적으로 사용해야하므로 파일의 머리 부분에 코어데이터 프레임워크를 임포트하는 구문도 하나 추가해주어야 한다. 또한 이 루트 뷰 컨트롤러가 테이블 뷰의 두 프로토콜을 따르는 부분이 있으므로 이에 관한 내용을 내부 인터페이스에 추가해주자.
#import <CoreData/CoreData.h>
@interface RootViewController () <MemoDocumentDelegate, MemoDetailDelegate, UITableViewDatasource, UITableViewDelegate >
다음은 코어데이터에서 내용을 가져오는 부분이다.
-(NSArray *)fetchData
{
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescroption *anEntity = [NSEntityDescription entityForName:@"Memo" inManagedObjectContext:self.managedObjectContext];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastModifiedDate" ascending:YES];
fetchRequest.entity = anEntity;
fetchRequest.sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
NSArray *result = [self.managedObjectContext excuteFetchRequest:fetchRequest error:nil];
}
이제 테이블 뷰의 데이터 소스 부분이다. 이 부분은 이전의 내용과 완전히 동일하다.
-(NSInteger)tableview:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.memoListArray count];
}
-(UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *cellIdentifier = @"MemoListCell";
UITableViewCell *cell = [tableView dequeReusableCellWithIdentifier:cellIdentifier];
cell.textLabel.text = [[self.memoListArray objectAtIndex:indexPath.row] valueForKey:@"title"];
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.timeStyle = NSDateFormatterMediumStyle;
dateFormatter.dateStyle = NSDateFormatterMediumStyle;
NSString *dateString = [dateFormatter stringFromDate:(NSDate*)[[self.memoListArray objectAtIndex:indexPath.row] valueForKey:@"lastModifiedDate"]];
cell.detailTextLabel.text = dateString;
return cell;
}
주의할 부분
앱을 빌드하고 실행하기 전에 루트 뷰 컨트롤러에서 신규 메모 작성 버튼이 컨텍스트가 존재할 때만 활성화되도록 변경하여 처리해준다. 이를 위해서는 툴바 버튼에 대한 아울렛을 하나 생성하고 초기화 시와 컨텍스트 획득시에 각각 코드를 처리한다.
@property (weak, nonatomic) IBOutlet UIBarButtonItem *composeButton;
synth 코드를 추가한 다음, 아래 코드를 -viewDidLoad 에 추가한다.
self.composeButton.enabled = NO;
컨텍스트를 확인하는 시점은 -managedDocumentDidFinishOpening: 이다. 마지막에 아래 코드를 추가한다.
if(self.managedObjectContext) self.composeButton.enabled = YES;
신규 메모를 작성하기 전에 처리할 내용
이제 실제로 디테일 뷰를 불러오기 전에 디테일 뷰에 신규 생성된 메모의 포인터를 넘겨주는 부분을 처리하자. 우리는 화면 전환을 segue로 하고 있으므로, segue가 이를 처리하면 된다. segue가 동작하기 전에는 -prepareForSegue: sender: 를 호출하는데 여기서 처리해주면 된다.
-(void)prepareForSeuge:(UIStoryboradSegue *)segue sender:(id)sender
{
if([segue.identifier isEqualToString:@"AddMemoSegue"]) {
DetailViewController *editor = segue.destinationViewController;
Memo *newMemo = [NSEntityDescription insertNewObjectForEntityForName:"@Memo" inManagedObjectContext:self.managedObjectContext];
editor.currentMemo = newMemo;
editor.delegate = self;
}
}
끝으로 메모 목록이 업데이트될 때 테이블뷰를 갱신하는 코드를 추가한다. 주석으로 목록을 갱신한다고 표시한 부분에 [self.listTableView reloadData]; 를 추가해준다. (두 델리게이트 메소드 부분에 있다.)
기존 메모 편집하기
이제 앱을 빌드하고 실행하면 기본적으로 메모를 작성하고 이 메모가 테이블에 추가되는 동작을 확인할 수 있다. (물론 아직 기존 내용을 확인하는 내용은 구현하지 않았다.)
주목할 부분은 어디에도 컨텍스트를 저장하는 부분이 없다는 것이다. 데이터의 저장은 UIManagedDocument가 알아서 처리하므로 따로 저장하는 액션을 취해줄 필요는 없다.
저장된 메모를 편집하기 위해서는 테이블 뷰 셀에서 디테일 뷰 컨트롤러로 segue를 추가한다음, prepareForSegue: 에서 해당 segue 일 때의 액션에서 디테일뷰에 현재 선택된 인덱스의 메모를 전달해주면 된다. 기존 메모를 편집하고 저장했을 때 변경 사항을 어떻게 처리하는가? 이 부분은 이미 다 처리되어 있다 (읭?)
스토리보드에서 테이블 뷰 셀에서 디테일 뷰 컨트롤러로 segue를 추가한다음, ViewMemoSegue라는 이름을 준다. 다음 루트 뷰에서 아래 코드를 추가하자.
-(void)prepareForSegue:(UIStoryboardSegue*)segue sender:(id)sender
{
....
else if ([segue.identifier isEqualToString:@"ViewMemoSegue"]) {
DetailViewController *editor = segue.destinationViewController;
editor.currentMemo = [self.memoListArray objectAtIndex:self.listTableView.indexPathOfSelectedRow.row];
editor.delegate = self;
}
}
메모를 삭제하는 코드는 역시 지난 시간에 만든 내용 똑같이 사용하면 되므로 기존 글을 참고하도록 한다.