2016년 11월 29일 화요일

[macOS] 개발중인 앱에서 키보드를 눌렀을 때 에러음(Beep)이 들리는 경우

macOS 용 앱에서 '드앵~' 이라고 들리는 에러비프음(Beep, NSBeep)이 들리는 경우는 대체로 사용자가 잘못된 동작을 하는 경우이다. 물론 이 경우 이런 소리가 들리는 건 맞겠지만, 앱에서 의도한 기능의 단축키를 NSView 가 아닌 NSEvent 의 local monitor 형태로 구현했다거나 혹은 다른 사정으로 소리가 안울리길 원할 수도 있다. 의도한 기능의 키를 누르면 시도 때도 없이 울리는 에러음이 좋을 리는 없다.

그렇다면 어떻게 하면 이 에러음을 잡을 수 있을까? 개인적으론 세 가지 요소가 필요하다고 본다. 다른 더 좋은 방법이 있을지도 모르겠지만 관련 자료가 부실(?)해서 삽질로 알아낸 내용임을 감안하자.

지금부터 설명하는 것들은 NSView 하위클래스에서 구현해야 하는 것들이다. 원래 NSResponder 에 해당하는 것들인데 유독 NSView 기반의 클래스들이 아니면 의미가 없어서 좀 아쉽긴 하지만...

acceptsFirstResponder

기본적으로 acceptsFirstResponder 를 재정의해서 true를 리턴하게 만들어야 한다. 의미로 보면 첫 응답자(responder)가 된다 라는 의미인데, 그냥 축약해서 키보드 입력을 받을 수 있다 라는 의미로 해석하자. 이 프로퍼티는 아래 처럼 오버라이드 하면 된다.
override var acceptsFirstResponder: Bool { return true }

performKeyEquivalent

이 메소드의 이름은 직역하면 도데체 뭔 소리인지 참 어려운 녀석이다. 대충 '이 키는 쓸거다' 라는 이름으로 해석하자. 아래 처럼 오버라이드 하면 된다.
override func performKeyEquivalent(with event: NSEvent) -> Bool {
  return true
}
만약 event 로 넘어온 키코드를 이 앱(혹은 뷰)에서 쓰고자 한다면 true를 리턴하면 된다.

주의 사항으로, 위 코드의 경우 무조건 true 를 반환하는데 이렇게 되면 이 앱에 전달되는 모든 키 입력을 이 뷰에서 가로채게 된다. 즉, 기본적인 단축키 - 예를 들어 앱을 종료할 때 이용하는 Command-Q 라던가 등등 - 가 동작하지 않게 된다.

따라서 performKeyEquivalent 를 구현할 때는 필요한 키만 파악해서 true 를 리턴하게 하고 나머지는 false 를 리턴하도록 하는게 좋다.

keyDown

마지막으로 keyDown을 오버라이드 해야된다. 믈론 이 메소드는 NSView 의 키 입력을 핸들링하는 메소드이기 때문에 당연히 구현해야 된다고 생각할 수도 있다. 중요한 점은 에러음이 나지 않기 원하는 키가 입력되었다면 절대로 super.keyDown을 호출하면 안된다는 점이다.
override func keyDown(with event: NSEvent) {
  if weUseThisKey(event: event) {
    // 단축키에 맞는 작업을 한다 (혹은 그냥 비워두거나)
  } else {
    // 이 앱에선 이 키를 쓰지 않으니 알아서 해라
    super.keyDown(with: event)
  }
}
이렇게 하면 에러음이 나질 않는다.

혹시나 오해가 있을까봐 미리 언급: weUseThisKey() 라는건 event 의 키코드를 체크해서 사용하는 키라면 true를 리턴하는 메소드라는 의미로 적어둔 것일 뿐 실제로 존재하는 코드가 아니다. 필요하다면 직접 구현해야 한다.

마무리

사실 앞의 두 단계, 즉 acceptsFirstResponder 와 performKeyEquivalent 만 어떻게 처리하면 에러음이 들리지 않을 수도 있다. 심지어 구글링 한 결과로는 첫 번째 것만 해도 된다는 식으로 이야기 하는 경우도 봤는데 이건 앱의 구현 상태에 따라 다르다고 본다. 지금까지 정리한 내용은 귀찮지만 가장 확실한 방법이라고 생각한다.

NSEvent 에서 어떤 키가 눌렸는지 파악하는 방법은 애플이 공식적으로 제공하는지 여부를 모르고 있다. 아무리 찾아도 안보이더라. 그래서 개인적으로 키코드 맵을 정의해서 사용하고 있다. 혹시나 궁금하다면 SRKeyMap.swift 를 참고하자.

댓글 없음 :