Updated on 20 August 2018

Figure 1: Dragging-and-Dropping multiple files into a window from the Finder.
Figure 1: Dragging-and-Dropping multiple files into a window from the Finder.

File handling can be accomplished through scripts and the command line but there is a satisfaction and practicality to the desktop metaphor that cannot be denied. This post is about implementing multiple file drag-and-drop, the non-deprecated way, from the Finder in macOS with F#.

What can you get from this?

A real Mac app created with Visual Studio for Mac and Xamarin that can process multiple files dragged into it from the Finder.

Who is this written for?

This article is written for someone having at least a little iOS/macOS SDK experience and who as an interest in life outside of the beautiful, walled garden of those SDKs.

Why F#?

Statically typed functional programming (FP) tends to lead to more correct software due to its logic being being more mathematical along with some additional benefits including the following:

  • The compiler becomes your friend because of strong types.
  • Favoring pure functions reduces side-effects, that can make debugging/maintenance difficult.
  • Reduction in the overall quantity of code.

FP may also expand your ingrained imperative programming thought processes so that working with functional paradigms becomes more natural.

Of all the functional languages, F# allows targeting perhaps the widest range of mainstream platforms (iOS/Android/macOS/Windows) with native code. This increases its practical value.

And if you can handle sarcasm, there are even more reasons!

Implementing drag-and-drop in F#

When searching for multi-file drag-and-drop examples, you may come across the deprecated global variable NSFilenamesPboardType. The equivalent Xamarin symbol is NSPasteboard.NSFilenamesType. Since the former symbol does not exist in Swift 4, some have suggested a workaround of hardcoding the string as “NSFilenamesPboardType” or using the alternate string kUTTypeFileURL.

In conclusion, using one of these strings

  • NSFilenamesPboardType
  • "NSFilenamesPboardType"
  • NSPasteboard.NSFilenamesType
  • kUTTypeFileURL

in registerForDraggedTypes(_ newTypes: [NSPasteboard.PasteboardType]) should allow an NSDraggingDestination receiver, such as an NSView or NSCollectionView, access to multiple dragged files.

However, the global variable NSFilenamesPboardType is deprecated according to Apple thereby suggesting that an alternative should be sought.

The symbol appears to have been introduced in 10.10, deprecated since 10.13 and will be removed after 10.14. The immediate documentation on the variable wasn’t a help in knowing what to use in place of it.

However, a diff of NSPasteboard.h kindly provided by Matt Stevens has a suggestion that leads to a solution.

  Availability Deprecation Message
From Available none
To Deprecated Create multiple pasteboard items with NSPasteboardTypeFileURL or kUTTypeFileURL instead

For convenience, it would be nice to get the file URLs into an array. That requires handing the URL type to readObjects(forClasses:options:). The code is shown here as parting of draggingEnded on the NSDraggingDestination implemented in DNDView.fs.

 1 // DNDView.fs
 2 
 3     override this.DraggingEnded(sender) =
 4         let dict = new NSDictionary()
 5         let t = typeof<NSUrl>
 6         let types: Class[] = [| new Class(t) |]
 7         let objs =
 8             sender
 9                 .DraggingPasteboard
10                 .ReadObjectsForClasses(types, dict)
11         for o in objs do
12             this.filecnt <- this.filecnt + 1
13             this.updatefn(this.filecnt)
14             printfn "%s" (o :?> NSUrl).Path // log the paths

Overriding an init in the macOS SDK

In getting started with the surrounding app that supports the drag-and-drop operations, the trickiest part was getting the constructor to override init(coder:), the init called when bringing up a view from the storyboard.

F# has many different constructor forms and I was not able to achieve the override by having a primary constructor even when setting a matching argument. The following code, for example, was not successful.

1 // DNDView.fs
2 
3 // This primary constructor did not work to override init(coder:).
4 [<Register("DNDView")>]
5 type DNDView(coder: NSCoder) =
6     inherit NSView(coder)

Instead, the problem can be solved with an explicit constructor form. This differs from having a single, primary constructor by not setting any arguments in the initial constructor. This allows multiple init overrides even though I only needed to override one.

 1 // DNDView.fs
 2 
 3 // The primary constructor is generic here.
 4 [<Register("DNDView")>]
 5 type SNKView =
 6     inherit NSView
 7 
 8     // Explicit constructors can override the needed macOS SDK inits.
 9     [<Export("initWithCoder:")>]
10     new(coder: NSCoder) =
11         {
12             inherit NSView(coder);
13         }

The export attribute inside the square and angled brackets, [< >], tells the compiler what init to override. Note that it uses the Objective-C form of writing method signatures where the word “with” is included.

Many Xamarin usages in F# involve translating from C# and that entails constraints that limit how something can be written. The full range of the F# language may not be available when it comes to translating code directly. For this case of constructor overriding, I don’t believe there is an another way to write this.

Making the user interface

In contrast to the previous explicit constructor form, a view controller for the UI can be made using the primary form where it takes a single argument. It has a few text fields where one of them is updated with a count of the number of files that have been dropped into the app. These are now, with the explicit constructor form, defined as mutable let bindings and then added as subviews in a, hopefully, familiar viewDidLoad override.

 1 // ViewController.fs
 2 
 3 [<Register("ViewController")>]
 4 type ViewController(handle: IntPtr) =
 5     inherit NSViewController(handle)
 6 
 7     [<DefaultValue>]
 8     val mutable tbHeight: float
 9 
10     let mutable descText =
11         new NSTextField
12             (
13                 Frame = new CGRect (0.0, 0.0, 100.0, 20.0),
14                 StringValue = "File Count",
15                 Editable = false,
16                 Bezeled = false,
17                 DrawsBackground = false,
18                 Selectable = false
19             )
20     let mutable cntText =
21         new NSTextField
22             (
23                 Frame = new CGRect (0.0, 0.0, 100.0, 20.0),
24                 StringValue = "",
25                 Editable = false,
26                 Bezeled = false,
27                 DrawsBackground = true,
28                 Selectable = false
29             )
30 
31     [<Outlet>]
32     member this.CountField with get() = cntText
33                            and set value = cntText <- value
34 
35     override this.ViewDidLoad() =
36         base.ViewDidLoad()
37         let v = this.View :?> SNKView
38         this.View.AddSubview (descText)
39         this.View.AddSubview (cntText)
40         this.CountField.StringValue <- (string)v.FileCount
41         v.UpdateView <- fun cnt -> this.CountField.StringValue <- (string)cnt

Passing data between components

The text field cntText has an outlet that defines a getter and setter to be able to update the field with the count of files.

The goal here is to have a loosely coupled architecture, allowing data to be passed between components while minimizing dependencies. In general, for MVC, views should not depend on the controllers using them.

Delegation is a solution to this problem and is used throughout the iOS/macOS SDKs. It promotes composition over inheritance as a way of extending classes. Since F# is a functional language, first class functions can be used to implement delegation without the additional overhead of delegate protocols.

The key idea of delegation is that the receiver can reference the self of the sender. I’ve left out that reference as I did not have a need for the full delegation pattern. For reference, there are nice comparative examples of delegation at Rosetta Code.

For my solution, it is the following line inside the view controller that passes in an anonymous function to update the text field containing the count of files.

1 // ViewController.fs
2 
3         v.UpdateView <- fun cnt -> this.CountField.StringValue <- (string)cnt

This reads as the view’s update member, UpdateView, gets a lambda function with an argument cnt that sets the string value of the label showing the count. That’s astounding clarity for complex functionality that might take more code to express in other environments.

That’s it! The DND view stays independent from the view controller and can be used anywhere while being able to update whoever decides to adopt it.

Let bindings vs vals

One F# language constraint is that the constructor form determines if let bindings are available in a class. When using explicit constructors, val is used instead of let to configure private backing variables or uninitialized fields for its members. These members can then be accessed externally.

1 // DNDView.fs
2 
3     val mutable private filecnt: int
4     val mutable private updatefn: int -> unit

These vals serve as the backing stores for the members (properties) defined in the following code.

1 // DNDView.fs
2 
3     member x.FileCount
4         with get() = x.filecnt
5         and set(value: int) = x.filecnt <- value
6     member x.UpdateView
7         with set(fn) = x.updatefn <- fn

The mutable vals can be function types indicated by a compact signature of the form: type -> type.

All together, the DNDView looks like the following code, showing the property definitions along with method overrides where the dragged types are handled. The snippet leaves out the draggingEnded method previously listed.

 1 // DNDView.fs
 2 
 3 [<Register("DNDView")>]
 4 type DNDView =
 5     inherit NSView
 6 
 7     val mutable private filecnt: int
 8     val mutable private updatefn: int -> unit
 9 
10     [<Export("initWithCoder:")>]
11     new(coder: NSCoder) =
12         {
13             inherit NSView(coder);
14             filecnt = 0
15             updatefn = fun cnt -> ()
16         }
17 
18     member x.FileCount with get() = x.filecnt
19                        and set(value: int) = x.filecnt <- value
20     member x.UpdateView with set(fn) = x.updatefn <- fn
21 
22     override this.AwakeFromNib() =
23         base.AwakeFromNib()
24         let types = (string)NSPasteboard.NSPasteboardTypeFileUrl
25         this.RegisterForDraggedTypes [| types |]
26 
27     override this.DraggingEntered(sender) =
28         NSDragOperation.Copy

Handling a programmatic UI

For this example, I chose to generate a UI programmatically. I know it is possible to use the storyboard but I still have yet to determine the optimal method for using one with F#.

The goal was to have a minimal, but functional, layout. The controls were recalculated based on the window size. NSViews in general have their origin at the lower left, this is different from iOS where it is the upper left corner. Performing the layout in viewDidLayout of the view controller allowed for maintaining the layout during such changes as window resizing. The additional code added to the view controller for this is shown below.

 1 // ViewController.fs
 2 
 3     override this.ViewDidLayout() =
 4         base.ViewDidLayout()
 5         layoutViews(this, cntText, descText)
 6 
 7     let layoutViews(this: ViewController,
 8                     cntText: NSTextField,
 9                     descText: NSTextField) =
10             let view_h = (float)this.View.Frame.Height
11             let margin = 10.0
12             descText.Frame <- CGRect
13                 (
14                     margin,
15                     view_h
16                   - this.tbHeight
17                   - margin,
18                     (float)descText.Frame.Width,
19                     (float)descText.Frame.Height
20                 )
21             cntText.Frame <- CGRect
22                 (
23                     margin,
24                     view_h
25                   - this.tbHeight
26                   - margin
27                   - (float)descText.Frame.Height,
28                     (float)cntText.Frame.Width,
29                     (float)cntText.Frame.Height
30                 )

I really like the sub-indenting with offside formatting for mathematically arranging expressions with a binary operator, as seen above.

Refining the dynamic layout

A window controller subclass was used solely for the purpose of being able to obtain the precise height value of the windows title bar by getting contentRect(forBounds:) and subtracting the content view frame height. The values are in units of abstract points, not pixels. The title bar height is saved into the VC.

 1 // WindowController.fs
 2 
 3 [<Register("WindowController")>]
 4 type WindowController =
 5     inherit NSWindowController
 6 
 7     [<Export("initWithCoder:")>]
 8     new(coder: NSCoder) =
 9         {
10             inherit NSWindowController(coder);
11         }
12 
13     override this.WindowDidLoad() =
14         base.WindowDidLoad()
15         let vc = this.Window.ContentViewController :?> ViewController
16         let crh = (float)(this.Window.ContentRectFor(this.Window.ContentView.Frame).Height)
17         vc.tbHeight <- (float)this.Window.ContentView.Frame.Height - crh

Conclusion

This example demonstrated how to create a Mac app that handles multiple files dragged in from the Finder, using a non-deprecated approach, while using the F# language. It consisted of three classes: ViewController, WindowController, and DNDView were the latter conformed to NSDraggingDestination.

Working in F# for macOS requires integrated knowledge that spans multiple languages. I recommend exploring it to learn functional programming principles that can lead to practical applications. It has an elegance that can benefit everything else you do. Stepping outside of the beautiful, walled garden of iOS/macOS can be scary at first, but the perspective gained will give you wings to fly over many technical challenges.

F# reduces syntax with a mathematical kind of offside-rule formatting. It is indentation based where whitespace is significant like Python but function oriented like Scala. There are no braces for code blocks and no return statements for functions. After awhile the extra syntax required in other languages may seem strange when you have to use it again.

It’s practically required to also know C#, the home language of most Xamarin examples and the assembly browser in Visual Studio for Mac. Being two to three levels removed from native iOS/macOS gives a unique perspective that can deepen understanding of diverse software principles.

Just like with statically typed Swift, the compiler can light your way. Following its hints and errors allowed me to know enough about what is allowed to develop the project in a way that was more like solving an algebraic puzzle by satisfying one constraint at a time rather than writing out the verbose steps of an imperative recipe.

Finally, this example was only meant as an introduction and there are many possible paths to be explored.



blog comments powered by Disqus