UndoManager는 Apple에서 undo/redo 기능을 쉽게 적용할 수 있도록 사전 정의해놓은 클래스이다. TextField와 같은 경우 이미 UndoManager를 활용해 undo/redo 기능을 구현해놓았고, 개발자들은 원하는 부분에 undo/redo 기능을 쉽게 적용할 수 있다.
다만 구글링해도 관련 자료가 거의 없다시피 하다보니 나 같은 초보 프로그래머가 호기심에 undo/redo를 적용해보려다 피보는 일이 없도록 정리글을 작성해보았다. 한번 알고나면 사용이 어렵지는 않지만, 처음 공부할 때는 꽤나 복잡하게 느껴진다.
UndoManager를 사용하는 과정을 크게 3단계로 나눌 수 있다.
1. UndoManager 생성
2. Undo 동작 저장
3. Undo/Redo 함수 호출
각각에 대해 자세히 살펴보자.
UndoManager를 알아보기 위해 다음과 같이 클래스를 정의해보았다. UndoManager 클래스는 Foundation
라이브러리에 정의되어 있다.
import Foundation
class Something {
let undoManager = UndoManager()
var interval = 0
}
위 코드에서 undoManager
프로퍼티가 싱글턴 인스턴스가 아님에 주목하자. UndoManager는 각 인스턴스마다 독립적으로 기능한다.
위 예제에 increase()
함수와 decrease()
함수를 구현하며 undo 및 redo 기능을 추가하는 방법을 알아보자.
목표 객체가 A -> A’로 동작할 때, UndoManager는 undo 스택에 역동작 A’ -> A를 저장하는 방식으로 undo 기능을 구현한다.
역동작을 저장하는 함수는 registerUndo()
이다. 다음 두 가지 방식으로 호출이 가능하다.
registerUndo(withTarget:handler:)
withTarget
: 동작의 주체가 되는 객체를 받는다. 상황상 일반적으로 self
를 넣는 편이다.handler
: undo를 수행할 때의 역동작을 지정해주는 클로저를 받는다. registerUndo
함수는 클로저가 실행될 때 withTarget
인자를 전달한다.extension Something {
func increase() {
interval += 1
undoManager.registerUndo(withTarget: self) {
$0.decrease()
}
}
func decrease() { ... }
}
registerUndo(withTarget:selector:object:)
withTarget
: 동작의 주체가 되는 객체를 받는다. selector
: undo를 수행할 때의 역동작을 지정해주는 셀렉터를 받는다. object
: 셀렉터에서 요구하는 인자를 받는다. 셀렉터가 실행될 때 이 인자가 전달된다. extension Something {
@objc func increase() {
interval += 1
undoManager.registerUndo(withTarget: self, selector: #selector(decrease), object: nil)
}
@objc func decrease() { ... }
}
그 외에 prepare
함수에서 반환하는 proxy 객체를 활용하여 역동작을 저장하는 방법도 있다.
prepare(withInvocationTarget:) -> Any
withInvocationTarget
: 동작의 주체가 되는 객체를 받는다. -> Any
: withInvocationTarget
의 proxy(가짜) 객체를 반환한다. 이 proxy 객체에서 내부 함수에 접근하면 바로 실행되지 않고 UndoManager가 호출 행위를 메시지로서 저장해두었다가 undo가 호출될 때 메시지로부터 실행할 함수를 찾아 호출한다. Any
타입으로 반환되기 때문에 as
키워드를 사용해 형변환을 해주어야 내부 속성에 접근 가능하다.extension Something {
func increase() {
interval += 1
if let targetProxy = undoManager.prepare(withInvocationTarget: self) as? Something {
targetProxy.decrease()
}
}
func decrease() { ... }
}
(*) registerUndo(withTarget:selector:object:)
함수와 prepare(withInvocationTarget:)
함수는 옛 OS의 하위호환 및 코드 레거시를 지원하기 위한 구 호출 방식이다. 가급적이면 무려 iOS 9 때 새롭게 추가된 1번 방법을 사용하는 것을 권장한다. 물론 target/action 패턴에 기반하는 UIControl
등의 객체를 사용한다면 2번 방법이 더 적합할 것이다.
undo/redo 기능은 각각 undo()
함수와 redo()
함수를 호출하여 실행할 수 있다.
var thing = Something()
thing.increase()
thing.undoManager.undo() //decrease 함수 실행
지금쯤 사용법 다 읽었네, 생각보다 쉽네, 라며 좋아하고 있을 분들... 아직 끝나지 않았다. 저런
한줄요약:
undo()
함수와redo()
함수는 실행 도중registerUndo
함수가 호출될 것을 상정한다.
undo()
함수 실행 도중 registerUndo
함수가 호출될 경우 undo의 역동작을 redo 스택에 저장하게 된다. 즉 undo()
함수가 실행되어 A’ -> A로 역동작을 수행할 때 registerUndo
함수는 A -> A’ 동작을 redo 스택에 저장한다. 해당 스택은 redo()
함수가 실행될 때 순차적으로 사용된다.
따라서 UndoManager를 통해 undo/redo 기능을 구현할 때는 undo()
함수와 redo()
함수 모두 실행 도중 registerUndo 함수가 호출되도록 작성해야 한다. 즉 registerUndo에서 동작 로직을 직접 수행할 경우 undo 또는 redo 기능이 기대한대로 작동하지 않게 된다.
extension Something {
func increase() {
interval += 1
undoManager.registerUndo(withTarget: self) {
// (x) $0.interval -= 1
// (x) $0.decrease_withoutregisterUndo()
}
}
// (x) func decrease_withoutregisterUndo() { value -= 1 }
}
일반적으로 UndoManager를 사용하는 패턴은 크게 2가지로 나뉜다.
extension Something {
func increase() {
interval += 1
undoManager.registerUndo(withTarget: self) {
$0.decrease() // 역함수를 호출
}
}
func decrease() {
interval -= 1
undoManager.registerUndo(withTarget: self) {
$0.increase() // 역함수를 호출
}
}
}
extension Something {
// example 1
func add(_ n: Int) {
interval += n
undoManager.registerUndo(withTarget: self) {
$0.add(-n) // 인자로 부호가 반전된 값을 전달
}
}
// example 2
func changeInterval(newInterval: Int) {
let oldInterval = interval
undoManager.registerUndo(withTarget: self) {
$0.changeInterval(newInterval: oldInterval) // 이전에 저장되어 있던 값을 전달
}
interval = newInterval
}
}
각자 상황에 맞는 것을 선택하면 된다.
위 예제가 잘 작동하는지 확인하기 위해 간단히 테스트를 해보자.
var thing = Something() // 0
thing.increase() // 1
thing.increase() // 2
thing.increase() // 3
thing.decrease() // 2
thing.undoManager.undo() // 0
thing.undoManager.redo() // 2
//어라?
결과가 이상하다. 분명 registerUndo
는 4회 실행되었을 텐데, undo()
함수를 호출하니 interval
값이 첫번째 값으로 되돌아왔다. redo()
함수도 마찬가지다.
이 글 맨 밑에 작성된 SwiftUI 코드는 동일한 구현부를 가지고 있음에도 불구하고 정상적으로 실행되었다. 아래는 그 일부이다.
struct ContentView: View {
var body: some View {
...
let numButtonSet = [
("increase", thing.increase),
("decrease", thing.decrease),
...
]
ButtonSet(undoManager: thing.undoManager, buttonPairs: numButtonSet)
...
}
...
}
// increase 버튼을 3회, decrease 버튼을 1회, undo 버튼을 3회, redo 버튼을 3회 선택했을 때,
// valueObj.value 값 변화 : 0 -> 1 -> 2 -> 3 -> 2 -> 3 -> 2 -> 1 -> 2 -> 3 -> 2
원인이 무엇일까? UndoManager 클래스 내부에는 다음과 같은 프로퍼티가 명시되어 있다.
var groupsByEvent: Bool { get set }
수신자가 실행 루프의 각 패스마다 undo 그룹을 자동으로 만들지 여부를 나타내는 논리 값. 기본값은 true이다.
즉 이벤트가 1회 발생했을 때 실행되는 동작들을 하나의 undo 그룹으로 자동으로 묶어준다는 것이다. 앞서 실행했던 테스트에서는 4회의 함수 호출이 모두 Main 이벤트 내에서 일어난 것이므로 자동으로 1개의 Undo 그룹으로 묶여 처리되었다는 결론을 내릴 수 있다.
그럼 그 기능을 꺼버리고 모든 동작들을 개별적으로 undo 동작으로 쌓으면 되겠네?
그렇게 간단하지는 않다. UndoManager의 다음과 같은 특징 때문이다:
groupsByEvent
를 비활성화할 경우 undo할 동작의 앞뒤에 beginUndoGrouping()
함수와 endUndoGrouping()
함수를 짝을 이뤄 명시적으로 작성해야 한다. undo 그룹 중첩은 트랜잭션 중첩과 비슷한 기능성을 제공한다는데… 내가 트랜잭션을 공부하질 않아서 잘 모르겠다 :)
아무튼 앞서 작성했던 테스트 코드는 이런 식으로 작성해야만 우리가 의도한 결과를 출력할 수 있다.
var thing = Something() // 0
thing.undoManager.groupsByEvent = false
thing.undoManager.beginUndoGrouping()
thing.increase() // 1
thing.undoManager.endUndoGrouping()
thing.undoManager.beginUndoGrouping()
thing.increase() // 2
thing.undoManager.endUndoGrouping()
thing.undoManager.beginUndoGrouping()
thing.increase() // 3
thing.undoManager.endUndoGrouping()
thing.undoManager.beginUndoGrouping()
thing.decrease() // 2
thing.undoManager.endUndoGrouping()
thing.undoManager.groupsByEvent = true
thing.undoManager.undo() // 3
thing.undoManager.redo() // 2
이전에 비해 줄이 (매우) 길어지긴 했지만, 기능상의 문제는 없으니 됐다. 사실 위처럼 테스트할 때를 제외한다면 자동 그룹 기능은 상당히 편리한 기능이다. 세부 동작들을 포함하는 자동화 동작을 하나의 undo 그룹으로 관리하는 등 undo/redo 단계를 세세하게 관리할 필요성이 있는게 아닌이상 groupsByEvent
기능을 끄고 begin.../end...
함수를 직접 사용할 이유는 없다.
한편, 왜 Undo 그룹만 있고 Redo 그룹은 없는지 의아할 수도 있다. redo 스택은 undo 함수의 실행을 통해서만 추가 가능하므로, redo 스택에는 불완전한 그룹이 없음이 보장된다.
그외 전반적으로 어떤 기능이 있는지 매뉴얼로는 한눈에 잘 들어오질 않아서 그냥 UndoManager의 모든 프로퍼티랑 메소드들을 간략하게 정리해봤다. 글 맨 밑에 UndoManager의 모든 프로퍼티 및 메소드들을 모아 정리한 부분도 있으니 참고하시면 되겠다.
func registerUndo<TargetType>(withTarget target: TargetType, handler: @escaping (TargetType) -> Void) where TargetType : AnyObject
func registerUndo(withTarget target: Any, selector: Selector, object anObject: Any?)
func prepare(withInvocationTarget target: Any) -> Any
설명 생략. (앞에서 설명했다)
var canUndo: Bool { get }
var canRedo: Bool { get }
undo/redo 스택이 비었는지 여부를 체크한다.
func undo()
func undoNestedGroup()
func redo()
사실 undo
함수는 열려있는 undo 그룹의 중첩 단계가 1인 경우 endUndoGrouping
함수를 호출해서 그룹을 닫아주고, 그 다음 undoNestedGroup
함수를 호출한다.(중첩 단계가 2 이상인 경우 컴파일 에러 발생) 즉 순수하게 undo 기능만을 수행하는 함수는 undoNestedGroup
함수다. 엄격한 undo 그룹 관리를 통해 성능상의 이점이 필요한 경우가 아니면 undoNestedGroup
함수를 굳이 직접 쓸 필요는 없다고 할 수 있겠다.
var levelsOfUndo: Int { get set }
기본값은 0
으로, 크기 제한이 없음을 의미한다. 쌓인 스택이 제한을 넘는 경우 혹은 쌓인 스택보다 제한을 줄일 경우 가장 오래된 동작 그룹부터 즉시 제거된다.
func beginUndoGrouping()
func endUndoGrouping()
var groupsByEvent: Bool { get set }
var groupingLevel: Int { get }
groupingLevel
프로퍼티는 endUndoGrouping
함수로 닫히지 않은 그룹이 몇 단계까지 열려 있는지를 반환한다. 즉 undo 기능을 사용하기 위해서는 groupingLevel
횟수만큼 endUndoGrouping
함수를 호출해야 한다. 닫히지 않은 그룹이 없다면 0
을 반환한다.
func disableUndoRegistration()
func enableUndoRegistration()
var isUndoRegistrationEnabled: Bool { get }
휴지통 비우기 등 undo/redo 동작의 필요 리소스가 큰 경우 성능 향상을 위해 사용한다.
var isUndoing: Bool { get }
var isRedoing: Bool { get }
설명 생략. (제곧내)
func removeAllActions()
func removeAllActions(withTarget target: Any)
removeAllActions()
함수는 모든 스택 내부의 모든 동작을 제거하며, removeAllActions(withTarget:)
함수는 인자로 받은 해당 대상에서 수행하는 동작만을 스택에서 찾아 제거해준다.
var undoActionName: String { get }
var redoActionName: String { get }
func setActionName(_ actionName: String)
undo/redo 스택의 최상단에 있는 동작의 이름을 지정하거나 불러올 수 있다. setActionName
함수는 일반적으로 registerUndo
함수 실행 직후에 호출된다.
var undoMenuItemTitle: String { get }
var redoMenuItemTitle: String { get }
func undoMenuTitle(forUndoActionName actionName: String) -> String
func redoMenuTitle(forUndoActionName actionName: String) -> String
위 두 프로퍼티는 undo/redo 스택의 최상단에 있는 동작의 이름을 로컬라이징된 "undo"
/"redo"
문자열과 붙여 반환해준다. 아래 두 함수는 actionName
인자에 대응되는 로컬라이징된 문자열을 반환한다. 두 함수는 원할 경우 Override할 수도 있다. macOS의 메뉴바에 로컬라이징된 undo/redo 제목을 표시하려고 할때 유용하다.
즉 위 이미지에서의 두 항목은 각각 undoMenuItemTitle
/redoMenuItemTitle
이 반환하는 String과 동일하다고 볼 수 있다.
runLoop는 쓰레드 프로그래밍을 할 때 사용되는 이벤트 처리 루프다.
var runLoopModes: [RunLoop.Mode]
현재 런 루프 모드를 지정하는 문자열 상수를 담은 배열이다.
이 배열은 기본값으로 `default` 모드만을 담고 있다. (연결 객체를 제외한 대부분의 입력 소스를 처리함.)
다른 용법으로서 몇 가지 예시를 들자면 eventTracking 모드로 설정해서 마우스 트래킹 세션 동안 수신받은 데이터로 입력을 제한하거나, modalPanel 모드로 설정해서 modal panel로부터 수신받은 데이터로 입력을 제한할 수 있다.
Foundation
에 정의된 NSRunLoopMode
타입이 obj-c에서 swift로 넘어오면서 RunLoop.Mode
타입으로 바뀌었다. 매뉴얼의 설명이 swift에 맞게 최신화되지 않았으므로, 다음과 같이 바꿔서 읽어야 한다:
NSRunLoopMode
(obj-c) => RunLoop.Mode
(swift) :
NSDefaultRunLoopMode
=> default
NSEventTrackingRunLoopMode
=> eventTracking
NSModalPanelRunLoopMode
=> modalPanel
UITrackingRunLoopMode
=> tracking
func setActionIsDiscardable(_ discardable: Bool)
var undoActionIsDiscardable: Bool { get }
var redoActionIsDiscardable: Bool { get }
setActionIsDiscardable
함수는 스택 최상단의 undo 동작이 안전하게 제거가 가능한지를 지정한다. 문서가 예기치 못한 이유로 저장이 불가능한 경우가 있는데, 영구 상태에 영향을 주지 않는 동작은 true
로 설정하는 것이 일반적이다.
deprecated 시키려는 건지 아니면 아무도 쓰는 사람이 없어서 그런 건지는 모르겠지만… 매뉴얼에서 UndoManager 클래스가 제공하는 상수에 대한 설명이 매우 부실했다. 아무튼 내가 찾은 바에 의하면,
let NSUndoCloseGroupingRunLoopOrdering: Int
이 상수는, RunLoop 클래스에 정의된 다음 함수에서 order
인자를 채울 때 사용한다.
func perform(_ aSelector: Selector,
target: Any,
argument arg: Any?,
order: Int,
modes: [RunLoop.Mode])
이 함수는 매뉴얼의 runLoopModes 페이지에 Related Documentation이란 항목으로 링크되어 있다.
더 자세한 설명이나 그 외의 기능은… 현재 내 지식으로는 더 작성할 수가 없다. 비동기 작업을 좀 파본 다음에야 이 부분 설명을 보충할 수 있을 듯하다. 어차피 이 정도 설명이면 전문가분들은 알아서 쓸거다
let NSUndoManagerGroupIsDiscardableKey: String // 사실상 Deprecated
이 상수는 사실상 deprecated되었다. UndoManager 클래스에서 발송(post)하는 NSNotification
객체는 언젠가부터 userInfo
딕셔너리 프로퍼티를 더 이상 갖지 않게 되었다. 본래 NSUndoManagerGroupIsDiscardableKey
상수의 역할은 notification이 발송되었을 때 userInfo
딕셔너리에 해당 키를 인자로 넣어서 undo 스택 전체가 안전하게 제거가 가능한지(discardable) 논리(bool) 값을 받아내는 것이었다.
매뉴얼의 설명이 부실하거나 안맞는 부분이 있어서 삽질을 좀 해봤다.
앞서 잠시 언급했듯이 UndoManager에서 발송(post)하는 모든 NSNotification
객체는 NSUndoManager
를 가지고 있으며, userInfo
프로퍼티를 가지지 않는다. 매뉴얼 믿지말자
UndoManager에서 발송하는 Notification은 8가지다. (스포: 하나는 안쓴다)
static let NSUndoManagerWillUndoChange: NSNotification.Name
static let NSUndoManagerWillRedoChange: NSNotification.Name
static let NSUndoManagerDidUndoChange: NSNotification.Name
static let NSUndoManagerDidRedoChange: NSNotification.Name
static let NSUndoManagerDidOpenUndoGroup: NSNotification.Name
static let NSUndoManagerDidCloseUndoGroup: NSNotification.Name
static let NSUndoManagerWillCloseUndoGroup: NSNotification.Name
beginUndoGrouping()
함수가 실행된 직후endUndoGrouping()
함수가 실행된 직후endUndoGruoping()
함수가 실행되기 직전 (매뉴얼이 최신화되지 않음)나머지 하나는 사용하지 않는 것을 권장한다. 나름 연구해봤지만 post하는 기준을 잘 모르겠다. 설명이랑도 다르고.
static let NSUndoManagerCheckpoint: NSNotification.Name
beginUndoGrouping()
함수 및 endUndoGruoping()
함수가 호출된 직후(beginUndoGrouping()
함수가 호출된 직후인데 groupingLevel이 1인 경우는 제외) 그리고 canRedo
가 호출되어 canRedo
내에서 redo 스택을 체크한 직후시니어 분들만 쓰는 걸로 하자
아래는 Swift Playground에서 실행되는 SwiftUI 예제 코드다. 필요하다면 쓰시면 되겠다.
import Foundation
import SwiftUI
import PlaygroundSupport
class Something: ObservableObject {
let undoManager = UndoManager()
@Published var interval = 0
func increase() {
interval += 1
undoManager.registerUndo(withTarget: self) {
$0.decrease() // 역함수를 호출
}
}
func decrease() {
interval -= 1
undoManager.registerUndo(withTarget: self) {
$0.increase() // 역함수를 호출
}
}
func add(_ n: Int) {
interval += n
undoManager.registerUndo(withTarget: self) {
$0.add(-n) // 인자로 부호가 반전된 값을 전달
}
}
func changeInterval(_ newInterval: Int) {
let oldInterval = interval
undoManager.registerUndo(withTarget: self) {
$0.changeInterval(oldInterval) // 이전에 저장되어 있던 값을 전달
}
interval = newInterval
}
}
class Something_Color: Something {
var color: Color {
switch interval {
case 0:
return Color.clear
case 1:
return Color.yellow
case 2:
return Color.green
case 3:
return Color.blue
default:
return Color.mint
}
}
}
struct ContentView: View {
@StateObject var thing = Something()
@StateObject var colorThing = Something_Color()
var body: some View {
VStack {
Spacer(minLength: 50)
RoundedRectangle(cornerRadius: 10.0)
.stroke(Color.black, lineWidth: 1.0)
.frame(width: 200.0, height: 150.0)
.overlay {
RoundedRectangle(cornerRadius: 10.0)
.foregroundColor(colorThing.color)
}
.animation(.easeOut(duration: 0.15))
let colorButtonSet = [
("Yellow", { colorThing.changeInterval(1) }),
("Green", { colorThing.changeInterval(2) }),
("Blue", { colorThing.changeInterval(3) })
]
ButtonSet(undoManager: colorThing.undoManager, buttonPairs: colorButtonSet)
Spacer()
RoundedRectangle(cornerRadius: 10.0)
.stroke(Color.black, lineWidth: 1.0)
.frame(width: 200.0, height: 150.0, alignment: .center)
.overlay {
Text(String(thing.interval))
.font(.largeTitle)
}
let numButtonSet = [
("increase", thing.increase),
("decrease", thing.decrease),
("init", { thing.changeInterval(0) }),
("add 10", { thing.add(10) }),
("subtract 10", { thing.add(-10) })
]
ButtonSet(undoManager: thing.undoManager, buttonPairs: numButtonSet)
Spacer(minLength: 50)
}
}
}
struct ButtonSet: View {
var undoManager: UndoManager
var buttonPairs: [( String, () -> () )]
var body: some View {
HStack {
ForEach(buttonPairs, id: \.0) { (title, action) in
Button(title, action: action)
}
}.buttonStyle(.bordered)
HStack {
Button("undo", action: undoManager.undo)
Button("redo", action: undoManager.redo)
}.buttonStyle(.borderedProminent)
}
}
PlaygroundPage.current.setLiveView(ContentView())
한눈에 보기 좋게 선언부만 모아 정리해봤다.
// Undo 동작 등록
func registerUndo<TargetType>(withTarget target: TargetType, handler: @escaping (TargetType) -> Void) where TargetType : AnyObject
func registerUndo(withTarget target: Any, selector: Selector, object anObject: Any?)
func prepare(withInvocationTarget target: Any) -> Any
// Undo 가능 여부
var canUndo: Bool { get }
var canRedo: Bool { get }
// Undo/Redo 실행
func undo()
func undoNestedGroup()
func redo()
// Undo 스택의 크기 제한
var levelsOfUndo: Int { get set }
// Undo 그룹 생성
func beginUndoGrouping()
func endUndoGrouping()
var groupsByEvent: Bool { get set }
var groupingLevel: Int { get }
// Undo 등록 활성화 및 비활성화
func disableUndoRegistration()
func enableUndoRegistration()
var isUndoRegistrationEnabled: Bool { get }
// Undo/Redo 동작이 실행중인지 체크
var isUndoing: Bool { get }
var isRedoing: Bool { get }
// Undo/Redo 스택 비우기
func removeAllActions()
func removeAllActions(withTarget target: Any)
// 동작 이름 관리
var undoActionName: String { get }
var redoActionName: String { get }
func setActionName(_ actionName: String)
// 현지화된 메뉴바 제목 가져오기
var undoMenuItemTitle: String { get }
var redoMenuItemTitle: String { get }
func undoMenuTitle(forUndoActionName actionName: String) -> String
func redoMenuTitle(forUndoActionName actionName: String) -> String
// runLoop 모드 설정(쓰레드 관련)
var runLoopModes: [RunLoop.Mode]
// Undo/Redo 동작의 제거 가능 여부
func setActionIsDiscardable(_ discardable: Bool)
var undoActionIsDiscardable: Bool { get }
var redoActionIsDiscardable: Bool { get }
// 상수
let NSUndoCloseGroupingRunLoopOrdering: Int
let NSUndoManagerGroupIsDiscardableKey: String // 사실상 Deprecated
// Notification
static let NSUndoManagerWillUndoChange: NSNotification.Name
static let NSUndoManagerWillRedoChange: NSNotification.Name
static let NSUndoManagerDidUndoChange: NSNotification.Name
static let NSUndoManagerDidRedoChange: NSNotification.Name
static let NSUndoManagerDidOpenUndoGroup: NSNotification.Name
static let NSUndoManagerDidCloseUndoGroup: NSNotification.Name
static let NSUndoManagerWillCloseUndoGroup: NSNotification.Name
static let NSUndoManagerCheckpoint: NSNotification.Name // 사용 비추천
매뉴얼에 나와있는 메소드랑 프로퍼티를 하나하나 다 따져본다는게 어떻게 보면 좀 미련해보일지도 모르겠지만… 이런 것도 나름 공부가 되는 것 같다. 의외로 유용할 것 같은 메소드도 많았고, runLoop나 notification에 대해서도 이것저것 알게 되었다. 이렇게 매뉴얼을 끝까지 파헤치는 식으로 계속해보면 라이브러리의 전반적인 구조를 이해하는데도 좋을 것 같고 라이브러리를 이해하는 속도도 빨라질 것 같기도 하고...? 일단 기분이 좋다 앞으로도 자료가 부족한 라이브러리가 있다면 시간을 들여서 통째로 익혀보는 것을 고려해볼 듯하다.