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 DBFileInfo
s. 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 DBFileInfo
s.
We use map
to get the file names out from the DBFileInfo
s.
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 String
s if it was
successful, so we use a switch
statement to check for success, then map over the
array of DBFileInfo
s 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:
- Efficient JSON in Swift with Functional Concepts and Generics
- Real World JSON Parsing with Swift
- Parsing Embedded JSON and Arrays in Swift
Also, take a look at Functional Swift for Dealing with Optional Values by Gordon Fontenot.