Reading, Writing, and Deleting Files in Swift
Most of today’s average apps communicate via HTTP. While we can read and write data this way, it is still very common to need to save data to files locally on the device. Let’s talk about how that is done and then address an easier way.
TL;DR
If you are not interested in learning how to do it yourself then check out my File project on GitHub (but you really should know how it is done, so keep reading!).
FileManager and URLs
When first starting out with file handling in iOS it can feel daunting. You have to learn the FileManager which doesn’t seem so bad until you find out that you need to pass it URLs. You then stop and think “wait, what do URLs have to do with saving files? This is confusing.”
Apple chose to use URLs as the universal language for communicating the location of files in the file system. Once you get your head wrapped around the concept, it really is quite ingenious. Working with file locations via strings can be quite messy (and I have many dozens of lines of old server code for traversing directories and files to prove it), but using URLs wraps it all in a much needed structure that is relatively easy to understand and familiar if you have previously used URLs for networking. And even if you haven’t used URLs before, what we need to get the basics going is really quite easy. In fact, it can all be boiled down to a pattern containing three simple steps:
1. Create the destination directory URL
2. Use the directory URL to create the file URL
3. Save the data to the file URL
Let’s take a look at how this is done.
Note: If you have never used FileManager before, just know that it uses the Singleton pattern and that you do not instantiate FileManager directly, but instead you use FileManager.default
to call FileManager’s properties and functions. If you are unfamiliar with the Singleton pattern that’s okay, it is just a way to ensure that only one instance of a class is ever instantiated in the application’s lifecycle.
Directory URL
To start with we need a URL for the documents directory. You don’t have to save everything to the documents directory, but it is the most logical place to start and once you know how to save there it is fairly straightforward to save to other locations. We’ll use the following code to get the URL to the documents directory:
let directoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
What we are doing here is asking FileManager for a list of URL’s for the documents directory in the user’s home directory (or, in the case of iOS, the app’s home directory). This returns an array of which the first entry will contain the documents directory which is why we specify the first array index of [0]
at the end of the statement. This will return a URL
object for the documents directory. It’s really that simple.
File URL
Now that we have the URL to the directory were the file will be saved, let’s create a URL to the file itself. To accomplish this we use URL(fileURLWithPath:relativeTo:)
. The path referred to in the fileURLWithPath
parameter is the file’s name excluding any file extension. For instance, if we want a file named “myFile.txt” the fileURLWithPath
would be “myFile” (I’ll address the “.txt” extension in just a moment). Next, the relativeTo
parameter is the directory to which the file will be saved. This parameter takes a URL
and, you guessed it, we will simply pass in the URL
that we got above:
let fileURL = URL(fileURLWithPath: “myFile”, relativeTo: directoryURL)
Okay, so what about that file extension? To add that simply add appendingPathExtension(:)
to the end of the call like this:
let fileURL = URL(fileURLWithPath: “myFile”, relativeTo: directoryURL).appendingPathExtension(“txt”)
That will give us a URL
to myFile.txt
in the app’s documents directory folder and do so in just two lines of code. That wasn’t too bad, right?
Saving Data
Now, let’s actually save some data:
// Create data to be saved
let myString = “Saving data with FileManager is easy!”
guard let data = myString.data(using: .utf8) else {
print(“Unable to convert string to data”)
return
}// Save the data
do {
try data.write(to: fileURL)
print(“File saved: \(fileURL.absoluteURL)”)
} catch {
// Catch any errors
print(error.localizedDescription)
}
First, we create a data object containing the data that we want to save. In this case a simple string. We then convert the string into data. Next, we use a do/catch
block to wrap the writing of the data just in case there is an error. As you can see, the actual writing of the data is quite simple and done by passing the file URL as a parameter to Data
’s write(:)
function. Finally, we’ll print the error if one occurs.
And that’s it. Assuming you already have your data created, this can all be done in just a few lines of code.
Reading Data
Now that we have saved the data, at some point we will want to read it back. It turns out that this is just as easy as saving it, especially now that we have the file URL:
do {
// Get the saved data
let savedData = try Data(contentsOf: fileURL)
// Convert the data back into a string
if let savedString = String(data: savedData, encoding: .utf8) {
print(savedString)
}
} catch {
// Catch any errors
print(“Unable to read the file”)
}
Deleting Data
Now that we have saved a file and read its contents, let’s look at how we delete that file. Once again, it is really quite simple.
try FileManager.default.removeItem(at: fileURL)
We simply pass the fileURL
object that we created earlier into FileManager
’s removeItem(at:)
method. This method does not return any values, but does throw an error if it is unable to delete the file so I suggest that you wrap it in a do/catch
block.
How Can This Get Easier?
As promised, saving, reading, and deleting data isn’t that difficult. But I did say I would show you an even easier way, so what is it?
On my GitHub I have a open-source project called File which is essentially a wrapper around FileManager
that can be used to save, read, or delete a file in just two lines of code:
let fileURLComponents = FileURLComponents(
fileName: “mySecondFile”,
fileExtension: “txt”,
directoryName: nil,
directoryPath: .documentDirectory)
try? File.write(data, to: fileURLComponents)
So, what is happening here? First, we are creating a FileURLComponents
object which is a construct that is part of File
. It essentially allows you to input all of the parameters that you would normally use to build the FileManager URL
object and from these File
will build the URL internally. The fileName
parameter is the name of the file, the fileExtension
parameter is the file extension, the directoryName
is the name of the sub-directory that the file should be saved to relative to the directoryPath
. In this case we passed nil
to directoryName
so the file will be stored directly in the documents directory just as before. Finally, you send the data and the URL components to the File.write
function which handles creating the URL
and saving the data. In a real app you will want to wrap File.write
in a do
statement with proper error handling.
Reading the data from the saved file is just as easy. Assuming you have the FileURLComponents
already, you simply use the File.read
function:
let savedData = try? File.read(from: fileURLComponents)
Finally, deleting files are probably exactly what you would expect it to be at this point:
try File.delete(fileURLComponents)
And that is pretty much all there is to it.
Where to Go From Here
If you are feeling adventurous, continue reading more about FileManager and URLs. Learning the Apple provided APIs is one of the best things you can do to make yourself a better iOS developer. Even if you use a wrapper like File, I highly encourage you to learn FileManager because there is much more that can be done with FileManager such as directory traversal and other operations which I plan to cover in future articles.
For those that just need to get up and running quickly, File is available for downloaded from Github. As of this article’s posting, File can only read, write, and delete files, but more capabilities will be coming soon.
Let me know if you have any questions or suggestions below. If you found a bug in the article please post a comment.
Photo by Fredy Jacob on Unsplash