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

Ray Wenderlich

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

Learn how to use iCloud in iOS 5!

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

2012/10/24 추가 : 본 튜토리얼의 iOS 6과 Xcode 4.5를 위한 새로운 버전을 원하시면  iOS 5 by Tutorials Second Edition 으로 방문하세요.

Ray가 알림 : 본 튜토리얼은 iOS 5의 진수성찬 (iOS 5 Feast) 중 9번째 튜토리얼로서 우리가 발간한 책 iOS 5 튜토리얼의 일부인 무료시식용 챕터입니다. 마음껏 드세요 ^^

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

우리는 아이폰과 아이패드를 이용하면서 문서, 사진, 동영상, 이메일, 일정, 음악, 주소록과 같은 것들을 그 속에 가지고 있다. 그런데 다른 디바이스에 저장되어 있는 문서들을 신속하고 재빠르게 열어 보기 위해 얼마나 많이 애쓰고 있나?

자!~ 이제 iOS 5의 새로운 기능인 아이클라우드를 이용하면 더 이상 애쓸 필요가 없다.

아이클라우드는 디바이스들간에 데이터를 동기화해주는 서비스이다. 즉, 문서를 저장하고 iPhone, iPod, iPad, Mac, PC등 iCloud를 지원하는 모든 디바이스의 앱에 최종 버전의 문서를 제공하는 중앙서버 집합이다.

본 튜토리얼에서는 문서를 읽고, 쓰고, 수정하기 위해 클라우드 서버와 상호작용하는 간단한 어플리케이션을 구현해 봄으로써 아이클라우드를 연구할 것이다. 그 과정에서 여러분은 UIDocument라는 새로운 클래스와 iCloud에서의 파일조회, 자동저장등에 관해 배우게 될 것이다.

성찬을 즐기기 전에…

시작하기 전에, 아이클라우드가 어떻게 동작되는지 알아보자.

iOS에서 각 어플리케이션들은 데이터를 로컬 디렉토리에 저장하고 각 앱들은 자신의 디렉토리에 있는 데이터만 접근할 수 있다. 이것은 다른 앱들이 임의로 데이터를 읽거나 수정하는 것을 방지해 준다. (iOS 기본 앱들간에는 데이터 이동을 위한 몇몇 전송방법이 있긴 하지만…)

아이클라우드는 여러분의 데이터를 온라인상의 중앙서버로 업로드할 수 있도록 해주고, 다른 디바이스에 의해 수정된 데이터를 받을 수 있도록 해준다. 다른 디바이스들간의 내용 복제는 문서나 데이터의 변경사항을 감시하고 이를 중앙서버로 업로드 해주는 데몬과 같은 지속적은 백그라운드 프로세스에 의해 이루어 진다.

이것은 실시간으로 이루어지며, 또 다른 흥미있는 가능인 알림, 즉, 노티피케이션을 작동시킨다. 예를 들어, 문서의 내용 불일치가 있을 때마다 해당 어플리케이션이 이를 인식할 수 있도록 하고  이에 대한 처리를 구현할 수 있다는 것이다.

만일 이와 같은 작업을 한 번이라도 앱안에서 시도를 해보았다면 이를 구현하는데 다음과 같이 몇 가지 주요한 난제가 있음을 알 것이다.

  • 불일치 해결. 아이폰에서 문서를 수정하고, 동시에 아이패드에서 동일한 문서를 수정한다면 어떤 일이 발생할까? 어떻게든 변경된 내용을 일치시켜야 한다. 아이클라우드는 믾은 병합이 복제되는 문제가 발생하지 않도록 문서를 덩어리로 쪼개어 질 수 있도록 해준다. (디바이스1에서 A덩어리를 변경하고 디바이스2에서 B덩어리를 변경한다면 A와 B덩어리가 달라진 이후 이들을 조립만 해주면 되기 때문이다.) 이러한 것이 문제가 될 경우에는 개발자가 코드로서 적절한 조치를 취할 수 있도록 해준다. (사용자에게 어떻게 할 것인지 물어 볼 수도 있다.)
  • 백그라운드 관리. iOS 앱은 백그라운드로 실행되는데에 제한이 있지만, 문서가 항상 최신상태로 유지되도록 할 수 있다. 아이클라우드 동기화 기능이 백그라운드 데몬으로 항상 활성화되어 있다는 건 정말 좋은 소식이다.
  • 네트워크 비용. 계속되는 문서 전송은 방대한 네트워크 트래픽을 유발시킬 수 있다. 위에서도 언급했듯, 아이클라우드는 각 문서를 덩어리로 쪼갬으로서 이 비용을 감소시킬 수 있도록 해 준다. 변경 내용이 감지되면 네트워크와 프로세싱 비용을 최소화하기 위해 해당 덩어리들만 클라우드로 업로드된다. 게다가 P2P 솔루션 기반으로 최적화되어 있는데 이것은 두 대의 디바이스가 동일한 아이클라우드 계정과 동일한 무선 네트워크에 있을 때 동작된다. 즉, 디바이스간 직접 연결로 데이터가 이동되는 것이다.

지금까지 설명한 메카니즘은 파일명, 파일 사이즈, 수정 일시, 버젼등과 같은 메타데이터 관리기법에 의해 작동된다. 이 메타데이터는 클라우드로 전송되고 아이클라우드는 이 정보를 이용해 각 디바이스에 전송할지 여부를 결정한다.

클라우드는 적절할 때 데이터를 디바이스로 전송한다. 이 의미는 OS와 플랫폼 의존적이라는 것이다. 예를 들어, 아이폰이 충전중이지 않고 전원이 연결된 아이맥보다 전력이 낮을 경우,  iOS는 다운로드 없이 새로운 파일의 존재 알림만 받는 반면, Mac OS X에서는 알림 이후 즉시 다운로드를 시작할 수 있다는 것이다.

중요한 것은 새로운 파일이 존재하거나 기존 파일이 수정되었다는 것을 항상 인식한다는 것이고 API를 통해 개발자들은 동기화 정책을 자유롭게 구현할 수 있다는 것이다.  본질적으로 API를 이용해 파일이 로컬에 있지 않다 하더라도 개발자가 언제 다운로드하고 버전 업데이트를 할지 자유롭게 선택할 수 있도록 아이클라우스상에서의 데이터 상황을 알 수 있도록 하는 것이다.

iOS 5에서의 아이클라우드 설정

처음 iOS 5를 설치하고나면 애플 아이디를 생성하거나 기존 아이디를 이용하여 아이클라우드 계정을 설정하게 된다. 그 과정에서 일정이나 연락처등 동기화를 원하는 것들을 지정하게 된다. 이러한 설정들은 기기의 설정->아이클라우드에서 다시 지정할 수 있다.

본 튜토리얼을 계속 진행하기 전에, 두 대의 테스트 기기에 iOS 5가 설치되어 있고 아이클라우드가 올바르게 작동하고 있는지 확인하기 바란다.

이를 확인하기 위한 가장 간단한 방법은 일정에 테스트 항목을 하나 추가하고 다른 기기에서 올바로 동기화되는 지 확인하는 것이다. http://www.icloud.com 에서 일정을 확인하는 방법도 있다.

아이클라우드가 제대로 작동되는 것을 확인하였다면 우리가 만든 앱에서도 작동될 수 있도록 해 보자!

앱을 위한 아이클라우드 활성화

본 튜토리얼에서 우리는 “dox”라고 불리는 아이클라우드용 공유 문서를 관리하는 간단한 앱을 만들 것이다. 아이폰과 아이패드에 실행될 수 있도록 유니버셜로 만들어서 한 기기의 변경내용이 다른 기기에 전달되는 것을 볼 수 있도록 할 것이다.

여기, 앱에서 아이클라우드를 사용하기 위한 세 단계의 과정이 있다. 새로운 프로젝트 시작을 위해 각 단계를 실행해 보자.

1. 아이클라우드용 App ID 생성

iOS 개발자 센터의 iOS Provisioning Portal로 가서 아래의 그림처럼 App ID를 하나 생성하자 (bundle identifier는 여러분 자신의 것으로 이전에 생성하지 않은 유일한 이름으로 입력해야 한다.)

주의: 곧 생성할 프로젝트 이름과 일치시켜야 하기 때문에 App ID가 “dox”로 끝나야 한다. 예를 들어 com.yourname.dox 이렇게 입력할 수 있을 것이다.

Creating an App ID

App ID를 생성했다면 Push Notification과 Game Center는 자동으로 활성화 되어 있을 것이고 iCloud는 직접 활성화 해야 한다. Configure 버튼을 눌러 계속해 보자.

Configuring an App ID to use iCloud

다음 화면에서 Enable for iCloud 체크박스를 클릭하고 팝업창이 나오면 OK를 클릭하자. 모든 것이 정상적이라면 Enabled라는 단어 옆에 연두색 아이콘이 보일 것이다. 그러면 Done을 클릭하여 App ID 생성과 설정을 끝내자.

Clicking Enable for iCloud in the iOS Provisioning Portal

2) 프로비져닝 프로파일 생성

iOS Provisioning Portal에서 Provisioning으로 이동하자. 그런 다음 New Profile을 클릭하고 App ID를 방금 생성한 App ID로 선택하자. 그리고 아래 그림과 같이 나머지 정보들을 입력하자.

Creating a Provisioning Profile

프로파일 생성 이후 다운로드가 가능할 때 까지 페이지를 리프레쉬 하자. 그리고 프로파일을 다운로드 하자. 다운로드된 프로파일을 더블클릭하면 Xcode로 보내지는데 Xcode의 오거나이저에서 보여지는 프로파일을 확인하자.

Viewing Provisioning Profiles in Xcode Organizer

3) Xcode 프로젝트 생성과 아이클라우드를 위한 설정

Xcode를 실행하여 싱글 뷰 템플릿으로 iOS Application 프로젝트를 생성하자. Product Name을 dox로 입력하고 Company Identifier에 위의 App ID를 생성할 때 사용했던 것을 입력하자. Device Family를 Universal로 선택하고 Use Automatic Reference Counting을 선택하자. (나머지는 언체크로 선택하자.)

Creating a new app in Xcode

프로젝트 생성이 끝나면 프로젝트 네비게이터에서 프로젝트를 선택, TARGETS애서 dox를 선택한 다음 Summary 탭에서 Entitlements 섹션이 보이도록 스크롤 이동하자.

그리고 Enable Entitlements 체크박스를 활성화하면 아래 그림 처럼 각 항목에 App ID를 기반으로 하는 정보들이 자동으로 채워질 것이다.

Setting entitlements in Xcode

각 항목들이 의미하는 바는 다음과 같다.

  • Entitlements File은 info.plist처럼 어플리케이션 언타이틀먼트에 관한 정보가 담겨 있는 퍼로퍼티 리스트 파일이다.
  • iCloud Key-Value Store는 클라우드에서의 key-value 저장소를 가리키는 고유식별자를 의미한다.
  • iCloud Containers는 클라우드에서 어플리케이션이 문서들을 읽고 쓸 수 있는 디렉토리임을 의미한다. 다른 어플리케이션들에 의해 관리될 수 있는 사용자 컨테이너이다. iTunes 개발자 센터에서 세팅된 같은 팀에 의해 만들어진 어플리케이션이기만 하면 관리가 가능하다.
  • Keychain Access Groups은 키체인 데이터를 공유하는 어플리케이션에서 필요로 하는 키들이다.

본 튜토리얼을 진행하기 위해서는 기본값에서 아무것도 변경할 필요가 없다. 계속 진행할 준비가 된 것이다. 원한다면 프로젝트에 포함된 dox.entitlements 파일을 수정함으로써 이 설정들을 변경할 수도 있다.

아이클라우드 활성화 여부 확인

아이클라우드를 사용하는 앱을 개발하면서 앱이 실행되자마자 아이클라우드가 활성화되어 있는지 체크하는 것이 가장 좋다. 모든 iOS  5 기기에서 아이클라우드가 사용가능할지라도 사용자가 아이클라우드 설정을 안했을 수 있기 때문이다.

의도하지 않은 동작이나 오류를 피하기 위해 아이클라우드를 사용하기 전에 아이클라우드가 활성화되어 있는지 확인해야 한다. 이 과정을 살펴 보자.

AppDelegate.m을 열고 아래의 코드를 didFinishLaunchingWithOptions 하단에 넣자.  (물론  return YES 위에.)

NSURL *ubiq = [[NSFileManager defaultManager] 
  URLForUbiquityContainerIdentifier:nil];
if (ubiq) {
    NSLog(@"iCloud access at %@", ubiq);
    // TODO: Load document... 
} else {
    NSLog(@"No iCloud access");
}

여기서 우리는 이전에 한 번도 본 적이 없는 URLForUbiquityContainerIdentifier를 호출한다 . 여기서 위의 프로젝트 설정의 iCloud Containers에서 설정했던 컨테이너 식별자를 넘길 수 있는데 그러면 아이클라우드 저장소에 접근할 때 사용되는 URL을 리턴한다.

접근하고자 하는 각 컨테이너에 대해 URL 접근 권한을 얻으려면 반드시 이 메소드로 시작해야 한다. 위의 코드처럼 nil을 넘기면 프로젝트 설정의 iCloud Containers에서 지정된 첫 번째 컨테이너가 리턴된다. 우리는 하나만 지정했으므로 그냥 이렇게만 하면 된다.

실제 기기상에서 컴파일과 실행을 해 보자. (시뮬레이터에서는 아이클라우드가 작동되지 않는다.) 모든 것이 정상적으로 동작한다면 콘솔에 아래와 같이 나타날 것이다.

iCloud access at file://localhost/private/var/mobile/Library/Mobile%20
  Documents/KFCNEC27GU~com~razeware~dox/

리턴된 URL은 시스템상의 로컬 URL임을 주의하자. 이것은 아이클라우드 데몬이 파일들을 중앙서버에서 여러분 기기의 로컬 디렉토리로 전송시키기 때문이다. 앱은 이 디렉토리에서 파일을 불러 들이거나 수정된 버전을 저장한다. 그리고 아이클라우드 데몬은 그것들을 동기화한다.

이 디렉토리는 여러분의 앱 디렉토리 외부에 있지만 위에서 언급했듯이 URLForUbiquityContainerIdentifier를 호출하면 이 디렉토리의 접근 권한을 획득할 수 있다.

iCloud API 둘러보기

코드를 더 진행하기 전에 잠깐동안 아이클라우드 문서 작업으로 이용하게 될 API들을 살펴 보자.

아이클라우드에 문서를 저장하기 위해 원한다면 NSFileManager의 새로운 메소드들과 NSFilePresenter와 NSFileCoordinator라는 새로운 클래스를 이용해 아이클라우드 디렉토리로 (또는 아이클라우드 디렉토리로 부터) 파일을 이동함으로써 직접 진행할 수 있다.

그러나 이것은 꽤 복잡하고 iOS 5에서 UIDocument라는 아이클라우드 문서 작업을 위한 굉장히 쉬운 클래스를 제공하기 때문에 대부분의 경우 그렇게 할 필요가 없다.

UIDocument는 파일 자신과 실제 데이터(여기서는 노트의 텍스트가 될 것이다.) 사이의  미들웨어로 동작한다. 앱에서는 일반적으로 UIDocument의 서브클래스를 생성하고 몇 개의 메소드를 override하게 되는데 이에 대해서는 나중에 다루도록 하겠다.

UIDocument는 NSFilePresenter 프로토콜을 백그라운드로 실행한다. 그래서 앱은 파일을 열거나 저장할 때 블로킹되지 않으며, 사용자는 조작을 계속할 수 있다. 이것은 이중 큐(queue) 아키텍쳐로 동작된다.

첫 번째 큐인 앱의 매인 쓰레드는 여러분의 코드가 실행되는 그것인데, 여기서 파일을 열고, 수정하고, 닫을 수 있다. 두 번째 큐는 UIKit에 의해 관리되는데 백그라운드로 동작한다. 예를 들어 문서 하나를 열고자 한다면 그것은 이미 백그라운드에 의해 아이클라우드 상에 생성되어 있다는 것이다.

아이클라우드에 저장된 문서를 열기 위해서는,  UIDocument 인스턴스가 있다고 가정하에 아래처럼 메시지를 보내면 된다.

[doc openWithCompletionHandler:^(BOOL success) {
    // 열기가 완료되었을 때 실행될 코드 입력
}];

이렇게 하면 백그라운드 큐에 read 메시지를 유발하게 되는데 openWithCompletionHandler가 실행되면 호출되는 것으로 직접 메소드로 실행될 수 없다. 이 작업은 파일이 매우 클 수도 있고, 아직 다운로드가 완료되지 않았을 수 있기 때문에 약간의 시간이 소요될 수도 있다.

그 시간동안 앱은 블럭킹되지 않기 때문에 UI와 같은 다른 작업들을 진행할 수 있다. 파일 읽기가 끝나면 읽기 동작에 의해 반환된 데이터를 자유롭게 로드할 수 있다.

UIDocument의 서브클래스로 데이터를 읽어 들이기 위해  loadFromContents:ofType:error 메소드를 오버라이드 하는건 매우 쓸모가 있다. 간단한 노트 앱을 위해 여기 그 코드가 있다.

- (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName 
  error:(NSError **)outError
{    
    self.noteContent = [[NSString alloc] 
        initWithBytes:[contents bytes] 
        length:[contents length] 
        encoding:NSUTF8StringEncoding];           
    return YES;    
}

이 메소드는 읽기 동작이 완료되었을 때 백그라운드 큐에 의해 호출된다.

여기서 가장 중요한 파라미터는 데이터 모델을 생성하거나 업데이트하기 위해 사용될 실제 데이터를 포함하는, 일반적으로 NSData 형식의 contents 라는 파라미터다. 여기 보이는 것 처럼 문서의 정보를 추출하고 UIDocument 서브클래스의 인스턴스 변수에 저장하기 위해 보통 이 메서드를 override 한다.

아래 그림처럼 loadFromContents:ofType:error 메소드가 완료되면 openWithCompletionHandler:의 블럭이 콜백될 것이다.

Order of operations when loading an UIDocument

요약하면, 파일을 오픈할 때 두 개의 콜백을 받는다. 첫 번째는 데이터 읽기가 끝났을 때 UIDocument의 서브클래서 안에서 받고, 두 번째는 열기 동작이 완전히 끝났을 때 받는다.

쓰기 동작도 같은 이중 큐를 이용한다는 점에서 비슷하다. 차이점은, 파일을 열때는 NSData 인스턴스를 파싱해야  하지만 기록할 때는 도큐먼트 데이터를 NSData로 변환하고 그것을 백그라운드 큐에 제공해야 한다는 것이다.

문서를 저장하기 위해 코드로 프로세스를 시작할 수도 있고 UIDocument에 구현된 자동저장 기능을 이용할 수 있다. 자동저장에 대해서는 나중에 설명하겠다.

코드로 직접 문서 저장을 원하면 아래와 같이 하면 된다.

[doc saveToURL:[doc fileURL] 
  forSaveOperation:UIDocumentSaveForCreating 
  completionHandler:^(BOOL success) {
            // Code to run when the save has completed
}];

파일을 열 때 처럼, 쓰기 동작이 끝나면  completion handler가 호출된다.

백그라운드 큐가 기록을 위한 UIDocument 서브클래스 내용을 요청하면 UIDocument의 메서드인 contentsForType:error를 overriding 함으로써 대응할 수 있다.

여기에서 저장이 되어야  할 데이터 모델을 NSData에 실어 리턴해야 한다. 우리 노트 앱은 다음과 같이 문자열을 NSData에 실어 리턴할 것이다.

- (id)contentsForType:(NSString *)typeName error:(NSError **)outError 
{
    return [NSData dataWithBytes:[self.noteContent UTF8String] 
                          length:[self.noteContent length]];

}

나머지는 데이터 저장을 관리하는 백그라운드 큐에서 알아서 진행된다. 저장이 끝나면 컴플리션 핸들러가 실행될 것이다.

읽기와 쓰기의 완료를 위해 NSData 대신 NSFileWrapper를 이용할 수도 있다. NSData가 균일한 파일들을 다루기 위한 용도라면 NSFileWrapper는 패키지, 즉 하나의 파일로 간주되지만 파일들을 포함하는 디렉토리를 다룰 수 있다. NSFileWrapper는 나중에 본 튜토리얼에서 다시 다룰 것이다.

앞서 말했듯, 저장동작은 코드나 자동으로 유발되는 과정을 통해 명시적으로 호출될 수 있다. UIDocument 클래스는 주기적 간격으로 또는 특정한 이벤트 발생 시 자동으로 데이터가 저장되는, 저장명령이 필요없는 모델이다. 즉, 사용자가 다른 문서로 전환해도 시스템에서 자동으로 관리되기 때문에 저장 버튼을 누를 필요가 없다는 이야기다.

UIKit 백그라운드 큐는  문서에 변경내용이 있는지, 저장이 필요한지에 대한 판단 여부를 리턴하는 hasUnsavedChanges라는 UIDocument의 메소드를 호출한다. 이에 YES를 리턴할 경우 문서는 자동으로 저장된다. 값을 직접적으로 지정할 수 있는 메소드는 없지만 그것에 영향을 줄 수 있는 방법은 두 가지가 있다.

첫 번째 방법은 updateChangeCount: 메소드를 명시적으로 호출하는 것이다. 이것은 변경에 대한 정보를 백그라운드 큐에 통보하는 것이다. 그리고 다음 대안으로 UIDocument 클래스에 내장된 undo 메니저를 이용할 수도 있다. undo / redo를 통해 변경 사항이 등록될 때마다 updateChangeCount가 자동으로 호출된다

어떤 경우든 변경 사항의 전달이 즉각적이지 않을 수 있다는 것을 기억하는 것은 중요하다. 이들 메시지를 보냄으로써 연결 형식과 기기 상태에 따라 적절한 때에 프로세스를 업데이트 하는 백그라운드 큐에 단지 힌트를 주는 것 뿐이다.

UIDocument 서브클래싱 하기

자 이제 UIDocument를 살펴 보자. 우리의 노트 앱을 위해 서브클래스를 만들고 어떻게 동작되는지 보자.

iOS Cocoa Touch의 Objective-C class template으로 파일을 생성하자. 클래스 이름은 Note로 하고, UIDocument를 베이스 클래스로 하자.

간단하게 하기 위해 노트내용을 저장하는 하나의 문자열 프로퍼티만 가질 것이다. 아레의 코드대로 Note.h를 수정하자.

#import <UIKit/UIKit.h>

@interface Note : UIDocument

@property (strong) NSString * noteContent;

@end

위에서 배웠듯이 읽어 들일 때와 저장할 때, 이 두 부분을 override 하자. 아래의 내용으로 Note.m을 수정하자.

 
#import "Note.h"

@implementation Note

@synthesize noteContent;

// 파일시스템으로 부터 데이터를 읽어 들일 때 호출된다.
- (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName 
  error:(NSError **)outError
{

    if ([contents length] > 0) {
        self.noteContent = [[NSStringalloc] 
            initWithBytes:[contents bytes] 
            length:[contents length] 
            encoding:NSUTF8StringEncoding];        
    } else {
        // 노트가 처음으로 생성되었을 때 기본값을 지정
        self.noteContent = @"Empty"; 
    }

    return YES;    
}
// 노트 데이터를 저장할 때 (자동 저장될 때) 호출된다.
- (id)contentsForType:(NSString *)typeName error:(NSError **)outError
{

    if ([self.noteContent length] == 0) {
        self.noteContent = @"Empty";
    }

    return [NSData dataWithBytes:[self.noteContent UTF8String] 
        length:[self.noteContent length]];

}

@end

파일을 로드할 때 백그라운드 큐에 의해 리턴되는  NSData 내용을 문자열로 변환하는 프로시져가 요구된다. 반대로 저장할 때는 문자열을 NSData 객체로 인코딩해야 한다. 이 두 가지 모든 과정애서 문자열이 비어 있는지를 체크하여 기본값을 대입하였다. 이것은 문서가 처음 생성되었을 때 해당된다.

믿거나 말거나지만, 우리가 필요한 도큐먼트 모델은 이걸로 끝이다. 이제 로딩과 업데이팅에 관련된 코드로 진행할 수 있다.

iCloud 파일 열기

먼저 문서의 파일명을 정하자. 본 튜토리얼에서는 하나의 파일명으로 시작할 것이다. 아래의 #define코드를 AppDelegate.m 상부에 입력하자.

#define kFILENAME @"mydocument.dox"

다음으로, 문서의 행적과 아이클라우드의 문서 연결용 메타데이터 쿼리를 유지하기 위해 application delegate를 확장하자.  AppDelegate.h를 아래와 같이 수정하면 된다.

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

@class ViewController;

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) ViewController *viewController;
@property (strong) Note * doc;
@property (strong) NSMetadataQuery *query;

- (void)loadDocument;

@end

그런 다음 AppDelegate.m으로 가서 두 개의 프로퍼티를 synthesize하자.

@synthesize doc = _doc;
@synthesize query = _query;

우리는 이미 아이클라우드 활성화를 체크하기 위해 AppDekegate의 didFinishLaunchingWithOptions에 관련 코드를 추가했다. 아이클라우드가 사용가능한 상태라면 아이클라우드로 부터 문서를 불러 오는 새로운 메서드를 원할 것이다. 따라서, 아래의 코드를 그 부분에 추가하고 그 오른편에  “TODO: Load document” 주석을 입력하자.

[self loadDocument];

그런 다음 우리는 loadDocument 메소드를 작성할 것이다. 아래의 코드를 이어서 추가하자.

- (void)loadDocument {

    NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
    _query = query; 

}

아이클라우드로 부터 문서를 불러오기 전에 거기에 어떤 것들이 있는지 먼저 확인해야 한다. 아이클라우드의 파일들이 아직 다운로드되지 않았을 수 있으므로  URLForUbiquityContainerIdentifier에서 리턴된 로컬디렉토리를 열거할 수 없음을 기억하자.

만일 Mac에서 스팟라이트 작업을 해 본 경험이 있다면 NSMetadataQuery 클래스에 친숙할 것이다. 이것은 파일과 같은 객체의 프로퍼티와 관련된 조회 결과를 제공한다.

이 쿼리를 만드려면 어디서 무엇을 조회하려는지에 관한 파라미터와 범위를 지정해야 한다. 아이클라우드 파일의 경우 범위는 항상 NSMetadataQueryUbiquitousDocumentsScope인데 하나의 항목에 배열로 다중범위를 지정할 수 있다.

아래의 코드로 loadDocument 구현을 계속하자.

- (void)loadDocument {

    NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
    _query = query; 
    [query setSearchScopes:[NSArray arrayWithObject:
        NSMetadataQueryUbiquitousDocumentsScope]];

}

이제 쿼리에 파라미터를 지정해야 한다. 여러분이 코어데이터나 배열을 사용한 경험이 있다면 그 접근법에 대해 알 것이다. 그와 같이 predicate를 구성하고 조회 파라미터를 지정하자.

여기서는 파일명으로 조회하는 경우이기 때문에 File System을 위한 “FS”가 포함되는 NSMetadataItemFSNameKey 키워드를 사용한다. predicate를 만들고 구성하는 코드를 아래와 같이 추가 입력하자.

- (void)loadDocument {

    NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
    _query = query;
    [query setSearchScopes:[NSArray arrayWithObject:
        NSMetadataQueryUbiquitousDocumentsScope]];
    NSPredicate *pred = [NSPredicate predicateWithFormat:
        @"%K == %@", NSMetadataItemFSNameKey, kFILENAME];
    [query setPredicate:pred];

}

%K 치환문을 본 적이 없을 지도 모르겠다. 이것은 predicate가 문자 포멧을 약간 다른 방법인 NSString의 stringWithFormat으로 다루겠다는 의미이다. predicates에서 %@을 사용할 경우그 값들은 따옴표료 감싸지게 된다. keypaths용 값을 위해 이러한 결과가 일어나지 않도록 하려면 따옴표로 감싸지는 것을 방지하기 위해 %K를 이용해야 한다. 보다 자세한 정보는 애플 개발 문서 Predicate Format String Syntax를 참조하기 바란다.

자, 이제 쿼리가 준비되었다. 그런데 이 쿼리는 비동기 프로세스이기 때문에 쿼리가 완료되었을 때 통보(Notification)를 받기 위한 observer가 필요하다.

여기서 필요한 통보는 꽤 긴~~ 이름인 NSMetadataQueryDidFinishGatheringNotification이다. (길지만 의미해석이 쉽다.) 이 통보는 아이클라우드로부터 주어진 쿼리에 대해 정보수집이 완료되었을 때 발생한다.

여기 그 구현의 최종 코드가 아래에 있다.

- (void)loadDocument {

    NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
    _query = query;
    [query setSearchScopes:[NSArray arrayWithObject:
        NSMetadataQueryUbiquitousDocumentsScope]];
    NSPredicate *pred = [NSPredicate predicateWithFormat: 
        @"%K == %@", NSMetadataItemFSNameKey, kFILENAME];
    [query setPredicate:pred];
    [[NSNotificationCenter defaultCenter] 
        addObserver:self 
        selector:@selector(queryDidFinishGathering:) 
        name:NSMetadataQueryDidFinishGatheringNotification 
        object:query];

    [query startQuery];

}

이제 쿼리가 완료되었을 때 호출될 메소드를 구현하자.

- (void)queryDidFinishGathering:(NSNotification *)notification {

    NSMetadataQuery *query = [notification object];
    [query disableUpdates];
    [query stopQuery];

    [[NSNotificationCenter defaultCenter] removeObserver:self 
        name:NSMetadataQueryDidFinishGatheringNotification
        object:query];

    _query = nil;

	[self loadData:query];

}

쿼리가 한 번 실행되면 이를 중지시키지 않는 한 앱이 종료될 때까지 계속해서 실행됨을 주의하자. 특히 클라우드 환경에서는 잦은 변경이 발생할 수 있다. 쿼리의 결과를 처리하는 동안 업데이트가 발생할 수 있고 그 쿼리결과가 변경될 수 있다. 따라서 disableUpdates와 stopQuery를 호출함으로서 프로세스를 정지시키는 것이 중요하다. 전자는 업데이트를 방지하는 메소드이고 후자는 이미 수집된 결과를 유지하면서 프로세스를 중지시키는 메서드이다.

그리고 더 이상의 통보를 받지 않기 위해 observer를 제거하고 마지막으로 문서를 불러오기 위한 메소드에 NSMetadataQuery 파라미터를 넘겨 호출하자.

queryDidFinishGathering 메소드 위에 아래의 코드를 추가하자.

- (void)loadData:(NSMetadataQuery *)query {

    if ([query resultCount] == 1) {
        NSMetadataItem *item = [query resultAtIndex:0];

	}
}

여기서 보듯이 NSMetadataQuery는 그 결과를 포함하는 NSMetadataItem의 배열로 감싸여 있다. 여기서 우리는 하나의 파일로만 작업하기 때문에 첫 번째 요소만 가지고 온다.

NSMetadataItem은 키-밸류가 저장된 딕셔너리와 유사하다. 이것은 파일의 정보를 이용할 수 있도록 아래와 같은 미리 정의된 키들을 가지고 있다.

  • NSMetadataItemURLKey
  • NSMetadataItemFSNameKey
  • NSMetadataItemDisplayNameKey
  • NSMetadataItemIsUbiquitousKey
  • NSMetadataUbiquitousItemHasUnresolvedConflictsKey
  • NSMetadataUbiquitousItemIsDownloadedKey
  • NSMetadataUbiquitousItemIsDownloadingKey
  • NSMetadataUbiquitousItemIsUploadedKey
  • NSMetadataUbiquitousItemIsUploadingKey
  • NSMetadataUbiquitousItemPercentDownloadedKey
  • NSMetadataUbiquitousItemPercentUploadedKey

우리는 Note 인스턴스를 구성하기 위해 필요한 URL인 NSMetadataItemURLKey를 사용할 것이다. 아래와 같이 loadData 메소드 구현을 계속하자.

- (void)loadData:(NSMetadataQuery *)query {

    if ([query resultCount] == 1) {

        NSMetadataItem *item = [query resultAtIndex:0];
        NSURL *url = [item valueForAttribute:NSMetadataItemURLKey];
        Note *doc = [[Note alloc] initWithFileURL:url];
        self.doc = doc;

	}
}

UIDocument 또는 Note 처럼 그의 서브클래스를 생성할 때, initWithFileURL를 이용해 초기화해야 하고 문서를 열기 위한 URL을 넘겨야 한다. 우리는 여기서 파일이 위치한 URL을 넘겨 호출하고, 이에 생성된 클래스를 인스턴스 변수로 저장하였다.

이제 노트를 열기 위한 준비가 되었다. 앞서 설명하였듯 openWithCompletionHandler 메소드를 이용해 문서를 열 수 있다. 아래의 코드대로 loadData 구현을 계속하자.

 
- (void)loadData:(NSMetadataQuery *)query {

    if ([query resultCount] == 1) {

        NSMetadataItem *item = [query resultAtIndex:0];
        NSURL *url = [item valueForAttribute:NSMetadataItemURLKey];
        Note *doc = [[Note alloc] initWithFileURL:url];
        self.doc = doc;
        [self.doc openWithCompletionHandler:^(BOOL success) {
            if (success) {                
                NSLog(@"iCloud document opened");                    
            } else {                
                NSLog(@"failed opening document from iCloud");                
            }
        }];

	}
}

이제 앱을 실행할 수 있다. 그리고 동작이 되는걸로 보인다. 위의 NSLog 메시지가 아무것도 콘솔에 나타나지 않는 것만 빼면 말이다. 현재 아이클라우드에 문서가 없기 때문에 아무것도 검색되지 않는다. 즉, resultCount가 0이다.

아이클라우드에 문서를 추가하는 방법은 앱을 통해서만 가능하기 때문에 문서를 생성하기 위한 코드를 추가할 필요가 있다. 우리는 잠시 후 loadData 메소드에 이 코드를 추가할 것이다. 쿼리가 resultCount로 0을 리턴할 경우 우리는 다음과 같이 해야 한다.

  • 로컬 아이클라우드 디렉토리 경로를 가져온다.
  • 위 경로의 문서 인스턴스를 초기화한다.
  • saveToURL 메소드를 호출한다.
  • 저장이 완료되면 openWithCompletionHandler 메소드를 호출한다.

따라서 아래의 코드를 loadData 메소드의 if ([query resultCount] == 1) 블럭 아래에 추가하자.

else {

    NSURL *ubiq = [[NSFileManager defaultManager] 
      URLForUbiquityContainerIdentifier:nil];
    NSURL *ubiquitousPackage = [[ubiq URLByAppendingPathComponent:
      @"Documents"] URLByAppendingPathComponent:kFILENAME];

    Note *doc = [[Note alloc] initWithFileURL:ubiquitousPackage];
    self.doc = doc;

    [doc saveToURL:[doc fileURL] 
      forSaveOperation:UIDocumentSaveForCreating 
      completionHandler:^(BOOL success) {            
        if (success) {
            [doc openWithCompletionHandler:^(BOOL success) {                
                NSLog(@"new document opened from iCloud");                
            }];                
        }
    }];
}

앱을 컴파일하고 실행해 보자. 처음으로 실행했다면 “new document” 메시지를, 두 번째 이후의 실행이었다면 “iCloud document opened” 메시지를 콘솔창에서 보게 될 것이다.

다른 기기에서도 테스트 해보자. 원래의 기기에서 한 번 이상 실행한 이후 다른 기기에서 처음으로 실행해 보면 아이클라우드에 문서가 이미 존재하기 때문에 “iCloud document opened” 메시지를 보게 될 것이다.

자, 우리의 앱은 거의 준비가 다 되어 간다. 아이클라우드 파트는 여기까지다. UI를 입히는 작업은 다음 파트에서 계속된다.

이제 어디로 갈 것인가?

지금까지 아이클라우드 작업의 기본적인 경험을 했을 것이다. 앱에 UI룰 추가하고 다중 문서 작업을 위해 앱이 어떻게 확장되는지 알아보기 위한 파트 2 를 진행할 준비가 된 것이다.

본 튜토리얼은 iOS 5 튜토리얼 책의 샘플 챕터이다. 본 튜터리얼이 도움이 되었다면 여기에 게시되지 않은 아이클라우드에 관한 모든 추가 챕터가 포함된 그 책을 원할지도 모르겠다.

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

 본 튜토리얼은 웹과 모바일 어플리케이션 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

  • Julian Meyer
  • Kyle Richter

... 50 total!

Update Team

... 15 total!

Editorial Team

... 23 total!

Code Team

  • Orta Therox

... 3 total!

번역 팀

  • David Xie
  • Vitaliy Zarubin

... 33 total!

Subject Matter Experts

  • Richard Casey

... 4 total!