Temporary Files
Volumes have been written about persisting data, but when it comes to short-lived, temporary files, there is very little to go on for Cocoa. (Or if there has, perhaps it was poetically ephemeral itself).
Temporary files are used to write data to disk
before either moving it to a permanent location
or discarding it.
For example, when a movie editor app exports a project,
it may write each frame to a temporary file until it reaches the end
and moves the completed file to the ~/Movies
directory.
Using a temporary file for these kinds of situations
ensures that tasks are completed atomically
(either you get a finished product or nothing at all; nothing half-way),
and without creating excessive memory pressure on the system
(on most computers, disk space is plentiful whereas memory is limited).
There are four distinct steps to working with a temporary file:
- Creating a temporary directory in the filesystem
- Creating a temporary file in that directory with a unique filename
- Writing data to the temporary file
- Moving or deleting the temporary file once you’re finished with it
Creating a Temporary Directory
The first step to creating a temporary file is to find a reasonable, out-of-the-way location to which you can write — somewhere inconspicuous that doesn’t get in the way of the user or get picked up by a system process like Spotlight indexing, Time Machine backups, or iCloud sync.
On Unix systems, the /tmp
directory is the de facto scratch space.
However, today’s macOS and iOS apps run in a container
and don’t have access to system directories;
a hard-coded path like that isn’t going to cut it.
If you don’t intend to keep the temporary file around,
you can use the NSTemporary
function
to get a path to a temporary directory for the current user.
let temporary Directory URL = URL(file URLWith Path: NSTemporary Directory(),
is Directory: true)
NSURL *temporary Directory URL = [NSURL file URLWith Path: NSTemporary Directory()
is Directory: YES];
Alternatively,
if you intend to move your temporary file to a destination URL,
the preferred (albeit more complicated) approach
is to call the File
method uri(for:in:appropriate
.
let destination URL: URL = /path/to/destination
let temporary Directory URL =
try File Manager.default.url(for: .item Replacement Directory,
in: .user Domain Mask,
appropriate For: destination URL,
create: true)
NSURL *destination URL = <#/path/to/destination#>;
NSFile Manager *file Manager = [NSFile Manager default Manager];
NSError *error = nil;
NSURL *temporary Directory URL =
[file Manager URLFor Directory:NSItem Replacement Directory
in Domain:NSUser Domain Mask
appropriate For URL:destination URL
create:YES
error:&error];
The parameters of this method are frequently misunderstood, so let’s go through each to understand what this method actually does:
- We pass the item replacement search path (
.item
) to say that we’re interested in a temporary directory.Replacement Directory - We pass the user domain mask (
.user
) to get a directory that’s accessible to the user.Domain Mask - For the
appropriate
parameter, we specify ourFor URL destination
, so that the system returns a temporary directory from which a file can be quickly moved to the destination (and not, say across different volumes).URL - Finally, we pass
true
to thecreate
parameter to save us the additional step of creating it ourselves.
The resulting directory will have a path that looks something like this: file:///var/folders/l3/kyksr35977d8nfl1mhw6l_c00000gn/T/TemporaryItems/(A%20Document%20Being%20Saved%20By%20NSHipster%208)/
Creating a Temporary File
With a place to call home (at least temporarily), the next step is to figure out what to call our temporary file. We’re not picky about what it’s named — just so long as it’s unique, and doesn’t interfere with any other temporary files in the directory.
The best way to generate a unique identifier
is the Process
property globally
:
Process Info().globally Unique String
[[NSProcess Info process Info] globally Unique String];
The resulting filename will look something like this: 42BC63F7-E79E-4E41-8E0D-B72B049E9254-25121-000144AB9F08C9C1
Alternatively,
UUID
also produces workably unique identifiers:
UUID().uuid String
[[NSUUID UUID] UUIDString]
A generated UUID string has the following format: B49C292E-573D-4F5B-A362-3F2291A786E7
Now that we have an appropriate directory and a unique filename, let’s put them together to create our temporary file:
let destination URL: URL = /path/to/destination
let temporary Directory URL =
try File Manager.default.url(for: .item Replacement Directory,
in: .user Domain Mask,
appropriate For: destination URL,
create: true)
let temporary Filename = Process Info().globally Unique String
let temporary File URL =
temporary Directory URL.appending Path Component(temporary Filename)
NSURL *destination URL = <#/path/to/destination#>;
NSFile Manager *file Manager = [NSFile Manager default Manager];
NSError *error = nil;
NSURL *temporary Directory URL =
[file Manager URLFor Directory:NSItem Replacement Directory
in Domain:NSUser Domain Mask
appropriate For URL:destination URL
create:YES
error:&error];
NSString *temporary Filename =
[[NSProcess Info process Info] globally Unique String];
NSURL *temporary File URL =
[temporary Directory URL
URLBy Appending Path Component:temporary Filename];
Writing to a Temporary File
The sole act of creating a file URL is of no consequence to the file system; a file is created only when the file path is written to. So let’s talk about our options for doing that:
Writing Data to a URL
The simplest way to write data to a file
is to call the Data
method write(to:options)
:
let data: Data = some data
try data.write(to: temporary File URL,
options: .atomic)
NSData *data = <#some data#>;
NSError *error = nil;
[data write To URL:temporary File URL
options:NSData Writing Atomic
error:&error];
By passing the atomic
option,
we ensure that either all of the data is written
or the method returns an error.
Writing Data to a File Handle
If you’re doing anything more complicated
than writing a single Data
object to a file,
you might instead create an empty file
and use a File
to write data incrementally.
file Manager.create File(at Path: temporary File URL.path, contents: Data())
let file Handle = try File Handle(for Writing To: temporary File URL)
defer { file Handle.close File() }
file Handle.write(data)
// ...
[file Manager create File At Path: [temporary File URL path]
contents: [NSData data]
attributes: @{}];
NSError *error = nil;
NSFile Handle *file Handle =
[NSFile Handle file Handle For Writing To URL:temporary File URL
error:&error];
[file Handle write Data:data];
// ...
[file Handle close File];
Writing Data to an Output Stream
For more advanced APIs,
it’s not uncommon to use Output
to direct the flow of data.
Creating an output stream to a temporary file
is no different than any other kind of file:
let output Stream =
Output Stream(url: temporary File URL, append: true)!
defer { output Stream.close() }
data.with Unsafe Bytes { bytes in
output Stream.write(bytes, max Length: bytes.count)
}
NSOutput Stream *output Stream =
[NSOutput Stream output Stream With URL:temporary File URL
append:YES];
[output Stream write:data.bytes
max Length:data.length];
[output Stream close];
Moving or Deleting the Temporary File
Files in system-designated temporary directories are periodically deleted by the operating system. So if you intend to hold onto the file that you’ve been writing to, you need to move it somewhere outside the line of fire.
If you already know where the file’s going to live,
you can use File
to move it to its permanent home:
let file URL: URL = /path/to/file
try File Manager.default.move Item(at: temporary File URL,
to: file URL)
NSFile Manager *file Manager = [NSFile Manager default Manager];
NSURL *file URL = <#/path/to/file#>;
NSError *error = nil;
[file Manager move Item At URL:temporary File URL
to URL:file URL
error:&error];
Although the system eventually takes care of files in temporary directories, it’s not a bad idea to be a responsible citizen and follow the guidance of “take only pictures; leave only footprints.”
File
can help us out here as well,
with the remove
method:
try File Manager.default.remove Item(at: temporary File URL)
NSFile Manager *file Manager = [NSFile Manager default Manager];
NSError *error = nil;
[file Manager remove Item At URL:temporary File URL
error:&error];
“This too shall pass” is a mantra that acknowledges that all things are indeed temporary.
Within the context of the application lifecycle, some things are more temporary than others, and it’s with that knowledge that we choose to act appropriately: seeking to find the right place, make a unique impact, and leave without a trace.
Perhaps we can learn something from this cycle in our own, brief and glorious lifecycle.