A World Without (NS)FileManager
Can you imagine a modern programming language without a standard library? This kind of vanilla technology wouldn’t be very useful. You would have to reinvent the wheel every time you’d need a string operations or a linked list. It’s hard to imagine but for the sake of learning, maybe you should.
By People from People
Writing the same functionality over and over again is contradictory to the common approach, standing on the shoulders of giants, taking battle tested code and building upon it. Standard libraries are not a divine gift sent to us from heaven by the Almighty.
This is just code written by people for people, so we can investigate it and learn from it. The Swift programming language has a solid standard library. You can find a ton of protocols and data structures there which are crucial for our day-to-day work. It doesn’t stop there though.
Where there is Swift, there must be the standard library, but you can also opt in to use Foundation framework. This framework is a legacy from Objective-C that provides us with access to operating system services (and more). As an iOS developer, whether you want it or not, Foundation is an essential tool in your work.
It is a well known fact that you appreciate the value of things more when they are taken from you. It is a great exercise that can help you grow. Let’s try this in practice. Imagine that you need to load a text file and print its content to the console. You have an empty playground and you can’t use Foundation. Where do we even begin?
The first important topic that we need to tackle is creating a file to read. Accessing the file is not trivial because of Playground’s sandbox, it only lets us access files in specific directories.
Sandboxing in Playground is much safer. With Foundation this is easy but we’ve just stripped ourselves from the privilege of using it. The most basic place for holding a file is in the Resources directory of the playground bundle. To access it you would use:
Bundle.main.path(forResource: “file”, ofType: “txt”)
The url that you would get is very hard to generate without Foundation.
Another place we can access is the Shared Playground Data folder, we can create this in our Documents. You can get the path by using playgroundSharedDataDirectory global variable (PlaygroundSupport module) but it contains the URL (Foundation struct!) so we will use a hard coded path for this exercise (this may not work in the future).
Prepare your test file by typing these commands in your terminal (-e in echo is a flag to enable interpretation of backslash escapes):
mkdir ~/Documents/Shared Playground Data
echo -e “Example text to print.nNew line of text.” &amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;gt; ~/Documents/Shared Playground Data/file.txt
We can now start our coding with a constants holding path to the file:
Power of Open Source
Our task is that much easier since Swift went open source. We can take a peek inside and decide what to do. Repository for Foundation is an open book for inspection. If we were to use FileManager we would load the file with:
Note: My link refers to the last commit on a swift 3 branch (instead of master) to preserve line number compatibility in the future. Let’s take a look into the FileManager implementation
Comment is pretty clear. Next stop — Data(contentsOf: options:)
You can see some NSData compatibility magic happening here but the only thing that’s interesting to us now is forwarding to the init of NSData.
We know that our path points to file, so we go to the other init method:
Finally we find the desired code in our last stop. Stacktrace of our journey would look like this:
And here is body of readBytesFromFileWithExtendedAttributres:
After a fast inspection of readBytesFromFileWithExtendedAttributes(_: options:) we can see several weird free functions (not belonging to any class) like stat, close and read all over this method.
Where do they come from? Import on the top of the file can enlighten us. They are from the Darwin (or Glibc if you are on Linux) module. Darwin is an open source operating system developed by Apple that provides core components for the macOS.
The Darwin module is just a bunch of headers that provides us with directions to the operating system services. This is as far as we will go, because those services are C functions that are bridged to Swift. Most of what we will use next is a part of a standard C library, so if you feel like it you can take a peek in one of the repositories that implement libc. Thanks to POSIX (a set of standards for the compatibility between families of Unix operating systems) all methods have the same interfaces, so it’s easy to navigate there.
Working with C methods can often feel strange for a Swift developer but this is the price we have to pay for access to the vast collection of C standard library functionality. Something often encountered is working with memory chunks, in the form of UnsafePointer.
If you don’t know what a pointer is or you see UnsafePointer class for the first time you should first read resource from links below before moving forward:
OK, time to code. Let’s start by creating a function that will hold all of the code for us. It will be useful to put instructions in a function (instead of global scope) because we can use guard instead of multilevel ifs.
Now inside printContent(atPath:) we need to get a file size before proceeding.
stat (from Darwin > POSIX > sys > stat ) takes a path in a form of UnsafePointer — list of characters terminated by . Fortunately, Swift does transition from String without our help. Second parameter is a pointer to a inout struct that will contain all of the information about a file. Return value is an Int32. It contains 0 if operation was successful or -1 otherwise.
It’s important to check for errors so that we can protect ourselves with if. The error code of the last operation is stored in the errno global variable. We use strerror (Darwin > C > String) to print messages related to the code. We encapsulate all of this in a simple function outside of printContent(atPath:)
Then, back in the main flow we can add the following:
If we change the file name in the path to something nonexistant, we should get a message in Playground’s Debug Area:
Error code 2: No such file or directory.
Open a File
To actually open the file we will use open, which takes a path and options (in the form of Int32 flags combined together into one value). It returns a file descriptor, a numerical id to the file that will be our handle, when reading from it. We also need to close the connection when we’re done. We can use defer to do it automatically when we leave the scope of printContent(atPath:)
Read a File
Now the fun part, reading from an file. First, we need to create a buffer for our content. malloc commands the operating system to allocate memory on the heap with given size and returns pointer to that memory. Unfortunately, pointer we recieve is not managed by ARC, so we have to release it manually with free.
To get the content of the file, we use read. The process of reading can take several iterations so we need some temporary variables to record the progress. If reading returns -1 at some point this means there’s an error so we finish the operation:
After running the code that we already have, you will see in the panel on the right, that our small file needed only one iteration to finish the operation.
There is nothing more left but to print the content of the file. Unfortunately, the data we have is not null-terminated (this is how String(cString:) knows how to read it) so we need to do some juggling before getting a String.
We need to change UnsafeMutablePointerRaw into UnsafePointer<Int8> (it happens, we know our file is UTF8 — storing one character per 8 bits (byte)). We then need to map integer into an array of characters which can be easily translated into String. The final step is printing the content of the file.
All of the code taken together looks like this:
Curiosity is a very important trait for software developers.
Curiosity has led us to ask:
How would the world look without FileManager?
In summary, we not only know how to read a file without Foundation but also how to work with C libraries from Swift. This opens a whole new spectrum of possibilities. We can now use old libraries that we know will work because they have been around for a while. Finally, curiosity has led you to reach the end of this deeply technical article, so thank you. You are awesome!
You can find the finished example from the article via this link.
Please feel free to share your experiences with us in the comments below or via social media (send us some photos or videos too), you can find us on Facebook, Twitter, Instagram and Pinterest, let’s connect!
All images used are CC0 1.0 Universal (CC0 1.0).