Foundation: UndoManager

Doldamul·2022년 9월 5일
0
post-thumbnail

UndoManager는 Apple에서 undo/redo 기능을 쉽게 적용할 수 있도록 사전 정의해놓은 클래스이다. TextField와 같은 경우 이미 UndoManager를 활용해 undo/redo 기능을 구현해놓았고, 개발자들은 원하는 부분에 undo/redo 기능을 쉽게 적용할 수 있다.

다만 구글링해도 관련 자료가 거의 없다시피 하다보니 나 같은 초보 프로그래머가 호기심에 undo/redo를 적용해보려다 피보는 일이 없도록 정리글을 작성해보았다. 한번 알고나면 사용이 어렵지는 않지만, 처음 공부할 때는 꽤나 복잡하게 느껴진다.

UndoManager 사용 방법

UndoManager를 사용하는 과정을 크게 3단계로 나눌 수 있다.
1. UndoManager 생성
2. Undo 동작 저장
3. Undo/Redo 함수 호출

각각에 대해 자세히 살펴보자.

1. UndoManager 생성

UndoManager를 알아보기 위해 다음과 같이 클래스를 정의해보았다. UndoManager 클래스는 Foundation 라이브러리에 정의되어 있다.

import Foundation

class Something {
    let undoManager = UndoManager()
    var interval = 0
}

위 코드에서 undoManager 프로퍼티가 싱글턴 인스턴스가 아님에 주목하자. UndoManager는 각 인스턴스마다 독립적으로 기능한다.

위 예제에 increase() 함수와 decrease() 함수를 구현하며 undo 및 redo 기능을 추가하는 방법을 알아보자.

2. Undo 동작 저장

목표 객체가 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번 방법이 더 적합할 것이다.

3. Undo/Redo 함수 호출

undo/redo 기능은 각각 undo() 함수와 redo() 함수를 호출하여 실행할 수 있다.

var thing = Something()

thing.increase()
thing.undoManager.undo() //decrease 함수 실행

지금쯤 사용법 다 읽었네, 생각보다 쉽네, 라며 좋아하고 있을 분들... 아직 끝나지 않았다. 저런

registerUndo 사용 패턴

한줄요약: 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가지로 나뉜다.

  • 2개의 함수가 서로의 역함수로 동작하도록 구현되어 있는 경우
extension Something {
    func increase() {
        interval += 1
        undoManager.registerUndo(withTarget: self) {
            $0.decrease() // 역함수를 호출
        }
    }
    
    func decrease() {
        interval -= 1
        undoManager.registerUndo(withTarget: self) {
            $0.increase() // 역함수를 호출
        }
    }
}
  • 1개의 함수가 인자를 받아 동작하도록 구현되어 있는 경우
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
    }
}

각자 상황에 맞는 것을 선택하면 된다.

Undo 그룹과 자동 그룹화

위 예제가 잘 작동하는지 확인하기 위해 간단히 테스트를 해보자.

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의 다음과 같은 특징 때문이다:

  • undo 그룹은 undo 가능한 최소단위다. 그룹으로 묶여있지 않으면 오류가 발생한다.
  • groupsByEvent를 비활성화할 경우 undo할 동작의 앞뒤에 beginUndoGrouping() 함수와 endUndoGrouping() 함수를 짝을 이뤄 명시적으로 작성해야 한다.
  • undo 그룹은 중첩하여 사용 가능하며, 이 경우 최상위 Undo 그룹을 undo/redo 최소단위로 간주한다. (괄호 중첩처럼 begin-begin-end-end 이런 식의 중첩이 가능하다)

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의 모든 프로퍼티 및 메소드들을 모아 정리한 부분도 있으니 참고하시면 되겠다.

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 스택이 비었는지 여부를 체크한다.

Undo/Redo 실행

func undo()
func undoNestedGroup()
func redo()

사실 undo 함수는 열려있는 undo 그룹의 중첩 단계가 1인 경우 endUndoGrouping 함수를 호출해서 그룹을 닫아주고, 그 다음 undoNestedGroup 함수를 호출한다.(중첩 단계가 2 이상인 경우 컴파일 에러 발생) 즉 순수하게 undo 기능만을 수행하는 함수는 undoNestedGroup 함수다. 엄격한 undo 그룹 관리를 통해 성능상의 이점이 필요한 경우가 아니면 undoNestedGroup 함수를 굳이 직접 쓸 필요는 없다고 할 수 있겠다.

Undo 스택의 크기 제한

var levelsOfUndo: Int { get set } 

기본값은 0으로, 크기 제한이 없음을 의미한다. 쌓인 스택이 제한을 넘는 경우 혹은 쌓인 스택보다 제한을 줄일 경우 가장 오래된 동작 그룹부터 즉시 제거된다.

Undo 그룹 생성

func beginUndoGrouping()
func endUndoGrouping()
var groupsByEvent: Bool { get set }
var groupingLevel: Int { get }

groupingLevel 프로퍼티는 endUndoGrouping 함수로 닫히지 않은 그룹이 몇 단계까지 열려 있는지를 반환한다. 즉 undo 기능을 사용하기 위해서는 groupingLevel 횟수만큼 endUndoGrouping 함수를 호출해야 한다. 닫히지 않은 그룹이 없다면 0을 반환한다.

Undo 등록 활성화 및 비활성화

func disableUndoRegistration()
func enableUndoRegistration()
var isUndoRegistrationEnabled: Bool { get }

휴지통 비우기 등 undo/redo 동작의 필요 리소스가 큰 경우 성능 향상을 위해 사용한다.

Undo/Redo 동작이 실행중인지 체크

var isUndoing: Bool { get }
var isRedoing: Bool { get }

설명 생략. (제곧내)

Undo/Redo 스택 비우기

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 모드 설정

runLoop는 쓰레드 프로그래밍을 할 때 사용되는 이벤트 처리 루프다.

var runLoopModes: [RunLoop.Mode]

현재 런 루프 모드를 지정하는 문자열 상수를 담은 배열이다.
이 배열은 기본값으로 `default` 모드만을 담고 있다. (연결 객체를 제외한 대부분의 입력 소스를 처리함.)
다른 용법으로서 몇 가지 예시를 들자면 eventTracking 모드로 설정해서 마우스 트래킹 세션 동안 수신받은 데이터로 입력을 제한하거나, modalPanel 모드로 설정해서 modal panel로부터 수신받은 데이터로 입력을 제한할 수 있다.

  • modalPanel은 다음을 지칭한다.

  • Foundation에 정의된 NSRunLoopMode 타입이 obj-c에서 swift로 넘어오면서 RunLoop.Mode 타입으로 바뀌었다. 매뉴얼의 설명이 swift에 맞게 최신화되지 않았으므로, 다음과 같이 바꿔서 읽어야 한다:

  • NSRunLoopMode(obj-c) => RunLoop.Mode(swift) :
    NSDefaultRunLoopMode => default
    NSEventTrackingRunLoopMode => eventTracking
    NSModalPanelRunLoopMode => modalPanel
    UITrackingRunLoopMode => tracking

Undo/Redo 동작의 제거 가능 여부

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) 값을 받아내는 것이었다.

Notification

매뉴얼의 설명이 부실하거나 안맞는 부분이 있어서 삽질을 좀 해봤다.

앞서 잠시 언급했듯이 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
  • undo/redo 동작이 실행되기 직전/직후
  • 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에 대해서도 이것저것 알게 되었다. 이렇게 매뉴얼을 끝까지 파헤치는 식으로 계속해보면 라이브러리의 전반적인 구조를 이해하는데도 좋을 것 같고 라이브러리를 이해하는 속도도 빨라질 것 같기도 하고...? 일단 기분이 좋다 앞으로도 자료가 부족한 라이브러리가 있다면 시간을 들여서 통째로 익혀보는 것을 고려해볼 듯하다.

참고자료

profile
덕질은 삶의 활력소다. 내가 애플을 좋아하는 이유. 재밌거덩

0개의 댓글