December 21, 2019

Save file to Documents directory with Swift

Save file to Documents directory with Swift

In this tutorial we will learn how to save a file to the Documents directory. There are many reasons why this would be useful. You might want to save an image locally, I have written a tutorial about this before. You might want to build a file manager app etc.

We will be learning how to read and write to the Documents directory, we will use Swift to create a new text file, we will then save that to the Documents directory. After it is saved we will read the file to make sure that everything has worked as expected.

We are going to be creating four methods, documentDirectory, append, save and read. documentDirectory will return a string path for our documentDirectory. append will allow us to append a component to a path. save will allow to create and save a text file and read will read a file in the Documents directory.

Step 1: Create Documents directory path method

I always remember this as being an annoying task, but looking at it now, it really is a simple task.

Add the following method to your code:

private func documentDirectory() -> String {
    let documentDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory,
                                                                .userDomainMask,
                                                                true)
    return documentDirectory[0]
}

There is not too much going on in this method. We look for the .documentDirectory in the .userDomainMask, using the NSSearchPathForDirectoriesInDomains method. This method will return an array of strings, we only want the first element of the array which is why we do documentDirectory[0].

Step 2: Create Append to path method

The next method that we need to create is the append method. This will allow us to append a component to our path. URL already has appendPathComponent but this will make a bit easier and cleaner for us later on.

Add the following method to your code:

private func append(toPath path: String,
                    withPathComponent pathComponent: String) -> String? {
    if var pathURL = URL(string: path) {
        pathURL.appendPathComponent(pathComponent)
        
        return pathURL.absoluteString
    }
    
    return nil
}

This append method will take two arguments, the first will be the path, the path is actually just a Documents directory path. The second argument will be the the rest of the path, in our case this will be a file name.

Inside append we will try and create a URL from our path, if that does not return nil we will append our pathComponent to the URL and return the absoluteString from the URL. If we cannot create a URL from the path that we provide, we will return nil.

Step 3: Create the read method

In this example of how to save to, and, read from the Documents directory, our read method will only allow us to pass in a file name and we will not be allowed to change our "base" directory, we will only be reading from the Documents directory.

Add the following read to your code:

private func read(fromDocumentsWithFileName fileName: String) {
    guard let filePath = self.append(toPath: self.documentDirectory(),
                                     withPathComponent: fileName) else {
                                        return
    }
    
    do {
        let savedString = try String(contentsOfFile: filePath)
        
        print(savedString)
    } catch {
        print("Error reading saved file")
    }
}

As mentioned before, the read method will only take the file name as an argument. We will try and get the filePath for that that file name using the append method. If the fileName does not exist in the Documents directory we return.

If the Documents directory does contain the file with the fileName that we passed in we will try and read it. To read it we will initialise a new String with the filePath. If we successfully initialise a String with the filePath we will just print out the file content, print(savedString).

The contentsOfFile initialiser for String can throw an error, that is why we need to use the do try catch. Generally by this point one would know if the file exists or not, but it might not be a format that a String can be initialised with, because of this it will need to be able to throw an error.

Step 4: Save file to Documents directory

The last method that we need to implement is the save method. The save method will take three arguments. The first will be content of the text file. The second will be the directory, and the third will be the file name.

This method is quite similar to the read method. Add the following method to your code:

private func save(text: String,
                  toDirectory directory: String,
                  withFileName fileName: String) {
    guard let filePath = self.append(toPath: directory,
                                     withPathComponent: fileName) else {
        return
    }
    
    do {
        try text.write(toFile: filePath,
                       atomically: true,
                       encoding: .utf8)
    } catch {
        print("Error", error)
        return
    }
    
    print("Save successful")
}

We use the append method to add the fileName to the directory, which is just the base path to the Documents directory.

The final part is to write the file. String's have multiple write methods, we have used the toFile one, which allows us to pass in our filePath as a String.

Because writing to the file is not something that can be guaranteed, we need to wrap it in a do try catch, otherwise we will print out the error.

Step 5: Call the save and read methods

Now that all the methods that we need have been created, we can all them. I am going to call them from viewDidLoad.

This is what my viewDidLoad looks like:

override func viewDidLoad() {
    super.viewDidLoad()
    
    let fileName = "testFile1.txt"

    self.save(text: "Some test content to write to the file",
              toDirectory: self.documentDirectory(),
              withFileName: fileName)
    self.read(fromDocumentsWithFileName: fileName)
}

I have created a let for the fileName because it is used in two places. Then I call the save method which will write "Some test content to write to the file" to a file, and then after the save method I call the read method. All the read method does is print out the contents of the file that we saved.

Conclusion

As I mentioned before, I remember writing to files being a pain, but it is actually quick an easy to read and write files.

If you want to look at the final source code, it can be found here.