iOS 5에서 아이클라우드 시작하기 파트 2

Ray Wenderlich

이 포스트는 영어 언어로도 제공됩니다.

Learn how to use iCloud in iOS 5!

iOS 5에서 아이클라우드 구현하기!

Ray가 알림: 본 튜토리얼은 iOS 5 진수성찬의 10번째 튜토리얼로 우리가 발행한 책  iOS 5 튜토리얼의 무료시식용 챕터입니다. 마음껏 드세요!

본 튜토리얼은 웹과 모바일 어플리케이션 UX 디자이너이자 개발자이며 iOS 튜토리얼 팀원인 Cesare Rocchi에 의해 작성되었습니다.

iOS 5에서 아이클라우드를 시작하기 튜토리얼, 두 개의 파트 시리즈 중 그 두 번째 파트를 시작하겠다.

시리즈의 첫 파트에서 우리는 아이클라우드가 어떻게 동작하고 UIDocument를 열고 저장하는 프로그래밍에 대해 다루었다.

이번 파트에서는 앱에 사용자 인터페이스를 추가하고 다중 문서로 작업하는 방법에 대해 다룰 것이다.

본 튜토리얼은 파트 1에서 이어지는 것으로 시작하기 전에 파트 1을 먼저 숙지하고 있어야 한다.

사용자 인터페이스 적용하기

우리는 Xcode 프로젝트 템플릿에서 빈 뷰 컨트롤러를 선택하였었다. 여기에 현재의 문서와 노트의 내용을 표시하기 위해 UITextView를 추가할 것이다.

아래와 같이 ViewController.h를 수정하는 것으로 시작하자.

#import <UIKit/UIKit.h>
#import "Note.h"
 
@interface ViewController : UIViewController 
 
@property (strong) Note * doc;
@property (weak) IBOutlet UITextView * noteView;
 
@end

텍스트 뷰의 이벤트를 받기 위해 뷰 컨트롤러에  UITextViewDelegate 구현을 추가하였다..

다음으로 ViewController_iPhone.xib를 열어 아래와 같이 수정하자.

  • 뷰에 텍스트 뷰를 끌어다 놓고 전체 영역으로 크기를 채우자.
  • File’s Owner를 control+클릭한 다음, noteView 아울렛을 Text View로 드래그 하자.
  • Text View를 control+클릭한 다음, delegate를 File’s Owner로 드래그 하자.

위와 같이 했다면 여러분의 화면은 아래와 같을 것이다.

Adding a text view into Interface Builder

ViewController_iPad.xib에서도 위와 같은 작업을 반복하자.

다음으로, ViewController.m을 열어 아래와 같이 추가된 프로퍼티들을 synchronize하자.

@synthesize doc;
@synthesize noteView;

그런 다음 문서가 변경되었을 때 코드가 보내는 통보(notification)를 등록하기 위해 viewDidLoad를 아래와 같이 수정하자. (나중에 이 통보를 보내는 코드를 작성할 것이다.)

- (void)viewDidLoad
{
    [super viewDidLoad];
 
    [[NSNotificationCenter defaultCenter] addObserver:self 
        selector:@selector(dataReloaded:) 
        name:@"noteModified" object:nil];
}

다음으로, 아래와 같이 통보를 받았을 때 호출되는 메소드를 구현하자.

- (void)dataReloaded:(NSNotification *)notification {
 
    self.doc = notification.object;
    self.noteView.text = self.doc.noteContent;
 
}

여기서는 간단하게 현재의 문서 객체를 인스턴스 변수에 대입하고 전달받은 새로운 내용을 텍스트 뷰에 적용할 뿐이다.

일반적으로 이전 내용을 새로운 내용으로 대체하는 것은 좋은 습관이 아니다. 아이클라우드로부터 변경 통보를 받았을 때 사용자가 로컬문서와의 불일치 내용을 받아들이거나, 거절하거나, 병합할 수 있도록 불일치 해결 정책을 제공해야 한다. 불일치 해결에 관해서는 나중에 논의하도록 하고 지금은 단순함을 우지하기 위해 매번 대체하는 것으로 하겠다.

다음으로 문서가 수정되었을 때 아이클라우드로 통보를 주기 위해 textViewDidChange를 구현하고, viewWillAppear에서 데이터가 리프레쉬 될 수 있도록 구현하자.

- (void)textViewDidChange:(UITextView *)textView {
 
    self.doc.noteContent = textView.text;
    [self.doc updateChangeCount:UIDocumentChangeDone];
 
}
 
- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    self.noteView.text = self.doc.noteContent;
}

위의 방법은 한 번의 수정(한 글자를 입력하거나, 삭제하거나)이 발생할 때 마다 아이클라우드로 각각 통보하는 것이기 때문에 그리 훌륭한 방법은 아니다. 아이클라우드에게 가끔씩 또는 사용자 편집의 한 묶음이 끝났을 때 통보를 주는 것이 보다 더 능률적이다.

이제 마지막 한 단계만 남았다. viewDidLoad에서 등록했던 “noteModified” 통보(notification)를 보내는 코드를 추가해야 한다. 가장 좋은 위치는 Note 클래스의 클라우드에서 데이터를 읽어 들이는 loadFromContents:ofType:error 메소드이다.

Note.m을 열어서 loadFromContents:ofType:error 메소드 하단에 아래의 코드를 추가하자.  (물론 return YES 위에 추가해야 한다.)

[[NSNotificationCenter defaultCenter] 
    postNotificationName:@"noteModified" 
    object:self];

이제 정말 준비가 다 되었다. 이 앱을 시험해 보기 위한 가장 좋은 방법은 두 대의 기기에 앱을 설치하고 실행해 보는 것이다. 한 대의 기기에서 문서를 수정해 보고, 변경 내용이 다른 기기에 주기적으로 전송되는 지 볼 수 있어야 한다.

변경 내용은 바로 전송되지 않으며 네트워크 연결 상태에 따라 의존적일 수 있다. 일반적으로 이 경우에서는 5~30초 정도 소요될 수 있다. 올바로 전송되었는지 확인하는 다른 방법으로는 아이클라우드에서 파일목록을 확인하는 것이다.

이것은 아이폰(또는 아이패드)메뉴에 약간 숨겨져 있는데 아래와 같이 하면 된다.

설정(Settings) -> iCloud -> 저장 공간 및 백업(Storage and Backup) -> 저장 공간 관리(Manage Storage) -> 문서 및 데이터(Documents & Data) -> Unknown

앱이 정상적으로 동작한다면 앱에서 생성된 노트를 볼 수 있을 것이다.

iCloud document for app in Settings

‘unknown’ 이라고 표시되는 이유는 앱이 아직 앱스토어에 올려지지 않았기 때문이다.

앱을 통하지 않고 이 화면에서 사용자가 아이클라우드 파일을 삭제할 수 있다는 것 또한 주의해야 한다. 앱을 개발할 때 이 점을 항상 염두해 두어야 한다.

축하한다. 아이클라우드가 적용된 첫 번째 앱을 만들게 되었다!

다중 문서 지원하기

훌륭하다. 우리의 앱은 잘 동작하고, 아이클라우드 적용에 대해 좀 더 친숙하게 되었다. 그러나 사용자를 만족시키고 가치 있는 앱이라고 하기엔 충분하지 않은 것이 있다. 누가 하나의 문서만 관리하겠는가?

그래서 다음으로, 우리는 하나 이상의 문서가 관리될 수 있도록 앱을 확장할 것이다. 현재의 구조에서 가장 자연스러운 개발은 아래와 같이 변형하는 것이다.

  • 앱이 노트의 목록을 보여주는 뷰로 시작될 수 있도록 한다.
  • 각 노트는 고유 id를 가지도록 한다.
  • 노트를 탭하면 그 내용으로 하나의 노트 뷰가 보여지도록 한다.
  • 사용자는 내용을 수정한다.
  • 앱이 실행되거나 새로고침 버튼을 탭하면 노트 목록이 갱신되도록 한다.

이전 프로젝트의 코드를 재사용할 것이지만 우리는 그것을 개편해야 한다. 사용자 인터페이스를 재배열하는 것으로 시작해보자.

사용자 인터페이스 개편

이제 하나의 뷰로는 충분하지 않다. 두 개가 필요한데, 하나는 노트의 목록을 보여주기 위한 테이블 뷰이고, 다른 하나는 위에서 메인 뷰로 사용되었던, 텍스트 뷰가 있는 그 뷰가 될 것이다.

New architecture for our app

테이블 뷰 컨트롤러를 추가하고 그것이 네비게이션 컨트롤러 안에서 앱 실행시 먼저 보여지도록 앱을 수정해 보자.

iOS Cocoa Touch의 UIViewController 서브클래스 템플릿으로, 클래스 이름은  ListViewController로 하는 파일을 생성하자. UITableViewController의 서브클래스여야 한다. 두 개의 채크박스는 모두 언체크되어야 한다.

AppDelegate.m을 열어서 상단에 ListViewController.h를 포함시키자.

#import "ListViewController.h"

그런 다음 application:didFinishLaunchingWithOptions의 시작 부분을 아래의 코드로 수정하자.

self.window = 
    [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
ListViewController * listViewController = 
    [[ListViewController alloc] initWithNibName:nil bundle:nil];
UINavigationController * navController = 
    [[UINavigationController alloc] initWithRootViewController:
        listViewController];
self.window.rootViewController = navController;
[self.window makeKeyAndVisible];

이것은 앱이 ListViewController가 루트가 되는  네비게이션 컨트롤러를 메인으로 시작할 수 있도록 한다.

여기까지 하고 일단 컴파일과 실행을 해보자. 비어있는 테이블이 보일 것이다. 좋은 시작이긴 하지만 아이클라우드 문서로 테이블을 채워야 한다. ListViewController.h를 아래와 같이 수정하자.

#import <UIKit/UIKit.h>
#import "Note.h"
#import "ViewController.h"
 
@interface ListViewController : UITableViewController
 
@property (strong) NSMutableArray * notes;
@property (strong) ViewController * detailViewController;
@property (strong) NSMetadataQuery *query;
 
- (void)loadNotes;
 
@end

여기서 우리는 노트들이 저장될 배열, 네비게이션 컨트롤러 스택에 푸쉬될 뷰 컨트롤러(앞서 구현한) 참조자, 노트들을 불러 들이는데 사용될 메타데이터 쿼리, 그리고 loadNotes 메소드를 추가하였다. 메소드는 나중에 구현될 것이다.

다음으로 ListViewController.m에서 프로퍼티들을 아래와 같이 synthesize하자.

@synthesize notes = _notes;
@synthesize detailViewController = _detailViewController;
@synthesize query = _query;

그런 다음 아래의 코드를 viewDidLoad 하단에 추가하자.

self.notes = [[NSMutableArray alloc] init];
self.title = @"Notes";
UIBarButtonItem *addNoteItem = [[UIBarButtonItem alloc] 
    initWithTitle:@"Add" 
    style:UIBarButtonItemStylePlain
    target:self 
    action:@selector(addNote:)];
self.navigationItem.rightBarButtonItem = addNoteItem;

여기서는 노트 배열을 초기화하고, 뷰 컨트롤러의 타이틀을 설정하고, 네비게이션 바에 “Add”버튼을 추가하였다. 사용자가 이 버튼을 탭하면 addNote 메소드가 호출되고 거기서 새로운 노트가 추가되도록 구현할 것이다.

하나의 파일명만 사용하여 하나의 문서만 관리되도록 했었던 이전 프로젝트 소스로 지금 진행하고 있는, 노트가 생성될 때 마다 다중 문서로 저장될 수 있도록 하는 목적으로 사용할 수 없을 것 같다. 따라서 우리는 식별될 수 있는 유일한 파일명을 생성하는 방법이 필요하다. 가장 쉬운 방법은 파일 생성일시(날짜와 시각) 문자열에 ‘Note_’ 라는 스트링을 앞에 붙히는 것이다.

addNote 메소드를 아래와 같이 구현하자.

- (void)addNote:(id)sender {
 
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    [formatter setDateFormat:@"yyyyMMdd_hhmmss"];
 
    NSString *fileName = [NSString stringWithFormat:@"Note_%@", 
                         [formatter stringFromDate:[NSDate date]]];
 
    NSURL *ubiq = [[NSFileManager defaultManager]
        URLForUbiquityContainerIdentifier:nil];
    NSURL *ubiquitousPackage = 
        [[ubiq URLByAppendingPathComponent:@"Documents"] 
            URLByAppendingPathComponent:fileName];
 
    Note *doc = [[Note alloc] initWithFileURL:ubiquitousPackage];
 
    [doc saveToURL:[doc fileURL] 
        forSaveOperation:UIDocumentSaveForCreating 
        completionHandler:^(BOOL success) {
 
        if (success) {
 
            [self.notes addObject:doc];
            [self.tableView reloadData];
 
        }
 
    }];
 
}

파일명이 현재의 날짜와 시각을 조합하여 생성되었다. saveToURL 메소드를 호출하였고, 성공하면 생성된 노트를 배열에 추가하고 테이블 뷰에 나타나도록 하였다.

노트 추가 기능이 거의 다 되었다. 노트 배열의 내용이 테이블 뷰에 나타나도록 코드를 추가하기만 하면 된다. 아래와 같이 테이블 뷰의 데이터 소스를 구현하자.

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}
 
- (NSInteger)tableView:(UITableView *)tableView 
  numberOfRowsInSection:(NSInteger)section
{
    return self.notes.count;
}
 
- (UITableViewCell *)tableView:(UITableView *)tableView 
  cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
 
    UITableViewCell *cell = [tableView 
      dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] 
            initWithStyle:UITableViewCellStyleDefault 
            reuseIdentifier:CellIdentifier];
        cell.accessoryType = 
            UITableViewCellAccessoryDisclosureIndicator;
    }
 
    Note * note = [_notes objectAtIndex:indexPath.row];
    cell.textLabel.text = note.fileURL.lastPathComponent;
 
    return cell;
}
 
- (void)tableView:(UITableView *)tableView 
  didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
        self.detailViewController = [[ViewController alloc] 
            initWithNibName:@"ViewController_iPad" bundle:nil];
    } else {
        self.detailViewController = [[ViewController alloc] 
            initWithNibName:@"ViewController_iPhone" bundle:nil];
    }
    Note * note = [_notes objectAtIndex:indexPath.row];
    self.detailViewController.doc = note;
    [self.navigationController 
        pushViewController:self.detailViewController animated:YES];
}

컴파일과 실행을 해보자. Add 버튼을 탭하면 새로운 노트를 추가할 수 있을 것이다. 그리고 아이폰/아이패드 설정에서 아이클라우드 관리자로 가보면 생성된 노트들이 실제로 보일 것이다.

그런데 앱을 종료하고 다시 실행하면 어떤 일이 발생하는가? 목록이 비어 있다! 앱이 실행되거나 액티브될 때 노트들을 테이블 뷰로 불러들여야 할 필요가 있다.

노트 불러오기

노트를 불러오기 위해서 하나의 노트를 로딩했던 이전 방법과 비슷하게 진행할 것이다. 그러나 이번에는 정확한 파일명을 모르기 때문에 파일명이 “Note_*”를 가지는 결과로 이루어지도록 검색 predicate를 살짝 변경해 주어야 한다.

ListViewController.m에 아래의 메소드를 추가하자.

- (void)loadNotes {
 
    NSURL *ubiq = [[NSFileManager defaultManager] 
        URLForUbiquityContainerIdentifier:nil];
 
    if (ubiq) {
 
        self.query = [[NSMetadataQuery alloc] init];
        [self.query setSearchScopes:
            [NSArray arrayWithObject:
                NSMetadataQueryUbiquitousDocumentsScope]];
        NSPredicate *pred = [NSPredicate predicateWithFormat: 
            @"%K like 'Note_*'", NSMetadataItemFSNameKey];
        [self.query setPredicate:pred];
        [[NSNotificationCenter defaultCenter] addObserver:self 
            selector:@selector(queryDidFinishGathering:) 
            name:NSMetadataQueryDidFinishGatheringNotification 
            object:self.query];
 
        [self.query startQuery];
 
    } else {
 
        NSLog(@"No iCloud access");
 
    }
 
}

이 loadNotes 메소드를 viewDidLoad에서 직접 호출하고 싶을 것이다. 그것도 맞지만 그렇게 하면 앱이 실행될 때만 호출된다. 앱이 열릴 때마다(백그라운드에서 돌아올 때도) 데이터를 reload하기를 원한다면 앱이 액티브되었을 때를 알려주는 observer를 추가하는 것이 나은 방법니다.

따라서 viewDidLoad 하단에 아래의 코드를 추가하자.

[[NSNotificationCenter defaultCenter] addObserver:self 
    selector:@selector(loadNotes) 
    name: UIApplicationDidBecomeActiveNotification object:nil];

이렇게 하면 앱이 실행되거나 백그라운드에서 돌아올 때 loadNotes 메소드가 호출된다. 그리고 메타데이터 검색이 완료되었을 때 queryDidFinishGathering 메소드가 호출되도록 loadNotes를 구현했었다. 이제 그 코드를 아래와 같이 구현하자.

- (void)queryDidFinishGathering:(NSNotification *)notification {
 
    NSMetadataQuery *query = [notification object];
    [query disableUpdates];
    [query stopQuery];
 
    [self loadData:query];
 
    [[NSNotificationCenter defaultCenter] removeObserver:self 
        name:NSMetadataQueryDidFinishGatheringNotification
        object:query];
 
    self.query = nil;
 
}

우리가 파트1에서 구현하였던 것과 정확히 일치한다. 다음으로 queryDidFinishGathering 메소드 바로 위에 아래와 같이 loadData 메소드를 구현하자.

- (void)loadData:(NSMetadataQuery *)query {
 
    [self.notes removeAllObjects];
 
    for (NSMetadataItem *item in [query results]) {
 
        NSURL *url = [item valueForAttribute:NSMetadataItemURLKey];
        Note *doc = [[Note alloc] initWithFileURL:url];
 
        [doc openWithCompletionHandler:^(BOOL success) {
            if (success) {
 
                [self.notes addObject:doc];
                [self.tableView reloadData];
 
            } else {
                NSLog(@"failed to open from iCloud");
            }
 
        }];        
    }    
}

loadData 메소드에서 아이클라우드로 부터 모든 노트목록을 가져오는데, 쿼리 결과로 얻어진 노트의 배열을 화면에 표시해야 한다.

마지막 남은 작업 하나, 테스트를 목적으로 loadNotes 메소드를 호출하는  ‘refresh’ 버튼을 네비게이션 아이템에 추가하자. 아래의 코드를 viewDidLoad 하단에 구현하면 된다.

UIBarButtonItem *refreshItem = [[UIBarButtonItem alloc] 
    initWithTitle:@"Refresh"                                                                 
    style:UIBarButtonItemStylePlain                                                              
    target:self 
    action:@selector(loadNotes)];
self.navigationItem.leftBarButtonItem = refreshItem;

자, 끝이다! 앱을 컴파일하고 실행해 보자. 그리고 몇 개의 노트를 작성해 보자. 그런 다음 다른 기기에서 앱을 실행하여 작성한 노트 목록이 테이블에 보여지는지 확인해 보자.

이제 어디로 갈까?

여기 지금까지의 모든 코드에 대한  프로젝트 소스가 있다.

축하한다. 여러분은 지금까지 다중 문서를 지원하는 아이클라우드 앱을 만들기 위해 아이클라우드의 기본 구현과 UIDocument라는 새로운 클래스 사용에 대해 직접 경험하였다.

우리는 단지 아이클라우드의 표면만 살짝 만졌을 뿐이다. 아이클라우드에 대해 좀 더 배우기를 원한다면 우리의 책  iOS 5 튜토리얼을 참고하라. 최종 버전에는 아이클라우드에서의 불일치 해결, NSFileWrapper 사용하기, 간단한 키-밸류 저장하기, 그리고 코어데이터 사용하기등이 포함된 모든 챕터들이 수록된다.

본 튜토리얼이나 아이클라우드에 관한 의견이나 질문이 있으면 포럼에 남겨주기 바란다!

본 튜토리얼은 웹과 모바일 어플리케이션 UX 디자이너이자 개발자이면서 iOS 튜토리얼 팀원인 Cesare Rocchi에 의해 작성되었습니다.  그는  Studio Magnolia 의 한 스튜디오에서 아주 훌륭한 웹과 모바일 어플리케이션을 만들고 있습니다.

 

 

 

본 튜토리얼의 한글 번역은 Wizsoft의 대표이면서 OS X와 iOS 개발자로 일하고 있는 장영준(@istsest)에 의해 작성되었습니다.

Ray Wenderlich

Ray is an indie software developer currently focusing on iPhone and iPad development, and the administrator of this site. He’s the founder of a small iPhone development studio called Razeware, and is passionate both about making apps and teaching others the techniques to make them.

When Ray’s not programming, he’s probably playing video games, role playing games, or board games.

User Comments

0 Comment

Other Items of Interest

Ray의 월간 뉴스레터

Sign up to receive a monthly newsletter with my favorite dev links, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Hang Out With Us!

Every month, we have a free live Tech Talk - come hang out with us!


Coming up in September: iOS 8 App Extensions!

Sign Up - September

RWDevCon Conference?

We are considering having an official raywenderlich.com conference called RWDevCon in DC in early 2015.

The conference would be focused on high quality Swift/iOS 8 technical content, and connecting as a community.

Would this be something you'd be interested in?

    Loading ... Loading ...

Our Books

Our Team

Tutorial Team

  • Brian Broom
  • Kirill Muzykov

... 50 total!

Update Team

... 14 total!

Editorial Team

... 23 total!

Code Team

  • Orta Therox

... 3 total!

번역 팀

  • David Hidalgo
  • Marina Mukhina
  • Vitaliy Zarubin

... 33 total!

Subject Matter Experts

  • Richard Casey

... 4 total!