This year at WWDC 17, several promising new technologies, frameworks and APIs were revealed to eager developers. Each year Andy and I try to focus on not only the keynote, but the Platforms State of the Union session to get a sense for what we should prioritize for the upcoming iOS release among all the new things introduced.
For iOS 9, Apple pushed multitasking. Last year, Message Extensions were quite prominent. This year, there was no doubt about what we really wanted to hone in on: The new Drag and Drop APIs.
Big Idea. Big Framework.
Aside from a consumer standpoint, technically speaking Drag and drop on iOS is quite impressive.
True to Apple standards, Curpertino’s sharp UIKit engineers implemented it in a way that’s both efficient and incredibly secure. By taking advantage of asynchronous data transfers and maintaining app sandboxing, it allows the system to open up to new workflows without making any concessions on how iOS should work, namely – keeping a premium on security, usability and privacy.
But as far as new APIs go, it’s big. There were four sessions dedicated to the topic at WWDC, and to my knowledge, no other API has warranted that much attention before. It begins to make sense after you start to realize all the ways you can use it on iOS:
– Receiving data (handling a drop)
– Presenting data (beginning a drag)
– Dedicated APIs for UITableView, UICollectionView and UITextView
– Springloading controls
– and more
We had decided we wanted to go all in for drag and drop. Not only do we view it as being a first class citizen on iOS, but it fits great with how people use Buffer as well.
So the question then became, where do we start?
Starting Small
To get things rolling, we thought it’d be great if users could drag media, links or text on top of our compose button in the tab bar to kick off authoring a post:
In the session 203, “Introducing Drag and Drop”, there is a blink and you’ll miss it moment where the presenter hovers his current drag session over the “+” icon in the navigation bar within safari to open a new tab with the content (at about the 3:44 mark).
Though there wasn’t really any mention of getting drag and drop wired up specifically within tabbars or navigation bars, this little segment indicated that it was a supported UX paradigm.
Doc Divin’
With that, I dove in head first. After viewing a few of the sessions and browsing sample code, I realized drag and drop is thankfully straightforward to implement in any codebase.
For our purposes, I was only focused on the receiving end right now – handling a drop proposal. To act on these, Apple kept with its common UIKit pattern and created a protocol/delegate setup to handle them for any UIView.
The steps are quite easy to comprehend, especially when you think of it in the context of someone dragging some data and wanting something else to process that data:
– Create a UIDropInteraction
object
– Supply it a delegate, something that conforms to UIDropInteractionDelegate
– Add the interaction to a UIView via [viewInstance addInteraction:dropInstance]
;
Now, think about that flow in terms of UITabBar.
A tabbar has a collection of tabbar items, which actually don’t inherit from UIView at all. In fact, if you travel far enough down the inheritance tree, you’ll see it ultimately ends up with NSObject at the root. Inherently, this rules out adding a drop interaction to the only tab bar item I’m concerned with, barring any hacks.
Springloading
One option we could’ve taken was using the new springloading API.
With springloading, iOS essentially lets you fire off the selector of a given control. So, if you’ve got a UIButton that sends a doSomething
message when it’s tapped, springloading would act as if the button had been tapped when something is drug on top of it and it stays there for about ~1 second.
Looking at my current situation, I did find that bar items support this out of the box:
#if TARGET_OS_IOS
@interface UITabBarItem (SpringLoading)
@end
#endif
Enabling that would give us this effect:
While I can certainly see its uses, for our purposes this isn’t what we wanted. Plus, that’d mean we’d also need to implement drop support on the composer. This is something we will implement, but it wasn’t the correct means to the end goal here.
So, springloading was out.
Hooking Up UIDropInteractionDelegate
Though bar items aren’t a UIView, it was clear that I needed some view to act as the delegate for the new drop protocol. With a tab bar controller, the tab bar itself does inherit from UIView. Revisiting Apple’s sample code, the normal flow goes something like this:
– Inherit from UIView
– Hook up the delegate methods in the subclass
– In your code, add a drop interaction onto the instance
That seems great, and normally that’s perfect, but tab bar controllers have a read only tab bar:
@property(nonatomic,readonly) UITabBar *tabBar NS_AVAILABLE_IOS(3_0);
While it may seem to complicate things at first, this isn’t as preventative as it may seem. We’ve got a UIView now, and that’s really all we need. It’s now just a matter of getting something to handle the delegate methods.
The Category Route
Essentially, one could solve this by creating a dedicated object to handle the delegate duties or offload the implementation to a category. I ended up taking the later approach, as we only have a single tab bar all throughout Buffer – and anytime it’s showing, it should have the drop functionality.
It ended up being as simple as that. All that’s really needed is to declare conformance in the category:
#import <UIKit/UIKit.h>;
@interface UITabBar (BFRDropDelegate) <UIDropInteractionDelegate>
@end
From there, handling the drop session is almost trivial. There is a delegate method that fires anytime a drag session is detected on the view:
- (UIDropProposal *)dropInteraction:(UIDropInteraction *)interaction sessionDidUpdate:(id)session
This is where you should be opinionated about what kind of proposal should return.
For us, we’re dealing with a tab bar that contains only one item that should trigger a drop. Fortunately, doing CGRect testing is lightweight. That’s essential here, because this method is called anytime the gesture recognizer of the drag changes its position:
- (UIDropProposal *)dropInteraction:(UIDropInteraction *)interaction sessionDidUpdate:(id)session {
// Avoids using private APIs to get the frame of a tab bar item
CGRect composerButtonRect = self.subviews[COMPOSER_INDEX].frame;
// See if we're dragging over that
CGPoint currentDropPoint = [session locationInView:self];
BOOL isDraggingOverComposeButton = CGRectContainsPoint(composerButtonRect, currentDropPoint);
// Respond accordingly
UIDropOperation currentProposal = isDraggingOverComposeButton ? UIDropOperationCopy : UIDropOperationCancel;
return [[UIDropProposal alloc] initWithDropOperation:currentProposal];
}
There is a little “sketch” here that can’t be avoided.
Recall that tab bar items aren’t views, thus, you have to drop down to a private API (UITabBarButton) to get a valid frame. I wanted to avoid that, so querying the vanilla subviews worked great. The downside is that if our composer button ever changes, or we otherwise mutate the bar items – we would need to revisit this.
Regardless, now that we’ve notified UIKit when a drop should be handled, we go ahead and do just that:
- (void)dropInteraction:(UIDropInteraction *)interaction performDrop:(id)session {
BFRComposerItemProviderHandler *handler = [[BFRComposerItemProviderHandler alloc] initWithDropSession:session completion:^ (BFRComposerViewController *composerController) {
//Present composer if everything worked, omitted for brevity
}];
[handler processItemsWithDropSession];
}
Since we also support a share extension that handles NSItemProvider
objects already, I refactored that logic away into a single place that can support extension contexts and drop sessions. Their logic is verbatim, so this worked out perfect.
If you’ve got a share extension already in your app binary which leads to the same experience as the drag and drop code you’re adding, this might be a good time to do the same. You’ll likely find that handling the UTI types and coercing their data to act on it runs similar to the code you’re working on now.
The last step was adding the drop interaction when we create the tab bar:
if (@available(iOS 11.0, *)) {
UIDropInteraction *dropinteraction = [[UIDropInteraction alloc] initWithDelegate:self.tabBarController.tabBar];
[self.tabBarController.tabBar addInteraction:dropinteraction];
}
The end result looks like this:
Wrapping Up
AS an iOS developer, what’s more fun than hacking on a fresh API from WWDC?
True, sometimes the documentation can be scant, other examples nonexistent and information scarce. That ended up being the case for wiring things up on a tab bar. But that’s part of the fun. I’m happy with how it turned out, and Apple also throws in things for free such a progress modal if the data transfer is taking a bit long (a cancel option included).
We’re pumped for iOS 11, and our users can expect Buffer to have drag and drop supported throughout the app. Heres to the next coding session with drag and drop!
Try Buffer for free
140,000+ small businesses like yours use Buffer to build their brand on social media every month
Get started nowRelated Articles
As part of our commitment to transparency and building in public, Buffer engineer Joe Birch shares how we’re doing this for our own GraphQL API via the use of GitHub Actions.
We recently launched a new feature at Buffer, called Ideas. With Ideas, you can store all your best ideas, tweak them until they’re ready, and drop them straight into your Buffer queue. Now that Ideas has launched in our web and mobile apps, we have some time to share some learnings from the development of this feature. In this blog post, we’ll dive into how we added support for URL highlighting to the Ideas Composer on Android, using Jetpack Compose. We started adopting Jetpack Compose into ou
With the surprising swap of Elasticsearch with Opensearch on AWS. Learn how the team at Buffer achieved secure access without AWS credentials.