iOS File System and Persistence with FileManager

File system may be defined as the system by which the permanent storage space of a device is organized and divided up to hold files. The file system a machine has is determined by the operating system running on that machine. iOS, iPadOS, macOS dictates a UNIX based file system made up of a hierarchy of directories. With the primary goals of security and simplicity, the file system treats apps as self-contained independent islands. Each app once installed is placed in what’s called a sandbox, or self-contained area of the file system in which to keep all of its data.

The iOS file system in a few ways resembles the macOS file system, and in a few ways it differs. Some similarities between the two file systems are:

  • They are both Unix based.
  • Both have special folders called Documents or Library for each user (macOS) or App (iOS)
  • Both may synchronize content with the cloud by using iCloud.

Differences:

  • While the security (who can access which folders) on macOS is relatively lax, on iOS it is strict.
  • The absolute paths for key folders in macOS, such as your home folder, don’t change every time you boot your Mac. On iOS the absolute path for key folders will change.

The sandbox

Having the basic understanding of how the overall file system works, let’s look at how sandbox looks like on the inside.

 An iOS app operating within its own sandbox directory.

When a user installs an app it’s sandbox is created with several pre-made containers.

The Bundle Container contains the application itself, or more specifically, a directory that holds the executable code and resources, like images and sound files or whatever else that code uses.

The Data Container, like its name might suggest, holds all the user and app data. Within it are three sub-containers: the documents, library, and temp directories. Temp is used for storing temporary data that needs no persistence across launches. Documents is where user data should go, and library is for non-user data files or files that you don’t want to expose to the user. Both directories contain a few standard sub-directories, such as Documents/Inbox, Library/Applications Support, Library/Caches, and several others.

You can also create subdirectories of your own to better organize your files. At app runtime, an app may also request access to additional containers such as the iCloud container.

Now that we know how the file system works and what the Sandboxes inside the file system look like, guess where UserDefaults live. If you guessed within the data container, you’d be right. It lives right here within the data container inside of library. Its actual path looks something like this Library/Preferences/info.myapp.mobile.plist.

If you read more about the iOS file system, you might find about a sub-folder of the Sandbox called tmp. Apparently, it’s very similar to caches. The key difference between them is when they are deleted. Tmp will be deleted more often under normal circumstances.

So the sandbox is where we may save information. Let’s look at those three sub-folders mentioned earlier. Documents, this is where you save important information. iTunes will make backups of this folder, and the OS will never delete its contents. Caches, this is for temporary info. iTunes and iCloud will never make a backup of its content. Library is for files you don’t want exposed to the user. The rule of thumb is, important stuff goes into Documents. Things that won’t be necessary in the future or easy to recreate should go into Caches.

To save something in the sandbox, we need to do two things. Find where the folder is within the sandbox and then write to a file within that folder. To achieve this, we can use the following classes: FileManager to get the path to the sandbox, and then String to write or read text files or Data to write or read by binary files.

Writing and reading from the Sandbox

We need to create an instance of the File Manager class. We’ll use this to get the path to the documents folder within our sandbox. Oddly enough instead of a URL, we will get an array of URLs. What’s happening? Well, if we think about it, there’s actually over one documents folder. There’s one for each app installed. This method works by returning all of them. But since we are only asking for one, we’ll add in the second parameter to filter out the ones we don’t need.

func sandboxPlayground() {
    let fm = FileManager.default
    let documentsDirectory = fm.urls(for: .documentDirectory, in: .userDomainMask)[0]
}

Now, we have an array with just the one URL we need. But we want a URL to the new file we will create. That’s easy. We only have to add the name of the file. Now we are ready to save something. We could use data or a string.

func sandboxPlayground() {
    let fileManager = FileManager.default
    let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
    let url = documentsDirectory.appendingPathComponent("file.txt")

     do {
        try "Hi There!".write(to: url!, atomically: true, encoding: String.Encoding.utf8)
    } catch {
        print("Error while writing")
    }

    // Reading

     do {
        let content = try String(contentsOf: url!, encoding: String.Encoding.utf8)

        if content == "Hi There!" {
            print("got it")
        } else {
            print("oops")
        }
    } catch {
        print(error.localizedDescription)
    }
}

Beyond the basics of files and FileManager

So right now we just save some data to disk and retrieve that back. But there is more.

To illustrate the code below performs a shallow search of the specified directory and returns URLs for the contained items with creationDate metadata fetched and cached in the URL object.

func loadFiles() -> [File] {
        var files = [File]()
          
        let fileManager = FileManager.default
        let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
        let filesDirectory = documentsDirecotry.appendingPathComponent("files", isDirectory: true)

        guard let URLs = try? fileManager.contentsOfDirectory(at: filesDirectory, includingPropertiesForKeys: [.creationDateKey], options: [.skipsHiddenFiles]) else {
            return files
        }

        for url in URLs {
            guard let date = try? url.resourceValues(forKeys: [.creationDateKey]).creationDate else {
                continue
            }
            files.append(File(url: url, creationDate: date))
        }

        return files
    }

This doesn’t exactly seem practical for saving and retrieving tons of data, many different types of object, several versions of the data or showing it in views, and handing it with controllers. It isn’t. But what we just did is just the base concept. Able to manipulate directories, files, their attributes and much more, the FileManager class is powerful and worth exploring.

References

Comment Rules: The goal is to become better at our jobs. To post code, insert it between the tags <code></code> Critical is fine, but if you’re rude, I'll delete your stuff. Please do not put your URL in the comment text and please use your PERSONAL name or initials and not your business name, as the latter comes off like spam. Have fun and thanks for adding value to the converstaion!

Leave a Reply

Your email address will not be published. Required fields are marked *