Adopting drag and drop in a custom view
At a glance
| Item |
Summary |
| Purpose |
Makes an image view a drag source and the surrounding custom view a validated image drop destination. |
| App architecture |
One view controller owns the image UI; separate extensions adapt it to drag-source and drop-destination delegate protocols. |
| Main patterns |
Framework delegates, extension-based decomposition, item-provider transfer object, and local-object fast path. |
| Project style |
Minimal storyboard target with one controller split into core, drag, and drop files. |
Project structure
CustomViewDragAndDrop/
├── AppDelegate.swift
├── ViewController.swift
├── ViewController+Drag.swift
├── ViewController+Drop.swift
└── Base.lproj/Main.storyboard
Structure observations
- The files split protocol conformances, not ownership: all state still belongs to one controller.
- Cross-process representation (
NSItemProvider) and in-process representation (localObject) are created together at the drag boundary.
Overall architecture
flowchart LR
Controller["ViewController"] --> Image["UIImageView"]
Controller --> Drag["UIDragInteraction"]
Controller --> Drop["UIDropInteraction"]
Image --> Drag
Root["Controller root view"] --> Drop
Controller ..-> DragDelegate["UIDragInteractionDelegate"]
Controller ..-> DropDelegate["UIDropInteractionDelegate"]
DragDelegate --> Item["UIDragItem and NSItemProvider"]
Item --> DropDelegate
DropDelegate --> Image
Reference code
CustomViewDragAndDrop/ViewController.swift:22 — setup attaches interactions to the views that own their lifetimes.
override func viewDidLoad() {
super.viewDidLoad()
imageView.isUserInteractionEnabled = true
let dragInteraction = UIDragInteraction(delegate: self)
imageView.addInteraction(dragInteraction)
let dropInteraction = UIDropInteraction(delegate: self)
view.addInteraction(dropInteraction)
}
Interpretation
The controller is both adapter and UI state owner. UIKit drives the delegates; the drag side publishes an image representation, and the drop side validates location/type before asynchronously assigning the result.
Ownership and state
classDiagram
UIViewController o-- UIImageView : storyboard outlet
UIImageView *-- UIDragInteraction : retains attached interaction
UIView *-- UIDropInteraction : retains attached interaction
UIDragInteraction o-- UIDragInteractionDelegate : delegate
UIDropInteraction o-- UIDropInteractionDelegate : delegate
UIDragItem *-- NSItemProvider : owns transfer provider
UIDragItem o-- UIImage : localObject fast path
Ownership evidence
CustomViewDragAndDrop/ViewController+Drag.swift:17 — each drag item owns a portable provider and also carries the same image for local use/preview.
func dragInteraction(_ interaction: UIDragInteraction,
itemsForBeginning session: UIDragSession) -> [UIDragItem] {
guard let image = imageView.image else { return [] }
let provider = NSItemProvider(object: image)
let item = UIDragItem(itemProvider: provider)
item.localObject = image
return [item]
}
| Owner |
Object or state |
Relationship |
Mutation authority |
| Storyboard/controller |
Image view outlet |
Receives reference to hierarchy-owned view |
Controller/delegate callbacks |
| Image view/root view |
Drag/drop interactions |
Retain after addInteraction |
UIKit interaction lifecycle |
| Drag item |
Item provider and optional local image |
Retains for session |
Drag creation; UIKit transports |
| Controller |
Current displayed image and border feedback |
Mutates view state |
Drop lifecycle callbacks |
Class and protocol design
| Type or protocol |
Responsibility |
Depends on |
ViewController |
Configure views/interactions and own displayed image |
UIKit view hierarchy |
UIDragInteractionDelegate extension |
Produce transfer item and targeted lift preview |
UIImage, NSItemProvider |
UIDropInteractionDelegate extension |
Validate type/count/location, propose copy/move, load image, update feedback |
UIDropSession |
No app-defined protocol is needed because one concrete controller implements both UIKit boundaries.
Access control
| Symbol |
Access |
Verified effect |
Likely rationale |
| Controller and outlet |
implicit internal |
Storyboard and target files can access them. |
Sample target integration. |
Delegate methods and updateLayers |
implicit internal |
Same-module callers could invoke them; UIKit dispatches delegate methods. |
Conformance split across files requires target visibility under this organization. |
| Drag/drop temporary values |
lexical locals |
Transfer/preview objects cannot be mutated elsewhere before return. |
Short-lived session construction. |
No explicit private / fileprivate |
verified absence |
Source establishes no lexical helper boundary. |
Simplicity; not required by the framework. |
Reference code
CustomViewDragAndDrop/ViewController+Drop.swift:91 — drop-target highlighting is a controller helper shared only by drop callbacks in that extension file.
func updateLayers(forDropLocation dropLocation: CGPoint) {
if imageView.frame.contains(dropLocation) {
view.layer.borderWidth = 0
imageView.layer.borderWidth = 2
} else if view.frame.contains(dropLocation) {
view.layer.borderWidth = 5
imageView.layer.borderWidth = 0
} else {
view.layer.borderWidth = 0
imageView.layer.borderWidth = 0
}
}
Logic ownership and placement
| Logic |
Owning type or file |
Placement rationale |
| Interaction attachment |
ViewController.swift |
Core view lifecycle. |
| Transfer representation and lift preview |
ViewController+Drag.swift |
Drag delegate boundary. |
| Acceptance, proposal, loading, highlight |
ViewController+Drop.swift |
Drop delegate boundary and UI mutation. |
Design patterns
| Pattern |
Source evidence |
Purpose or tradeoff |
| Framework delegate |
CustomViewDragAndDrop/ViewController+Drag.swift:10 |
UIKit pulls data and lifecycle decisions from the owner. |
| Extension-based decomposition |
CustomViewDragAndDrop/ViewController+Drop.swift:12 |
Keeps source/destination APIs readable while sharing controller state. |
| Transfer object |
CustomViewDragAndDrop/ViewController+Drag.swift:20 |
NSItemProvider supports process-independent image delivery. |
| Local fast path |
CustomViewDragAndDrop/ViewController+Drag.swift:22 |
Avoids decoding for same-app preview/coordination. |
Naming conventions
+Drag and +Drop filenames name protocol-bound responsibilities.
- Delegate methods retain UIKit terminology; the one helper names its visible effect,
updateLayers.
dragInteraction/dropInteraction variables mirror framework types and lifecycle direction.
Architecture takeaways
- Attach interactions once, then keep session decisions in framework delegate conformances.
- Publish a transferable item-provider representation even when a local object is available.
- Validate type, cardinality, and location before proposing a drop operation.
- Splitting conformances across files does not create separate owners.
Source map
| Source file |
Relevant symbols |
CustomViewDragAndDrop/ViewController.swift |
View and interaction setup |
CustomViewDragAndDrop/ViewController+Drag.swift |
Drag item and targeted preview |
CustomViewDragAndDrop/ViewController+Drop.swift |
Drop validation, proposal, loading, feedback |
CustomViewDragAndDrop/AppDelegate.swift |
Legacy application/window lifecycle |