UI컨트롤의 활성화여부를 동적으로 결정하는 법 – Cocoa, Swift

NSUserInterfaceValidations

특정한 메뉴 항목이나 버튼, 테이블뷰, 텍스트뷰 및 텍스트 필드등이 특정한 조건에 따라 활성화/비활성화되어야 하는 경우에 이를 처리하는 전략으로는 두 가지 방법이 있다.

  1. 특정 조건값이 변경될 때 (변경지점에서, 혹은 옵저빙을 통해서) 관련된 컨트롤의 활성화 여부를 변경해주는 방법
  2. 특정 조건값을 계산 프로퍼티로 만든 후, 이를 코코아 바인딩으로 컨트롤의 enabled 키와 연결하는 방법

코코아에서는 이 외에도 컨트롤에 대한 유효성 검사 매커니즘을 별도로 가지고 있다. 컨트롤이 화면에 표시될 때, 해당 컨트롤의 타깃NSUserInterfaceValidations 프로토콜을 따르고 있다면 해당 프로토콜의 메소드를 호출하여 자신의 유효성 여부를 판단하게하고, 그에 따라 자동으로 활성/비활성 여부를 결정해줄 수 있는 것이다.

이 매커니즘은 다음과 같이 구성된다.

  1. 컨트롤 자체는 NSValidatedUserInterfaceItem 프로토콜을 따른다. 이는 tagaction 프로퍼티로만 구성되는 매우 간단한 프로토콜이며, 대부분의 코코아 컨트롤이 이를 따른다고 보면 된다.
  2. 컨트롤의 타깃이 되는 객체는 NSUserInterfaceValidations 프로토콜을 따르게 한다. 여기에는 validateUserInterfaceItem(:)이라는 메소드가 하나 정의되는데, 이메소드를 구현하면 된다.
  3. UI 상에서 특정한 컨트롤의 활성/비활성화 여부를 체크하기 위해서 앱킷은 해당 컨트롤에 대해서 valdateUserInterfaceItem()을 호출한다. 타깃은 해당 컨트롤의 태그값이나 해당 컨트롤이 보내는 액션 메시지의 셀렉터를 이용해서 컨트롤을 구분하고, 컨트롤의 활성화 여부를 판단하여 보고한다.

타깃이 되는 객체는 다음 메소드를 구현하면서, NSUserInterfaceValidations를 따르도록한다.

func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool

예를 들어 어떤 이미지를 붙여넣을 수 있는 앱의 툴바를 추가한다고 가정하자. 이 때 문서객체 혹은 뷰 컨트롤러는 클립보드에서 이미지를 붙여넣을 수 있는 상황에서만 활성화되어야 한다. 그리고 이를 결정하는 상태값은 해당 객체 내부가 아닌 외부(클립보드)의 상태에 의해 결정되므로 옵저빙이나 바인딩을 쓰기가 어렵다. 따라서 NSUserInterfaceValidations를 이용한다. 여기서는 “붙여넣기”라는 액션이 가능한지 여부에 따라서 활성화 여부가 결정된다. 따라서 셀렉터를 기준으로만 판단하면 해당 셀렉터를 호출하려는 모든 컨트롤들이 같은 규칙을 따를 수 있다.

extension ViewController: NSUserInterfaceValidations {
  func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
    if item.action == #selector(self.paste(:)) {
      let pb = NSPasteboard.general
      return pb.canReadObject(forClasses: [NSImage.self])
    }
    return true
  }
}

만약 복사 버튼에 대해서도 이를 구현하고자 한다면, 셀렉터가 copy: 인 컨트롤인지를 추가로 판단하면 된다.

... 
   else if item.action == #selector(self.copy(:)) {
     return self.imageView.image != nil
   }
...

정리

  1. NSUserInterfaceValidations 프로토콜은 어떤 컨트롤러가 자신을 타깃으로 하는 뷰/컨트롤의 활성화 여부를 앱킷에 보고하는 메소드를 정의한다.
  2. 컨트롤러는 UI 컨트롤의 태그값이나, 컨트롤이 보내는 액션메시지의 셀렉터를 기반으로 컨트롤의 활성화여부를 리턴해주면 된다.