iOS file management with FileManager in protocol-oriented Swift 4

NOTE: My latest tutorial has just been released, which uses the code shown herein to introduce error handling in Swift 4.

[Download the Xcode 9 project from GitHub so you can follow along with my code explanation and try iOS file management features yourself!]

How many of you have written iOS apps that work with files? I’m talking about developing apps that read, write, create, copy, move, and delete files in the app’s sandboxed file system. I’m not talking about reading an image from your app bundle so you can display it on screen, like so:

I’m talking about apps like Adobe Photoshop Express which is only useful if it can edit image files; Apple’s Pages and Numbers apps which are only useful if they can edit word processor and spreadsheet/chart files, respectively; and, Microsoft Word which is only useful if it can edit word processor documents. Yes, you can solely work from/in the cloud with all these apps, but you can also opt to store files locally on your devices. What if you open an email attachment or download a file from Safari? I guarantee you that many apps with associations to certain file extensions store those attachments or downloads locally first for editing and display, and only later sync files with iCloud or Dropbox.


PROTOCOL-ORIENTED PROGRAMMING AND VALUE SEMANTICS

In this tutorial, I’m going to show you how to develop support for manipulating files inside your app’s sandboxed directory structure — but, my code has been designed and written in Swift by leveraging two relatively new methodologies: protocol-oriented programming (POP) and value semantics.

I’ve written extensively about POP and value semantics in previous posts. If you’re not familiar with these concepts, please read my articles on protocols, an introduction to POP, using advanced POP, and value semantics. You’ll see no classes or class inheritance trees in this tutorial. You’ll see functionality that is logically broken into protocols, only one instance of protocol inheritance, and finally, to get the core functionality, the composition of four protocols into one struct that defines an iOS local file. Before writing the code for this article, I created a design using a UML (Unified Modeling Language) diagram:

Instead of going into a long-winded discussion of UML, I challenge you to learn a little bit about this design tool at these links here, here, here, here, and here. Without deep knowledge of UML, you should be able to see how I’ve logically separated dissimilar functionality and, conversely, grouped similar operations together.

I can turn to composition as opposed to inheritance, while still having the opportunity to inherit from protocols if I like. I can compose together as much or as little file-related functionality by adopting as many or as few protocols as I need. Protocol extensions allow me to define base/default functionality that can be easily overridden in structs that adopt from my existing protocols or in new protocols that inherit from my existing protocols.

Notice my use of a struct, a value type. Every variable of my AppFile type has an independent value. If I make a copy of an instance of AppFile, changes to the copy are not reflected in the original variable. Apple claims that this prevention of unintended mutation makes it “easy to reason about your code.” See this link for more on value semantics.

By separating and organizing file system management features into protocols, you can see how my code schema can be easily reused, maintained, and extended. You should also notice that I’ve avoided the pitfalls a starting with a base (abstract) “file” class, probably being tempted to place most file-related functionality in the base class, and then trying to specialize the base with inheritance. In fact, my file model doesn’t look like a good candidate for a lot of inheritance. The model looks more like a candidate for dividing up features “horizontally” using protocols and composition as opposed to “vertically” as is the case with classes and inheritance.

Please follow and read the hyperlinks I’ve included throughout the article. This is important information required for developers to fully understand how the iOS file system works and how Apple provides support for managing the iOS file system. DOWNLOAD THE XCODE 9 PROJECT TO FOLLOW ALONG WITH MY CODE, TOO.

INTRODUCTION TO THE iOS FILE SYSTEM

Even if you’ve only been developing software for a brief period of time, you should understand the rudimentary concepts of a local file. Whether you’re a beginner or an advanced developer, please read up on iOS’s “file system basics”, and note that my code is based around the use of the FileManager.default shared instance.

For a comprehensive discussion of iOS file system management, please go through Apple’s File System Programming Guide. Here’s a brief definition from that guide:

A file system handles the persistent storage of data files, apps, and the files associated with the operating system itself. Therefore, the file system is one of the fundamental resources used by all processes. …

The FileManager

My code is a convenience wrapper around the iOS FileManager class:

An FileManager object lets you examine the contents of the file system and make changes to it. The FileManager class provides convenient access to a shared file manager object that is suitable for most types of file-related manipulations. A file manager object is typically your primary mode of interaction with the file system. You use it to locate, create, copy, and move files and directories. You also use it to get information about a file or directory or change some of its attributes.

When specifying the location of files, you can use either NSURL or NSString objects. The use of the NSURL class is generally preferred for specifying file-system items because they can convert path information to a more efficient representation internally. You can also obtain a bookmark from an NSURL object, which is similar to an alias and offers a more sure way of locating the file or directory later.

The “shared file manager object” to which Apple refers is FileManager.default, which “Returns the shared file manager object for the process” — in other words, it is the singleton you use throughout your app’s code.

iOS app file system sandboxing

When writing iOS apps that perform at least some type of local file manipulation, you need to understand Apple’s rules about “sandboxing.” For security purposes, you can only manipulate files in the device’s solid state hard drive (“memory”) space specifically allocated to your app. Apple doesn’t want you nosing around in other apps, nor do you want other apps poking around in your app. A sandbox is a virtual fence around your app. It is that unique corner of device memory that iOS specifically grants access to your app:

For security purposes, an iOS app’s interactions with the file system are limited to the directories inside the app’s sandbox directory. During installation of a new app, the installer creates a number of container directories for the app inside the sandbox directory. Each container directory has a specific role. The bundle container directory holds the app’s bundle, whereas the data container directory holds data for both the app and the user. The data container directory is further divided into a number of subdirectories that the app can use to sort and organize its data. The app may also request access to additional container directories—for example, the iCloud container–at runtime.

Standard iOS app directories available to your app

iOS provides every app with a standard file system and directory structure, whether you use it or not. Since we’re interested in using that structure, you need to know which directories you can access, the rules you must follow when accessing those directories, and the purposes of each of the directories made available to you. There are three directories which are generally sufficient for most developers’ purposes. You can go digging through Apple’s documentation if you need more features, but I generally find that I only need to use the three iOS-provided directories as shown in my diagram below:

Here’s the directory structure created by iOS when a new app is installed on an Apple device such as an iPad or iPhone:

Note that the Documents/Inbox directory is generally not created until needed, i.e., until your app snags an email attachment because it has registered with iOS to recognize a specific file extension like “.txt” or “.png” — or some extension specific only to your business model.

Regarding those three directories I find most useful, here is Apple’s guidance on said folders:

Documents/: Use this directory to store user-generated content. The contents of this directory can be made available to the user through file sharing; therefore, his directory should only contain files that you may wish to expose to the user.

The contents of this directory are backed up by iTunes and iCloud.

Documents/Inbox: Use this directory to access files that your app was asked to open by outside entities. Specifically, the Mail program places email attachments associated with your app in this directory. Document interaction controllers may also place files in it.

Your app can read and delete files in this directory but cannot create new files or write to existing files. If the user tries to edit a file in this directory, your app must silently move it out of the directory before making any changes.

The contents of this directory are backed up by iTunes and iCloud.

tmp/: Use this directory to write temporary files that do not need to persist between launches of your app. Your app should remove files from this directory when they are no longer needed; however, the system may purge this directory when your app is not running.

The contents of this directory are not backed up by iTunes or iCloud.

MY SWIFT 4 CODE FOR FILE MANAGEMENT

Let’s walk through my code for simplifying most iOS file operations you’d need if your were writing a file-centric app, or one that even occasionally needs access to local files. You can build on this code to refine it for say, manipulation of binary files. I’ll conclude this tutorial with some suggestions for resources you can turn to if you need more elaborate file manipulation features.

A note on the absence of error handling in my code

I purposefully left error handling out of this tutorial’s code so you could concentrate on the main topic of the day: file system management. When manipulating files, a lot can go wrong, for example, a developer could pass a name of a file that doesn’t exist to a method meant to delete that file. Boom. Exception thrown.

If I were to have added robust error handling to the code presented here, you’d probably have trouble reading my code and concentrating on file system management. There would be too many if let, guard, and do try catch statements clouding up the core file-related code. That’s why you’ll see me using shortcuts like try!.

In my latest tutorial, just released, I use the code shown herein to introduce error handling in Swift 4.

Convenient access to predefined directories

As we dig into the code, you’ll see why this enum makes it easy for you to stay out of trouble and manipulate files solely in the folders that iOS allows you to touch:

Convenient access to the URLs for predefined directories

You can’t just use the simple names of these predefined folders to access them. Each app is assigned a unique ID and a unique path to it’s sandboxed file system on the device on which the app is installed. For example, here’s the path, a URL, that iOS assigned to the sample app I wrote for this article:

Note that Apple advises that:

The preferred way to specify the location of a file or directory is to use the NSURL class. Although the NSString class has many methods related to path creation, URLs offer a more robust way to locate files and directories. For apps that also work with network resources, URLs also mean that you can use one type of object to manage items located on a local file system or on a network server.

Despite this statement, Apple’s file management SDK is still a bit inconsistent, allowing developers to access files using three different forms of resource locators:

All of the following entries are valid references to a file called MyFile.txt in a user’s Documents directory:

Path-based URL: file://localhost/Users/steve/Documents/MyFile.txt

File reference URL: file:///.file/id=6571367.2773272/

String-based path: /Users/steve/Documents/MyFile.txt

In the code I show you below, I intentionally called some methods that take String paths and some that take URLs as arguments to highlight the flexibility (or the inconsistency, depending on how you look at things) afforded by Apple’s file management API.

Let’s look at my protocol and protocol extension for conveniently referring to commonly-used iOS app sandboxed folders. We’ll discuss the code after you read through it and digest it:

You’ll see my already ubiquitous use of the the FileManager.default shared instance. In the case of finding URLs for the most-commonly used iOS app folders, I’m calling the urls(for:in:) method of FileManager.default. Note in my commented out code that there are several ways in which to invoke urls(for:in:). Note also, for some bizarre reason, there’s no predefined call to get the URL to the Documents/Inbox directory (correct me if I’m wrong).


Checking the access level of files

Here’s my protocol and protocol extension for determining whether a file exists, whether it’s writeable, and/or whether it’s readable. This is not an exhaustive list. Apple provides methods for determining if a file is deletable or executable. Feel free to add wrappers for those methods. Despite Apple’s stated preference for URLs, note that all these methods take String-based paths as arguments.

I’ve written convenience wrappers because I find these methods useful (like determining if a file exists or not), but Apple advises:

Attempting to predicate behavior based on the current state of the file system or a particular file on the file system is not recommended. Doing so can cause odd behavior or race conditions. It’s far better to attempt an operation (such as loading a file or creating a directory), check for errors, and handle those errors gracefully than it is to try to figure out ahead of time whether the operation will succeed. For more information on file system race conditions, see Race Conditions and Secure File Operations in Secure Coding Guide.

Let’s review my protocol and protocol extension for conveniently checking file status:

Discovering meta data describing the file system

I separated my functionality for discovering high-level meta data describing the file system from code in the previous section which describes an individual characteristic of each file. To me, this is a logical distinction. It’s somewhat of a subjective call. Anyway, here’s my code for listing the contents of an entire directory or getting all attributes assigned to a file (and there’s a lot of attributes). Here’s my protocol and protocol extension:

The meat: read, write/create, delete, move, rename, and copy files

Let’s review my protocol and protocol extension for reading, writing/creating, deleting, moving, renaming, and copying files. I’ll provide some insights into the code after you read and ingest it:

A note on using NSString for paths

I wrote the changeFileExtension method in the last section’s protocol extension mainly to show you that, by Apple’s own admission:

… the NSString class has many methods related to path creation…

That’s just a bit of an understatement. Take advantage of NSString. I don’t know why Apple has not migrated all of the NSString file path manipulation support from Objective-C to the Swift String class, but they should. Otherwise, we’re going to have to keep introducing Objective-C types into our Swift code. That’s probably unavoidable now as many of the core frameworks we use are still written in Objective-C (and some C++ and C), like Foundation and UIKit.

Putting it all together

I’ve laid out a convenient API for file management. Great, but how do I use it to actually manipulate files? How do I make my code safe and readable by using value semantics? How do I stay inside the protocol-oriented programming paradigm?

Simple. I use a value type, a struct which adopts and conforms to one or more protocols.

Let’s consider a very common use case. There are many apps that are configured to respond to a user-initiated action with files that have certain extensions. Consider a text editing app or an image editing app that are configured to offer the user a chance to open those apps when a file with a “.txt” or “.png” extension, respectively, is acted upon by an iPhone or iPad user. Generally, “acted upon” means the user tapped on a file attachment in an email or downloaded a file using Safari.

When such an event occurs, iOS presents the user with a selection of apps associated with the “.txt” or “.png” extensions. When the user selects their favorite app for that extension, iOS copies the associated file into the app’s Documents/Inbox folder. Developer’s must immediately copy the file out of the Documents/Inbox folder and generally move it to the Documents or tmp/ folder for processing.

Here’s a type we could define to respond accordingly to such a scenario:

Here’s how we’d initialize an instance of our specific type and tell it to move the incoming file from Documents/Inbox to Documents:

Watch my code in action:

Consider another common use case. Developers generally use the tmp/ folder for intermediate processing of app files. It’s good practice to delete files in tmp/ as soon a you’re done with them. Here’s a type we could define to respond accordingly to such a scenario:

Here’s how we’d initialize an instance of our specific type and tell it to delete the file from tmp/:

Watch my code in action:

I’ve written a few methods to add to AppFile just to give you a little more insight into what’s possible with my file management protocols. These are rather informal and have some hard-coded values, but they should make sense to you.

Here’s a method that writes two files to the Documents/ folder:

Watch the two files get written:

This is a method that does a listing of the Documents/ folder:

Here’s the output to console from list(directory: URL):

Finally, I knocked out a quick method the write the attributes of a single file to console:

These are the file attributes written to console:

I’ve shown you enough to get started using my code. Leave a comment if you have questions.

CONCLUSION

You should now have a decent grasp of how to write an iOS app that manipulates local files. Let’s review some concepts that you should always keep in mind when handling local files on an Apple iOS device:

  • app sandboxing;
  • names of directories accessible from your app bundle;
  • purpose of each of the directories accessible from your app bundle;
  • getting information (meta data) about files and directories;
  • getting information about specific files; and,
  • performing basic file manipulation (read, write, create, rename, delete, move, and copy).

Moving forwards, you should look into more advanced file management features. One notable example is the creation of symbolic links. Another example is fine grained manipulation of individual files using the FileHandle class:

You use file handle objects to access data associated with files, sockets, pipes, and devices. For files, you can read, write, and seek within the file. For sockets, pipes, and devices, you can use a file handle object to monitor the device and process data asynchronously.

Getting up to speed on Apple’s CloudKit should be on your to-do list. Everybody’s using the cloud, even if just to back up their files.

Finally, instead of writing your next project using reference semantics and classes, give value semantics and protocol-oriented programming a try. I’m not suggesting you abandon OOP. I’m advocating that you keep an open mind to new technologies. Learning and adopting a new technology like protocol-oriented programming (POP) is not an all or nothing proposition. POP and OOP can not only co-exist, they can compliment each other.

Get out there, study, and practice, practice, practice. Don’t forget to enjoy your work and your lives. Have fun!

Author: Andrew Jaffee

Avid and well-published author, software engineer, designer, and developer, now specializing in iOS mobile app development in Objective-C and Swift, but with a strong background in C#, C++, .NET, JavaScript, HTML, CSS, jQuery, SQL Server, MySQL, Oracle, Agile, Test Driven Development, Git, Continuous Integration, Responsive Web Design, blah, blah, blah ... Did I miss any fad-based catch phrases? My brain avatar was kindly provided by http://icons8.com under a Creative Commons Attribution-NoDerivs 3.0 Unported license.

Leave a Reply

Your email address will not be published. Required fields are marked *