Have you ever wondered how all those people out there figured out how to manipulate the iOS file system in their apps? For some strange reason, Apple has never provided well-organized documentation on the subject. Here’s how I feel: “Ask and simple question and get an obtuse and overly complex answer.” There are many articles and tutorials out there, including my own, showing you examples of Objective-C or Swift code for manipulating the iOS file system, and most of the code looks basically the same. Nonetheless, this code is deceivingly complex, often underestimated, and rarely well-explained or well-understood.
Where did everybody find this boilerplate code? From simple observation, I’ve found that in many cases, developers use a copy and paste methodology, i.e., look up a few keywords in a web search engine, find the code needed on sites like StackOverflow or some blog, copy it, paste it into an Xcode project, and beat on it until it works. I don’t want you to feel this way after reading my tutorials.
I hope you’ll find it edifying and interesting to read about how I figured out how to understand and navigate the iOS file system using the “most of the code looks basically the same” boilerplates. But I bet you’ll find it even more intriguing to find that I’ve discovered an much better alternative to the boilerplate code.
INTRODUCTION
How many of you have worked with the sandboxed file system that comes provided with your iOS apps? Notice I said “comes provided with” because, whether you use it or not, it’s there. If you haven’t availed yourself of the iOS file system, there’s a good chance that, if you’re dedicated to iOS app development long-term, you’ll end up using it sooner rather than later. I’ve used the local iOS file system extensively.
HISTORICAL BACKGROUND
So how did I figure out how to use the iOS local file system? Apple’s documentation on it isn’t the greatest, but after reading through it several times, experimenting with my own code (mainly Objective-C while learning it early on), and talking with other developers, I came to understand the iOS file system and wrote some of my own standard code for using it.
How did I figure out that you should store files that will be directly presented to and/or manipulated by your users in your app’s Documents
directory? Where did I get the code I showed you in my iOS file system tutorial for determining the path to an app’s Documents
directory, like shown here?
1 2 3 4 5 |
func documentsDirectoryURL() -> URL { return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! //return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] } |
Notice there are two lines of code in my method, but one is commented out. Both lines provide the same functionality. I showed you two alternatives, i.e.,
1 |
return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! |
… and …
1 |
return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] |
for historical reasons. Back in the day when only Objective-C was available, I obtained the very first element of an array by addressing its first or 0-th (zero-th) element. Generally, this is the most commonly used technique for getting an app’s Documents
directory.
Now that we have Swift, I can get the very first element of an array by using the first
instance property of the Array
collection type.
HOW I FIGURED THINGS OUT
When first presented with an app that required use of the iOS file system, I logged into my Apple Developer account and headed for the developer documentation or “knowledge base,” if you will. I first found and read the “File System Programming Guide”, especially the sections on “iOS Standard Directories: Where Files Reside”, “Where You Should Put Your App’s Files”, and “Locating Items in the Standard Directories.”
Defining a “standard directory”
Apple specifically tells us where to put local files associated with an iOS app — and they tell us why. Remember that unless you specify otherwise, your app’s own little file system is “sandboxed” from the rest of the host device’s file system for security purposes. I advise you to become familiar with the concept of sandboxing.
For the sake of brevity, I’ll urge you to read the contents at the Apple links I provided herein and to refer to my own discussion of which local directories you should use in apps that need to access to the local file system. Here’s a diagram that I created to help you understand the most commonly used app directories:
Finding the “standard directories”
When you’re “Locating Items in the Standard Directories,” here’s the advice that Apple provides:
When you need to locate a file in one of the standard directories, use the system frameworks to locate the directory first and then use the resulting URL to build a path to the file. The Foundation framework includes several options for locating the standard system directories. By using these methods, the paths will be correct whether your app is sandboxed or not:
The URLsForDirectory:inDomains:
method of the NSFileManager
class returns a directory’s location packaged in an NSURL
object. The directory to search for is an NSSearchPathDirectory
constant. These constants provide URLs for the user’s home directory, as well as most of the standard directories.
The NSSearchPathForDirectoriesInDomains
function behaves like the URLsForDirectory:inDomains:
method but returns the directory’s location as a string-based path. Use the URLsForDirectory:inDomains:
method instead. …
Apple’s discussion on “Locating Items in the Standard Directories” shows some Objective-C sample code for finding your app’s Application Support
directory, meant “for any files that are not user data files” (see “Listing 2-2 Creating a URL for an item in the app support directory”):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
- (NSURL*)applicationDataDirectory { NSFileManager* sharedFM = [NSFileManager defaultManager]; NSArray* possibleURLs = [sharedFM URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask]; NSURL* appSupportDir = nil; NSURL* appDirectory = nil; if ([possibleURLs count] >= 1) { // Use the first directory (if multiple are returned) appSupportDir = [possibleURLs objectAtIndex:0]; } // If a valid app support directory exists, add the // app's bundle ID to it to specify the final directory. if (appSupportDir) { NSString* appBundleID = [[NSBundle mainBundle] bundleIdentifier]; appDirectory = [appSupportDir URLByAppendingPathComponent:appBundleID]; } return appDirectory; } |
Wait a minute… We were just looking for the Application Support
directory. Why are we possibly getting multiple versions of it?! Huh?
The example code tells you that if any directories are found, there may be several. While this seems obtuse and confusing, it is a result of providing cross-portability with macOS. I’ll expand on the topic of providing a unified API for macOS and iOS, something Apple’s been talking about for awhile, later on in this tutorial.
Notice that the Objective-C version of the shared NSFileManager
has a URLsForDirectory:inDomains:
method. It corresponds one-to-one to the Swift FileManager
method for urls(for:in:)
.
The Objective-C NSSearchPathDirectory
enumeration’s case for NSApplicationSupportDirectory
corresponds to the Swift FileManager.SearchPathDirectory
enumeration’s case for applicationSupportDirectory
.
It should more than obvious to you by now that you ask for the NSURL
or URL
to the “standard directory” of interest — Documents/
, Library/
, tmp/
— by specifying an instance of the NSSearchPathDirectory
or FileManager.SearchPathDirectory
, when using Objective-C or Swift, respectively.
What’s this “domain” stuff?
The discussion and sample code shown in Apple’s documentation doesn’t say much about the inDomains
parameter in the URLsForDirectory:inDomains:
method, and certainly doesn’t mention the Swift equivalent of the in
parameter in the urls(for:in:)
method. This is an example of a situation where you have to be a good reader and researcher.
While Apple’s documentation goes into great depth for macOS as it pertains to “Domains Determine the Placement of Files”, there’s about zero discussion of considering the same topic for iOS. Perhaps this is the key phrase you want to consider:
The user domain contains resources specific to the users who log in to the system. Although it technically encompasses all users, this domain reflects only the home directory of the current user at runtime. User home directories can reside on the computer’s boot volume (in the /Users
directory) or on a network volume. Each user (regardless of privileges) has access to and control over the files in his or her own home directory.
Think about it. A device like an iPhone or iPad is generally considered to be dedicated to one person (one user). These devices can only have one Apple ID logged in at a time. Thus, we can be confident that, on iOS, we’ll always be using the NSUserDomainMask
constant for the inDomains
argument when calling the URLsForDirectory:inDomains:
method in Objective-C, and using the .userDomainMask
constant for the in
argument when calling the urls(for:in:)
method in Swift.
Why iterate through multiple URL
objects to find a “standard directory?”
Why do iOS methods like URLsForDirectory:inDomains:
return an array of NSURL
objects when most of us developers are just looking for one NSURL
? (Most of us are now using Swift, so we’re looking for one URL
.) The stated objective of the method is that it “Returns an array of URLs for the specified common directory in the requested domains” and the signature or declaration of the Objective-C method is:
1 2 |
- (NSArray<NSURL *> *)URLsForDirectory:(NSSearchPathDirectory)directory inDomains:(NSSearchPathDomainMask)domainMask; |
The Swift equivalent is:
1 2 |
func urls(for directory: FileManager.SearchPathDirectory, in domainMask: FileManager.SearchPathDomainMask) -> [URL] |
The documentation for method’s “Return Value” continues, stating, “The directories are ordered according to the order of the domain mask constants, with items in the user domain first and items in the system domain last.”
Compare this to my Swift code for retrieving the “standard” Documents
directory. I point to an example of getting the Documents
directory because, from my experience, when first installing and using an iOS app, barring intervention on your part, Documents
is likely to be the only directory created for you when you install your app, and thus the only one available to you initially.
The Apple Objective-C example code for “Locating Items in the Standard Directories” calls URLsForDirectory:inDomains:
and gets an NSArray
back. Notice the comment in Apple’s sample code, “Use the first directory (if multiple are returned),” which looks like this:
1 2 3 4 5 6 |
... if ([possibleURLs count] >= 1) { // Use the first directory (if multiple are returned) appSupportDir = [possibleURLs objectAtIndex:0]; } ... |
Whenever I call the Swift equivalent, urls(for:in:)
, I do the same thing and “Use the first directory (if multiple are returned),” as in my protocol-oriented version of the code:
1 2 3 4 5 |
func documentsDirectoryURL() -> URL { return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! //return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] } |
You may ask, “Why the forced unwrapping of the call to get the first
element of the array of URL
objects?” Good question and here’s the answer.
I’m using first!
because I’ve never encountered an app with no Documents
directory. But, I know of no guarantee that everything will work perfectly during app development. What if something goes wrong during an app installation an no Documents
directory is created for your app? The urls(for:in:)
method could return what it promised, an instance of [URL]
, initialized, but possibly empty, so the first element of [URL]
could be nil if the array is empty, by definition:
1 |
var first: Element? { get } |
So now I’m taking a shortcut and forcibly unwrapping the optional first
to cut down on the amount of code you have to read. You’ll see below that I error check for a nil
value in first
.
Legitimate reasons to iterate through multiple URL
objects to find a “standard directory”
Remember I mentioned cross-portability between iOS and macOS? If I build an Xcode project targeting macOS and using the Cocoa App
template, I can compile and run the following Swift code on my MacBook Pro:
1 2 3 4 5 |
let allUserDirectories = FileManager.default.urls(for: .userDirectory, in: .allDomainsMask) for dir in allUserDirectories { print("\(dir.path)") } |
In this case, I do get an array of multiple directories, like so:
1 2 |
/Users /Network/Users |
I printed the path
components of the URL
objects returned so I could copy the output from my Xcode console and paste into Terminal and prove to myself that I could cd
(change directory) into both the /Users
and /Network/Users
directories.
This example highlights the usage of domains. You may have multiple domains on a possibly multi-user device like a MacBook Pro. On an iOS device, you’re most likely confined solely to the .userDomainMask
because of sandboxing, though this may change in future iOS versions. (EXCEPTION: There are apps sold through the Mac App Store that use sandboxing.)
I tried another macOS-based experiment. I wanted to see if I could write some Swift code to list all user home directories on my MacBook Pro. Here’s the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
let userDirectory = FileManager.default.urls(for: .userDirectory, in: .localDomainMask) let pathToUserDirectory = userDirectory.first?.path do { let listing = try FileManager.default.contentsOfDirectory(atPath: pathToUserDirectory!) for subDirectory in listing { print("\(subDirectory)") } } catch let error { print(error.localizedDescription) } |
The code worked perfectly and here’s the Xcode console output:
1 2 3 4 5 6 7 |
All users root: /Users .localized andrewjaffee Shared Guest softwaretesting |
HOW I SHOULD WRITE MY CODE
Given the discussion so far, do you think my current Swift code for getting an app’s Documents
directory is robust? Let me remind you of the code I’m discussing:
1 2 3 4 |
func documentsDirectoryURL() -> URL { return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! } |
While this will probably work 99% of the time, it doesn’t account for a scenario in which, for some unforeseen reason, the Documents
directory doesn’t get created when an app is installed from the App Store — say, a scenario in which an iPhone crashes during app installation. What if somehow the Documents
directory is deleted due to some strange circumstance(s) while the user is actually running the app, or perhaps due to an operating system upgrade.
After 30 years of developing software, I’ve seen pretty much everything.
To be really robust, I would add some error checking to my code. During any app operation which requires access to the Documents
directory, or any other such similar sandboxed directory, I could check to see if my iOS file management code returns a bonafide URL
or nil
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
func documentsDirectoryURL() -> URL? { let possibleURLs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) if possibleURLs.count >= 1 { // Use the first directory (if multiple are returned) return possibleURLs.first } else { return nil } } |
If I encounter nil
in a file-centric app, I would report to the user via a UIAlertController
.
OH THE PAIN OF IT ALL!
A year or so after Swift came out, I stumbled upon a method, url(for:in:appropriateFor:create:)
, that I consider a much cleaner iOS/macOS file system API method for getting a URL
for a “standard directory” like Documents
. I don’t know why I went so long without ever noticing this method. I don’t know why I went so long without ever noticing that someone else had noticed this method. I just did a web search and found almost no references to url(for:in:appropriateFor:create:)
. One of the only mentions I could find was in a technical/release note for iOS 8. url(for:in:appropriateFor:create:)
is so much easier to understand and use because it returns one non-optional URL
and doesn’t involve that nonsense about “Use the first directory (if multiple are returned).”
Let me first show you how I’m now retrieving each URL
for the the iOS “standard directories:”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
func documentsDirectoryURL() -> URL? { do { let possibleURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) return possibleURL } catch let error { print("ERROR: \(error.localizedDescription)") return nil } } |
Here’s the declaration for the url(for:in:appropriateFor:create:)
instance method of the FileManager
class:
1 2 3 4 |
func url(for directory: FileManager.SearchPathDirectory, in domain: FileManager.SearchPathDomainMask, appropriateFor url: URL?, create shouldCreate: Bool) throws -> URL |
Notice that we’ve got a shouldCreate
argument which, if set to true, determines “Whether to create the directory if it does not already exist.” Pretty nice.
Don’t worry about the argument label and argument appropriateFor url
. Most developers will always leave this argument as nil
because its use case will be relatively rare. I encourage you to read up on this method and start using it as soon as possible in your apps.
Hallelujah!
CONCLUSION
My main goal in writing this article was to convince you that you need to understand the development tools you’re using. You want to be more than a “programmer.” You want to be the best of the best; not only a a software developer, designer, and software engineer, but a bit of a researcher. The more you understand your tools and what’s going on “under the hood” of the proverbial facade covering your car engine, the better you’ll be at solving difficult problems and coming up with innovative designs for new products.
Trust me. You don’t want to be one of those developers who rely on copy and paste programming. I mean that you don’t want to be one of the hackers who look up a few keywords in a web search engine, find the code needed on sites like StackOverflow or some blog, copy it, paste it into an Xcode project, and beat on it until it works.
Of course you want — sometimes need — to do research and reach out for help from other developers and resources. But try first to figure out problems yourself by looking at your development environment’s documentation. In the case of iOS and macOS developers, that would mean turning first to the Apple Developer portal.
As you can see from this post, Apple’s documentation doesn’t always have the clearest and most up-to-date information. I have to turn to other resources to find answers to my questions. It’s nothing to be embarrassed about. But I went far beyond just copying and pasting code. I strove to understand the problem at hand and solve it myself.
Make understanding what you’re doing a top priority during development. Don’t make “rush-and-just-get-it-done” your mantra. I know there is a lot of deadline-related pressure out there in the software world, but the deeper you understand your methodologies and tools, the higher quality your code, the more you’ll be admired and sought-after as a go-to developer. There will always be the high-pressure type management personalities trying to push you around. As you grow as a developer, you’ll find more and more that you can avoid these kind of people. You’ll eventually find that you can say “no” to these “Type A” personalities or just get up and leave and find a better position with a different employer.