File handling with Swift. Focusing on atomic operations
By: Steven Curtis
Tags: iOS App Development, Ios Development, Swift, Programming

Files are notoriously tricky, but seems to be a little bit worse in Swift with deprecated classes and methods. How can we make this a little bit easier? A nice little tutorial with code examples? OK, if that is what you want.

Ensuring operations are atomic means that the operations will be complete, with no intermediate states and potentially corrupted data.

Difficulty: Easy | Normal | Challenging

Prerequisites:

Some familiarity with the debugger

Error handling with try

Terminology

Atomic file operations: Methods that cannot be interrupted or partially completed.

File System: Handles the persistent storage of data files, apps, and the files associated with the operating system itself.

SearchPathDirectory: The location of significant folders on iOS devices.

Sandbox: A set of fine-grained controls that limit the app’s access to files, preferences, network resources, hardware.

The sandbox

iOS helps you out in interacting with the file system, by stopping competing Apps making changes to your file directories. This means that you, as an App, also have restrictions. In any case, here is Apple’s take on the interactions an App should have with the file system:

For security purposes, an iOS app’s interactions with the file system are limited to the directories inside the app’s sandbox directory. During installation of a new app, the installer creates a number of container directories for the app inside the sandbox directory. Each container directory has a specific role. The bundle container directory holds the app’s bundle, whereas the data container directory holds data for both the app and the user. The data container directory is further divided into a number of subdirectories that the app can use to sort and organize its data. The app may also request access to additional container directories — for example, the iCloud container–at runtime.

Top tips

Seeing your files

Using the simulator you can see the files that are currently within your App bundle. Where?

User>Library>Developer>CoreSimulator>Devices

To find which GUID is your App is running you can use the following LLDB command:

po NSHomeDirectory()

to return your home directory from the debugger which returns something like the following:

“/Users/user/Library/Developer/CoreSimulator/Devices/37A85B0B-F2B7–4A0C-BAA7-E05A831FFAE0/data/Containers/Data/Application/AC630F5A-71D2–4DE4–82E2-D7CF328015C8”

and files will even be saved in this folder, or within the library folder contained within this folder.

File folders

You will be aware that there are folders within a file system. You can add files into the folders in your file system and keep your work nice and organised.

Restrictions in the provided code

Error handling

The presented code does not handle errors. This is for brevities sake, but do NOT be too upset if you see something where I’m just printing out the error for you.

SearchPathDirectory

These are the significant directories that are used. The documentation mentions them in turn, but here I just mention the ones that I will refer to within this tutorial.

applicationSupportDirectory: A place to store files that will be used within your App

cachesDirectory: A good place to store the caches that can be removed in your App

itemReplacementDirectory: A temporary directory

Paths or URLs

There is often a mix of url and files

A path is of the following format, and in this case is to file.json

/Users/user/Library/Developer/CoreSimulator/Devices/37A85B0B-F2B7–4A0C-BAA7-E05A831FFAE0/data/Containers/Data/Application/9FC4F273–2AD5–48E1-BCEC-5A2CCDD6CD7F/Library/Application Support/file.json

A URL may we be of the following format

file:///Users/user/Library/Developer/CoreSimulator/Devices/37A85B0B-F2B7–4A0C-BAA7-E05A831FFAE0/data/Containers/Data/Application/9EE0E3F2-CC53–4462-B63C-949B8C98A3BD/Library/Application%20Support/StagingBundle.bundle/Version.json

notice that a url has file:/// in addition to the file path.

Absolute paths, such as those shown above give the full path of a file.

Generally the use of URLs is preferred to paths, as they are seen as more flexible. However FileManager has methods that can deal with both so it may well be better to choose one of URLs and file paths, and stick to it (but choose URL, yeah?)

The simple actions (that you might use)

These functions are intended to have copy-and-paste simplicity and be suitable for use in any Apps.

CreateDirectory

FileManager allows you to create directories, either with a URL or with a file path

try fileManager.createDirectory(at: newURL, withIntermediateDirectories: true, attributes: nil)

or alternatively with a file path

try fileManager.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil)

In the examples here, I will choose using URLs

Now where this is more interesting is that we can create a directory with folder name and in the SearchPathDirectory of your choice:

Which gives you the following result:

You need to bear in mind that the Application Support directory is not guaranteed to be present in the file system at any time. We avoided an error here, since the withIntermediateDirectories boolean is set to true.

RemoveDirectory

FileManager allows us to remove items.

try fileManager.removeItem(at: originURL)

Equally we can use the filepath equivalent

try fileManager.removeItem(atPath: originURL)

and once again here we cover the use of url

which removes the directory added above.

Writing files

FileManager has a createFile method that writes out some data.

To obtain this data I’ll just transform a String into data for you

let dataToWrite = "My Interesting Data".data(using: .utf8)

Then we can use FileManager’s createFile(atPath

func create(atPath path: String, contents data: Data?, attributes attract: [FileAttributeKey : Any]? = nil) -> Bool

Now unfortunately this function only accepts a file path, and also calling this does not throw and rather produces a Boolean result. Also this method isn’t atomic so it is rather tricky to know whether our operation to write has finished at any given point in time.

Many people forget to first create the file, now don’t worry — I’ve got you covered:

But the real issue here is the API uses atPath. It is much better to use an API that supports URLs, and even better if we could write atomically (that is, guarantee that the write finishes.

So once again we will need the data to write, and then also call the method

let dataToWrite = "My Interesting Data".data(using: .utf8)

createFileToURL(withData: dataToWrite, withName: "MyFile.txt")

This can easily be updated to allow us to create subfolders. However, is the subfolder has not already been created, an error will be thrown. In this case we need to create the subdirectory too!

let dataToWrite = "My Interesting Data".data(using: .utf8)

createFileToURL(withData: dataToWrite, withName: "MyFile.txt", withSubDirectory: "test")

Which would give the following result:

Delete items

FileManager has a .removeItem method that does have a choice of either using URL or a filepath

try fileManger.removeItem(atPath: originPath)

try fileManger.removeItem(at: originURL)

Which can (as before) be developed into a reusable method:

and it is trivial to update this to deal with subfolders in the path

Now I prefer to create a method to write a string directly to a directory. As it turns out, this is not a complicated function to write and this is what has been achieved as follows:

Copying items

Now we are getting in the swing of things and the API we can use fileManager.copyItem to move an item, whether it is using either filePath or URL to do so.

try fileManager.copyItem(at: originURL, to: destinationURL) try fileManager.copyItem(atPath: originPath, toPath: destinationPath)

But there is one problem here. A large problem, as so it turns out.

This file copy operation is not atomic. That means we can’t guarantee it completes.

A better way is to read the contents of a file into a temporary file (and, indeed directory as .itemReplacementDirectory is a good choice here).

Create a temp file

So we can create a temp file using the atomic data.write operation

Replace the temp file, using the atomic operation data.write

Moving items

Now moving is similar to copying, right?

There is are FileManager methods for move as well

try fileManager.moveItem(at: originURL, to: destinationURL) try fileManager.moveItem(atPath: originPath, toPath: destinationPath)

but, rather depressingly (and predictably) they are non-atomic.

Now moving is the same as copying, but the original file is removed after the operation.

Now you might remember that

fileManger.removeItem(at: originURL)

is not guaranteed to be atomic.

So what to do?

Well, although removeItem is not atomic, it is a short operation. Secondly, we are only removing a reference to the file in practice (how can this particular operation be incomplete), and it also returns a boolean depending on whether the operation is completed or not.

If you aren’t sufficiently happy with this answer there is another solution; take the original file and wipe over it with a blank file (this is an atomic operation). I imagine you’d need to set a boolean somewhere to indicate that the file should not be accessed, but I think this will be no problem for you.