Working with Files on iOS with Swift

Written by Reinder de Vries on October 1 2020 in App Development, iOS, Swift

Working with Files on iOS with Swift

You use FileManager to work with files and directories on iOS. It’s a Swift API that helps you read from, and write to, various data and file formats. In this tutorial, you learn how to work with files on iOS with Swift.

Here’s what we’ll discuss:

  • How to work with files and directories with FileManager
  • Reading from and writing to text files, plists, images and JSON
  • How to write strings to a file, and read them back
  • Getting a handle on directories your iOS app has access to
  • Working with Swift’s Data type, and more

Ready? Let’s go.

  1. What’s FileManager on iOS?
  2. Reading a File with Swift
  3. Writing to a File with Swift
  4. Reading from an App Bundle File
  5. Working with Directories in FileManager
  6. Quick Tips and Tricks
  7. Further Reading

What’s FileManager on iOS?

The FileManager component, for iOS development, is an interface to the iPhone’s file system. You use it to read from and write to files in your iOS app.

In short, you use FileManager to get the path of files on the iPhone that your iOS app has access to. You can then use that path to read the file, or write to it, depending on your app’s needs. You can also use FileManager to work with directories.

Working with FileManager, when building your iOS app, can be a bit counter-intuitive. If you’ve worked with files before, on your Mac for example, you’re well aware of how files are organized in a file system that consists of hierarchical directories. You can access any file through its path, something like /home/reinder/Documents/todo-list.txt, provided you’ve got permission to access that file.

Working with files on iOS is different in a few ways:

  1. Apps on iOS are constrained to a sandbox, which means they’ve got no access to system files and resources. This is a security measure.
  2. Files on iOS have a path, but you usually only work with a URL object that contains that path. You rarely work with paths directly.
  3. You typically read/write app files in a few designated directories, like your app’s document directory or from the app bundle.

iOS itself doesn’t have a file manager app like Finder, except for the Files app that gives you access to files in iCloud. iOS is a closed system, which is important to keep in mind.

The FileManager component serves as a wrapper on top of the file system. You use it to get a reference to a file. Here’s an example:

let path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("todos.txt")

Funky, right? We’ve only created a reference to a todos.txt file in the app’s document directory. The actual file is saved on the iPhone at:

file:///var/mobile/Containers/Data/Application/···/Documents/todos.txt

Let’s dive in, and see how that works!

Working with iPhone Simulator? Do a print(path.absoluteString) and then $ open path in Terminal, to open the file or folder at path. Keep in mind that the directory (on your Mac) where a Simulator’s files are stored can change between running your app. The contents of the Simulator is copied, so you may be looking at an outdated file (and wonder why)…

Learn how to build iOS apps

Get started with iOS 14 and Swift 5

Sign up for my iOS development course, and learn how to build great iOS 14 apps with Swift 5 and Xcode 12.

Reading a File with Swift

Reading a file with Swift on iOS requires a two-step approach:

  1. Get a reference to the file
  2. Read the file

Imagine we’ve stored a few to-do list items in a comma-separated file called todos.txt. How can we read that file from disk and do something with the contents?

1. Get Default FileManager

The first step is to get a reference to the default FileManager component, like this: FileManager.default ···. This is a shared component on iOS, unique to our app’s process.

We can’t get any arbitrary file from iOS, so we’ll need to use a starting point: the document directory. This is where you store user documents, i.e. files the user of your app wants saved.

2. Get Document Directory

We’re going to use the urls(for:in:) function of FileManager to get a reference to the app’s document directory. Like this:

FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]

The urls(for:in:) is intended to return common file system directories, such as the documents and temporary files directories. The .documentDirectory is an enumeration value from FileManager.SearchPathDirectory, which is kinda like a hard-coded list of iOS directories you use often. The .userDomainMask additionally specifies where to look for the requested directory.

The above urls(for:in:) function call returns an array of URL objects. In fact, for the common document directory call, it just returns an array with one URL object. We’re getting that through the subscript ···[0].

When working with files on iOS, you use the document directory so often that it makes sense to create a helper function for it. Like this:

func getDocumentDirectory() -> URL {
    return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
}

Depending on your preferred architecture, you could even create a simple function in an extension:

extension FileManager {
    func documentDirectory() -> URL {
        return self.urls(for: .documentDirectory, in: .userDomainMask)[0]
    }
}

Good To Know: Files stored in an app’s document directory are also backed up to iCloud when that app is backed up. Only use it for files a user would want to keep. You can exclude files from backing up by marking them with an additional file flag.

3. Append Path Component

At this point, the urls(for: ···, in: ···)[0] code contains a URL to the document directory with the following path:

file:///var/mobile/Containers/Data/Application/···/Documents/

The next step is appending the actual file we want to read from, called todos.txt. You can do this with the appendPathComponent() function of the URL object. Like this:

let path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("todos.txt")

The above code adds a filename to the path we already had. If the filepath we’re working with was an actual hard-coded string-like path, we would have just “added” the filename after the last slash.

Right now, this is what we got:

file:///var/mobile/Containers/Data/Application/···/Documents/todos.txt

This is a filepath we can read from, and get the data from the file. The good news is that we’re still working with Swift objects, like URL, in favor of plain ol’ filepaths. The bad news is that this filepath-getting code looks awful…

You may recognize the concept of URLs from browsing web pages, but you can also use the URL standard to identify files on a computer. When doing so, URLs will be prepended with the file:// schema, as opposed to https:// for the web.

4. Read From File

Alright, let’s read those todos! With path in hand, reading the contents of the file is easy:

let todos = try String(contentsOf: path)

See the try there? That means we’ll have to handle errors, like this:

do {
    let todos = try String(contentsOf: path)

    for todo in todos.split(separator: ";") {
        print(todo)
    }
} catch {
    print(error.localizedDescription)
}

Given a todos.txt with the text Do the dishes;Make dinner;Walk pet lizzard, the above code outputs:

Do the dishes
Make dinner
Walk pet lizzard

To read from a file in Swift, you can use the String type directly. In the above code, we’re using the String(contentsOf:) initializer to read from a file at the given URL.

You can also use the Data(contentsOf:) initializer as an alternative. Instead of reading to a string, this will directly read bytes from the file. You can then work with those bytes further, for example, by creating an image view.

Awesome! Let’s move on to writing to files.

Writing to a File with Swift

Let’s take a look at how you can write to a file in Swift. Just as before, you take a two-step approach:

  1. Get a reference to a file
  2. Write to the file

In the previous section, we’ve discussed how you can get a reference to a file with FileManager, to get a URL of that file. Like this:

let path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("todos.txt")

If you’ve got a string called todos with a few to-do items, you can write them to todos.txt like this:

let todos = "Attain world domination;Eat catfood;Sleep"

let path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("todos.txt")

do {
    try todos.write(to: path, atomically: true, encoding: .utf8)
} catch {
    print(error.localizedDescription)
}

In the above code, we’ve got a handle on path and todos. You then call the write(···) function on the string to write it to a file directly.

A few notes:

  • The atomically parameter (and concept) is a good one to remember. Writing to a file atomically means the file is first written to a temporary file, which is then renamed to path. This ensures the file doesn’t become corrupt if your app crashes during the write operation.
  • You need to specify an encoding to use for writing the string to a file. A safe choice here is .utf8. When you read from the same file, make sure to also choose UTF-8 encoding (i.e. the same). Learn more about encoding here.

Just as before, we’re using do-try-catch to handle errors that might occur during writing to the file. It’s also good to know that files that don’t exist yet will be created, and files that do exist will get overwritten.

Reading from an App Bundle File

iOS apps are distributed via a so-called app bundle, which contains the app binary and any resources that you distribute together with your app, such as an app icon.

When you add a file to your app project in Xcode, it’s added to the app bundle. Now that we’ve looked at reading/writing arbitrary files on iOS, how do you read from files in the app bundle?

The approach is the same as before:

  1. Get a reference to the file in the app bundle
  2. Read the file

First, you need to get a reference to the file in the app bundle. The starting point here is Bundle.main, and not FileManager. Here’s how you get the file’s path:

let path = Bundle.main.url(forResource: "todos", withExtension: "txt")

With the above code, we’re creating a constant path of type URL that contains a reference to the todos.txt file in the app bundle. Just as before, you can now read from that file. Note that path is an optional – in case the file doesn’t exist – which is why you need to unwrap it.

A few things worth noting:

  • The above approach obviously only works for files that you’ve added to the app bundle, i.e. for files added to your project in Xcode. For assets you’ve added to an .xcassets file, you can use the common approach, i.e. Image(named:).
  • Unlike before, you don’t append a file path to, for example, a document directory. Instead, you define a file name and extension with url(forResource:withExtension:). The syntax is a bit verbose, and it obsures working with a file system, which can be an advantage by aiding clarity.
  • You can’t write or change files in the app bundle. This is a security measure – otherwise you’d be able to insecurely rewrite or patch an app binary, for example. Sandboxing is a good thing!

Let’s move on!

Working with Directories in FileManager

So far we’ve only looked at working with individual files, with the idea that you’d want to get one specific file from the app package. What if you want to organize more files, in directories?

A few common scenarios include …

  • … organizing photos and recordings in specific subdirectories
  • … organizing by dated directories, for simpler exporting
  • … creating subfolders because you just feel like it

Here’s how you create a new directory with FileManager:

let path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("photosFolder")

if !FileManager.default.fileExists(atPath: path.absoluteString) {
    try! FileManager.default.createDirectory(at: path, withIntermediateDirectories: true, attributes: nil)
}

Whoah! A few things are going on here:

  • We’re first setting the path of the directory we want to create, a folder called ···/photosFolder. We’re preparing the path prior to creating a directory at that path.
  • Then, we’re checking if a file or directory exists at the given path. This is to ensure we’re not attempting to create a directory at a path that’s already taken.
  • Finally, we’re creating a new directory at path.

A few things worth noting:

  • In the above code example, we’ve silenced errors from createDirectory(···) with try!. In your own code, you’ll need to wrap that line in a do-catch block and handle the errors properly.
  • Note that, at the time of let path = ···, the path we’re defining does not exist yet! There’s nothing there. The path is not the file system. It’s just a location in the file system, and we don’t know what’s there until we read (meta)data from it.
  • Files aren’t required to have an extension, so it may well be that photosFolder is a text file with no extension. That’s why we’re checking if a file or folder exists at the given path. (Same for directories that end with .txt…)
  • When withIntermediateDirectories parameter is set to true, the function will create any non-existing directories “before” the ones in your path. So if your path is photos/2020/08 and none of those directories exist yet, they’ll get created in one go.
  • The path.absoluteString is a string representation of the path constant of type URL. It’s “absolute” in the sense that it’s an absolute path starting at the root of the file system. Some APIs in FileManager use strings, others use URL. Good to know!

Quick Note: Directories and “folders” are the same thing. Technically, any directory is a subdirectory unless that directory is the system root /.

Quick Tips and Tricks

Pfew! Are you starting to feel grateful for APIs like UIImage(named: "cats") yet? If you’d have to read bytes from the file system directly for every image you want to show on screen, your code would get messy pretty quickly. Working with FileManager isn’t the prettiest, and that’s OK.

Before we call it quits, lets look at a few tips and tricks.

In the below code samples, the documents constant is the path of the app’s document directory. It’s set with let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].

Read/Write a JSON File

Given the following JSON file:

{
    "userID": 99,
    "name": "Zaphod",
    "loggedIn": false
}

How do you read and parse a JSON file with Swift?

let path = documents.appendingPathComponent("config.json")

do {
    let data = try Data(contentsOf: path)
    let json = try JSONSerialization.jsonObject(with: data, options: [])

    if let root = json as? [String: Any],
       let name = root["name"] as? String {
        print(name)
    }

} catch {
    print(error.localizedDescription)
}

How do you write JSON back to a file?

let json = #"{"loggedIn": false, "name": "Zaphod"}"#

do {
    try json.write(to: path, atomically: true, encoding: .utf8)
} catch {
    print(error.localizedDescription)
}

Most libraries that work with JSON have APIs to return JSON as a Swift Data object, which you can also write to disk as discussed. Keep in mind that Codable and SwiftyJSON are simpler alternatives for JSONSerialization.

See that string in the above code sample? That’s a raw string, wrapped between #. When you code a string literal like that, you don’t have to escape double quotes ", which is helpful when working with JSON strings.

Read/Write a Plist File

Let’s say you’ve got a .plist file embedded in your app project. How do you read from it?

if let path = Bundle.main.path(forResource: name, ofType: "plist"),
   let xml = FileManager.default.contents(atPath: path)
{
    if let fruits = try? PropertyListSerialization.propertyList(from: xml, options: .mutableContainersAndLeaves, format: nil)) as? [String] {
        print(fruits)
    }
}

You can learn more about plists in this tutorial: How To: Working with Plist in Swift

Read/Write an Image or Data File

What about images? If you can’t rely on good ol’ UIImage(named:) or Image(named:), how do you go from bits to pixels? Here’s how you can read from an image file and write to it.

let data = Data(···)
imageView.image = UIImage(data: data)

You can obtain data from various sources, such as downloading an image over the network. Keep in mind that the above UIImage(data:) initializer is scale-agnostic. If you want to show images in a specific 1x, 2x or 3x scale, use UIImage(data: ···, scale: 2.0) or UIImage(data: ···, scale: UIScreen.main.scale).

Working with images and files in Swift is affected by the image file format, such as JPEG or PNG. You can transform UIImage objects to Data objects represented in various file formats. With an image object of type UIImage, the functions image.pngData() and image.jpgData() return Data objects you can save to a .png or .jpg file.

Read/Write with NSKeyedArchiver

The NSKeyedArchiver component is super helpful for creating simple data stores. You can create your own Swift object classes and save a bunch of them to a binary file, and vice versa.

With a few prerequisites in place, you can convert a NSCoding compliant object to a data store with:

let data = try NSKeyedArchiver.archivedData(withRootObject: ···, requiringSecureCoding: false)
try data.write(to: path, ···)

You can read the same data file back with NSKeyedUnarchiver, like this:

let data = try Data(contentsOf: path)
let todos = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data)

Learn more about working with NSKeyedArchiver in this tutorial: Storing Data with NSCoding and NSKeyedArchiver

Read/Write a Text File

We’ve discussed it before, but here are a few snippets for working with humble plain text files. Here’s how you read a text file with Swift:

let path = ···

do {
    let todos = try String(contentsOf: path)
    print(todos)
} catch {
    print(error.localizedDescription)
}

Here’s how you can write a string to a text file:

let text = "Hello world!"
let path = ···

do {
    text.write(to: path, atomically: true, encoding: .utf8)
} catch {
    print(error.localizedDescription)
}

Awesome!

Learn how to build iOS apps

Get started with iOS 14 and Swift 5

Sign up for my iOS development course, and learn how to build great iOS 14 apps with Swift 5 and Xcode 12.

Further Reading

In this tutorial, we’ve looked at how you can work with files and directories on iOS with Swift. You’ve learned how to read from, and write to, various file formats. We’ve discussed working with Data, images, text files, JSON, plists, and much more. Awesome!

Want to learn more? Check out these resources:

Reinder de Vries

Hi, I'm Reinder.
I help developers play with code.

Get the Weekly

Get iOS/Swift tutorials and insights in your inbox, every Monday.
  • This field is for validation purposes and should be left unchanged.

Most Popular

Browse Topics

Swift Sandbox

Code Swift right in your browser!
Go to the Swift Sandbox

Reinder de Vries

Reinder de Vries

Reinder de Vries is a professional iOS developer. He teaches app developers how to build their own apps at LearnAppMaking.com. Since 2009 he has developed a few dozen apps for iOS, worked for global brands and lead development at several startups. When he’s not coding, he enjoys strong espresso and traveling.

×

Build great iOS apps
Learn how in my free 7-day course

  • This field is for validation purposes and should be left unchanged.

No spam, ever. Unsubscribe anytime. Privacy Policy