SwiftUI on macOS: Drag and drop, and more
This article has a source code supplement. To read that in parallel, open this link in a separate window or tab so you can follow this account with its source code. I also provide a link below for you to download the complete Xcode project if you wish.
My previous articles (listed at the end) about implementing macOS apps using SwiftUI have explored its current limits and shortcomings, so it’s time to look at what SwiftUI should be good at. In this case, it’s providing a platform to drag and drop files to obtain information about them. While this could be implemented in a document-based AppKit app, it’s often not a good fit, and proves a bit more fiddly than it should be.
Fortunately, Javier of The SwiftUI Lab has written an excellent account of how to implement drag and drop in SwiftUI, and that’s the basis of the approach taken here, using SwiftUI’s DropDelegate protocol with NSItemProvider.
App
Doing a proper job using SwiftUI requires two extras: a Credits.rtf file to supplement the information provided in the app’s About window, and providing a Help document, also cast in Rich Text. I have already described how to implement both of those in previous projects in this series.
Dropera’s App source therefore starts with the standard WindowGroup for its ContentView, following which is the HelpView and the added Help menu command.
Help Commands
The change to the app’s menu commands simply adds Dropera Help to its Help menu.
Content View
This view depends on a State variable bound in the DropDelegate to pass a String array containing the file information for display in a ScrollView. Whenever that String array changes, the view will update itself. It’s also responsible for linking in the DropDelegate action when items are dropped on the active drop zone in the view.
Getting this to work properly is fiddly, and isn’t helped by the Preview, because this view relies on the DropDelegate action to populate the text in the ScrollView. To guide the user, a newly opened window is provided with a magnified image of doc.badge.plus from SF Symbols, and that’s the only content that’s shown in the Preview. Once the user has dropped the first file(s) on the view, their details replace that image.
The ScrollView simply joins the Strings in the array in fileInfo and lays out those lines in the scroller. To ensure that the scroller fills the view, the minimum dimensions of its frame are set, and the DropDelegate called when one or more items with file URLs are dropped onto that view. Most examples of drag and drop use drop zones of fixed size to display images. In this case, we want the whole of the window to work as the drop zone, and the scroller to resize with the window, requiring minimum dimensions or SwiftUI will make a new window with an empty fileInfo too small a target.
Drop Delegate
Apple’s documentation on the DropDelegate protocol points out that the only function that must be provided is performDrop(), called when the action takes place. The second function implemented here is validateDrop() to confirm that the item(s) dropped have file URLs, following Javier’s example code.
That example code plays system sounds to mark other phases of the drop, which are useful when testing, but unlikely to be something to be appreciated during use. AppKit conventions change the colour of the view background to indicate when the dragged items are over the active drop zone, but there doesn’t appear to be a straightforward way to implement that in SwiftUI. That’s not necessary anyway, as the pointer changes to display a green + symbol when it’s over the active drop zone, providing feedback to the user.
Although the DropDelegate protocol is straightforward to implement, it relies on the more opaque NSItemProvider, documented by Apple here. This uses UTTypes, here public.file-url for file (and folder) URLs, and an indirect method to extract the URLs of the items dropped. The code here iterates through the list of NSItemProviders, loading their file URLs using NSItemProvider’s loadItem(), documented here.
Apple draws attention to the background processing involved here: “The system uses an internal queue when calling the completion blocks for the NSItemProvider class. When using an item provider with drag and drop, ensure that UI updates take place on the main queue” using DispatchQueue.main.async. This is reinforced for the loadItem() function: “Call this method when you want to retrieve the item provider’s data. If the item provider object is able to provide data in the requested type, it does so and asynchronously executes your completionHandler block with the results. The block may be executed on a background thread.”
Here, changes made to the bound array of Strings are made on the main queue as recommended by Apple.
File Info
The performDrop() function assembles each string to contain the file’s name and path, with file size data obtained from a function of the FileInfo class. That code isn’t shown in the code supplement, but is provided in the downloadable source code. File information consists of three file sizes: one giving the data size, another (by an extension to the URL class) giving the total size of extended attributes, and the third giving the sum of data and xattrs for that file.
Help View
The Help View simply displays the Rich Text in Help.rtf as an AttributedString in a scroller, in the Help window, as previously shown. Although this doesn’t preserve layout, I wanted to avoid calling on AppKit in this example.
Full source and executable
The full source code for Dropera is available from here: droperasource
and the notarized executable app, which requires Sonoma, is available from here: droperaapp1
Outcome
At present, Dropera is a simple demonstration of how a basic macOS app can be implemented without relying on AppKit features, and I’m pleased and impressed by the result. However, much of this has been dependent on Javier’s article and invaluable app A Companion for SwiftUI rather than anything provided by Apple.
Even with that assistance, coding for SwiftUI feels like painting in watercolour, in that each line requires careful thought and sometimes repeated experiment. The end result is concise, even elegant in places, but getting there can be a slow process. This isn’t helped by the limitations of Xcode’s view Previews, which so often turn out to be non-contributory in the design and implementation of the interface. There seems to be no obvious solution to that.
Having got a useful basic app up and running, in future articles I intend to see how its interface can be advanced, which is after all one of the big goals of SwiftUI.
As ever, if you can spot the errors in my code and correct them, I’ll be deeply grateful.
Previous articles
SwiftUI on macOS: Life Cycle and AppDelegate
SwiftUI on macOS: Life Cycle and App Delegate source code
SwiftUI on macOS: PDF Help book
SwiftUI on macOS: PDF Help book source code
SwiftUI on macOS: Settings, defaults and About
SwiftUI on macOS: Settings, defaults and About source code
SwiftUI on macOS: text, rich text, markdown, html and PDF views
SwiftUI on macOS: text, rich text, markdown, html and PDF views source code
SwiftUI on macOS: Documents
SwiftUI on macOS: Documents source code