Add macOS Drag and Drop Support to a React Native App

Project Source Code

Get the project source code below, and follow along with the lesson material.

Download Project Source Code

To set up the project on your local machine, please follow the directions provided in the README.md file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.

This lesson preview is part of the Building React Native Apps for Mac course and can be unlocked immediately with a \newline Pro subscription or a single-time purchase. Already have access to this course? Log in here.

This video is available to students only
Unlock This Course

Get unlimited access to Building React Native Apps for Mac, plus 70+ \newline books, guides and courses with the \newline Pro subscription.

Thumbnail for the \newline course Building React Native Apps for Mac

On this lesson, we will implement our most complex feature, drag and drop on the OS level. It's fairly difficult and it be able to know the knowledge that we acquired, so we will start with the native side of things. Now, before we start, we need to talk about how drag and drop works on macOS. First, we need to register our application for different types. That is, we need to tell macOS which elements are dropable. Unlike the web counterparts, the dropable interfaces are very powerful. We can register our app to take objects, string, specific file types, etc. One important tidbit is that drag and drop from another program. There, we will not receive the usual file types, but we will receive a promise- like object which we need to resolve on our own. This is already abstract, so I think it's better if we just start coding. First, we need to register our app for the drop event. In order to do this, I'm going to go into the app delegate. I'm going to add one variable, which is... give me one second. Let me just copy the code for a little bit. Where I initialize my status bar button, I'm just going to copy the code in here. I have created a new variable which are my register dragged types. This is an array of pay-sport types. Like I mentioned, we can register for different types, and this is the interface that macOS uses to know which elements should be registered. We're going to tell macOS that our application can take in URLs, file URLs, PNG files, and strings. There's a lot more types, so if you want, you can explore this, the documentation for this class, but this is enough for our purposes. Then we will also register a file promise receiver type. This is, like mentioned, whenever we drag from another application, let's say from your web browser, we don't get any of the basic types, but we actually get a special pay-sport type. I'm just going to append my file promise receiver into my register drag types. Then we can take care of registering our status bar button. I'm just going to take this piece of code, and I'm just going to replace it for my previous implementation. Not much has changed, as you can see, the action and the title are still the same, but I am calling this method, which is part of the styles bar item button, which is where I'm telling macOS this button should receive any drag and drop event. Great. In order to make our life easy, we could immediately start coding and start handling to the events, but we're going to create some helper classes, which are just going to help us in some of the more annoying tasks, right? I'm going to create a file, and I'm going to call this URL extension. Inside of here, I'm just going to take the code from the lesson. I'm going to create an array of image extensions, PNG, JPEG, GIF, JPG. I should also do, I don't know, it doesn't matter. You can create any extension in here. You can create any extension in here. You can create any extension in here. You can do SVG files, if you want, for example. I'm going to create an extension to the base URL object. In Swift, you can easily extend the native classes by using this modifier. You can just say on the base URL class, which is available throughout your application, now I'm going to add these two variables to any URL object. The first variable that I'm going to add is image, which is a Boolean. I'm just going to check if I have a URL inside of this object. I'm going to check for its path extension, going to lowercase it, just in case, because they are still valid if these are uppercase. I'm just going to check if it's part of my image array. This is going to allow me to tell if I have a URL, which could be a file in my system, if it's an image or not. Then I'm going to add another variable, which is the local file, which does something fairly simple. I'm just going to check for the scheme, if it's file. The scheme is, if you have a URL, it could be HTTP. It could be HTTPS. It could be file. It could be any other protocol you can think of. It could be a Git, for example, or something like that. I'm just going to check if it's a file in my system. Then I'm going to create another extension, this time for the file manager. Again, with class, I'm going to say file manager extension. Once again, I'm just going to take the code from the lesson, because it's quite long. Let's walk over it. I'm extending my file manager, and I'm creating two functions, the extract, wherefrom, and the secure copy item. Now, the extract wherefrom is what we're going to use for this type of special promise files. The promise file, it could be because it's a file from the web browser. Before we can save it to the disk, we're going to receive a promise. We need to take that promise and save it into the disk, and then we can move it into our application. No, wait, that is wrong. What we're going to do. I have my extension for the file manager class, which is a class that we will use once we are trying to extract files. I have a special function in here, which is called the extract wherefrom, and I have a path. What this is going to allow me to do is to trace back where a file has been retrieved from. Let's say if I drag and drop an image from my browser into my app, then I want to know the original URL. It could be, I could have taken it from Facebook, I could have taken it from Instagram, so this might be useful for the user to know. This is just very internal macOS data. Whenever you save a file, it has some attributes, and upon this attribute, there is this special key, which is the metadata. KMDI, 8 item, wherefroms. This is where macOS is going to store this data. Basically, I'm just extracting this special key. Next, I have created a secure copy item function that is going to check before moving files, if the file exists. Then I'm going to try to remove it from the destination URL, and I'm going to copy it again. This is just a way to make sure that my copy operation doesn't fail. I'm going to save that, and I'm going to create one more file, which is the file constant file. This is just going to be some, let me just paste it. One, it's going to be my document URL, because we are running our application in sandboxing mode. I cannot quite easily put the file wherever I want. I also want to have a protected directory within my application where the user might not delete stuff while it's being uploaded, or where I am manipulating somehow. I'm just going to put this as a variable so I can easily access it afterwards. Then I'm going to create a queue. We have visited queues before in the previous lesson where we talk about the main queue. Remember, whenever you want to do any UI operation, you have to do it from the main queue, the main thread. I'm going to create a new operation queue. This operation queue is going to have a quality of service that is the macOS system might decide to lower the priority to give more resources to the main thread, to update in the UI, while reducing the speed of this queue. I'm going to use this queue to copy files and do other operations which are not vital for the user. Great. Now, we can start with these files, we can actually start handling our drop event. In order to do this, let me just copy the contents of the file. I am going to create one final file and I'm going to call this the NS status bar button extension. I'm just going to copy the file. Let's walk over this because this is actually the most complicated file that we have seen so far. So, once again, I'm extending my NS status bar button, which is the same one that we had on our app delegate. We've registered for the drag types, which is this button that appears on my status bar. I'm going to extend it. And since now it has been registered for drag event, there are some functions that we need to override. First is the prepare for dragging operation, which basically says, yes, this button is ready to be to accept dropped event. So, I'm just going to return true to that. Then we have the on dragging entered function, which again, whenever the user has clicked on an object and has begun the drag operation and is currently hovering the file or the string or whatever inside of our button, then this function gets called. So, what we're going to do is tell the button to highlight itself, right? We need to give the user some feedback. The dragging exit function is similar. If the user is no longer holding the file on top of our button, we need to turn the highlight in off. And finally, the dragging ended function. If the user had the file and then releases the mouse, we should tell again our button to un-highlight itself. And then comes the main operation, which is performing our drag operation. Now, bear with me, this will get a little bit more complex. I included some comments or actually is quite well-commented. And there are many, many operations that could happen during the drag operation . For example, we have the is command press on drop variable. And this is just some internal macOS flags and logics. You will need to read the documentation if you need to use them. If you need to, for example, modify the behavior. Whenever you're dragging and dropping and the user holds the command button, you might want to do something different. You might want to move the file instead of copying it. You maybe want to redirect the UI directly into a creation page, for example. So you can actually apply certain masks. You can hear, you can get the state of the keyboard while dragging. So this is just an example. We're not going to use it, but I put it in there for you in case you want to do something like that. So then we can actually check if the items or any item has been dropped into our button. In order to do this, I'm going to access the sender, dragging in, dragging in PaySport. And I'm going to read the objects from that PaySport. I'm going to once again extract the classes. So I had my PromiseReceiver, I had a URL, and I had a string. And this is going to be placed into the objects variable. Now, we have a bunch of PaySport files. This is not directly the file reference, or it may not always be. And they will be saved into a temporary directory. The temporary directory, if you restart your computer, they're going to be lost . So what we want to do is move them into our Documents folder. In our Documents folder, they're completely safe. We can upload them. If something happens and we restart our computer, they're still going to be there. We can retry the upload, etc. etc. So that's what we're going to take care of. So on my Objects array, I'm going to do a forEach, and I'm going to get each object in a function. Now, I'm going to try to cast my object as a FilePromise. If it's a FilePromise, I'm going to handle it one way. Then I'm going to try to cast it as a URL. I'm going to do a different way if it's a URL. And I could keep doing this. I could try to cast it as a string. I could try to cast it as a different class. It doesn't matter. This is how I'm going to deal with each type of object that I receive. And finally, after I have handled my file, I'm going to do the same thing that I did before. We already learned how to toggle our popover from our native code. I'm just going to tell my application to start. Finally, I have to return true for the Performed drag operation. I'm basically telling the OS, "Yes, I am handling this event. You don't have to do it for me. I have my own internal logic." Great. So, let's just go over the case where I have received a FilePromise. I'm going to have to cast my object as a FilePromise receiver. And I'm going to tell it to receive the Promise files. Once again, this is the case where I want to drop something from my web browser , for example. It's not on disks yet. It's on the web page. And it might need to be downloaded . So, I'm telling my Quest, "Take this promise. I'm going to give you a destination where you should save it. I'm going to give you a queue for you to do it with a certain priority. And just put it in there." So, that's basically what we're doing. We're passing our Document URL, the internal sandbox folder of our application. And my Quest is going to do this for us. Then we need to pass a Lambda function. So, again, if you see this in, these are Lambda functions. These are very similar to the JavaScript ones. This Lambda function is going to take two parameters. One is a File URL, and the second one is the error. If we have an error, something went wrong receiving the file. Maybe the connection dropped, maybe the disk is full. It doesn't matter. Something happened, and we cannot operate on that file. However, if it correctly saved, then we can actually start manipulating that file. So, here I'm going to go into my File Manager. We already saw this pattern. Some of these classes have a default instance, which is already running on the OS. And I'm going to call the function that I added on my File Manager extension. I want to know where did this file was downloaded from? Or which one was the application, which one was the website? Where did I get this object from? Then I'm going to check if this file URL is a local file, just to make sure that I could also have dragged a file, but somehow it was turned into a promise. I'm just going to replace the occurrence of the scheme with an empty string, just because sometimes something weird could happen. Now, I have commented this code for now because we're not going to deal with these promises files. At this point, this just becomes a normal file. The code that we have down here will also apply for this case. But I just wanted you to see there are many different types of file types that my Quest has to deal with. But basically, the next step would be now that I have my file and it saved all my Documents folder, I have to pass the data into the JavaScript side of things, which is what we will do in a little bit for now. This should be the next step. Now, let's look into a different type of object that could have been dropped into our application. The next type is URL. So, if I have a URL, the first thing that I'm going to do is I'm going to check if it's a local file. Remember, we added the URL on the URL object, our extension is added in this property. Then, I'm going to take the file name because this is important for me. I want to display it to the user in my application. Then, this URL, for example, if I would drag something from my desktop and I drag it into my application, I'm just going to get the URL from my desktop. So, it's fairly unsafe if I try to do any operation on that document because the user might delete it while I'm doing the operation, right? While I'm uploading the file, maybe while I'm copying it, it doesn't matter. So, what I want to do is I want to copy, this is why we had the secure copy item function, from the original URL into our document sandbox. I'm going to check if the item was correctly copied. If it was not copied, then it's better not to operate on it. The user might lose some data and that's not what we want. Then, I'm going to get the original location. I'm just going to replace the schema because that's not very useful to me. Then, finally, once again, the last step would be not define my React Native application, the JavaScript side of things, that the file has been dropped. I pass its original URL in case the user or in case we want to display it to the user. I pass the final URL, which should be the direction on my sandbox documents folder. I pass a name, I pass its extension, and I pass the command flag drop, right? In case I want to modify the behavior of my program whenever the user drags something, this is just very advanced functionality, right? It's also kind of hard to communicate to the user that he could do this, but we're doing this just as an exercise, just for you to see what are the possibilities of having access to the native APIs. That's it. Now, you will see I'm getting an error right now, which is this class is not found. This is why we will take a look into our next lesson, is sharing or emitting events, right? So on our JavaScript side of things, we will start registering listeners, and whenever something happens on the native side, a user or a file could be drag and drop, some data might be fetched, some system event might happen, we want to react to those events. So we will achieve that through listening for the emitter. [ Silence ]