Previously we looked at how to read and write data to UserDefaults
, which works great for user settings or small amounts of JSON, and we also looked at SwiftData, which is a great choice for times when you want relationships between objects, or more advanced sorting and filtering.
In this app we’re going to look at a middle ground: we'll just write our data to a file directly. This isn't because I hate SwiftData, and in fact I think SwiftData would make a good choice here. Instead, it's so that I can show you the full spread of what's possible in iOS development, because there are lots of apps you'll work on that use exactly this approach to saving their data – it's good that you can at least see how it works.
That being said, using UserDefaults
is definitely a bad idea here, because there's no limit to how much data users can create in the app. UserDefaults
is better used for simple settings and similar.
Fortunately, iOS makes it very easy to read and write data from device storage, and in fact all apps get a directory for storing any kind of documents we want. Files here are automatically synchronized with iCloud backups, so if the user gets a new device then our data will be restored along with all the other system data – we don’t even need to think about it.
There is a small catch – isn’t there always? – and it’s that all iOS apps are sandboxed, which means they run in their own container with a hard to guess directory name. As a result, we can’t – and shouldn’t try to – guess the directory where our app is installed, and instead need to rely on a special URL that points to our app’s documents directory:
print(URL.documentsDirectory)
That documents directory is ours to do with as we please, and because it belongs to the app it will automatically get deleted if the app itself gets deleted. Other than physical device limitations there is no limit to how much we can store, although remember that users can use the Settings app to see how much storage your app takes up – be respectful!
Now that we have a directory to work with, we can read and write files there freely. You already met String(contentsOf:)
and Data(contentsOf:)
for reading data, but for writing data we need to use the write(to:)
method. This takes two parameters:
URL
to write to.The first of those can be created by combining the documents directory URL with a filename, such as myfile.txt.
As for the second, I prefer to give it two values as an array: .atomic
and .completeFileProtection
. They do very different things, but both are important:
To put all this code into action, we’re going to modify the default code from our template so that it writes a test string to a file in the documents directory, reads it back into a new string, then prints it out – the complete cycle of reading and writing data.
Change the body
property of ContentView
to this:
Button("Read and Write") {
let data = Data("Test Message".utf8)
let url = URL.documentsDirectory.appending(path: "message.txt")
do {
try data.write(to: url, options: [.atomic, .completeFileProtection])
let input = try String(contentsOf: url)
print(input)
} catch {
print(error.localizedDescription)
}
}
When that runs you should be able to tap the label to see “Test message” printed to Xcode’s debug output area.
Before we move on, here’s a small challenge for you: back in project 8 we looked at how to create a generic extension on Bundle
that let us find, load, and decode any Codable
data from our app bundle. Can you write something similar for the documents directory, perhaps making it an extension on FileManager
?
SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure and A/B test your entire paywall UI without any code changes or app updates.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.