#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) } } } } }}