Back to blog

Working with Files on iOS with Swift


Aasif Khan
By Aasif Khan | Last Updated on December 8th, 2023 7:26 am | 5-min read

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 app development 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

Describe your app idea
and AI will build your App

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.

FileManager is an essential tool for developers working with iOS. It acts as a bridge between your application and the device’s file system, allowing you to manage and manipulate files with ease. Think of it as a librarian that knows where every book (file) is and can fetch it for you when needed.

For beginners, it’s crucial to understand that while iOS devices have a file system like computers, they operate under stricter rules. The sandboxing of apps ensures that each app only has access to its designated space and cannot interfere with other apps. This design is a protective measure to maintain the integrity and security of the device and user data.

Moreover, while on platforms like macOS or Windows, you might be used to directly dealing with file paths, on iOS, you’ll often work with URLs. These URLs are not your typical web addresses but are references to file locations.

In essence, FileManager is your gateway to efficiently storing, retrieving, and managing files on iOS devices. Whether you’re saving user preferences, storing images, or caching data, FileManager is the tool you’ll use. 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)…

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.

Reading files in Swift is a fundamental skill for any iOS developer. While the process might seem intricate at first, with practice, it becomes second nature. The key is understanding the structure of the iOS file system and the tools Swift provides to interact with it.

When reading a file, it’s essential to ensure that the file path is correct and that the file exists at the specified location. Handling errors gracefully is also crucial. For instance, if the file doesn’t exist or there’s an issue with its format, your app should provide a clear message to the user or take corrective action.

It’s also worth noting that while we often use text files for examples, the principles apply to other file types as well, such as images, audio files, and more. The difference lies in how you process the data once it’s read.

In modern iOS development, many libraries and frameworks can simplify file reading. However, understanding the basics, as outlined above, provides a strong foundation to tackle more complex tasks.

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.

Writing to files is a common operation in many applications, from saving user preferences to storing large datasets. Understanding how to write to files in Swift is crucial for any iOS developer.

When writing to a file, it’s essential to be aware of potential pitfalls. For instance, always ensure you have the necessary permissions to write to the specified location. Also, be cautious about overwriting existing files unless that’s the intended behavior.

Additionally, while the example above demonstrates writing a simple text string to a file, the principles can be extended to other data types. Whether you’re saving images, audio clips, or structured data like JSON, the process remains similar. The primary difference lies in how you prepare and format the data before writing.

Lastly, always consider the user’s privacy and the security of the data you’re writing. If storing sensitive information, consider encrypting the data or using secure storage mechanisms provided by iOS.

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)
}

Conclusion

Working with files on iOS using Swift is a fundamental skill for app developers. The FileManager component offers a structured way to interact with the iOS file system, allowing developers to read, write, and manage files efficiently. While the process might seem intricate at first, with a clear understanding and practice, it becomes intuitive. Remember, iOS operates under specific rules, like sandboxing, to ensure the security and integrity of data. As you delve deeper into iOS development, mastering file management will empower you to create more dynamic and data-driven applications.


Aasif Khan

Head of SEO at Appy Pie

App Builder

Most Popular Posts