객체(Object)가 가지고 있는 자료를 저장하거나 무언가로 전달하기 위해서 사용하는
세련된 방법이 아카이빙(Archiving)이다.
Xcode의 UI편집툴인 인터페이스빌더 (Interface Builder)를 xib로 저장하는것도 아카이빙이다.
복잡한 인터페이스빌더의 UI구조와 속성들을 xib로 일일히 저장한다면 끔찍한 일일것이다.
예를 들면 이런 복잡한 객체의 연결구조와 내장된 자료를 간편하게 직렬화(바이트처리)하는 것이 아카이빙이다.
일반적인 용어는 직렬화(Serialization, 시리얼라이제이션)이라고 하는데 자바(Java)에서도 이 표현을 사용하고 있다. ^^
Employee객체가 여러개 담겨 있는 배열을 아카이빙하면
그 안에 담겨있는 값들이 순차적으로 아카이빙이 된다.
* 프로토콜 (Protocol)
자바의 인터페이스 (Interface)와 비슷한것으로 Objective-C에서 쓰이는 것이 바로 프로토콜(Protocol) 이다.
프로토콜의 대표적인것은 델리게이트(delegate)가 있다.
클래스가 상속(Inheritance)을 받게되면 부모의 속성을 물려 받게 되는데
프로토콜을 사용하게 되면 상속과 비슷하게 추가 기능을 구현할수 있게 된다.
이것은 일종의 추상(abstract) 클래스이며 구조만 정해져 있을뿐 실제 기능은 개발자가 완성하여야 한다.
아카이빙도 추상클래스격인 프로토콜로 설계되어 있어 개발자가 그 규격에 맞게 추가해 주어야 한다.
* 아키이빙 대상
1) 아카이빙이 되는 것
- 인스턴스 변수
- 클래스명
2) 아키이빙 대상에서 제외되는것
- 메소드 ( 코드 )
* NSCoder와 NSCoding
아카이빙은 NSCoding 프로토콜에 의존합니다.
즉, 아카이빙 기능을 구현하려면 NSCoding 프로토콜을 준수하여야 하고 그렇기 위해서는 다음 메소드를 클래스에 구현하여야 한다.
init(coder aDecoder: NSCoder)
func encodeWithCoder(aCoder: NSCoder)
NSCoder는 추상 클래스와 같습니다. 절대 추상 클래스의 인스턴스를 만들지 않습니다. 대신, 추상 클래스는 서브 클래스에 의해 구현되도록 의도 된 일부 기능을 가지고 있다. 구체적인 하위 클래스의 인스턴스를 만듭니다. 즉, NSKeyedUnarchiver를 사용하여 데이터 스트림에서 객체를 읽고 NSKeyedArchiver를 사용하여 객체를 데이터 스트림에 쓴다.
책에는 이렇게 설명하고 있습니다. 조금 이상했다.
Cocoa Foundation에서 NSCoding은 찾아보니 NSObject내에 프로토콜로 구현되어 있다. 즉, 프로토콜이니 추상화 클래스라 봐야 한다.
그리고 NSCoder는 NSObject를 상속받아 구현되어 있었다.
이상하네 ㅜㅠ 그래서 찾아 봤다.
아래 클래스다이어그램을 보면 NSCoder는 분명 NSObject를 상속받았다.
그리고 NSCoder를 상속받아 NSKeyedArchiver, NSKeyedUnArchiver등이 있다.
http://prog3.com/sbdm/blog/likendsl/article/details/44085199
위 사이트에서 발췌함.
참고로 UI쪽은 아래와같다는걸 찾았으니 참고하시면 좋을듯 하다.
http://stpeterandpaul.ca/tiger/documentation/Cocoa/Conceptual/CocoaFundamentals/WhatIsCocoa/chapter_2_section_5.html
NSCoding 프로토콜 선언과 NSCoder 구현은
실제로는 다음과 같이 구현되었다.
class Employee: NSObject, NSCoding {
// MARK: - NSCoding
func encode(with aCoder: NSCoder) {
if let name = name {
aCoder.encode(name, forKey: "name")
}
aCoder.encode(raise, forKey: "raise")
}
required init?(coder aDecoder: NSCoder) {
name = aDecoder.decodeObject(forKey: "name") as! String?
raise = aDecoder.decodeFloat(forKey: "raise")
super.init()
}
required init?(coder aDecoder: NSCoder)
초기화 메소드는 위와 같이 구현되어 있는데 객체 생성 초기에
NSData형태의 자료를 Decode하기 위해서 구현한다고 이해하면 될것 같다.
여기서 주의할 점은 프로토콜의 초기화 함수는 required 를 반드시 붙여야 한다는 것이다.
다른방법으로는 클래스 앞에 final class 정의해도 된다.
만약 required를 빼먹으면 파이널 클래스가 아닌경우 requred를 붙여야 한다고 경고메시지가 출력되니 주의가 필요하다.
initalizer requirement 'init(coder:)' can only be satisfied by a requred initializer in non-final class 'Employee'
func encode(with aCoder: NSCoder)
이 메소드는 인코드가 필요한 시점에 불려질 것이다.
즉, 저장하기시에 호출될 것이므로 개발자가 필요에 맞게 구현되어야 한다.
* 의문점
All the commonly used AppKit and Foundation classes implement the NSCoding protocol, with the notable exception of NSObject. Because it inherits from NSObject, Employee does not call super.encodeWithCoder(coder). If Employee’s superclass had implemented the NSCoding protocol, the method would have looked like this:
일반적으로 사용되는 AppKit 및 Foundation 클래스는 모두 NSObject를 제외하고는 NSCoding 프로토콜을 구현합니다. NSObject에서 상속되므로 Employee는 super.encodeWithCoder (코더)를 호출하지 않습니다. Employee의 수퍼 클래스가 NSCoding 프로토콜을 구현했다면 메소드는 다음과 같이 보일 것입니다.
이게 말이야 방구야?
알듯 말듯 머리속이 혼란스러운 상태가 된다.
super.init(aDecoder, aDecoder: NSCoder)
이렇게 초기화 할것 같은데 그게 아니라
super.init()
이걸로 초기화 하고 있다. ㅜㅠ
Employee 클래스가 NSObject를 상속받고 있기 때문에
NSObject에는 super.init(aDecoder, aDecoder: NSCoder) 구현되어 있지 않다.
따라서 NSObject는 최상위 클래스 이므로 이경우는 예외적으로 super.init()로 초기화 해야 한다.
그렇게 때문에 만약 super.init(aDecoder, aDecoder: NSCoder)로 초기화 할려고 하면 다음과 같이 오류가 발생한다.
* 도큐멘트 아키텍쳐
여러 문서를 다루는 응용 프로그램은 공통점이 많습니다. 이러한 모든 응용 프로그램은 새 문서를 만들거나 기존 문서를 열거나 열려있는 문서를 저장하거나 인쇄 할 수 있으며 창을 닫거나 응용 프로그램을 종료 할 때 편집 한 문서를 저장하도록 사용자에게 상기시킨다.
Cocoa AppKit은 이렇한 공통점을 정리하여 아케텍쳐형태로 제공한다.
아키텍쳐라는것은 반제품 같은거라고 보면 된다.
즉, 집을 만들때 재료를 일일히 만드는게 아니라 창틀, 마루, 단열재등을 구매해서 조립하는것과 같다.
NSDocumentController, NSDocument 및 NSWindowController와 같은 세가지 클래스가 핵심이며 세부 정보 대부분을 처리하는 클래스가 제공된다.
아래 표 Cocoa Class 계층구조에서 NSDocument와 NSDocumentController에 대한 상속관계를 확인할수 있다.
NSWindowController는 앞전에 그림에서 확인이 가능하다.
NSDocument의 상속 클래스(sub class)는 WindowController로 작동합니다. 모델 객체에 대한 참조를 가지며 어레이 컨트롤러를 통해 직접 또는 다음 작업을 수행합니다. => sub class라는 표현은 상속 클래스 또는 구현클래스라고 하는게 맞다고 생각된다. 왜냐면 NSDocument를 개발자가 상속받아 구현하기 때문이다.
- saving the model data to a file ( 모델 데이터를 파일에 저장 )
- loading the model data from a file ( 파일로부터 모델 데이터로드하기 )
- giving the views the data to display ( 보기에 표시 할 데이터 제공 )
- taking user input from the views and updating the model ( 뷰에서 사용자 입력을 받아 모델을 업데이트한다. )
* Info.plist and NSDocumentController
응용 프로그램이 시작되면 Info.plist에서 주요정보를 참조한다.
이 Info.plist는 작업 할 파일 유형을 알려줍니다.
문서 기반 응용 프로그램 인 경우 NSDocumentController 인스턴스를 만듭니다
(그림 12.2) 드물게 문서 제어기를 다룰수도 있는데 백그라운드에서 숨어 있고 당신을 위해 세부 사항들을 다루고 있습니다.
예를 들어, 새로 작성 또는 모두 저장 메뉴 항목을 선택하면서 제어기가 요청을 처리합니다.
NSDocumentController는 개발자가 외형적으로 볼수는 없는데요.
프로젝트 생성시 Create Document-Based Application을 선택하면 document controller인스턴스를 생성하고 Info.plist에 저장되는듯 합니다.
위와 같이 프로젝트를 만들고 나면 도큐멘트 컨트롤러의 설정이나 여타 흔적은 개발자가 외형적으로 볼수는 없네요.
문서 컨트롤러에 메시지를 보내야하는 경우 다음과 같이 인스턴스를 획득한후에 메시지를 보낼 수 있다.
아래 코드는 Swift 2 기준인데 예제에서는 사용하는예는 없다.
let documentController = NSDocumentController.sharedDocumentController()
4판에서는 도표에 NSMutableArray라고 되어 있었는데 5판에서는 표에 AnyObject라고 되어 있는게 차이네요.
근데 설명에서는 Document Controller는 열려 있는 각 문서에 대해 NSDocument 상속클래스(sub class)의 배열을 관리한다라고 적혀 있네요.
파일 메뉴에서 "New" 새로만들기를 선택하면 도큐멘트 컨트롤러는 새 문서 인스턴스를 생성 처리합니다.
(그림 12.2)
* NSDocument
1) Saving documents
NSDcocument에서 저장 기능을 구현하기 위해서는 다음 func을 구현하여야 한다.
NSKeyedArchiver를 사용해 NSData type으로 변환하여야 한다.
override func data(ofType typeName: String) throws -> Data {
// End editting
tableView.window!.endEditing(for: nil)
// Create an NSData object from the Employees array
return NSKeyedArchiver.archivedData(withRootObject: employees)
}
다음 func는 아직 구현되지 않았는데 다음장에서 처리하는지 확인해 봐야 할듯 하다.
func fileWrapperOfType(typeName: String!,
error outError: NSErrorPointer) -> NSFileWrapper?
func writeToURL(url: NSURL,
ofType typeName: String,
error outError: NSErrorPointer) -> Bool
2) Loading documents
Open ..., Open Recent 및 Revert To Saved 메뉴 항목은 서로 다르지만 동일한 기본 문제를 다룬다.
파일 또는 파일 래퍼에서 모델 가져 오기하여 객체에 복원한다.
NSLog("About to read data of type \(typeName).");
employees = NSKeyedUnarchiver.unarchiveObject(with: data) as! [Employee]
}
다음 func도 필요에 따라 구현 해야할 수 있다.
func readFromFileWrapper(fileWrapper: NSFileWrapper,
ofType typeName: String,
error outError: NSErrorPointer) -> Bool
func readFromURL(url: NSURL,
ofType typeName: String,
error outError: NSErrorPointer) -> Bool
override func windowControllerDidLoadNib(_ aController: NSWindowController) {
super.windowControllerDidLoadNib(aController)
// Add any code here that needs to be executed once the windowController has loaded the document's window.
}
NSDocument 상속 클래스에서 이 메서드를 구현하여 사용자 인터페이스 개체를 업데이트합니다.
사용자가 메뉴에서 Revert To Saved를 선택하면 모델이 로드되지만 windowControllerDidLoadNib (_ :)가 호출되지 않습니다.
따라서 데이터를 로드하는 메소드의 사용자 인터페이스 객체를 업데이트 해야합니다 (복귀 작업 인 경우).
이 가능성을 다루는 한 가지 일반적인 방법은 NIB 파일에 설정된 콘센트 중 하나를 확인하는 것입니다.
nil이면 NIB 파일이로드되지 않았으므로 사용자 인터페이스를 업데이트 할 필요가 없습니다.
* NSWindowController
문서는 표시하고있는 각 창에 대해 NSWindowController 인스턴스를 갖습니다.
이러한 인스턴스는 NSDocument의 windowControllers 속성에 저장됩니다.
Xcode 템플릿의 기본 동작은 문서가 NSWindowController의 인스턴스를 만들어 문서의 XIB에서 로드 된 창을 부분적으로 제어하는 것입니다.
대부분의 응용 프로그램은 문서 당 하나의 창만 있기 때문에 일반적으로 창 제어기의 기본 동작에 문제없이 완벽합니다.
그림 12.3은 기본 설정에서 NSWindowController가 문서 아키텍처에 어떻게 적용되는지 보여줍니다.
[그림 12.3]
기본 설정은 정확히 원하는대로 이루어 지지만 NSWindowController의 사용자 정의 상속 클래스(sub class)를 만들려는 경우가 있습니다.
동일한 문서에 둘 이상의 창(window)이 있어야 하는 경우도 있는데,
예를 들어, CAD 프로그램에서 그려지는 객체를 설명하는 텍스트 윈도우와 그 객체를 렌더링하는 다른 윈도우를 가질 수 있습니다.
이러한 응용 프로그램의 문서 아키텍처는 [그림 12.4]와 같습니다.
[그림 12.4]
이경우 NSDocument의 window Controller는 두개 이상이 필요할수도 있습니다.
사용자 인터페이스 컨트롤러 로직과 모델 컨트롤러 로직을 별도의 클래스에 넣으려고합니다.
이렇게 할 때, 문서 아키텍처는 [그림 12.6]처럼 보일 것이다. ??? 잘이해가 않되네요ㅜㅠ
[그림 12.6]
XIB 파일이로드 된 후 개발자가 직접 사용자 인터페이스를 업데이트 할 수 있지만 NSArrayController가 알아서 처리한다.
따라서 현재로서는 windowControllerDidLoadNib (_ :) 메서드는 아무 것도 할 필요가 없다.
지금 당장은 이대로 두고 14 장에서 추가된다는데 그때 좀더 자세히 봐야 할듯 하다.
override func windowControllerDidLoadNib(_ aController: NSWindowController) {
super.windowControllerDidLoadNib(aController)
// Add any code here that needs to be executed once the windowController has loaded the document's window.
}
문서를 열거 나 만들 때로드 할 XIB 파일을 문서에 묻습니다.
override var windowNibName: String {
// Returns the nib file name of the document
// If you need to use a subclass of NSWindowController or if your document supports multiple NSWindowControllers, you should remove this property and override -makeWindowControllers instead.
return "Document"
}
참고로 Swift를 사용하더라도 파운데이션클래스는 Objective-C로 되어 있고 Core는 C로 되어 있다.
확인해 보니 상위 클래스 Document.h에는 아래와 같이 정의되어 있다.
알고보니 windowNibName을 Objective-C property 였네요. 읽기전용 속성에 매번 새로운 공간을 재할당하는 copy 속성을 가지고 있다.
/* Return the name of the nib to be used by -makeWindowControllers. The default implementation returns nil. You can override this method to return the name of a nib in your application's resources; the class of the file's owner in that nib must match the class of this object, and the window outlet of the file's owner should be connected to a window. Virtually every subclass of NSDocument has to override either -makeWindowControllers or -windowNibName.
*/
@property (nullable, readonly, copy) NSString *windowNibName;
참고로 Objective-C에서 property는 setter / getter를 의미하는 것으로
1) assign 은 그냥 값만 set / get 하는 것 ( int, float등에 적합 )
2) retain 은 set / get 할때 기존것을 release하고, retain count 를 올려준다.
3) copy 는 set / get 할때 기존것을 release하고 새로운 주소를 만들고 값을 할당하는 것.
* 좀더 알아보기 - 자동 문서 저장
Mac OS X Lion (10.7)에서 Apple은 Cocoa에 자동 문서 저장 지원을 도입했습니다.
자동 문서 저장 기능을 사용하면 더 이상 수동으로 문서를 저장하지 않아도됩니다.
변경 카운트 (후술)를 모니터함으로써, 코코아는 적절한 시간에 문서를 저장하도록 큐에 지시합니다.
사용자가 수동으로 문서를 저장하면 새 버전이 만들어집니다.
사용자는 Time Machine과 유사한 인터페이스를 사용하여 이전 버전을 탐색 할 수 있습니다.
제목없는 문서 (사용자가 명시 적으로 저장하지 않은 문서)는 응용 프로그램 실행간에 보존됩니다.
자동 문서 저장을 지원하려면 NSDocument 서브 클래스는 autosavesInPlace 클래스 메소드를 대체하여 true를 반환하도록 선택해야합니다.
return true
}
* 좀더 알아보기 - 실행 취소하지 않은 문서 기반 응용 프로그램
Document Based Application을 작성했지만 변경 사항을 UndoManager에 등록하지 않으면 어떻게 될까요?
NSDocument는 얼마나 많은 변경이 이루어 졌는지 추적합니다.
이렇한 목적으로 사용하는 메소드(function)이 있습니다.
override func updateChangeCount(_ change: NSDocumentChangeType) {
NSLog("NSDocumentChangeType = \(change)")
}
NSDocumentChangeType은 ChangeDone, ChangeUndone 또는 ChangeCleared 중 하나 일 수 있습니다.
ChangeDone은 변경 카운트를 증가시키고
ChangeUndone은 변경 카운트를 감소시키고
ChangeCleared는 변경 카운트를 0으로 설정합니다.
변경 카운트가 0이 아니면 창이 특별한 무늬(dirty?)로 표시됩니다.
* 좀더 알아보기 - 무한 루프 방지
* 좀더 알아보기 - 프로토콜 만들기
'TIP-맥OS' 카테고리의 다른 글
macOS용 XCode 스토리보드 프로젝트 XIB프로젝트로 변경하기 (0) | 2017.06.25 |
---|---|
macOS 파일 유형에 맞는 확장자와 아이콘 설정하기 (0) | 2017.05.14 |
macOS NSUndoManager 세련된 방법으로 실행취소(되돌리기) 구현 (0) | 2017.05.07 |
macOS TableView와 NSArrayController에서 Formatters and Validation (0) | 2017.04.22 |
macOS TableView와 NSArrayController사용하여 구현하기 - #3 (0) | 2017.04.19 |