Sample CodeiOS, iPadOS, Mac CatalystReviewed 2026-07-21View on Apple Developer

Data delivery with drag and drop

At a glance

Item Summary
Purpose Transfers contact cards within the table or across apps as vCard and UTF-8 text item-provider representations.
App architecture The domain object implements item-provider read/write protocols; a table controller adapts drag/drop sessions to one data-source owner and commits asynchronous external loads into the table.
Main patterns Transferable model, multiple representations, framework delegate adapter, async placeholder/commit, local fast path, and extracted data source.
Project style Four Swift files: transferable model, table/drop coordinator, detail screen, and app lifecycle.

Project structure

ClientList/
├── ContactCard.swift
├── ContactsTableViewController.swift
│   ├── ClientsDataSource
│   ├── UITableViewDropDelegate extension
│   └── UITableViewDragDelegate extension
├── ContactDetailsViewController.swift
├── AppDelegate.swift
└── Base.lproj/Main.storyboard

Structure observations

  • Data encoding/decoding belongs to ContactCard, not the drag delegate.
  • Row storage and local moves belong to ClientsDataSource.
  • Drag/drop policy stays in same-file protocol extensions on the table controller.

Overall architecture

Reference code

ClientList/ContactCard.swift:34 — the domain object itself declares supported types and decodes either a vCard or a plain-text fallback into the same model.

static var readableTypeIdentifiersForItemProvider = [
    kUTTypeVCard as String,
    kUTTypeUTF8PlainText as String
]

static func object(
    withItemProviderData data: Data,
    typeIdentifier: String
) throws -> ContactCard {
    let contact = ContactCard(name: "")
    if typeIdentifier == kUTTypeVCard as String {
        let values =
            try CNContactVCardSerialization.contacts(with: data)
        guard let value = values.first else {
            throw ContactCardError.invalidVCard
        }
        contact.name = value.givenName + " " + value.familyName
    } else if typeIdentifier == kUTTypeUTF8PlainText as String {
        contact.name = String(data: data, encoding: .utf8)!
    }
    return contact
}

Interpretation

The transfer boundary is ContactCard, not a view controller. The table controller only decides move/copy/placement policy; NSItemProvider negotiates a representation and asks the model to encode or decode it.

Ownership and state

Ownership evidence

ClientList/ContactsTableViewController.swift:55 — the table controller keeps one strong data-source owner and installs it for data, drag, and drop collaboration.

class ContactsTableViewController: UITableViewController {
    let dataSource = ClientsDataSource()

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dropDelegate = self
        tableView.dragDelegate = self
        tableView.dataSource = dataSource
    }

    override func prepare(
        for segue: UIStoryboardSegue,
        sender: Any?
    ) {
        let details =
            segue.destination as! ContactDetailsViewController
        details.contactCard =
            dataSource.clients[tableView.indexPathForSelectedRow!.row]
    }
}
Owner Object or state Relationship Mutation authority
ClientsDataSource Ordered clients array Strong model owner Local move and external-drop commit
ContactsTableViewController Data-source instance Strong stable owner for table callbacks Controller initialization only
Table view Drag/drop delegates and row presentation Framework callback references UIKit interaction lifecycle
UIDragItem / NSItemProvider Transfer promise/representations Framework-managed for drag session Provider load and model protocols
Placeholder context Pending external row insertion Temporary transaction commitInsertion closure
Detail controller Injected contactCard Strong session reference Set by source controller before display

Class and protocol design

Type or protocol Responsibility Depends on or conforms to
ContactCard Contact values plus vCard/plain-text serialization NSItemProviderReading, NSItemProviderWriting
ContactCardError Invalid type/vCard failure cases Error
ClientsDataSource Own rows, bind cells, and perform synchronized local moves UITableViewDataSource
ContactsTableViewController Negotiate drag/drop policy and async insertion UITableViewDragDelegate, UITableViewDropDelegate
ContactDetailsViewController Present one injected contact UIViewController

This is protocol-oriented integration even without an app-defined protocol: ContactCard conforms directly to two narrow data-transfer contracts, so the system can request representations without knowing the table/controller.

Access control

Symbol Access Verified effect Why it fits this design
ContactCard implicit internal final class Module-visible and cannot be subclassed. Fixes serialization behavior to one concrete transfer model.
Contact properties and serialization methods implicit internal Table/detail code and item-provider machinery can access them within target. Simple sample binding; production could use private setters.
ClientsDataSource.clients implicit internal var Drop controller mutates the array directly. Enables concise placeholder commits but weakens encapsulation.
Table controller’s dataSource implicit internal let Stable reference readable across module. Strong ownership is important; private let plus data-source methods would be narrower.
Detail contactCard implicit internal IUO Source controller injects it before viewDidLoad. Storyboard/segue contract; nonoptional initializer injection would be safer programmatically.
Detail outlets weak, implicit internal View hierarchy retains controls. Correct storyboard ownership.
Explicit private / fileprivate / public Not used No lexical/public library boundary. Sample prioritizes transfer flow over defensive access design.

Reference code

ClientList/ContactCard.swift:60 — writing exposes the same two ordered representations and rejects unsupported identifiers explicitly.

static var writableTypeIdentifiersForItemProvider = [
    kUTTypeVCard as String,
    kUTTypeUTF8PlainText as String
]

func loadData(
    withTypeIdentifier typeIdentifier: String,
    forItemProviderCompletionHandler completion:
        @escaping (Data?, Error?) -> Void
) -> Progress? {
    if typeIdentifier == kUTTypeVCard as String {
        completion(createVCard(), nil)
    } else if typeIdentifier == kUTTypeUTF8PlainText as String {
        completion(name.data(using: .utf8), nil)
    } else {
        completion(nil, ContactCardError.invalidTypeIdentifier)
    }
    return nil
}

Logic ownership and placement

Logic Owning type or file Placement rationale
vCard/text encoding and decoding ContactCard Representation invariants travel with the transferable model.
Row storage/cell binding/local reordering ClientsDataSource One owner synchronizes array and table mutations.
Drag item construction and preview Drag-delegate extension Requires table index path and drag session context.
Move/copy/drop-intent policy Drop-delegate extension Framework asks the destination controller during tracking.
Async placeholder and insertion commit Drop controller + placeholder context Coordinates promised data with final UIKit index path.
Contact presentation Detail controller Owns labels/image view and navigation title.

Design patterns

Pattern Source evidence Purpose or tradeoff
Transferable model ClientList/ContactCard.swift:19 Makes domain data directly readable/writable by item providers.
Multiple representations ClientList/ContactCard.swift:36 Offers rich vCard first and interoperable text fallback.
Drag/drop adapter ClientList/ContactsTableViewController.swift:97 Translates UIKit session callbacks into model/table actions.
Local fast path ClientList/ContactsTableViewController.swift:113 Reorders existing data without serializing/loading it.
Async placeholder transaction ClientList/ContactsTableViewController.swift:131 Reserves UI position, then commits when promised data arrives.
Extracted data source ClientList/ContactsTableViewController.swift:12 Separates ordered storage/cell binding from interaction policy.
Constructor through system protocol ClientList/ContactCard.swift:39 Lets NSItemProvider create the model from negotiated bytes.

Naming conventions

  • Domain and screen roles are explicit: ContactCard, ClientsDataSource, ContactsTableViewController, and ContactDetailsViewController.
  • Transfer methods keep framework-required names; internal commands use moveClient and createVCard.
  • clients names the mutable collection while client(index:) names one lookup.
  • Error cases name invalid inputs directly: invalidTypeIdentifier and invalidVCard.

Architecture takeaways

  • Put representation negotiation/serialization on the transferable model, not in UI delegates.
  • Use the drag/drop controller for policy and the data source for ordered model mutation.
  • Separate local moves from cross-app promised loads.
  • Use placeholder commit APIs so asynchronous data and final table indices stay synchronized.
  • Narrow mutable array access in production even when direct mutation keeps a sample short.

Source map

Source file Relevant symbols
ClientList/ContactCard.swift Transferable model, representations, vCard codec
ClientList/ContactsTableViewController.swift Data source, drag/drop policy, async insertion
ClientList/ContactDetailsViewController.swift Detail injection and display
ClientList/AppDelegate.swift Application/window lifecycle