콘텐츠로 건너뛰기
Home » 드래그 앤 드롭으로 콘텐츠 복사를 구현하기 – Cocoa, Swift

드래그 앤 드롭으로 콘텐츠 복사를 구현하기 – Cocoa, Swift

많은 코코아 관련 서적에서 복사/붙여넣기를 구현하는 다음 과정으로 드래그 앤 드롭을 소개하는데, 대부분이 어떤 이미지 뷰에 이미지를 끌어다 놓는 부분까지만 소개하고 있다. 아마도 드래그 앤 드롭 자체가 실제로는 상당히 복잡한 매커니즘이라 그런 듯 한데, 이번 글에서는 드래그 앤 드롭을 구현하기 위해서 소스 뷰와 타깃 뷰 그리고 전달되는 데이터 모델에 대해 어떤 클래스와 구현이 필요한지를 살펴보도록 하겠다.

드래그 앤 드롭이 일어나는 과정

드래그 앤 드롭이 일어나는 과정은 사실 상당히 복잡하다. 드래그가 시작되면서 끝나는 과정을 세션이라 하는데, 이 세션은 계속해서 상태가 바뀌게 된다. 드래그를 시작하면서 세션이 시작된다. 세션이 시작되면 드래그되는 콘텐츠를 표현하는 이미지가 마우스 포인터 부근에 생기면서 이를 “끌고다니는” 것 처럼 표현된다. 우리가 이 이미지를 끌고다니면서 같은 앱의 다른 뷰, 혹은 다른 앱의 어떤 뷰에 올라가게 되면 그 때마다 세션은 “목적지 후보”를 해당 뷰로 바꿔가면서 붙여넣을 수는 있는지, 있다면 목적지에 대해서 UI 업데이트를 요청하게 하고 하는 등의 작업을 계속 수행한다. (그리고 드래그 세션이 있는 동안에 특정한 뷰에 올라가게 되면 해당 앱의 윈도를 자동으로 맨 위쪽으로 끌어올려주고 하는 등의 작업도 처리해야한다.)
사용자가 드롭을 결정하게 되면 세션은 드롭 과정을 준비한다. 실제 드롭은 일종의 복사/붙여넣기 과정으로 드롭이 시행되면 세션은 소스뷰에게 복사할 데이터를 클립보드로 복사하게 하고, 다시 타깃뷰에게는 클립보드로부터 데이터를 읽어내도록 한다. (이 때 사용되는 클립보드는 우리가 복사/붙여넣기를 할 때 사용하는 것과는 다른 별개의 것이다.) 드롭이 완료되는 시점에 최종 정리작업이 수행되고나면 세션이 종료된다. 만약 드롭할 수 없는 곳에 떨어뜨린 경우에는 세션은 끌고다디니던 이미지를 원래 위치로 돌려놓는 작업을 처리하고 뒷정리가 수행되는 식이다.

  1. 드래그를 시작할 수 있는 뷰는 mouseDragged(:) 메소드를 오버라이딩하면서 그 안에서 beginDraggingSession(with:event:source:)를 호출하게끔 구현한다.
  2. 드래깅 세션의 시작되면 드래그 이미지가 마우스 포인터 근처에 나타나면서 드래그 작업이 시작된다.
  3. 시스템은 드래깅 세션이 변경되거나, 다른 앱으로 넘어가는 등의 상황이 발생하면 소스 객체에게 draggingSession(:sourceOperationMaskFor:)를 호출하여 어떤 작업을 수행할것인지를 한 번 이상 묻게 된다.
  4. 드래그를 받으려는 앱은 register(forDraggedTypes:)를 오버라이딩하여 특정한 타입의 드래그를 받을 수 있음을 앱킷이 보고한다.
  5. 드래그를 이리저리 움직이다가 어떤 뷰에 들어가게 되었을 때, 해당 뷰가 드래그 아이템 데이터타입에 대해 등록된 클래스라면, (즉 NSDraggingDestination이 될 수 있다면) 해당 뷰가 타깃 후보가 되면서 이 뷰의  draggingEntered(:)가 호출된다. 실질적으로 이 과정 이후부터는 드래그 & 드롭 프로세스는 destination 혹은 destination 후보가 본격적으로 주도한다.
  6. 마우스가 움직이는 사이에 draggingEntered(:), draggingUpdated(:)를 호출받는 destination 후보는 데이터 사용 여부나 오퍼레이션 마스크 등을 이용해서 어떤 동작을 할 것인지를 시스템에게 알려준다. 이에 따라서 마우스 포인터 근처에 + 표시가 나타나나는 등의 세션 UI가 변경될 수 있다.
  7. 사용자가 마우스버튼을 놓으면, (드롭이 일어나면) 목적지 객체는 “목적지 후보”에서 “목적지”로 바뀐다. 이 때 prepareForDragOperation(:)이 호출되고, 여기서 실제 처리 여부를 다시 한 번 확인할 수 있다.
  8. 곧이어 performDragOperation(:)이 일어난다.
  9. 최종적으로 concludeDragOperation(:)이 호출된다. 여기서는 목적지가 정리 작업을 할 수 있는 기회가 된다.
  10. 드래그 세션이 종료되면 드래그 소스는 draggingSession(:endedAt:operation:)이 호출된다. 여기서의 operation 마스크 값에 따라서 적절한 후조치를 취한다. 이동이나 삭제의 경우라면 원본 위치의 데이터를 제거하고 초기화하는 과정이 필요할 것이다.

주요 객체

드래그 앤 드롭은 실질적으로 매우 복잡한 프로세스이며, 마우스로 클릭하고 끌어다 놓는 과정은 다시 여러 단계로 세분화된다. 그리고 이 단계들 사이에서 주도적으로 움직이는 객체가 서로 다르다. 드래그 작업에 관여하는 객체들은 다음과 같다.

  • NSView : 드래그받을 수 있는 타입을 정의해주고, 실제 드래그 세션을 시작한다.
  • NSPasteboard : 드래그하여 이동/복사하는 데이터는 클립보드를 통해서 전송된다. 따라서 NSPasteboardWriting, NSPasteboardReading을 지원하는 타입이거나, NSPasteboardItem을 통해서 복사/붙여넣기를 지원하는 타입의 데이터만 전송이 가능하다.
  • NSDraggingSource : 드래그 작업으로 전송할 데이터를 제공하는 객체. 드래그가 시작되는 뷰나 그 뷰의 컨트롤러가 이 역할을 담당한다.
  • NSDraggingDestination : 드래그 작업으로 전송할 데이터를 받는 객체. 실제로 받는 부분은 NSView가 보통 많이 선택된다.
  • NSDraggingSession : 드래그가 시작되고, 이리저리 움직이는 동안 소스 객체에게 정보를 제공해주기 위해 시스템이 생성하는 객체이다.
  • NSDraggingItem : 드래그하는 데이터를 감싸는 객체이며, 드래그 시에 표시될 아이콘 이미지를 제공하는 역할도 담당한다.
  • NSDraggingInfo : 목적지의 후보가 될 객체로 진입하거나, 진입한 이후의 과정에서 목적지 객체가 필요로 할만한 정보를 담은 객체로, 시스템이 만들어서 프로포콜 메소드 호출 시 넘겨주게 된다.

어떤 뷰가 드래그 앤 드롭을 지원하게끔 하려면 다음과 같은 부분들을 직접 작성해야 한다.

  1. NSView :
    1. 적절한 위치에서 register(forDraggedTypes:)를 호출하여 뷰가 드래그를 받을 수 있는 데이터 타입을 등록해준다. 이 위치는 viewDidMoveToWindow()가 적절할 것이다.
    2. mouseDragged(:)를 오버라이딩하여 이 곳에서 드래그가 시작될 때, 드래깅 세션을 개시한다. 이는 NSViewbeginDraggingSession(with:event:source:)를 호출하는 것으로 이루어지며, 이를 위해 NSDraggingItem을 생성해야 한다.
  2. NSDraggingItem :
    1. 드래그 작업으로 전송될 정보와 아이콘 이미지 및 이미지를 표시할 영역을 정의한다. 따라서 이런 데이터들을 준비해야 한다.
    2. 전송될 정보를 준비하여 init(pasteboardWriter:)로 생성하고
    3. setDraggingFrame(_:contents:)를 호출해서 끌고다닐 이미지 정보를 셋팅한다.
  3. NSDraggingSource : 주로 드래그가 시작되는 뷰가 이 역할도 수행한다.
    1. draggingSession(_:sourceOperationMaskFor:)를 오버라이딩하여 드래그 작업시 제공하려는 작업 종류를 정의해준다. 이 메소드는 필수로 구현해야 한다.
    2. draggingSession(_:endedAt:operation:) 메소드를 필요 시 구현한다. .copy 작업일 때는 상관없는데, .move, .delete 타입 작업인 경우에는 원본을 지워야할텐데, 여기서 처리해주면 된다.
  4. NSDraggingDestination : 목적지 역할을 담당할 때 수행해야 하는 것들이다.
    1. draggingEntered(:)를 작성하여 드래그가 뷰로 들어왔을 때 시각적 표현을 업데이트 한다.
    2. draggingExited(:)를 작성하여 드래그가 빠져나갔을 때 시각적 표현을 업데이트한다.
    3. prepareForDragOperation(:)은 드롭이 시작될 때 이를 수락할 것인지를 결정한다.
    4. performDragOperation(:)에서 클립보드로부터 필요한 데이터를 받아오는 등의 작업을 처리한다.
    5. concludeDragOperation(:)는 뒷정리로, 드래그가 들어오는 시점에 변경된 뷰의 외양을 원상태로 되돌리는 작업을 한다.

드래그하기

드래깅 소스가 되려면 NSDraggingSource 프로토콜을 구현하는 것과 드래그 세션을 시작하도록 하는 메소드를 호출하는 것이 필요하다. 사실 이 프로토콜은 드래깅세션간에 어떤 동작을 해줄 수 있는지를 알려주는 역할을 담당하는데, 핵심적인 메소드는 다음과 같다.

func draggingSession(_: NSDraggingSession, sourceOperationMaskFor: NSDraggingContext) -> NSDraggingOperation

대부분의 경우 여기서는 return .copy 정도만으로도 충분하다.
여기서 말하는 동작이란, 드래그의 결과를 말하는 것이다. 예를 들어 텍스트 편집기에서 선택된 텍스트를 같은 문서 내로 드래그했을 때는 이동이 되고, 휴지통으로 드래그했을 때는 지워진다. 그리고 옵션 키를 누른채로 끌었을 때는 복사를 할 수 있다. 이렇게 상황에 따라 다른 결과를 나타내는 것은 이 메소드가 세션과 컨텍스트 관련 정보가 변경될 때마다 호출되어 계속 확인하게 된다.
이 메소드는 두 개의 인자를 받는데 하나는 NSDraggingSession 타입의 session, 그리고 다른 하나는 NSDraggingContext 타입의 context이다.
컨텍스트는 해당 드래깅 세션이 동일 앱내에서 일어나는 중인지, 다른 앱 간에 일어나는 중인지에 대한 정보를 담고 있다. NSDraggingContext 레퍼런스에 보면 다음의 두 개 case를 정의하고 있다.

  • .outsideApplication
  • .insideApplication

이를 통해서 같은 앱간 혹은 다른 앱간의 드래깅이 진행되는 중인지를 알 수 있고, 그에 따라 어떤 동작을 할 것인지를 결정할 수 있을 것이다.
드래깅 세션은 드래그 동작과 관련된 정보들을 가지고 있다.

  • draggingPasteboard : 드래그드롭 액션을 관장하는 클립보드
  • animatesToStartingPositionsOnCancelOrFail : 취소/실패시 되돌아가는지
  • draggingLocation : 화면에서의 드래그 아이템들의 현재 위치

참고로 드래깅 세션은 destination에 대한 정보는 가지고 있지 않다. 왜냐하면 드래깅 세션은 드래그가 일어나는 중에 계속해서 바뀔 수 있고, 아직 목적지 역시 그 때 그 때마다 달라지거나 없을 수 있기 때문이다. destination은 결국 드롭이 결정되는 시점에 정해진다고 봐야 한다. 즉 드래그 세션은 “지금 사용자가 드래그 아이템을 잡고 이리저리 방황하고 있는 중이야”라는 상황에서 결정된 정보들만을 알려줄 수 있다.
또, 드래깅 세션은 enumerateDraggingItems(options:for:classes:searchOptions:using:)이라는 어마어마한 이름의 메소드를 가지고 있는데, 이를 이용해서 세션에 접근할 수 있는 위치에서는 끌고 가고 있는 아이템들을 순회하여 특정한 처리를 할 수 있다.

드래그를 시작하기

드래그를 시작하는 메소드는 NSViewbeginDraggingSession(with: [NSDraggingItem], event: NSEvent, source: NSDraggingSource) 를 이용한다. 드래그를 시작할 수 있는 뷰는 mouseDragged(with: ) 메소드 내에서 이 메소드를 호출하면 드래깅 세션을 시작할 수 있다. (드래깅 세션이 시작되고 나면, 실제 마우스 추적은 세션이 담당하며, 따라서 원본 뷰 내에서 마우스를 움직일 때 mouseDragged(with:)는 호출되지 않는다.) 이 메소드가 호출되고 나면 시스템은 드래그 세션을 하나 시작하게 되고, 드래깅 소스와 상호작용을 시작한다.

  • 뷰의 -mouseDragged:로부터 -beginDraggingSession(with:event:source:)를 호출하여 세션을 시작한다.
  • 드래그 소스는 뷰 자신이거나 뷰의 컨트롤러일 수 있다.

아래 예제에서는 커스텀 뷰를 NSDraggingSource로 만들고, 드래그를 시작할 수 있게끔 내용을 구현한다.

class SomeDraggingView: NSView, NSDraggingSource {
    var myText: String
    var downEvent: NSEvent?
    // ...
    func draggingSession(_ session: NSDragginSession, sourceOpaerationMaskFor context: NSDraggingContext) -> NSDragOperation {
        return .copy
    }
    override func mouseDown(with event: NSEvent) {
        downEvent = NSEvent
    }
    override func mouseDragged(with event: NSEvent) {
          // 여기는 참고로 3포인트 이상 끌었을 때 드래그를 시작하도록 하는 부분
          guard let down = downEvent?.loactionInWindow,
          case let drag = event.locationInWindow,
          case let distance = hypot(down.x - drag.x, down.y - drag.y),
          distance > 3 else { return }
          // ...
          // <# 준비작업 #>
          // let item: NSDraggingItem = ....
          beginDraggingSession(with: [item], event: event, source: self)
    }
}

드래그 세션을 시작하려면 드래그 아이템(NSDraggingItem)이 필요하다. 이건 어떻게 만들어야 할까?

드래그 아이템

드래그 아이템(NSDraggingItem)은 드래그되는 데이터인 동시에, 드래그 간에 시각적으로 표현되는 이미지를 뜻한다. 하나 혹은 그 이상의 시각 요소를 선택해서 드래그 하는 경우에 개별 데이터들은 각각의 드래그 아이템이 된다. 예를 들어 파인더의 뷰에서 3개의 파일을 선택해서 끌기 시작하면 선택된 세 파일들은 3개의 드래그 아이템이 되며, 이 때 시각적으로도 각 드래그 아이템을 대표하는 3개의 아이콘이 커서를 따라 움직일 것이다.
OSX 라이언부터 드래그 API가 바뀌게 되었는데, 이는 드래그 아이템들이 하나의 이미지를 사용하는 것이 아니라, 개별적으로 그룹지어지며, 특정 상황에 따라서는 그 모양이 변할 수 있다.
예를 들어 파인더에서 아이콘 보기 상태에서 많은 파일을 선택해서 드래그를 시작하면, 해당 파일의 아이콘이미지들이 그 위치 그대로 움직이기 시작한다. 하지만 그 상태로 다른 창에서 움직이다보면 리스트 타입의 형태로 변하면서 조금 더 콤팩트한 모습이 된다.
결국 이는 드래그 아이템의 집합이 1개의 이미지 표현을 갖는 것이 아니라 개별 아이템이 모두 자신의 표현형을 가질 수 있으며, 이 시각적 표현형은 상황에 따라 바뀔 수 있다는 것이다.
그리고 예전 API와 달리 페이스트 보드를 직접적으로 액세스할 필요를 아예 없애버렸다. 대신에 드래그되려는 콘텐츠는 클립보드에 올라갈 수 있도록 NSPasteboardWriting 을 지원하거나 NSPasteboardItem으로 래핑하여야 한다.
드래그 아이템은 다음 두 가지 요소를 갖추고 있어야 완전한 형태가 된다.

  • 원본데이터 : 원본데이터는 NSPasteboardWriting을 지원해야 한다.
  • 이미지표현형 : NSDraggingImageComponent의 타입으로 이미지 데이터와 표시될 영역의 프레임 정보를 가지고 있다.

특히 이미지 표현형은 데이터 하나에 2개가 배당될 수 있다. 이미지 표현형은 상황에 따라 아이콘 혹은 레이블 형식을 사용하게 되는데, 이는 NSDraggingImageComponentKey에 정의되어 있다. 그리고 각각의 키에 대응하여 이미지 데이터와 그려질 영역(NSRect)에 대한 정보를 가지고 있다.

  • NSDraggingImageComponentIconKey
  • NSDraggingImageComponentLabelKey

으잉? 그럼 드래그 하려는 데이터에 대해서 2개씩이나 이미지를 준비해야 할까? 그건 아니다. 파인더와 같이 멋진 드래그를 구현하려면 모르겠지만, 보통의 경우에는 아이콘 타입으로도 충분하다. 따라서 복사할 데이터 및 이미지와 영역이 이미 준비되어 있다면 다음과 같은 코드로 드래그 아이템을 만들 수 있다.

let imageComponent = NSDraggingImageComponent(key: NSDraggingImageComponentIconKey)
imageComponent.frame = rect
imageComponent.contents = myNSImage
let item = NSDraggingItem(pasteboardWriter: theSourceString as NSString)
item.imageComponents = [imageComponent]

이런식으로 바닥부터 구성하는 것은 좀 불편하다. 그래서 AppKit은 보다 편하게 쓸 수 있는 편의 메소드를 준비하고 있다. 그것이 setDraggingFrame(:contents:)이다. 이는 기본적으로 아이콘 모드의 이미지 요소를 영역과 이미지데이터 (NSImage,CGImage) 타입이 허용된다)를 사용해서 바로 만들 수 있으며, 이를 사용하면 위 코드는 아래와 같이 조금 더 간단해진다.

// String은 NSPasteboardWriting을 지원하지 않으므로 `NSString`으로 캐스팅한다.
let item = NSDraggingItem(pasteboardWriter: theSourceString as NSString)
item.setDraggingFrame(rect, contents: myNSImage)

자 그려면 이번에는 실제로 아이템을 준비하는 부분의 코드를 작성해보자. 이미지는 귀찮으니까 그냥 뷰 전체 영역을 잡아버린다.

override func mouseDragged(with event: NSEvent) {
  // 여기는 참고로 3포인트 이상 끌었을 때 드래그를 시작하도록 하는 부분
  guard let down = downEvent?.loactionInWindow,
  case let drag = event.locationInWindow,
  case let distance = hypot(down.x - drag.x, down.y - drag.y),
  distance > 3 else { return }
  let item = NSDraggingItem()
  let image: NSImage = {
    let data = dataWithPDF(inside: bounds)
    let image = NSImage(data:data)
    return image
  }()
  item.setDraggingFrame(NSRect(x: drag.x - bounds.midX, y: drag.y - bounds.midY,
                           width: bounds.size, height: bounds.size),
                      contents: image)
  beginDraggingSession(with: [item], event: event, source: self)
}

여기까지 구현했다면, 해당 뷰에서 드래그를 시작하면 뷰의 내용이 끌려가기 시작할 것이다.

드래그 소스의 다른 메소드

NSDraggingSource에는 몇 가지 옵셔널 메소드들이 정의되어 있다. 이들은 드래그 작업이 시작되기 전, 이동하는 중, 끝났을 때 각각 호출된다.

  • draggingSession(_: endedAt: operation:) – 드래그 작업이 끝났을 때
  • draggingSession(_: moveTo:) – 드래그하여 이동하는 중에
  • draggingSession(_:willBeginAt:) – 드래그가 시작되려고 할 때

참고로 draggingSession:endAt:의 경우에는 드래그 소스가 정의하지 않은 작업이라도 처리하는 경우가 있는데, 예를 들어서 .copy를 정의한 드래깅 소스가 휴지통으로 드래그되었다면 이 메소드를 받게 되고 이 때의 작업은 .delete이다. 이 메소드의 내부를 적절히 구현하면 드래그가 삭제로 끝났을 때, 처리를 할 수 있다.

드래그 목적지/타깃

원래는 Destination이지만, ‘목적지’라고 옮기니 좀 어색한 느낌이라 타깃이라고 했음

드래그 타깃은 사용자가 끌고온 아이콘/데이터를 떨굴 수 있는 시각영역이며, 따라서 NSView의 서브 클래스인 경우가 대부분이다. 드래그 타깃이 되기 위해서는 두가지 요건을 필요로 한다.

  1. 해당 뷰가 register(forDraggedTypes:)를 호출하여 페이스트보드로부터 받을 수 있는 타입을 등록해야 한다. 드래그가 어떤 뷰 속으로 진입하려는 시점에 해당 뷰에 대해서 시스템은 이 메소드를 호출해서 드래그 아이템의 타입과 비교해본다. 이 과정이 성공하면 해당 뷰는 잠재적인 드래그 목적지의 후보가 된다. 이 작업을 하지 않으면 draggingEntered(:)가 호출되지 않는다.
  2. NSDraggingDestination의 필요한 메소드들을 구현한다. 잠재적인 후보가 결정된 이후부터 드래그 작업은 이 객체가 주도하게 된다. 시스템은 드래그위치가 이동하거나 마우스 버튼이 풀리는 시점까지 계속해서 이 객체를 귀찮게 만들 것이다. 프로토콜의 모든 메소드들은 옵셔널인데 다음과 같은 순서로 호출된다고 보면 된다.
    1. draggingEntered(:) : 드래그가 뷰 영역으로 들어옴
    2. draggingUpdated(:) : 드래그가 움직이고 막 돌아다님
    3. draggingEnded(:) : 드래그가 끝남 (주로 다른 타깃에서 끝났을 때 알아차리기 위해서)
    4. draggingExtied(:) : 드래그가 빠져나감
    5. prepareForDragOperation(:) : 드롭이 시작되어서 실제 전송을 할 것인지 결정
    6. performDragOperation(:) : 드롭된 경우 처리
    7. concludeDragOperation(:) : 뒷정리

보통 이 중에서 일반적으로 작성해야 하는 것은 다음과 같다.

  1. draggingEntered(:) – 드래그가 들어왔을 때, 드래깅 정보를 통해 데이터를 받을 수 있는지 확인하고 그런 경우 드롭을 받을 준비가 됐다고 뷰의 시각적 상태를 업데이트한다.
  2. draggingExited(:) – 드래그가 밖으로 나갔을 때 드롭을 받지 않는 상태로 뷰를 업데이트 한다.
  3. performDragOperation(:) – 전달된 데이터를 받아와서 처리한다.
  4. concludeDragOperation(:) – 드롭된 이후에는 처음에 변경한 뷰의 시각적 상태를 복원해줘야 한다.

draggingEntered, draggingUpdated는 모두 NSDragOperation을 리턴한다. 이 값들은 시스템에 대해서 “여기에 떨구면 XX한 일이 일어나는 거야”라는 걸을 알리게 된다. 필요한 경우 시스템은 드래그 아이템에 + 표시 같은 걸 더해서 복사가 일어날 것인지 등을 암시한다.

드래깅 정보

NSDraggingDestination 객체는 프로토콜 내 메소드를 호출받으면서 지속적으로 NSDraggingInfo 타입의 객체를 전달받게 된다. 이 객체는 세션과 마찬가지로 시스템이 생성해주는데, 드래그와 관련된 정보들을 담고 있다.

  • 클립보드 : draggingPasteboard()
  • 드래그소스 : draggingSource()
  • 소스작업마스크 : draggingSourceOperationMask()
  • 가용한 아이템 수 : numbersOfValidItemsForDrop
  • 이미지: draggedImage()
  • 이미지 위치 : draggedImageLocation()

그럼 한 번 구현해보자.

extension SomeDraggingView {
    // 텍스트를 드래그하는 경우, 받을 수 있음을 표시한다.
    override func viewDidMoveToWindow() {
        register(forDraggedTypes: [NSPasteboardTypeString])
    }
    // 드래그가 들어왔을 경우
    func draggingEntered(_ info: NSDraggingInfo) -> NSDragOperation {
        // 텍스트를 받을 수 있고, 복사 동작인 경우에 나도 복사동작 할 수 있다고 알려준다.
        let pb = info.draggingPasteboard()
        if pb.canReadObjects(forClasses:[NSString.self]),
        info.draggingSourceOperationMask().contains(.copy),
        info.draggingSource() !== self
        {
            // 드래그를 받을 수 있으니 뷰를 오렌지색으로 칠한다.
            bgColor = NSColor.orange
            setNeedsDisplay(bounds)
            return .copy
         }
         // 그렇지 않은 경우 허락하지 않는다.
         return .none
     }
     func draggingExited(_ info: NSDraggingInfo) {
        // 드래그가 빠져나갔으니 다시 흰색으로 돌아간다.
        bgColor = NSColor.white
        setNeedsDisplay(bounds)
     }
     func performDragOperation(_ info: NSDraggingInfo) -> Bool {
        let pb = info.draggingPasteboard()
        // 드롭된 데이터 중에서 첫번째 문자열 데이터를 얻어서 이를 사용한다.
        let item = pb.readObjects(forClasses: [NSString.self])?.first as? String {
            self.string = item
            // 사실 바로 다음에 배경색을 바꿀 때 또 리드로잉할것이라, 따로 호출하지 않아도 좋다.
            // setNeedsDisplay(bounds)
            // 사용후에는 꼭 true를 리턴해주자.
            return true
        }
        return false
     }
     // 드롭이 완료된 후에 뷰 배경색 복원
     func conludeDragOperation(_ info: NSDraggingInfo) {
        bgColor = NSColor.white
        setNeedsDisplay(bounds)
     }
}

참고할 정보

이미지의 드롭

드래그앤드롭으로 이미지를 끌어와서 받으려는 경우에 등록해야 하는 타입은 NSImage클래스가 알고 있다.

...
register(forDraggedTypes: NSImage.imageTypes())
...

또, 클립보드에서 생성될 수 있는 타입인지에 대한 내용도 canInit(with: NSPasteboard) 메소드로 바로 확인할 수 있다. 이는 NSImage가 사실은 여러가지 비트맵 포맷들을 감싸는 배열이기 때문에, 이미지에 대해서는 개별 UTI 별로 체크하는 것 보다, 이 방식을 쓰는 것이 좋다. 역시 페이스트보드로부터 init?(pasteboard:)를 써서 바로 이미지를 생성할 수 있다.

뷰의 내용으로 이미지를 만들기

NSViewdataWithPDF(inside:)를 이용해서 뷰의 일부분을 PDF 데이터로 만들 수 있으며, 이 데이터는 NSImage(data:)를 통해서 바로 이미지로 전환이 가능하다.
이 메소드를 호출받으면 뷰는 draw(:)를 호출한다. 만약 드래그 시에는 배경을 그리기 싫다면 NSGraphicsContext를 이용해서 화면에 그리는 중인지 아닌지를 판단할 수 있다.

override func draw(_ rect: NSRect) {
    // ...
    if NSGraphicsContext.currentContextDrawingToScreen() {
        화면에 그리는 중....
    } else {
        이미지나 프린터용으로 그리는 중
    }
}

테이블 뷰
NSTableView의 경우에 각 테이블 Row를 선택해서 드래그하여 순서를 변경할 수 있게끔 제작된 앱들이 있다. 테이블 뷰는 기본적으로 드래그 앤 드롭에 필요한 API들을 내부적으로 구현해놓고 있으며, 셀을 드래그하여 이동하거나 복사하기 위해서 구현해야할 API들을 델리게이트 메소드에서 정의하고 있다. 다음 번에는 테이블 뷰 내에서의 셀 드래그 앤 드롭에 대해서도 한 번 알아보도록 하자.