Using the Dropbox Objective-C API in Swift

Tony DiPasquale

Using Objective-C sources and frameworks with Swift is made possible through Interoperability. The compiler has all the magic built in that allows us to call Objective-C methods as if they were Swift functions. However, Objective-C and Swift are very different languages and what works well in Objective-C might not be the best for Swift. Let’s look at using an Objective-C API, the Dropbox Sync API, with Swift to keep our app’s files synced with the cloud.

The Dropbox Sync SDK is a precompiled framework that we can download and add to our Xcode project. Carefully follow the instructions on the download page to install the SDK and setup a Dropbox App to obtain a Key and Secret. You can follow the tutorial online to implement the SDK and have files syncing rather quickly. Interoperability allows us to use this Objective-C API in Swift, but let’s see how we can abstract this SDK and sprinkle in some Functional Programming concepts to make it easier to use.

Connecting to Dropbox

After we have installed the framework into our Xcode project, we need to import it into Swift. This is done via a bridging header. We can easily create a bridging header by adding a .m file to our project. Xcode will ask us if we’d like to create a bridging header. Accept, and then delete the temporary .m file. Now in our bridging header import the Dropbox SDK.

#import <Dropbox/Dropbox.h>

Great! Let’s move on to our custom class to interface with the SDK. We can call it DropboxSyncService.

class DropboxSyncService {}

The first thing we need to do is create a DBAccountManager with our Key and Secret. Let’s create a setup function to handle all the Dropbox setup code.

class DropboxSyncService {
  func setup() {
    let accountManager = DBAccountManager(appKey: "YOUR_APP_KEY", secret: "YOUR_APP_SECRET")
    DBAccountManager.setSharedManager(accountManager)
  }
}

Now, we call our setup function from the application delegate function application:didFinishLaunchingWithOptions:.

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject : AnyObject]?) -> Bool {
  dropboxSyncService.setup()
  return true
}

Next, we need to link a user account to the account manager. Somewhere in our app, there should be a button that, when tapped by the user, will tell the Dropbox SDK to attempt authentication.

Let’s add an initiateAuthentication function for that button action to call.

class DropboxSyncService {
  // ...

  func initiateAuthentication(viewController: UIViewController) {
    DBAccountManager.sharedManager().linkFromController(viewController)
  }
}

// In some View Controller with a "Link Dropbox" button
@IBAction func linkDropbox() {
  dropboxSyncService.initiateAuthentication(self)
}

The Dropbox SDK is awesome and will handle all the authentication for us. If the user has the Dropbox app installed, the app will open and authenticate them. If not, the SDK will open a modal popup to the Dropbox web based OAuth login. In either case, the authentication process will open a custom URL scheme to our app. We created this URL scheme in the installation process. All we have to do now is capture the URL in our app delegate and pass it along to the Dropbox SDK.

// In the application delegate
func application(application: UIApplication, openURL url: NSURL, sourceApplication: String?, annotation: AnyObject?) -> Bool {
  return dropboxSyncService.finalizeAuthentication(url)
}
class DropboxSyncService {
  // ...

  func finalizeAuthentication(url: NSURL) -> Bool {
    let account = DBAccountManager.sharedManager().handleOpenURL(url)
    return account != .None
  }
}

The handleOpenURL function returns an implicitly unwrapped optional, DBAccount!, which means it could fail returning nil. This happens because the API is written in Objective-C where any object could be nil and Xcode automatically makes objects implicitly unwrapped so developers can use them without the optional syntax. We return a Bool by checking if the optional account is .None. If the Dropbox account was successfully linked we can fire off a notification so our original view controller with the button to link Dropbox can react.

Getting the Files

When we start looking at files and retrieving data, we see that dealing with errors becomes important. Many of the Dropbox functions can return an object or mutate a DBError pointer that we hand it. This is a common practice in Objective-C for many functions that can fail, but mutable pointer passing doesn’t feel right in Swift.

If a function can have a result or error, then the Result type introduced in an earlier post sounds like a perfect choice.

enum Result<A> {
  case Success(Box<A>)
  case Error(NSError)

  static func success(v: A) -> Result<A> {
    return .Success(Box(v))
  }

  static func error(e: NSError) -> Result<A> {
    return .Error(e)
  }
}

final class Box<A> {
  let value: A

  init(_ value: A) {
    self.value = value
  }
}

Here we see our Result type is an enum with two states: Success and Error. We have to use a Box type that is a final class because of a deficiency in the Swift compiler. We also created two convenience static functions so we do not have to wrap our value in a Box every time we create a Success result.

First, we need a list of files contained within our app folder on Dropbox. The SDK provides DBFilesystem.listFolder(path: DBPath, inout error: DBError) to get a list of DBFileInfos. This Objective-C API isn’t as nice to use in Swift so let’s create an extension that gives us a nicer function to call that returns a Result.

extension DBFilesystem {
  func listFolder(path: DBPath) -> Result<[DBFileInfo]> {
    var error: DBError?
    let files = listFolder(path, error: &error)

    switch error {
    case .None: return .success(files as [DBFileInfo])
    case let .Some(err): return .error(err)
  }
}

Now in our DropboxSyncService class we can add a getFiles() function to call the extension. Before we do that, we need to create the shared filesystem using the account. Let’s put this in the finalizeAuthentication function.

class DropboxSyncService {
  // ...

  func finalizeAuthentication(url: NSURL) -> Bool {
    let account = DBAccountManager.sharedManager().handleOpenURL(url)
    DBFilesystem.setSharedFilesystem(DBFilesystem(account: account))
    return account != .None
  }

  func getFiles() -> Result<[DBFileInfo]> {
    return DBFilesystem.sharedFileSystem().listFolder(DBPath.root())
  }
}

This works but there are a couple issues. The .setSharedFilesystem() function takes a non-optional value, which could result in a runtime exception if the DBFileSystem is nil. Let’s use bind (>>-) to set the shared filesystem if the initializer doesn’t fail.

infix operator >>- { associativity left precedence 150 }

func >>-<A, B>(a: A?, f: A -> B?) -> B? {
  switch a {
  case let .Some(x): return f(x)
  case .None: return .None
  }
}

class DropboxSyncService {
  // ...

  func finalizeAuthentication(url: NSURL) -> Bool {
    let account = DBAccountManager.sharedManager().handleOpenURL(url)
    DBFilesystem(account: account) >>- DBFilesystem.setSharedFilesystem
    return account != .None
  }
}

Also, I would prefer to have a list of the file names instead of DBFileInfos. We use map to get the file names out from the DBFileInfos.

class DropboxSyncService {
  // ...

  func getFiles() -> Result<[String]> {
    let fileInfoArrayResult = DBFilesystem.sharedFileSystem().listFolder(DBPath.root())

    switch fileInfoArrayResult {
    case let .Success(fileInfoArrayBox):
      return fileInfoArrayBox.value.map { fileInfo in
        fileInfo.path.stringValue()
      }
    case let .Error(err): return .error(err)
    }
  }
}

The listFolder function returns a Result<[DBFileInfo]> that could be in an error or success state. We only want to extract an array of Strings if it was successful, so we use a switch statement to check for success, then map over the array of DBFileInfos and return the string value of the path.

What this switch statement is really doing is applying a function to the value inside a successful Result then returning it’s output as a new Result; otherwise, if the Result is in the error state, it returns a new Result with the error.

This is what fmap does. We’ll use the fmap operator (<^>) to clean up that function.

infix operator <^> { associativity left precedence 150 }

func <^><A, B>(f: A -> B, a: Result<A>) -> Result<B> {
  switch a {
  case let .Success(aBox): return .success(f(aBox.value))
  case let .Error(err): return .error(err)
  }
}

class DropboxSyncService {
  // ...

  func getFiles() -> Result<[String]> {
    let fileInfos = DBFilesystem.sharedFileSystem().listFolder(DBPath.root())
    let filePaths: [DBFileInfo] -> [String] = { $0.map { $0.path.stringValue() } }
    return filePaths <^> fileInfos
  }
}

Now we have a nice Swift-like API to get a list of files. We will use a similar process to get the file data. First, add an extension to the Dropbox SDK to use the Result type. Then, use the functional operators to make operations with the Result type easy to work with. Here is everything we need for retrieving the file data.

extension DBFilesystem {
  // ...

  func openFile(path: DBPath) -> Result<DBFile> {
    var error: DBError?
    let file = openFile(path, error: &error)

    switch error {
    case .None: return .success(file)
    case let .Some(err): return .error(err)
    }
  }
}

extension DBFile {
  func readData() -> Result<NSData> {
    var error: DBError?
    let data = readData(&error)

    switch error {
    case .None: return .success(data)
    case let .Some(err): return .error(err)
    }
  }
}

func >>-<A, B>(a: Result<A>, f: A -> Result<B>) -> Result<B> {
  switch a {
  case let .Success(aBox): return f(aBox.value)
  case let .Error(err): return .error(err)
  }
}

class DropboxSyncService {
  // ...

  func getFile(filename: String) -> Result<NSData> {
    let path = DBPath.root().childPath(filename)
    return DBFilesystem.sharedFilesystem().openFile(path) >>- { $0.readData() }
  }
}

Finally, let’s use the same process again to implement the function for creating files.

extension DBFilesystem {
  // ...

  func createFile(path: DBPath) -> Result<DBFile> {
    var error: DBError?
    let file = createFile(path, error: &error)

    switch error {
    case .None: return .success(file)
    case let .Some(err): return .error(err)
    }
  }
}

extension DBFile {
  // ...

  func writeData(data: NSData) -> Result<()> {
    var error: DBError?
    writeData(data, error: &error)

    switch error {
    case .None: return .success(())
    case let .Some(err): return .error(err)
    }
  }
}

class DropboxSyncService {
  // ...

  func saveFile(filename: String, data: NSData) -> Result<()> {
    let path = DBPath.root().childPath(filename)
    return DBFilesystem.sharedFilesystem().createFile(path) >>- { $0.writeData(data) }
  }
}

Conclusion

We see that using the Dropbox SDK is a great way to have automatic file syncing within our apps. The SDK is written in Objective-C but we can easily modify its functions using a Result type and some functional concepts like bind and fmap to make it convenient to use in Swift as well.

Further Learning

To learn more about functional programming in Swift, read this series on JSON in Swift:

Also, take a look at Functional Swift for Dealing with Optional Values by Gordon Fontenot.