커스텀 뷰로 만든 UI 컴포넌트의 포커스링 그리기 - Cocoa

포커스 링

코코아 UI 요소에서 현재 포커스를 받은 UI 컴포넌트는 외부에 흐릿한 푸른 색 후광이 그려지며, 현재 포커스를 받고 있는 입력 디바이스라는 점을 시각적으로 피드백한다.  시중의 코코아 관련한 대부분의 책에서는 다음과 같이 draw: 메소드 내에서 포커스링을 그리는 것으로 포커스링을 흉내낼 수 있다고 한다. 1

override func draw(_ dirtyRect: NSRect) {
    // 배경을 칠하고,
    super.draw(dirtyRect)
    self.bgColor.set()
    NSBezierPath.fill(bounds)
    // 자신이 속한 윈도에서 자기가 제1응답자라면?
    if let fr = window?.firstResponder, fr === self {
        // 포커스 컬러로 세팅하여, 자신의 테두리 영역을 그린다.
        NSColor.keyboardFocusIndicatorColor.set()
        NSBezierPath.setDefaultLineWidth(4.0)
        NSBezierPath.strokeRect(bounds)
    }
}

그리고 제 1 응답자가 되었을 때 포커스링을 그리기 위해서는 뷰가 응답자 상태에 진입하거나 빠져나올 때마다 뷰를 새로 그리도록 갱신해야 한다.

override var acceptsFirstResponder: Bool { return true }
override func resignFirstResponder() -> Bool {
    NSLog("Resigning...")
    setNeedsDisplay(bounds)
    return true
}
override func becomeFirstResponder() -> Bool {
    NSLog("Becoming...")
    setNeedsDisplay(bounds)
    return true
}

참고: 10.7 라이언부터는 AppKit에서 포커스링을 그리는 API가 변경되었으므로, 이 글을 끝까지 읽어보세요.

하지만 이 방법은 텍스트 필드에서 볼 수 있는 예쁜(?) 포커스링을 그리는 것이 아니라 그냥 투박한 파란 사각형을 그릴 뿐이다.  그래서 포커스링을 그리는 방법은 이래야 한다고 설명하기도 하더라만….

if let fr = window?.firstResponder, fr === self,
NSGraphicsContext.currentContextDrawingToScreen {
    NSGraphicsContext.saveGraphicsState()
    NSSetFocusRingStyle(.only)
    NSBezierPath.fill(bounds)
    NSGraphicsContext.restoreGraphicsState()
}

하지만 이것도 기대한 것처럼 근사한 포커스링을 그리지 않는다.
뭐가 문제인가? 앞서 말했지만 이런 코코아 교재들이 너무 오래전에 작성된 것이 많아서 그런거다. OSX 10.7(라이언)이후부터는 컨트롤의 포커스링은 컨트롤의 외부에 그려지므로 뷰 내부에서 draw() 메소드로 그릴 수 없다. 이제부터는 AppKit이 자동으로 관리하게끔 API가 변경되었다. 그래서 어쩌라는 말이냐면… 모르겠다.  일단 NSView클래스 레퍼런스에서 포커스링을 그리는 부분을 한 번 찾아보자.

예전  API – 포커스링 타입 프로퍼티

NSViewfocusRingType 프로퍼티는 포커스링의 표시 타입을 결정한다. 이 값은 enum으로 구성된 다음의 값을 갖는다. 참고

  • default – NSView와 NSCell의 기본 포커스
  • none – 포커스 링 없음
  • exterior – 표준 아쿠아 포커스링

이 값을 .none으로 설정하면 시스템은 포커스링을 그리지 않는다. 이는 자신이 직접 포커스링을 그리려고 하는 경우에 설정하는데, 예를 들어 뷰의 백그라운드 색으로 포커스 상태를 표시하려하거나, 포커스링을 그리는데 충분한 공간이 없을 때 사용한다.
이 값을 바꾼다고해서 뷰가 실제로 포커스링을 그리지 않는다. 포커스링은 draw(:) 메소드 내에서 직접 그려야 한다. (해당 뷰가 제1응답자가 되었는지 확인해야 한다.)
정확한 포커스링의 리드로잉을 위해서 AppKit은 자동적으로 새로 그려야하는 뷰를 그리면서 해당 영역을 그린다. 예를 들어 focusRingType.none이 아닌 값을 설정된 뷰가 제 1 응답자가 된 경우에 자동으로 뷰를 새로 그리려고 한다. 만약 커스텀 뷰를 만들고 이 뷰가 제 1 응답자가 될 수 있지만, 별도의 포커스링을 그리지 않는다면 불필요한 리드로잉을 줄이기위해 해당 속성을 .none으로 설정한다.

OSX 10.7 에서의 변경

포스트의 서두에서 언급된 내용은 Snow Leopard 기준이며, 라이언 (10.7)이후부터는 새로운 방법으로 포커스링을 그리도록 API가 변경되었다. (참고: 릴리즈노트)

이전 방법

  1. draw(:) 내에서 현재 뷰가 제 1 응답자인지 확인한다.
  2. NSSetFocusRingStyle(:)함수를 호출하여 포커스링 스타일을 설정한다.
  3. NSBesizerPath.fill(self.bounds)를 호출하여 포커스링을 그린다.

변경된 방법

  1. 포커스링의 리드로잉은 AppKit이 알아서 결정한다.
  2. 뷰는 AppKit이 포커스링을 위한 자동 리드로잉을 할 것인지 여부를 알려주어야 한다. 이는 focusRingType 프로퍼티를 오버라이드 하여 결정한다.

결국 뷰는 앱킷에게 “어디에”, “어떻게” 포커스링을 그리면 되는 것인지만 말해주면 그 정보를 가지고 알아서 그리겠다는 것같다.  새롭게 추가된 세 가지 API는 다음과 같다.

  • func drawFocusRingMask()
  • var focusRingMaskBounds: NSRect { get }
  • func noteFocusRingMaskChanged()

AppKit은 자동으로 focusRingMaskBoundsdrawFocusRingMask를 호출한다. 따라서 우리가 수정해야 하는 부분도 이 두 곳이다.
drawFocusRingMask에 대한 NSView의 기본 구현은 아무일도 하지 않는 것이며, focusRingMaskBounds는 빈 사각형 (zero 사각형)을 리턴하는 것이다. 따라서 이 두 메소드를 오버라이드하여 포커스링의 템플릿을 만들어주도록 해야 한다. 방법은 간단하다. 포커스링의 경계는 결국 뷰 자체의 경계와 동일하고, 그 영역을 채우라는 명령으로 모든 준비가 끝난다.

override var focusRingMaskBounds: NSRect {
    return bounds
}
override func drawFocusRingMask() {
    NSBezierPath.fill(bounds)
}

앱킷은 자동으로 이들 메소드를 적절한 시점에 호출하고 포커스링을 그리게 된다. 따라서 *firstResponder() 메소드들에서 포커스 링을 그리기 위해 직접 setNeedsDisplay(:)를 호출해야 할 필요가 없다. 제1응답자가 바뀌는 것을 프레임워크가 추적해서 자동으로 포커스링을 그려주는 처리를 하게 된다.
포커스링을 직접 그려야하는 경우보다 훨씬 더 코드가 깔끔해진다.


  1. 물론 대부분의 책은 엄청 오래된 것일 대부분이라, 이렇게 Swift코드로 적혀있을리가 없다. 이 글의 모든 코드는 Swift3으로 코딩한 것이다. 

Read more

워드프레스에서 고스트로 이전

워드프레스에서 고스트로 이전

이 글을 쓰면서도 믿기 힘든 사실인데, 블로그라는 걸 처음 시작한지가 20년이 되었습니다. 이글루스에서 처음 시작했다가, SK컴즈가 인수한다고 발표함과 동시에 워드프레스로 플랫폼을 옮겼죠. 워드프레스오 옮긴 이후에는 호스팅 환경을 이리 저리 옮기긴 했지만 거의 18년 가까이 워드프레스를 사용해온 것 같습니다. 그 동안 워드프레스는 블로깅 툴에서 명실상부한 범용CMS로 발전했습니다. 사실 웬만한 홈페이지들은 이제

By sooop
띄어쓰기에 대한 생각

띄어쓰기에 대한 생각

업무 메일을 쓸 때 가장 많이 쓰는 말 중에 하나가 메일 말미에 ‘업무에 참고 부탁 드립니다.‘인데요, 어느 날부터 아웃룩에서 이 ‘부탁 드립니다’가 틀렸다고 맞춤법 지적을 하기 시작했습니다. 맞는 말은 ‘부탁드립니다’라고 붙여 쓰는 거라고. 사실 아래아한글 시절부터 이전의 MS워드까지, 워드프로세서들의 한국어 맞춤법 검사 실력은 거의 있으나 마나 한

By sooop

구글 포토에서 아이클라우드로 탈출한 후기

한 때 구글 포토가 백업 용량을 무제한으로 제공해 주겠다고해서, 구글 포토를 사용해서 사진을 백업해왔습니다. 물론 이 이야기의 결말은 저나 이 글을 읽고 있는 여러분이나 모두 알고 있습니다. 사실 AI에게 학습 시킬 이미지 데이터를 모으기 위한 것일 뿐이라거나 하는 이야기는 그 당시에도 있었습니다만, 에이 그래도 구글인데 용량은 넉넉하게 주겠지…하는 순진한

By sooop

Julia의 함수 사용팁

연산자의 함수적 표기 Julia의 연산자는 기본적으로 함수이며, 함수 호출 표기와 같은 방식으로 호출하는 것이 가능합니다. 또한 그 자체로 함수이기 때문에 filter(), map() 과 같이 함수를 인자로 받는 함수에도 연산자를 그대로 적용하는 것이 가능합니다. 특히 + 연산자는 sum() 함수와 같이 여러 인자를 받아 인자들의 합을 구할 수 있습니다. 2 + 3 # = 5 +(2,

By sooop