#author("2018-05-07T16:18:57+09:00","default:wikiwriter","wikiwriter")
#author("2018-05-07T16:31:32+09:00","default:wikiwriter","wikiwriter")
&tag(Drag and Drop Tutorial for macOS);
*目次 [#ucabbfd7]
#contents
*関連ページ [#qaf5514b]
*参考情報 [#bc66b812]
-[[Drag and Drop Tutorial for macOS:https://www.raywenderlich.com/136272/drag-and-drop-tutorial-for-macos]]
-NSViewサブクラスのドラッグ&ドロップ実装
-アプリのその他のビューにドラッグできるデータを提供
-カスタムドラッグタイプ

*Getting Started [#laf58f4a]
-同ページからスタータープロジェクトをダウンロード。
-メインのSticker Viewは画像などをドラッグ先。
-画面下部の二つのビューはデータソース。
-Dragging DestinationとDragging  Sourcesに大別できる。
-Dragging Destination
--StickerBoardViewController.swift: メインビューコントローラー
--DestinationView.swift: 画面上部のビュー
-Dragging Source
--ImageSourceView.swift: ユニコーンのイメージがついたビュー。
--AppActionSourceView.swift Sparklesというラベルがつい他ビュー

*ペーストボードとドラッギングセッション [#jd409a75]
-ドラッグとドロップはソースとデスティネーションが関連する。
-sourceからドラッグし、NSDraggingSourceプロトコルを実装する。
-destinationにドロップし、NSDraggingDestinationプロトコルを実装する。
-NSPasteboardはデータ交換を円滑する。

*Dragging Destinationの作成 [#lc882280]
-Dragging Destinationは、dragging pasteboardからのデータを受け入れることができるビューまたはWindow。
-NSDraggingDestinationを実装する。
-手順
--ビュー構築時にdragging sessionから受け取るデータ型を宣言する
--dragging imageがビューに入ったとき、ビューがデータを受け入れるかどうかを決定する。
--dragging imageがビューの上になったら、ビューの上でその画像を表示する。
-実装
- DestinationView.swiftを開き、setup()含め以下のように変更する。
#pre{{
var acceptableTypes: Set<String> { return [NSURLPboardType] }

func setup() {
  register(forDraggedTypes: Array(acceptableTypes))
}
}}
-さらに以下を追加(ドラッグエンターで呼び出される内部処理)。
#pre{{
  //1.受け入れるタイプ。
  let filteringOptions = [NSPasteboardURLReadingContentsConformToTypesKey:NSImage.imageTypes()]
  
  func shouldAllowDrag(_ draggingInfo: NSDraggingInfo) -> Bool {
    
    var canAccept = false
    
    //2.ペーストボードを取得
    let pasteBoard = draggingInfo.draggingPasteboard()
    
    //3.ペーストボードのデータが読み取れるかどうかを決定
    if pasteBoard.canReadObject(forClasses: [NSURL.self], options: filteringOptions) {
      canAccept = true
    }
    return canAccept
    
  }
}}
-NSViewはNSDraggingDestinationを実装している。このメソッドをオーバーライドしていく。
-まずdraggingEnteredを実装する。
#pre{{
  //1.ドラッグを受け入れるかどうか
  var isReceivingDrag = false {
    didSet {
      needsDisplay = true
    }
  }
  
  //2.ドラッグがエンター為たときの処理
  override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
    let allow = shouldAllowDrag(sender)
    isReceivingDrag = allow
    return allow ? .copy : NSDragOperation()
  }
}}
-exit処理の作成
#pre{{
override func draggingExited(_ sender: NSDraggingInfo?) {
  isReceivingDrag = false
}
}}

-描画処理の変更
#pre{{
override func draw(_ dirtyRect: NSRect) {
  
  if isReceivingDrag {
    NSColor.selectedControlColor.set()
    
    let path = NSBezierPath(rect:bounds)
    path.lineWidth = Appearance.lineWidth
    path.stroke()
  }
}
}}
-ここまでで画像をドラッグしたらカーソルが+にかわり、外にでると通常に戻る。
-最後にデータを受け取る処理
-DestinationViewに以下を追加
#pre{{
override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool {
  let allow = shouldAllowDrag(sender)
  return allow
}

  override func performDragOperation(_ draggingInfo: NSDraggingInfo) -> Bool {
    
    //1.ドラッグ中フラグをリセット
    isReceivingDrag = false
    let pasteBoard = draggingInfo.draggingPasteboard()
    
    //2.ドロップポイントを取得
    let point = convert(draggingInfo.draggingLocation(), from: nil)
    //3.データ処理
    if let urls = pasteBoard.readObjects(forClasses: [NSURL.self], options:filteringOptions) as? [URL], urls.count > 0 {
      delegate?.processImageURLs(urls, center: point)
      return true
    }
    return false
    
  }

}}
-StickerBoardViewController.swiftを開き、以下のメソッドを追加。
#pre{{
  func processImage(_ image: NSImage, center: NSPoint) {
    
    //1.ラベルを非表示にする
    invitationLabel.isHidden = true
    
    //2.サイズを取得
    let constrainedSize = image.aspectFitSizeForMaxDimension(Appearance.maxStickerDimension)
    
    //3.NSImageViewを作成して追加
    let subview = NSImageView(frame:NSRect(x: center.x - constrainedSize.width/2, y: center.y - constrainedSize.height/2, width: constrainedSize.width, height: constrainedSize.height))
    subview.image = image
    targetLayer.addSubview(subview)
    
    //4.ランダム
    let maxrotation = CGFloat(arc4random_uniform(Appearance.maxRotation)) - Appearance.rotationOffset
    subview.frameCenterRotation = maxrotation
    
  }
}}
-以下のイメージも追加。
#pre{{
  
  func processImageURLs(_ urls: [URL], center: NSPoint) {
    for (index,url) in urls.enumerated() {
      
      //1.イメージを作成
      if let image = NSImage(contentsOf:url) {
        
        var newCenter = center
        //2.一つ伊集か
        if index > 0 {
          newCenter = center.addRandomNoise(Appearance.randomNoise)
        }
        
        //3.画像処理
        processImage(image, center:newCenter)
      }
    }
  }
  
}}


*Dragging Sourceの作成 [#iba71ca8]
-ドラッグ元はNSDraggingSourceプロトコルを実装する必要がある。
-ImageSourceViewはユニコーンを持つビュー。ユニコーンをドラッグできるようにする。
-クラスはNSDraggingSourceとNSPasteboardItemDataProviderを実装する必要がある。
-ImageSourceView.swiftに以下のメソッドを実装する。
#pre{{
// MARK: - NSDraggingSource
extension ImageSourceView: NSDraggingSource {
  //1.NSDraggingSourceに必要。どのようなドラッグ操作を実装しているか。
  func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation {
    return .generic
  }
}

// MARK: - NSDraggingSource
extension ImageSourceView: NSPasteboardItemDataProvider {
  //2.この段階ではスタブ
  func pasteboard(_ pasteboard: NSPasteboard?, item: NSPasteboardItem, provideDataForType type: String) {
    //TODO: Return image data
  }
}

}}
-ドラッグの開始。
-ImageSourceViewに以下を実装する。
#pre{{
  override func mouseDown(with theEvent: NSEvent) {
    //1.ペーストボードアイテムを生成(要求に応じてデータを供給する)
    let pasteboardItem = NSPasteboardItem()
    pasteboardItem.setDataProvider(self, forTypes: [kUTTypeTIFF])
    
    //2.ドラッギングデータを生成
    let draggingItem = NSDraggingItem(pasteboardWriter: pasteboardItem)
    draggingItem.setDraggingFrame(self.bounds, contents:snapshot())
    
    //3.ドラッギングセッションを開始
    beginDraggingSession(with: [draggingItem], event: theEvent, source: self)
  }
}}
-この段階でユニコーンがドラッグできるようになる。
-これを受け入れるためにはDestinationView.swiftでTIFFを受け入れるようにする。

#pre{{
var nonURLTypes: Set<String>  { return [String(kUTTypeTIFF)] }
var acceptableTypes: Set<String> { return nonURLTypes.union([NSURLPboardType]) }
}}
-setupを変更。
#pre{{
    register(forDraggedTypes: Array(nonURLTypes))
}}
-shouldAllowDragを変更する。
#pre{{
else if let types = pasteBoard.types, nonURLTypes.intersection(types).count > 0 {
  canAccept = true
}}
-performDragOperationw変更する
#pre{{
else if let image = NSImage(pasteboard: pasteBoard) {
  delegate?.processImage(image, center: point)
  return true
}
}}
-この段階でドラッグすると+が表示されるようになる。
-実際にデータを追加できるようにするため、ImageSourceView.swiftのpasteboardメソッドを実装する。
#pre{{
  func pasteboard(_ pasteboard: NSPasteboard?, item: NSPasteboardItem, provideDataForType type: String) {
    //1.
    if let pasteboard = pasteboard, type == String(kUTTypeTIFF), let image = NSImage(named:"unicorn") {
      //2.
      let finalImage = image.tintedImageWithColor(NSColor.randomColor())
      //3.
      let tiffdata = finalImage.tiffRepresentation
      pasteboard.setData(tiffdata, forType:type)
    }
  }
}}
-さまざまな色のユニコーンが追加できるようになる。

*カスタムデータのドラッグ [#ub2f8668]
-カスタムデータソースの実装。新しいデータタイプ。
-AppActionSourceView.swiftを開く。
#pre{{
enum SparkleDrag {
  static let type = "com.razeware.StickerDrag.AppAction"
  static let action = "make sparkles"
}
}}
-ドラッグデータソースはUTIで表す。
-AppActionSourceViewに以下を実装。
#pre{{

// MARK: - NSDraggingSource// MARK: - NSDraggingSource
 extensionextension  AppActionSourceViewAppActionSourceView: : NSDraggingSourceNSDraggingSource  {
  
  {     
   funcfunc  draggingSessiondraggingSession((__ session: NSDraggingSession, sourceOperationMaskFor
    context: NSDraggingContext) session: NSDraggingSession, sour  -> NSDragOperation {
    
    switch(context) {
    case .outsideApplication:
      return NSDragOperation()
    case .withinApplication:
      return .generic
    }
  }
}
}}
-アプリ外にはドロップできないようになっている。
-AppActionSourceViewにmouseDownを実装。
#pre{{
override func mouseDown(with theEvent: NSEvent) {
  
  let pasteboardItem = NSPasteboardItem()
  pasteboardItem.setString(SparkleDrag.action, forType: SparkleDrag.type)
  let draggingItem = NSDraggingItem(pasteboardWriter: pasteboardItem)
  draggingItem.setDraggingFrame(self.bounds, contents:snapshot())
  
  beginDraggingSession(with: [draggingItem], event: theEvent, source: self)
  
}
}}
-新しいタイプを受け入れる。DestinationView.swiftを変更する。
#pre{{
  var nonURLTypes: Set<String>  { return [String(kUTTypeTIFF),SparkleDrag.type] }
}}
-performDragOperation新しいelseifを追加。
#pre{{
else if let types = pasteBoard.types, types.contains(SparkleDrag.type),
  let action = pasteBoard.string(forType: SparkleDrag.type) {
  delegate?.processAction(action, center:point)
  return true
}
}}
-StickerBoardViewControllerにprocessActionを実装。
#pre{{
  func processAction(_ action: String, center: NSPoint) {
    //1.アクションを確認
    if action == SparkleDrag.action  {
      invitationLabel.isHidden = true
      
      //2.画像を生成
      if let image = NSImage(named:"star") {
        
        //3.
        for _ in 1..<Appearance.numStars {
          
          //A.
          let maxSize:CGFloat = Appearance.maxStarSize
          let sizeChange = CGFloat(arc4random_uniform(Appearance.randonStarSizeChange))
          let finalSize = maxSize - sizeChange
          let newCenter = center.addRandomNoise(Appearance.randomNoiseStar)
          
          //B.
          let imageFrame = NSRect(x: newCenter.x, y: newCenter.y, width: finalSize , height: finalSize)
          let imageView = NSImageView(frame:imageFrame)
          
          //C.
          let newImage = image.tintedImageWithColor(NSColor.randomColor())
          
          //D.
          imageView.image = newImage
          targetLayer.addSubview(imageView)
        }
      }
    }
  }
}}

トップ   編集 差分 履歴 添付 複製 名前変更 リロード   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS