[iOS] 코어데이터를 활용한 간단 메모장 – 다시 쓰기

작년인가 썼던 코어데이터를 사용해서 간단한 메모앱을 만드는 예제를 설명하는 글이 있었는데, 시간이 지나서 살펴보니 댓글도 좀 달릴 만큼 관심을 받았는데…. 지금에와서 읽어보니 글(글에서 소개하는 코드가)이 좀 엉망이라 부끄러운 관계로, 조금 더 간단히 써보고자 한다. 코어 데이터와 관련된 내용은 다루지 않고 (이 부분은 시간이 나면 다시 따로 정리하기로 하고) 코드에 대해서만 집중해보도록 한다. 이번 글의 목표는 중구난방이 아닌 보다 깔끔한 구현을 목표로 한다.

요건

만들고자 하는 앱이 다루는 범위는 이전과 동일하다. 새로 메모를 작성하고, 이를 저장한다. 앱은 시동시에 저장된 메모 내용을 불러와서 수정할 수 있다. 그외 ARC, 스토리보드를 사용하는 앱 기준으로 코드의 내용을 바꾼다. 또한 관련된 클래스의 디자인도 조금 더 간결하게 변경하고자 한다.

프로젝트는 Empty Application으로 시작해서 스토리보드를 하나 새로 추가하는 형태로 제작한다. 프로젝트 생성시 코어데이터 사용에 체크한다.

설계

이 앱은 간단히 3개의 클래스로 구성된다. 각각 앱 델리게이트, 리스트 뷰 컨트롤러, 디테일 뷰 컨트롤러로 이들은 다음과 같이 구성한다.

  • 앱 델리게이트 : 코어데이터에 필요한 ManagedObjectContext 객체를 제공해 준다. 그외 디폴트로 앱이 종료되는 시점에 자동으로 컨텍스트를 저장한다. (프로젝트 템플릿에 이미 있는 기능임)
  • 리스트 뷰 컨트롤러 : 테이블 뷰 컨트롤러로, 화면에 메모 목록을 표시한다.
  • 디테일 뷰 컨트롤러 : 데이터 필드 1개와 텍스트 뷰 1개로 이루어진 뷰. 선택된 메모의 상세 내용을 표시하고, 수정/작성한다.

또한 코어데이터 모델로는 Memo가 있고 이는 다음과 같은 속성들을 가지고 있다.

  • title (text)
  • content (text)
  • createdDate (date)
  • lastModifiedDate (date)

앱 델리게이트

앱 델리게이트는 별도로 손 볼 곳은 없다. 단, Empty Application으로 프로젝트를 만들면, 이 클래스에서 코드를 통해 앱의 화면을 구성하기 때문에 이후에 추가한 스토리보드가 표시되지 않는다. 따라서 -application:didFinishLaunchingWithOptions:의 내용을 다음과 같이 모두 제거하고 YES를 리턴하도록 변경한다.

-(BOOL)application:(UIAppliaction *)application didFinishLaunchingWithOptions:(NSDictionary *)options {
    return YES;
}

리스트 뷰 컨트롤러

지난 번 글에서의 구현은 완전 중구난방의 디자인이었던 것을 조금 깔끔하게 변경하고자 한다. 리스트 뷰 컨트롤러는 다음과 같은 프로퍼티들이 필요하다.

  • context : 저장된 메모들을 제어할 수 있는 managed object context.
  • memoListArray : 저장된 메모들이 들어있는 배열 객체
  • indexedMemo : 신규로 작성하거나 수정할 때 그 대상이 되는 메모 객체

그리고 디테일 뷰에서 편집을 완료한 후에는 수정한 내용을 저장하고, 목록을 갱신해야 한다. 이는 상세 뷰에서 목록으로 돌올 때 -viewWillAppea를 쓰지 않고 디테일 뷰 컨트롤러의 델리게이트로 자신을 등록해서, 이를 처리하도록 한다. 따라서 델리게이트 프로토콜을 하나 정의해 줘야 한다.

디테일 뷰 컨트롤러

디테일 뷰 컨트롤러는 편집기의 역할을 한다. 편집을 취소하거나 확정하는 2개의 경우가 있을 수 있는데, 이 때 편집한 내용을 잃지 않고 저장하기 위해서는 저장 작업은 리스트 뷰 컨트롤러에게 위임할 것이다. 그외에는 특별한 부분이 없다.

구현

스토리보드

스토리보드를 새로 생성해서 테이블 뷰 컨트롤러 하나와, 일반 뷰 컨트롤러 하나를 끌어다 놓는다. 각각의 클래스 이름은 ANListViewControllerANDetailViewController가 된다. 리스트 뷰 컨트롤러를 editor > embed > navigation controller를 선택해 네비게이션 컨트롤러 내에 두고, 네비게이션 컨트롤러가 초기 화면이 되도록 initial scene 속성에 체크해준다.

리스트 뷰 컨트롤러의 테이블 뷰 셀에서 마우스 오른쪽 클릭&드래그로 디테일 뷰 컨트롤러로 연결하여 segue를 하나 만들어주고, 이 segue의 식별자를 “addMemoSegue”로 지정한다. 또, 신규 메모 작성을 위해 네비게이션 바 우측에 툴바 아이템을 하나 추가해서 여기서 디테일 뷰 컨트롤러로 이어지는 segue를 추가한 뒤, 이름을 “viewDetailSegue”로 지어준다. 테이블 뷰 셀의 재사용ID는 적당한 값을 준다.(이들 이름은 모두 적당한 값을 주면 되나, 코드상에서는 동일한 문자열을 입력해줘야 한다.)

ANListViewController.h

리스트 뷰 컨트롤러의 헤더는 별다른 프로퍼티를 선언할 게 없고(프로퍼티는 객체 외부에서 참조하려는 속성인데, 외부 객체가 액세스할 필요가 없다) 대신에 프로토콜을 하나 정의해준다. 그리고 인터페이스 선언시에는 자기 스스로가 이 프로토콜을 따르도록 한다.

#import <UIKit/UIKit.h>
#import "Memo.h"

// 델리게이트 프로토콜 정의
@protocol ANMemoEditorDelegate
@optional
-(void)editorDidFinishedEditing:(id)sender
@end

@interface ANListViewController : UITableViewController <ANMemoEditorDelegate>
// 테이블 뷰 컨트롤러 외부에서 참조할 프로퍼티가 없으므로, 모든 프로퍼티는 *.m 파일 내에서 선언함
@end

ANListViewController.m

예전 글의 구현에서 가장 지저분하게 돼 있던 점은 앱 델리게이트의 관리객체 컨텍스트를 찾아서 저장을 하거나 새 메모를 만들거나 하는 식으로 작업을 했는데, 그냥 깔끔하게 리스트 뷰 컨트롤러의 프로퍼티로 지정해버린다.

리스트 뷰 컨트롤러의 컨텍스트 프로퍼티에 앱 델리게이트의 관리 객체 컨텍스트를 대입시키려면 예전 같으면, -init…류의 초기화 메소드에서 처리하려고 했겠지만, 프로퍼티 자체는 “함수의 리턴값”이므로, 접근자 함수에서 이 일을 처리해주면 별도의 초기화 메소드가 필요없다. 같은 방식으로 메모 리스트 접근자 함수에서도 컨텍스트를 사용하여 리스트를 로딩해오는 내용을 넣어버렸다.

셀이나 버튼을 클릭해서 뷰를 전환하던 방식은 모두 segue를 통해 처리한다. 어디를 눌러서 화면이 전환되는지는 segue의 identifier 속성을 통해 알 수 있으므로 디테일 뷰 컨트롤러에서 사용할 메모를 지정해 줄 수 있다.

#import "ANListViewController.h"
#import "ANDetailViewController.h"
#import "ANAppDelegate.h"

@interface ANListViewController ()
//
@property (strong, nonatomic) NSManagedObjectContext *context;
@property (strong, nonatomic) NSArray *memoListArray;
@property (strong, nonatomic) Memo *indexedMemo;
@end

@implementation ANListViewController
@synthesize context=_context, memoListArray=_memoListArray, indexedMemo;

-(NSManagedObjectContext *)context
{
    // 앱 델리게이트의 컨텍스트를 참조하여 컨텍스트로 사용함
    if(!_context)
    {
        ANAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
        _context = appDelegate.managedObjectContext;
    }
    return _context;
}

-(NSArray *)memoListArray
{
    // 메모목록의 배열은 참조할 때 마다 코어데이터로부터 신규 목록을 갱신하도록 함. 
    // 이 방식은 오버헤드를 크게 만들 소지는 있지만, 메모앱 수준에서는 큰 문제는 없음.
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *anEntity = [NSEntityDescription entityForName:@"Memo" inManagedObjectContext:self.context];
    NSSortDescriptor *sd = [[NSSortDescriptor alloc] initWithKey:@"lastModifiedDate" ascending:NO];
    [fetchRequest setEntity:anEntity];
    [fetchRequest setSortDescriptors:@[sd]]; //
    _memoListArray = [self.context executeFetchRequest:fetchRequest error:nil];
    return _memoListArray;
}

-(void)editorDidFinishedEditing:(id)sender
{
    // 디테일뷰에서 편집/작성이 끝나면 저장하고 테이블 뷰를 갱신함
    [self.conext save:nil];
    [self.tableView reloadData];
}

#pragma mark - table view datasource
// 테이블 뷰 데이터 소스는 기존과 다를 부분이 특별히 없음.
-(NSInteger)numberOfSectionsInTableView:(UITableView*)tableView {return 1;}
-(NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section
{
    return [self.memoListArray count];
}
-(UITableViewCell*)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"MemoListCell";
    UITableViewCell *cell = [tableView dequeReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    if(!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault];
    }

    Memo *currentMemo= [self.memoListArray objectAtIndex:indexPath.row];
    cell.textLable.text = currentMemo.title;
    NSDateFormatter *df = [[NSDateFormatter alloc] init];
    [df setDateStyle:NSDateFormatterNoStyle];
    [df setTimeStyle:NSDateFormatterShortStyle];
    cell.detailTextLabel.text = [df stringFromDate:currentMemo.lastModifiedDate];

    return cell;
}

-(void)prepareForSegue:(UIStoryboardSegue *)segue
{
    if([segue.identifier isEqualToString:@"addMemoSegue"])
    {   // 신규 메모 추가시 준비 작업 - 새 메모 객체를 생성함
        ANDetailViewController *detailViewController = [segue destinationViewController];
        self.indexedMemo = [NSEntityDescription insertNewObjectForEntityName:@"Memo" inManagedObjectContext:self.context];
        detailViewController.currentMemo = self.indexedMemo;
        detailViewController.delegate = self;
    }

    if([segue.identifier isEqualToString:@"viewDetailSegue"])
    {
        // 기존 메모 보기/편집 시 준비작업 - 선택된 셀이 가리키는 메모를 전달함.
        ANDetailViewController *detailViewController = [segue destinationViewController];
        self.indexedMemo = [self.memoListArray objectAtIndex:[[self.tableView indexPathForSelectedRow] row]];
        detailViewController.currentMemo = self.indexedMemo;
        detailViewController.delegate = self;
    }
}

ANDetailViewController.h

디테일 뷰 컨트롤러는 리스트 뷰 컨트롤러에서 접근해야 할 프로퍼티가 2개 있다. 하나는 편집할 메모 객체, 다른 하나는 델리게이트(리스트 뷰 컨트롤러 자신이된다.) 이 두 개를 헤더 파일에서 선언해준다. 나머지 선언해야 할 프로퍼티와 메소드는 IB상에서 지정하면 된다. (구성은 이전 글의 것과 동일하다.)

뷰 컨트롤러의 뷰 내에 텍스트 필트 하나와 텍스트 뷰 하나를 추가하고 각각 IBOutlet으로 연결한다. 그리고 네비게이션 바 양쪽에 Cancel, Done 버튼을 추가해서 각각 탭했을 떄 호출될 액션 메소드를 하나씩 만들어 둔다. 다음은 소스이다.

//ANDetailViewController.h
#import <UIKit/UIKit.h>
#import "ANListViewController.h"
#import "Memo.h"

@interface ANDetailViewController : UIViewController
@property (weak, nonatomic) id <ANMemoEditorDelegate>delegate;
@property (strong, nonatomic) Memo *currentMemo;
@end

ANDetailViewController.m

//ANDetailViewController.m

이 클래스가 하는 일은 간단한데, 뷰가 표시되기 전에 segue 준비 동작에서 할당 받은 currentMemo 객체로부터 제목과, 본문의 내용을 가져와서 각각의 뷰에 표시해주기만 하면 된다. 편집을 완료하거나 취소하는 시점에서는 해당 메모 객체의 값을 변경해주고, 델리게이트(리스트 뷰)로 하여금 편집이 종료되었을 때의 처리(리스트 뷰에서는 컨텍스트를 저장하고, 리스트를 갱신함)하도록 지시만 해주면 된다.

뷰를 제거하고 리스트로 돌아가는 것은 네비게이션 컨트롤러의 -popViewController:를 호출하면 된다. (네비게이션 컨트롤러는 뷰 컨트롤러를 스택으로 관리하기 때문에 pop만 해주면 알아서 나머지 처리가 된다.)

#import "ANDetailViewController.h"

@interface ANDetailViewController ()
@property (weak) IBOutlet UITextField *titleField;
@property (weak) IBOutlet UITextView *contentView;
-(IBAction)doneTapped:(id)sender;
-(IBAction)cancelEdit:(id)sender;
@end

@implementation ANDetailViewController

-(void)viewWillAppear:(BOOL)animated
{
    self.titleField.text = self.currentMemo.title;
    self.contentView.text =  self.currentMemo.content;
}

-(IBAction)doneTapped:(id)sender
{
    self.currentMemo.title = self.titleField.text
    self.currentMemo.content = self.contentView.text;
    self.currentMemo.lastModifiedDate = [NSDate date];
    self.currentMemo.createdDate = (self.currentMemo.createdDate) ? self.currentMemo.createdDate : [NSDate date];

    [self.delegate editorDidFinishedEditing:self];
    [self.navigationController popViewControllerAnimated:YES];
}

-(IBAction)cancelEdit:(id)sender
{
    [self.navigationController popViewControllerAnimated:YES];
}

프로젝트 전체 파일은 여기에서 다운로드 받을 수 있습니다.

  • ㄹㅁㄹㅁ

    ㅔ리굿베리굿감사

  • Pingback: 20120102 :: [iOS] 저장이 가능한 간단 메모장 3 (코어데이터) | Wireframe()

  • 이지섭

    안녕하세요! 구글링하다 찾게됬습니다. 제가 swift를 공부하고있는데 이 예제에서 사용하는것들이 제가 찾고있던 정보들이라서 혹시 이 예제를 swift로 알려주실순 없을까요?
    꼭 부탁드리고 싶습니다

    • Xcode8, Swift3 기준이면 본문에 올라온 코드는 다음과 같이 작성됩니다.

      https://gist.github.com/sooop/6fb0fbe501511e174e3be97e4d2fe14f

      Xcode로 작성한게 아니라 그냥 Swift로 옮겨서만 써본거라, 에러가 날지도 모릅니다.

      최신 내용으로 조만간 업데이트한 글을 한 번 올려보도록 할게요.

      • 이지섭

        와!! 너무너무 감사드려요!!

      • 이지섭

        cell.dateLabel?.text = df.string(from: currentMemo.lastModifiedDate as! Date) 이부분에서 에러가나는데 이유를 못찾겠네요 ㅠㅠ 언래핑문제같은데…

        • 에러 메시지를 같이 보내주시면 좋을 거 같은데요. 아마 `lastModifiedDate`값이 nil 이러서 그런게 아닌가 싶네요.


          if let date = currentMemo.lastModifiedDate {
          cell.dateLabel?.text = df.string(from: currentMemo.lastModifiedDate as! Date)
          }

          이런 식으로 처리해보세요.

          • 이지섭

            어찌 처리는 했으나 제가 하던 코드에 적용을 하니 에러는 안나는데, 새 메모를 열고, 작성을 해서 완료버튼을 누르면 창이 닫히면서 저장이되어 셀에 표시되게 했는데 저장/불러오기가 되지 않네요..ㅠ
            Optional([]) 이런식으로 로그가뜨면서 저장이 되질 않는데… 뒷부분은 계속 바뀌구요.. 단지 셀에 표시만 안되는건지, 아예 저장자체가 안되는건지 모르겠네요 ㅠ 아무튼 답글 감사드립니다! 연말연시 즐겁게 보내셨으면 좋겠네요!

          • 이지섭

            억!! 어떻게 해결해버렸습니다!!ㅋㅋ 여전히 문제가좀있는데 제가 어디 여쭤볼데가 없네요ㅠ 일단 완료누르면 창 나와지고 테이블뷰 셀에 표시도 되는데 방금한건 저장이 안되고 앱을 다시실행해야 저장된게 표시가 되네요.. 그리고 셀을 클릭해도 수정도안되고… 이것저것 만져봐야겠습니다 너무 감사드립니다! 그런데 혹시 셀의 크기를 컨텐츠의 크기에따라 커지고 작아지게 하려면 뭘 공부해야할까요??

          • 리스트로 돌아왔을 때 tableView.reloadDate() 를 호출해서 데이터 상의 반영을 테이블뷰에 적용해줘야 합니다. 만약 애니메이션으로 추가된 걸 적용하고 싶으면, `tableView.insertRowsAt(:with:)` 메소드를 호출해서 삽입된 데이터의 인덱스와 같은 위치에 셀을 추가할 수 있습니다.

            셀마다 다른 크기를 지정하고 싶다면, 콘텐츠에 따라서 셀의 높이가 얼마가 될 지 구할 수 있는 방법만 있으면 됩니다. 이 부분은 테이블 뷰의 델리게이트가 결정해 줄 수 있습니다. 자세한 내용은 UITableViewDelegate 의 레퍼런스 페이지를 참고해보세요.

          • 이지섭

            정말 너무 감사드립니다!! 자꾸 질문…귀찮게해드려서 죄송하네요ㅠ혹시 아무것도 작성하지 않았을 때 저장하지 않으려면 editorDidFinishedEditing요녀석을 수정하는건가요?? 아무것도 안쓰고 완료를 누르면 빈페이지가 저장이되버려서요 ..그리고 tableView.reloadData()를 viewDidLoad()에 넣어도 안되고 editorDidFinishedEditing에 넣어도 안되는데 다른곳에 넣어야하나요??

          • 이지섭

            제가 메인뷰컨트롤러를 테이블뷰 컨트롤러로 안하고 일반 뷰컨트롤러로 해서 extension으로 tableViewDatasource랑 Delegate를 설정한건데 이게 뭔가 영향을 준 것일 수 있을까요??

          • 액션을 구분하도록 프로토콜을 바꿔야 합니다.

            func editor(_: didFinishEditingWith savingAction: EditAction) 같은 식으로 메소드를 변경해서, 저장했는지, 변경은 없는지 혹은 삭제인지… 이런 식으로 구분해서 호출해주는 거죠. EditAction은 간단하게 별도의 enum 타입을 만들어서 .done, .cancel, .delete 같은 식으로 만들어 주고, 디테일 뷰 내에서 상황에 따라 새 편집을 처리 완료하는 것인지, 변경사항을 저장하지 않은 것인지, 아니면 그 메모를 삭제하는 것인지를 결정합니다.

            제가 쓴 코드 상에서는 일단 추가를 누르면 무조건 빈 메모가 추가되는 것을 가정한 것이고요.

            아니면 디테일 뷰 컨트롤러를 열기 전에 prepare(for:) 이 부분에서 새로 만든 메모를 리스트 배열에 넣지 않고, 델리게이트 메소드에서 메모 객체를 아예 넘겨 받아서 배열에 추가할 것인지 말 것인지를 결정하는 것도 하나의 방법입니다.

          • 이지섭

            아…!! 자세한 설명 정말 감사드립니다!! 정말 진심으로 감사드려요ㅠ 혹시 강의같은건 안하시나요?ㅎㅎ

          • 네 뭐… 개발자도 아니고… 저도 그냥 공부하는 입장이라 ㅎㅎ

          • 이지섭

            헐..공부하시는구나.. 뭔가좀 막막해지네요 엄청 많이 아시는것같은데… 음.. 또하나 질문드려도 괜찮을까요…reloadData를 어디다 넣어도 다 작동을 하지 않는것 같아서요.. beginUpdate,endUpdate이런거 써봐도 안되고 계속 앱 종료했다가 다시 켜야 저장한게 정상적으로 표시가되네요..

          • 소스 없이 판단하기는 어려운데요.. 디버깅하면서 찾아보세요.

            1. 먼저 tableView.reloadData()는 실제로 호출되는지요? 혹시 tableView 프로퍼티를 옵셔널로 선언하고 tableView?.reloadData() 이런식으로 호출하면 nil 인 경우에 그냥 패스해버립니다.

            2. 이게 제일 의심되는 부분인데요. tableView.reloadData() 가 호출되는 시점 직전에 브레이크 포인트를 걸고, 그 시점의 메모 리스트를 살펴봅니다. 원데이터 배열에 변경이 없다면, 당연히 reloadData() 를 해도 변화가 없겠죠. 새로 생성한 메모가 메모리스트 배열에 추가되지 않은채로 reloadData() 되었을 수 있습니다. 앱을 닫았다 새로 열면 추가된다고 했는데, 이는 앱 종료시에 앱델리게이트가 아마 저장을 하고, 앱 재시작시에는 저장된 전체 내용을 다시 불러오기 때문으로 보입니다.

          • 제가 gist에 올려놓은 소스를 보니까, 그 문제가 맞네요. 본문의 예제에서 메모리스트는 참조시마다 매번 컨텍스트로부터 메모전체를 로드해주는데, gist에 올린 코드에서는 최초 초기화시에만 로드해오고 있습니다.

            해결 방법은 새 메모를 추가할 때 prepare(for:sender)에서 새로 만든 메모를 리스트에 추가해주거나, 아니면 본문 예제처럼 메모리스트 프로퍼티를 아예 computed property로 정의해서 참조할 때마다 다시 로드해오는 식으로 만들면 됩니다.

            물론 전자가 더 나은 방법일 듯 하네요.

          • 이지섭

            음.. 첫번째방법은 제가 잘 몰라서 실패했는데.. .두번째 방법으로 해봤거든요. 그런데 이미 저장된 메모를 클릭해서 들어가서 수정을하고 저장을 하면 그건 바로바로 셀에 적용이 되는데 여전히 새 메모를 저장하면 적용이 안되는군요 ㅠ 본문같은 방식으로 참조할때마다 다시 로드하게 했는데 왜 새 메모는 불러오질 못할까요?ㅋㅋㅋ;;; 이거참 힘드네요 ㅎ

          • 이지섭

            아 그리고…죄송한데 제가 셀 크기를 콘텐츠 크기에 맞춰 자동변경하게 하려고 estimatedRowHeight 와 rowHeight = UITableViewAutomaticDimention 이걸 썻는데요, 잘 되긴하는데 메모가 한두줄짜리는 칸이 너무 작더군요.. 혹시 크기의 최소치를 설정하는 방법이 있을까요? 문서찾아봐도 잘 모르겠어서..

          • 의심되는 곳에 브레이크 포인트를 걸고 디버깅해보셔야 할 것 같네요.

            1. 저장에 실패하고 있거나 (gist에 올린 코드에서는 그냥 try? 라고 쓰고 넘어가버릴 수도 있고요)
            2. 실제로는 매번 호출할 때마다 fetch를 하지 않거나 하는 부분이 의심됩니다.

            3 기존거 수정의 경우에는 배열 자체가 클래스 인스턴스를 잡고 있는 것이기 때문에 fetch를 하지 않아도 상관없어요. 모든 변경사항은 context 내에 그대로 적용돼 있는 상태라서요.

          • https://github.com/sooop/ANMemoEditor 여기에 프로젝트 올려놨으니 참고해보세요. 추가/편집/삭제 모두를 구현하시려는 거 같아서 그 부분까지는 구현해봤습니다.

          • 이지섭

            매번 너무 감사드립니다 ㅠ 정말 많은부분 너무 많이 도움이 되었어요! 아무래도 계속 이것저것 물어볼거같긴하지만.. 너무 귀찮게 해드리는것 같네요 ㅠ 아무쪼록 새해에는 좋은일만 있으시길 바랍니다! 새해 복 많이 받으세요!

          • 이지섭

            흑… 아직도 에러가납니다 ㅠ3일동안 이것만붙잡고있는데 도저히 해결이안되서… 수정을하고 완료버튼을 누르거나 삭제버튼을 누르면 let d = tableView.indexPathForSelectedRow!.row
            여기서 계속 에러가나면서 앱이멈추네요ㅠ
            fatal error: unexpectedly found nil while unwrapping an Optional value라고 뜹니다… 뭔짓을해도안잡히네요 ㅠ

          • 이지섭

            수정부분은 didFinishEditingWithAction부분을 .add로 바꿨더니 고쳐졋는데 삭제부분은 여전하네요..거기에 같은코든데 제건 아직도 새로생성한 메모가 바로바로 표시가 되지않고있어요 ㅠ 너무 답답하네요 ㅎㅎ;

          • 말씀하시는 에러는 !문법을 써서 강제로 언래핑한 옵셔널 타입 값이 nil일때 발생합니다. 아마 테이블 뷰의 선택된 행이 없는 경우인데요. 그렇다면 저 메소드가 불리는 시점이 제가 드린 내용하고 다르지 않을까 예상해봅니다.

            사실, 소스를 다 드린 후라, 님께서 안되는 부분의 소스를 올려주셔야 같이 고민할 수 있지 않을까 싶네요.

          • 이지섭

            어.. 어떻게올려야하지.. 이메일로 보내드려도 될까요….?

          • snotice@네이버 로 보내주세요

          • 이지섭

            전송 했습니다! 봐주셔서 감사해요!

          • 이지섭

            으악…이런 간단한문제를… 셀 선택했을때 선택한 회색이 그대로 남아있을까봐 쓴거였는데요..ㅠ 감사합니다! 그런데 혹시
            새롤운 메모 생성했을때 바로 테이블뷰에 적용안되는건 왜그런지 혹시 못보셧나요?ㅜ

          • 이지섭

            혹시나해서 case .add에 reloadRows로 0번row 리로드하면 새로운 메모도 바로바로 적용이 되긴하지만… 당연하게도 맨 밑으로 들어가게 되네요. memoList.count – 1로 indexpath를 잡아도 마찬가지로 맨 밑으로 들어가구요 그렇다는건 배열에 안들어가는건 아닌것 같아reloadData를 지우고 reloadData를 해보니.. 왠걸… 맨 밑으로 들어가는거였습니다;; 앱 종료하고 다시 켜면 맨위로 올라오구요;; 이거도대체가 왜이러는건지;;;;

  • Tony Kim

    안녕하세요! 혹시 전체 프로젝트 파일을 다운받고 싶은데 가능할까요? 페이지에 있는 다운로드 링크는 더 이상 사용할 수 없다고 나오네요.