Did you know that as of iOS 17, you can’t AirDrop custom models from your app to another instance of your app anymore?
I do. And this post is about how I found out, how AirDrop even works because nobody seems to know that, and where Apple might go from here.
Discovering the Problem
I was chatting with my friend Mikaela about this topic around a year ago. She mentioned that the code sample wasn’t working as intended found in the chapter over AirDrop from my book series. I remember testing this quite a bit, and knew that (at the time) it was functioning.
And so, I told her I’d check into it. Then my kids started fighting, they also needed dinner, I finally cleaned up the kitchen afterwards and just like that — about a calendar year went by.
That brings me to last weekend. I noticed that in Elite Hoops, a team AirDrop transaction was opening Files.app instead of, well, Elite Hoops. You can try this yourself — AirDrop anything from an app not made by Apple and see where it ends up. If it’s not something defined in Apple’s public UTTypes
, like a spreadsheet or word document, it’ll likely open Files.
In my case, this was (and isn’t) ideal because new iPhones are coming and coaches will want to move their data over. AirDrop is one way they could.
So I poured into the issue, determined to figure this out.
How AirDrop Works
Search for AirDrop solutions and you’ll find tumbleweeds, dead ends and years-old code samples. Apple’s own solution on the matter? It’s from iOS 7, and doesn’t build. No matter, because you’ll get the same result there — Files.app opening the drop.
To wit, there is no “Here is how AirDrop Works for 3rd Party Developers” page anywhere.
Plus, even if you knew where to look API wise — you’d find exactly one mention of AirDrop across all of the docs as far I’ve been able to find:
If there are more, please tell me and I’ll include them. I’d love to be wrong about this.
So, how does it work? It’s fairly simple. Consider this model:
struct Person: Identifiable, Codable {
let name: String
let id: UUID
}
You would create your own UTType
for it within Xcode and using the (very nice and not-talked-about-enough) framework UniformTypeIdentifiers
:
This basically says that this type is from your app — you own it and are the authority over it — and you can import it. And, in code, you reference it when needed:
extension UTType {
static var personType: UTType = .init(exportedAs: "com.testApp.person")
}
From here, there are two roads you could take:
- Modern Approach: You use
Transferable
, which is actually a lovely way to describe how your data should be transported, what kind of data it is, and how it can be represented. You could vend that data with a ShareLink
to expose the action sheet, and thus — AirDrop.
extension Person: Transferable {
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(contentType: .personType)
DataRepresentation(importedContentType: .personType) { data in
let person = try JSONDecoder().decode(Person.self, from: data)
return person
}
DataRepresentation(exportedContentType: .personType) { person in
let data = try JSONEncoder().encode(person)
return data
}
FileRepresentation(contentType: .personType) { person in
let docsURL = URL.temporaryDirectory.appendingPathComponent("\(person.name)\(UUID().uuidString)", conformingTo: .personType)
let data = try JSONEncoder().encode(person)
try data.write(to: docsURL)
return SentTransferredFile(docsURL)
} importing: { received in
let data = try Data(contentsOf: received.file)
let person = try JSONDecoder().decode(Person.self, from: data)
return person
}
}
}
And later in the interface:
ShareLink(item: person, preview: .init(person.name))
- Classic Approach: Your model adopts
UIActivityItemSource
and uses UIActivityViewController
. For Swift apps, this means you have to leave a Struct
behind and use a reference type, notably NSObject
:
class PersonObject: NSObject {
let name: String
let id: UUID
init(person: Person) {
self.name = person.name
self.id = person.id
}
}
extension PersonObject: UIActivityItemSource {
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
return name
}
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
return self
}
func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivity.ActivityType?) -> String {
return "Sharing Person: \(name)"
}
func activityViewController(_ activityViewController: UIActivityViewController, dataTypeIdentifierForActivityType activityType: UIActivity.ActivityType?) -> String {
return UTType.personType.identifier
}
}
And then it’s just off to the share sheet:
UIActivityViewController(activityItems: [personObj], applicationActivities: nil)
Doing all of this and AirDroppin’ from your app to another version of your app imports it into Files.app.
Why does it do that though!?
Type Handler Jacking
The reason Apple changed this is because any app can say they are the owner of any UTType
. That means if you opened a Facebook-specific type, and two apps on the device said they own it — well who knows what opens? Maybe Facebook, maybe the other app. And, well — that is confusing and not great.
This concept is briefly alluded to in (another excellent Tech Talk) here:
The system now knows we can open this file but that we may not be the best choice if the user has Company’s Food installed, since that app is the owner of the file type. We want to be good citizens on the platform, so it’s important to respect type ownership this way even though we both know our code is way better than our competitor’s code.
I personally ran into this at my previous job at Buffer, where confused customers didn’t know why some random app was opening app-specific types. The same kind of thing can happen with custom URL schemes. So in iOS 17, Apple nixed this for that reason:
And that is where things start to make sense. There are several hits of “My app AirDrops something but it always opens Files, please help” if you search for them.
The Programmer Fix
This feels like what I like to call the “Programmer Fix” solution. I’ve been a part of several. Was the ticket solved by definition? Yes, it was. Apps that aren’t the owner of a specific type can no longer open them by default. Case closed.
But, did it make the user experience much worse? 1,000%. Now, your best bet is to add an action extension to import that data after it’s already imported into Files. Food Noms does this the best from what I’ve seen so far.
So, to recap, the AirDrop flow for 3rd party developers shuttling their data before was:
- Open app -> Pick data -> Open share sheet to AirDrop -> AirDrop is handled in the app (via
URL
app or scene delegate functions).
Now, that same flow is:
- Open app -> Pick data -> Open share sheet to AirDrop -> Files opens the data -> 1A. Open the Share Sheet again -> Hope the developer has an action or import extension to bring it into their app, or 1B. Hope the app has document importing and viewing capabilities, or 1C. The user has not a freaking clue what the Files app even is, becomes confused and sends you an email.
If I asked my wife to do this flow, she’ll just stop and decide it’s not worth the hassle. I can’t much blame her.
Apple Should’ve Looked to macOS
How I wish Apple would’ve solved this would’ve been by simply looking at macOS. If there are multiple apps that say they are the handler of a type, or the extension is unknown to be handled by the apps you’ve got installed, you’ll get a nice modal dialog that lets you make a decision on which app should open it. Even better, if you right click it and choose “Open With..” -> “Other…”, you can pick an app and mark it as the default:
Problem solved. And the problem has been solved for a long time.
I hope to see this implementation in the future on iOS. Or, maybe Apple could even let you officially register a type on App Store Connect or something (though thinking through that route presents other challenges).
I do want to point out that I am not privy to the challenges of developing on an operating system used by billions of people. Maybe their end result (opening AirDropped data in Files) wasn’t a “programmer fix” at all, maybe it was weighed heavily in every which direction and that’s where they ended up.
Who knows? All I can say is that it doesn’t feel great as a developer or end user of iOS.
The Documentation…Or Lack Thereof
It would be one thing if it worked this way and it was clearly documented, it’s another when it “magically changes” and you find out about it within the deep depths of Google search. That’s all I really want to say on this topic.
As far as I can tell, the only mention of this is in the thread I linked above in the developer forums. Is that the right place for it? I dunno, I guess it’s not wrong. Though, I imagine a lot of developers would simply search developer.apple.com, and you won’t find the answer there (even if you select “Forums”!!!):
Final Thoughts
Remember the beautiful animation Apple added for AirDrop? It rocks. But now, it really only rocks for Apple. And that’s a shame, because AirDrop embodies that Apple magic of something just working the way it should. “I have some stuff here, I want it there. Boom, done.”
This post probably comes off a bit pessimistic when compared to my usual fare, and perhaps a pinch of agitation shines through. But, you know, we want to make great apps for iOS. Not just okay apps. I want AirDrop to work just as great as it does across the rest of the system in my apps. This platform still remains my favorite place to create things, but I hope Apple can become a bit more proactive with changes like this. Things like this matters a lot to developers who put a lot of work into tiny interactions, like supporting AirDrop.
Here’s hoping this situation changes in the future. Or, maybe I saved you some time. Maybe both!
Until next time ✌️