FileManager
One of the most rewarding experiences you can have as a developer is to teach young people how to program. If you ever grow jaded by how fundamentally broken all software is, there’s nothing like watching a concept like recursion click for the first time to offset your world-weariness.
My favorite way to introduce the concept of programming is to set out all the ingredients for a peanut butter and jelly sandwich and ask the class give me instructions for assembly as if I were a robot 🤖. The punchline is that the computer takes every instruction as literally as possible, often with unexpected results. Ask the robot to “put peanut butter on the bread”, and you may end up with an unopened jar of Jif flattening a sleeve of Wonder Bread. Forget to specify which part of the bread to put the jelly on? Don’t be surprised if it ends up on the outer crust. And so on. Kids love it.
The lesson of breaking down a complex process into discrete steps
is a great encapsulation of programming.
And the malicious compliance from lack of specificity
echoes the analogy of “programming as wish making”
from our article about numeric
.
But let’s take the metaphor a step further,
and imagine that instead of commanding a single robot to
(sudo
)
make a sandwich,
you’re writing instructions for a thousand different robots.
Big and small, fast and slow;
some have 4 arms instead of 2,
others hover in the air,
maybe a few read everything in reverse.
Consider what would happen if multiple robots tried to make a sandwich
at the same time.
Or imagine that your instructions
might be read by robots that won’t be built for another 40 years,
by which time peanut butter is packaged in capsules
and jelly comes exclusively as a gas.
That’s kind of what it’s like to interact with a file system.
The only chance we have at making something that works
is to leverage the power of abstraction.
On Apple platforms,
this functionality is provided by the Foundation framework
by way of File
.
We can’t possibly cover everything there is to know about working with file systems in a single article, so this week, let’s take a look at the operations you’re most likely to perform when building an app.
File
offers a convenient way to create, read, move, copy, and delete
both files and directories,
whether they’re on local or networked drives or iCloud ubiquitous containers.
The common currency for all of these operations are paths and file URLs.
Paths and File URLs
Objects on the file system can be identified in a few different ways. For example, each of the following represents the location of the same text document:
- Path:
/Users/NSHipster/Documents/article.md
- File URL:
file:///Users/NSHipster/Documents/article.md
- File Reference URL:
file:///.file/id=1234567.7654321/
Paths are slash-delimited (/
) strings that designate a location
in the directory hierarchy.
File URLs are URLs with a file://
scheme in addition to a file path.
File Reference URLs identify the location of a file
using a unique identifier separate from any directory structure.
Of those,
you’ll mostly deal with the first two,
which identify files and directories using a relational path.
That path may be absolute
and provide the full location of a resource from the root directory,
or it may be relative
and show how to get to a resource from a given starting point.
Absolute URLs begin with /
,
whereas relative URLs begin with
./
(the current directory),
../
(the parent directory), or
~
(the current user’s home directory).
File
has methods that accept both paths and URLs —
often with variations of the same method for both.
In general, the use of URLs is preferred to paths,
as they’re more flexible to work with.
(it’s also easier to convert from a URL to a path than vice versa).
Locating Files and Directories
The first step to working with a file or directory
is locating it on the file system.
Standard locations vary across different platforms,
so rather than manually constructing paths like /System
or ~/Documents
,
you use the File
method url(for:in:appropriate
to locate the appropriate location for what you want.
The first parameter takes one of the values specified by
File
.
These determine what kind of standard directory you’re looking for,
like “Documents” or “Caches”.
The second parameter passes a
File
value,
which determines the scope of where you’re looking for.
For example,
.application
might refer to /Applications
in the local domain
and ~/Applications
in the user domain.
let documents Directory URL =
try File Manager.default.url(for: .document Directory,
in: .user Domain Mask,
appropriate For: nil,
create: false)
NSFile Manager *file Manager = [NSFile Manager default Manager];
NSString *documents Path =
[NSSearch Path For Directories In Domains(NSDocument Directory, NSUser Domain Mask, YES) first Object];
NSString *file Path = [documents Path string By Appending Path Component:@"file.txt"];
Determining Whether a File Exists
You might check to see if a file exists at a location before trying to read it,
or want to avoid overwriting an existing one.
To do this, call the file
method:
let file URL: URL = /path/to/file
let file Exists = File Manager.default.file Exists(at Path: file URL.path)
NSURL *file URL = <#/path/to/file#>;
NSFile Manager *file Manager = [NSFile Manager default Manager];
BOOL file Exists = [file Manager file Exists At Path:[file URL path]];
Getting Information About a File
The file system stores various pieces of metadata
about each file and directory in the system.
You can access them using the File
method attributes
.
The resulting dictionary contains attributes keyed by File
values,
including .creation
:
let file URL: URL = /path/to/file
let attributes =
File Manager.default.attributes Of Item(at Path: file URL.path)
let creation Date = attributes[.creation Date]
NSURL *file URL = <#/path/to/file#>;
NSFile Manager *file Manager = [NSFile Manager default Manager];
NSError *error = nil;
NSDictionary *attributes = [file Manager attributes Of Item At Path:[file URL path]
error:&error];
NSDate *creation Date = attributes[NSFile Creation Date];
Listing Files in a Directory
To list the contents of a directory,
call the File
method
contents
.
If you intend to access any metadata properties,
as described in the previous section
(for example, get the modification date of each file in a directory),
specify those here to ensure that those attributes are cached.
The options
parameter of this method allows you to skip
hidden files and/or descendants.
let directory URL: URL = /path/to/directory
let contents =
try File Manager.default.contents Of Directory(at: directory URL,
including Properties For Keys: nil,
options: [.skips Hidden Files])
for file in contents {
…
}
NSFile Manager *file Manager = [NSFile Manager default Manager];
NSURL *bundle URL = [[NSBundle main Bundle] bundle URL];
NSArray *contents = [file Manager contents Of Directory At URL:bundle URL
including Properties For Keys:@[]
options:NSDirectory Enumeration Skips Hidden Files
error:nil];
NSPredicate *predicate = [NSPredicate predicate With Format:@"path Extension == 'png'"];
for (NSURL *file URL in [contents filtered Array Using Predicate:predicate]) {
// Enumerate each .png file in directory
}
Recursively Enumerating Files In A Directory
If you want to go through each subdirectory at a particular location recursively,
you can do so by creating a File
object
with the enumerator(at
method:
let directory URL: URL = /path/to/directory
if let enumerator =
File Manager.default.enumerator(at Path: directory URL.path)
{
for case let path as String in enumerator {
// Skip entries with '_' prefix, for example
if path.has Prefix("_") {
enumerator.skip Descendants()
}
}
}
NSFile Manager *file Manager = [NSFile Manager default Manager];
NSURL *bundle URL = [[NSBundle main Bundle] bundle URL];
NSDirectory Enumerator *enumerator = [file Manager enumerator At URL:bundle URL
including Properties For Keys:@[NSURLName Key, NSURLIs Directory Key]
options:NSDirectory Enumeration Skips Hidden Files
error Handler:^BOOL(NSURL *url, NSError *error)
{
if (error) {
NSLog(@"[Error] %@ (%@)", error, url);
return NO;
}
return YES;
}];
NSMutable Array *mutable File URLs = [NSMutable Array array];
for (NSURL *file URL in enumerator) {
NSString *filename;
[file URL get Resource Value:&filename for Key:NSURLName Key error:nil];
NSNumber *is Directory;
[file URL get Resource Value:&is Directory for Key:NSURLIs Directory Key error:nil];
// Skip directories with '_' prefix, for example
if ([filename has Prefix:@"_"] && [is Directory bool Value]) {
[enumerator skip Descendants];
continue;
}
if (![is Directory bool Value]) {
[mutable File URLs add Object:file URL];
}
}
Creating a Directory
To create a directory,
call the method create
.
In Unix parlance, setting the with
parameter to true
is equivalent to passing the -p
option to mkdir
.
try File Manager.default.create Directory(at: directory URL,
with Intermediate Directories: true,
attributes: nil)
NSFile Manager *file Manager = [NSFile Manager default Manager];
NSString *documents Path =
[NSSearch Path For Directories In Domains(NSDocument Directory,
NSUser Domain Mask,
YES) first Object];
NSString *images Path = [documents Path string By Appending Path Component:@"images"];
if (![file Manager file Exists At Path:images Path]) {
NSError *error = nil;
[file Manager create Directory At Path:images Path
with Intermediate Directories:NO
attributes:nil
error:&error];
}
Deleting a File or Directory
If you want to delete a file or directory,
call remove
:
let file URL: URL = /path/to/file
try File Manager.default.remove Item(at: file URL)
NSFile Manager *file Manager = [NSFile Manager default Manager];
NSString *documents Path =
[NSSearch Path For Directories In Domains(NSDocument Directory,
NSUser Domain Mask,
YES) first Object];
NSString *file Path = [documents Path string By Appending Path Component:@"image.png"];
NSError *error = nil;
if (![file Manager remove Item At Path:file Path error:&error]) {
NSLog(@"[Error] %@ (%@)", error, file Path);
}
FileManagerDelegate
File
may optionally set a delegate
to verify that it should perform a particular file operation.
This is a convenient way to audit all file operations in your app,
and a good place to factor out and centralize business logic,
such as which files to protect from deletion.
There are four operations covered by the
File
protocol:
moving, copying, removing, and linking items —
each with variations for working with paths and URLs,
as well as how to proceed after an error occurs:
If you were wondering when you might create your own File
rather than using this shared instance,
this is it.
From the documentation:
You should associate your delegate with a unique instance of the
File
class, as opposed to the shared instance.Manager
class Custom File Manager Delegate: NSObject, File Manager Delegate {
func file Manager(_ file Manager: File Manager,
should Remove Item At URL: URL) -> Bool
{
// Don't delete PDF files
return URL.path Extension != "pdf"
}
}
// Maintain strong references to file Manager and delegate
let file Manager = File Manager()
let delegate = Custom File Manager Delegate()
file Manager.delegate = delegate
NSFile Manager *file Manager = [[NSFile Manager alloc] init];
file Manager.delegate = delegate;
NSURL *bundle URL = [[NSBundle main Bundle] bundle URL];
NSArray *contents = [file Manager contents Of Directory At URL:bundle URL
including Properties For Keys:@[]
options:NSDirectory Enumeration Skips Hidden Files
error:nil];
for (NSString *file Path in contents) {
[file Manager remove Item At Path:file Path error:nil];
}
// Custom File Manager Delegate.m
#pragma mark - NSFile Manager Delegate
- (BOOL)file Manager:(NSFile Manager *)file Manager
should Remove Item At URL:(NSURL *)URL
{
return ![[[URL last Path Component] path Extension] is Equal To String:@"pdf"];
}
When you write an app that interacts with a file system, you don’t know if it’s an HDD or SSD or if it’s formatted with APFS or HFS+ or something else entirely. You don’t even know where the disk is: it could be internal or in a mounted peripheral, it could be network-attached, or maybe floating around somewhere in the cloud.
The best strategy for ensuring that things work
across each of the various permutations
is to work through File
and its related Foundation APIs.