macOS programming · · 14 min read

Beginning macOS Programming: Learn to Develop an Image Uploader App in Swift

Beginning macOS Programming: Learn to Develop an Image Uploader App in Swift

Do you want to learn how to develop your very own macOS app that you can be proud of and use it on your personal MacBook? Or maybe you have a stirring passion to start developing on Mac? Then you are at the right place! Here, I will walk you through the steps in developing what could be your first very macOS app with one of the most modern languages, Swift!

Pre-requisites

  • Have some interest in programming
  • Have some basic understanding of Swift-programming (advantage)
  • Xcode 9 installed
  • Passion to build a macOS app

What will you learn?

  • Basic concepts of macOS development
  • How to integrate Alamofire with macOS app to perform network calls
  • How to create a drag and drop mechanic
  • Some Swift 3.2 syntax

What will we build?

I’m sure you are excited to know what good stuff we will be building! In this tutorial, we will be working on Mac’s main application layer, Cocoa. This layer is responsible for the appearance and user actions’ responsiveness of the app which is also where we will be introducing all our visual elements, networking, and app logics. We will upload our images to uploads.im as it provides an open API for us to use. This will be the end product after going through this entire tutorial,

pushimage-demo

The user will first be presented with a home screen with instruction saying “Drag and Drop your image here”. Then the user can drag any jpg format image into the app, then the app will present a loading spinner to inform the user that it is uploading the image to server. Once the server respond with success, the user will get a pop-up alert window where he/she can copy URL to clipboard, then he/she can paste the URL anywhere, like on a internet browser to view his/her image from the server.

Enough of talking, let’s get started!

Creating your macOS project

First, let’s launch our Xcode 9 and create your macOS app project call PushImage. Choose macOS and Cocoa App under Application.

You can follow the settings I put in the next page as follow, then choose a directory of your choice for your project files.

Now that you have your project all setup, I want to take this chance to introduce one of my favourite new feature in Xcode 9, Refactor! If you have used the past versions of Xcode, refactoring of Swift code is not possible. Let’s give it a shot. Highlight ViewController and go to Editor->Refactor.

I don’t want to use the default name ViewController. Let’s change it to HomeViewController and click Rename. This will perform a global change to your filename, class name and Storyboard viewcontroller class name, how cool is that!

Going back to our HomeViewController class, you would have noticed that this controller is a subclass of NSViewController. If you come from developing iOS apps, you would have straight away notice the difference that we are using NS and not UI here. That’s because we are using Cocoa (AppKit) and not Cocoa Touch (UIKit). I believe Apple made these 2 distinct frameworks mainly to separate the technologies behind mobile and OS. In short, UIKit is a slimmed down version of AppKit.

Creating our Home Page

You can download a cloud image icon from this link. Once you have the asset, drag it into Assets.xcassets and lets rename is to uploadCloud and move the asset to 2x.

Next, let’s move to our Main.Storyboard to create our view which user will interact with. We will be using Xcode’s beautiful interface builder to design the looks of our home page. In the bottom right container, go to the third item which is an object library and search for NSImageView, then drag in into the center of the HomeViewController view.

Now, click the NSImageView you just dragged in once, and go to the fourth item, which is the Attributes Inspector to set the Image to uploadCloud. Because in our last step, we have renamed it to uploadCloud, Xcode automatically registers the name and we can use it directly by inserting the name here without the need to specify its file extensions. One of the wonders of Xcode!

Let’s also increase the width and height a little so that it will look nicer as a whole. Click the fifth item in the right (Utilities) panel. which is the Size Inspector and increase its width and height to 150x150.

We will also need a text to tell user what they need to do, so now go ahead and search for label and put in below the imageView. Again, go to the Utilities panel, under Attributes Inspector, set the Title as Drag and Drop a .jpg image file here. You can see that the text is totally truncated, let’s increase the width to 300 in Size Inspector and set the Text Alignment to be Center-aligned.

Let’s now align both the imageView and the label to the center of the window by first clicking on the imageView, then hold CMD and click on the label. This should let you highlight both, then drag them till you see a cross, which the the guiding lines for you to accurately guage if the components are centered.

The second last UI component we want to add is the Indicator. Go ahead and search for it and drag it into the window, place it at the center of your label.

The final component we need is our most important piece, the DragView! We need a View that act as a droppable area in our app. So following the same ritual of searching for components, search for NSView and drag it in, then extend it such that it fills the entire window. The Storyboard ordered the layers from bottom up or LastInFirstOut concept. So now, our view covers all the other view components. Later on, we will set the view such that it has a transparent background, and it won’t block any view underneath it.

Way to go! That is all the components we needed to add into our Storyboard, let’s now connect Outlets, which are references the Interface Builder used to connect to a property declared in its ViewController.

With our Storyboard as our active window, we can open a separate window showing our HomeViewController class by activating Assistant Editor located at the top right.

Holding ctrl, click on ImageView and drag it just below our class declaration and let’s call it imageView. Great! Now your outlet for the imageView is connected to a property in our class, we can now programmatically adjust its propertise and work with it.

Challenge Time

Now, try doing the same for the view, label and the indicator, let’s call them dragView, staticLabel and loadingSpinner respectively. Your code should look like this at the end.

import Cocoa

class HomeViewController: NSViewController {
    @IBOutlet weak var imageView: NSImageView!
    @IBOutlet weak var staticLabel: NSTextField!
    @IBOutlet weak var loadingSpinner: NSProgressIndicator!
    @IBOutlet weak var dragView: NSView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }

    override var representedObject: Any? {
        didSet {
        // Update the view, if already loaded.
        }
    }
}

To get back some space in our window, let’s go back to Standard Editor by clicking the first item at the top right and we will move to our next mission.

Creating our Drag View

Our next mission is really to make our DragView a proper “drop-off” point for our .jpg image files. Go ahead and right-click on your root folder and choose New File. Choose Cocoa Class and create a new DragView subclass of NSView. We will also need to go back to our Storyboard and set our DragView’s class as DragView.

Then go back to our HomeViewController and change NSView! to DragView!:

@IBOutlet weak var dragView: DragView!

If we look closely at NSView, we found out that it actually automatically conforms to NSDraggingDestination protocol which is exactly what we need to make our “drop-off” space.

Before we can use all the draggable methods, we first need to register our view. Replace the code in the view class with this:

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        registerForDraggedTypes([NSPasteboard.PasteboardType
            .fileNameType(forPathExtension: ".jpg")])
    }

This line of code registers the view for any dragged item thrown inside the app with the file extension of “.jpg”. All we did is registering, our view is not ready to accept files just yet.

Following the NSDraggingDestination documentation, we can conclude that we need these functions to achieve what we want:

So let’s go ahead and add these chunk of code. I will explain what they do :

import Cocoa

class DragView: NSView {
        
    //1
    private var fileTypeIsOk = false
    private var acceptedFileExtensions = ["jpg"]
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        register(forDraggedTypes: [NSFilenamesPboardType])
    }
    
    //2
    override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
        fileTypeIsOk = checkExtension(drag: sender)
        return []
    }
    
    //3
    override func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation {
        return fileTypeIsOk ? .copy : []
    }
    
    //4
    override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
        guard let draggedFileURL = sender.draggedFileURL else {
            return false
        }
        
        return true
    }
    
    //5
    fileprivate func checkExtension(drag: NSDraggingInfo) -> Bool {
        guard let fileExtension = drag.draggedFileURL?.pathExtension?.lowercased() else {
            return false
        }
        
        return acceptedFileExtensions.contains(fileExtension)
    }

}

//6
extension NSDraggingInfo {
    var draggedFileURL: NSURL? {
        let filenames = draggingPasteboard().propertyList(forType: NSFilenamesPboardType) as? [String]
        let path = filenames?.first
        
        return path.map(NSURL.init)
    }
}

1 – Firstly, we need to create a boolean flag call fileTypeIsOk, defaulted to false to help us push forward only the right file format of our images. We also create a acceptedFileExtensions which is an array of acceptable file format in string.

2draggingEntered function will be called when the file first enter the “drop area”. Here, we will call a function checkExtension which we will discuss later, to set our fileTypeIsOk boolean to true if the file type is of .jpg or false if it is not.

3draggingUpdated function is implemented here to get the details of the image. In this case, if the fileTypeIsOk, we will return the copy of the image, else it will return an empty data represented by [].

4performDragOperation function is called once the user releases his mouse, we will make use of this function to pass the url to our HomeViewController later.

5checkExtension is our “home-made” function where we check our drag object, grab the url of the file coming in, and check if it complies with our acceptedFileExtensions.

6 – Here, we extend our NSDraggingInfo which is actually all the senders we see in our draggingEntered and draggingUpdated. We added a variable call draggedFileURL here to reference the url of our image file.

If you run the app now, you should be able to drag in an image file with .jpg and see a green + sign at your cursor, but not for other file types. Great! Now that we know our app only accepts a specific file type correctly, let’s move on to establishing a communication between the view and our controller.

Creating our Delegate

Delegation Pattern is one of the most commonly seen patterns in Cocoa programming. It is like creating a command center to broadcast what A has done, so now B should do this. In short, we will be creating these:

  • Our command center (DragViewDelegate)
  • Function call didDragFileWith
  • View to hold a reference to DragViewDelegate‘s subscribers and call didDragFileWith
  • ViewController to subscribe to DragViewDelegate to do something when didDragFileWith is called in the View.

So, let’s head back to our DragView and input this code on top of our class declaration just below import Cocoa:

protocol DragViewDelegate {
    func dragView(didDragFileWith URL: NSURL)
}

We create a delegate protocol here which defines the responsibilities that the delegate wants to command. And here the sole responsibility is when the dragView receive an event call dragViewdidDragFileWith, the delegate will be called, and subscribers will react. So let’s also create a variable reference for subscribers and put this just after the opening bracket of our class declaration:

class DragView: NSView {
    
    var delegate: DragViewDelegate?

We want to immediately inform our HomeViewController that a correct file is placed, so the best place for our delegate to broadcast will be in the performDragOperation after the user releases his drag event, and when all the checks are done. So go ahead and add this line of code:

    override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
        guard let draggedFileURL = sender.draggedFileURL else {
            return false
        }
        
        //call the delegate
        if fileTypeIsOk {
            delegate?.dragView(didDragFileWith: draggedFileURL)
        }
        
        return true
    }

Now let’s head back to our HomeViewController and extend our class to implement our delegate method. Go ahead and add these lines of code:

extension HomeViewController: DragViewDelegate {
    func dragView(didDragFileWith URL: NSURL) {
        print(URL.absoluteString)
    }
}

If we run the app now, we will expect to see our file URL being printed in our console, let’s try it!

Hmmm… nothing happens when we drag our .jpg files in, did we miss out anything? Yes! We need our HomeViewController to subscribe to our View‘s delegate. This is a common omissions where developers forgot to add the subscriber, so lets go ahead and fix that. Add this in viewDidLoad():

    override func viewDidLoad() {
        super.viewDidLoad()
        dragView.delegate = self
    }

After you done that, run the app again and you will see the URL printed. Great Job! You deserved a big pat on your back for coming this far. We are almost there, we can now push our files up to the server!

Integrating Alamofire

Alamofire is a powerful networking library written in Swift. Formerly known as AFNetworking, it is the most well maintained networking library to date.

Installing Alamofire using CocoaPods

Cocoapods helps to define our project’s dependencies all in one file and it automatically links them to our Xcode project by generating a workspace file. If you have no prior experience with Cocoapods, please check out this tutorial.

We first need to create a PodFile. You can open up Terminal and navigate to the project directory ({ROOT}/PushImage). Then run pod init. Now that our Podfile is created, go ahead and open PodFile and fill them with this code:

platform :osx, '10.10'
target 'PushImage' do
use_frameworks!

  # Pods for PushImage
    pod 'Alamofire', '~> 4.5'

end

Next run pod install. This will clone the dependencies and you should get Alamofire framework linked to a newly created workspace file. You should see this screen at the end:

cocoapods alamofire

Close your current project and from now on we will work on Cocoapod’s newly created PushImage.xcworkspace.

Way to go! You now have your networking library all set up!

Making our network call to uploads.im

Following the api document written by Uploads.im, we will be making use the following API to do our POST just like the example given in the document as well.

  • upload – URL or image sent by POST method. The system automatically detects that you upload.

So let’s go ahead and import the library:

import Cocoa
import Alamofire

And then, add these lines of codes in dragView function:

extension HomeViewController: DragViewDelegate {
    func dragView(didDragFileWith URL: NSURL) {
        Alamofire.upload(multipartFormData: { (data: MultipartFormData) in
            data.append(URL as URL, withName: "upload")
        }, to: "http://uploads.im/api?format=json") { [weak self] (encodingResult) in
            switch encodingResult {
            case .success(let upload, _, _):
                upload.responseJSON { response in
                    guard
                        let dataDict = response.result.value as? NSDictionary,
                        let data = dataDict["data"] as? NSDictionary,
                        let imgUrl = data["img_url"] as? String else { return }
                    
                    print(imgUrl)
                }
            case .failure(let encodingError):
                print(encodingError)
            }
        }
    }
}

That’s a lot of codes!!! Don’t be frightened, special thanks to Alamofire, the library provided us with a single upload function call that allows us to POST file with multipartFormData, which is a networking method we normally use to upload files. Here, we appended the url of our file, which is the given location of where our file is in string, passed in as a URL object with parameter name upload.

The endpoint http://uploads.im/api?format=json is appended with query parameter format=json to also specify the format response we want. When the server response with upload success, we grab the img_url by parsing the JSON response, and print it out. Let’s try it!

Oops! There will be another common error message we may see when we make the first networking call in our apps. It goes something like:

App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure. Temporary exceptions can be configured via your app's Info.plist file.

Apple has recently introduced this new ruling where all apps have to communicate through https protocols. Since we are just experimenting this and making this for personal use, we can perform a workaround.

Head over to Info.plist at your project directory panel, right click on it and select open as source code.

Then add these code:

  NSAppTransportSecurity
    
        NSAllowsArbitraryLoads
        
    

This will bypass the ATS and allow us to communicate with non-https protocols. Run the app and try to upload another image file! After a few seconds, you should see your file URL uploaded printed in the console! Go ahead and paste it in your browser to see the image on your local machine now loaded from uploads.im server!

Polishing up

We know that our Image Loader is almost done, but we need some additional UX (User Experience) added so that it is more complete.

Loading Spinner

Previously we talked about our loading spinner, so lets go ahead and implement the logic of our loading spinner to:

  • Hide when app starts
  • Show and animated when uploading
  • Hide and stop animation when uploading finishes or failed

Hide when app starts

Add this in our viewDidLoad():

    override func viewDidLoad() {
        super.viewDidLoad()
        dragView.delegate = self
        loadingSpinner.isHidden = true
    }

Upload Animation

    func dragView(didDragFileWith URL: NSURL) {
        loadingSpinner.isHidden = false
        loadingSpinner.startAnimation(self.view)
        
        Alamofire.upload(multipartFormData: { (data: MultipartFormData) in
            data.append(URL as URL, withName: "upload")
        }, to: "http://uploads.im/api?format=json") { [weak self] (encodingResult) in
            switch encodingResult {
            case .success(let upload, _, _):
                upload.responseJSON { response in
                    guard
                        let dataDict = response.result.value as? NSDictionary,
                        let data = dataDict["data"] as? NSDictionary,
                        let imgUrl = data["img_url"] as? String else { return }
                    
                    self?.loadingSpinner.isHidden = true
                    self?.loadingSpinner.stopAnimation(self?.view)
                }
            case .failure(let encodingError):
                print(encodingError)
            }
        }
    }

Add them in our dragView function. Run the app and drag in an image file, when the uploading starts, you should see the loading spinner in action!

Show/Hide Label

We can see that sometimes the label is obstructing our loading spinner, let’s:

  • Show when loading spinner is inactive
  • Hide when loading spinner is active

Add staticLabel.isHidden = true after loadingSpinner.startAnimation(self.view), which is right after loading spinner is shown. and self?.staticLabel.isHidden = false after self?.loadingSpinner.stopAnimation(self?.view) after loading spinner is hidden.

Run the app again and you should see a beautiful loading spinner non-obstructed!

Pop-up Alert

The last piece of UI component we want to add is our Alert Box. We will be making use of NSAlert which is a built-in alert sheet that comes with simple settings that allow us to tweak and present a nice pop-up alert box.

Go ahead and add this function which will present our NSAlert pop-up box:

    fileprivate func showSuccessAlert(url: String) {
        let alert = NSAlert()
        alert.messageText = url
        alert.alertStyle = .informational
        alert.addButton(withTitle: "Copy to clipboard")
        let response = alert.runModal()
        if response == NSAlertFirstButtonReturn {
            NSPasteboard.general().clearContents()
            NSPasteboard.general().setString(url, forType: NSPasteboardTypeString)
        }
    }

Then, go ahead and add it right after self?.staticLabel.isHidden = false with this line of code:

self?.showSuccessAlert(url: imgUrl)

Run the app now and drag an image in, you should see a nice native pop-up alert box with a button which when you click, it will copy the URL to the clipboard, and you can paste it anywhere for your personal usage!

Recap

So what have we achieved?

  • We learnt how to build a macOS app entirely from scratch using Cocoa.
  • We learnt how to use Xcode IB to design our UI elements.
  • We learnt how to create our custom view class.
  • We learnt how to design delegate pattern.
  • We learnt how to install external library with Carthage.
  • We learnt how to use Alamofire to upload image.
  • We learnt how to use open source API of uploads.im.
  • We learnt how to use NSAlert.

This is generally how creating a macOS app is like. There are a lot more that I did not cover, and it is up to you to further explore and make meaningful and useful products for people all around the world to use!

Wrapping Up.

If you have any questions about the tutorial, please leave your comment below and let me know.

For the sample project, you can download the full source code on GitHub.

Read next