Monday, February 8, 2016

Removing sound files from iPhone simulator during testing (Swift 2.1)

I was playing around with a recording app. During my tests, though, I realized the saved files could end up taking a bunch of space. So, I went looking for a way to handle this issue. I found two ways.

1) I could use a single, static filename and leave it in place. Then, each recording would overwrite any previous recording. Of course, this would leave the last file hanging out there when the app was done running.
2) I could somehow delete the file(s) when the app terminated.
I decided to go with the second option as a learning experience. In case you feel this is TL;DR, here’s my end code. I called the code in the View Controller’s applicationWillTerminate function. Further code explanation will follow this snippet.
func removeWavFiles() {
    // get directories our app is allowed to use
    let pathURLs = NSFileManager.defaultManager().URLsForDirectory(NSSearchPathDirectory.DocumentDirectory, inDomains: .UserDomainMask)

    // if no directories were found, exit the function
    if pathURLs.count == 0 {
        return
    }

    // we found directories, so grab the first one
    let dirURL = pathURLs[0]

    // try to retrieve the contents of the directory
    // if the results are nil, we’ll exit; otherwise we can continue
    //    without implicitly unwrapping our variable (due to guard)
    guard let fileArray = try? NSFileManager.defaultManager().contentsOfDirectoryAtURL(dirURL, includingPropertiesForKeys: nil, options: .SkipsHiddenFiles) else {
        return
    }

    // create a string to describe our sound file’s extension
    let predicate = NSPredicate(format: "SELF MATCHES[c] %@", "wav")

    // use our file list array to locate file paths with the right extension
    let wavOnlyFileArray = fileArray.filter { predicate.evaluateWithObject($0.pathExtension) }

    // iterate through each path in our new wav-only file list
    for aWavURL in wavOnlyFileArray {
        do {
            // try removing the current item; if we get an error, catch it
            try NSFileManager.defaultManager().removeItemAtURL(aWavURL)
        }
        catch {
            // we caught an error, so do something about it
            print("Unable to remove file: \(aWavURL)")
        }
    }
} // end removeWavFiles()
To start, I had to find where the saved file was located. I had saved the file using this code:
let dirPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as String
To find the OS X directory this pointed to from the Simulator, I ran this code:
let pathURLs = NSFileManager.defaultManager().URLsForDirectory(NSSearchPathDirectory.DocumentDirectory, inDomains: .UserDomainMask)

if pathURLs.count > 0 {
    let dirURL = pathURLs[0]
    print(dirURL)
}
This resulted in a string something like:
file:///Users/myName/Library/Developer/CoreSimulator/Devices/5BE7BEE7-1AA7-4BB8-BAE3-7E444441D4F3/data/Containers/Data/Application/0D401AA5-19CC-4F50-B956-34F89D671FA9/Documents/
Using Terminal, I traversed my way to that directory. Sure enough, the file was there.
Next, I needed to find out how to retrieve the filename(s) from that directory. Using Apple’s documentation, I found that this code would give me an array of NSURL objects representing the contents of a directory:
let fileArray = try? NSFileManager.defaultManager().contentsOfDirectoryAtURL(dirURL, includingPropertiesForKeys: nil, options: .SkipsHiddenFiles)
  • The try? is necessary because the results could be nil or NSFileManager.defaultManager().contentsOfDirectoryAtURLcould otherwise throw an error. This is indicated in Apple’s documentation by the keyword throws:
func contentsOfDirectoryAtURL(_ url: NSURL,
   includingPropertiesForKeys keys: [String]?,
                      options mask: NSDirectoryEnumerationOptions) throws -> [NSURL]
  • Note: in the end code, I wrapped this statement with a guard/else statement. Since there’s no point in continuing if no files are present, this way the function can simply return if there’s an error. If files are found, though, I then don’t need to implicitly unwrap the array (no ! needed when I use the array).
  • The first parameter of contentsOfDirectoryAtURL is the path to search. That was my dirURL variable.
  • Next, contentsOfDirectoryAtURL wants to know if I’m looking for specific properties, such as file permissions. I didn’t need to specify any properties, so I just put nil there.
  • There are multiple NSDirectoryEnumerationOptions values; however, the only one that works in the .contentsOfDirectoryAtURL function is .SkipsHiddenFiles. My file was not hidden, so I went with that option. (If I needed to include hidden files, I’d just put nilthere.)
Okay, so now I have a list of files in the directory, but… how do I find exactly the file(s) I want???
Well, I learned a bit about NSPredicate. An NSPredicate object lets me specify a string to search on in an array’s contents. I found a helpful cheatsheet for NSPredicate here:
A handy guide to using NSPredicates
I had named my file with a wav extension, so I wanted to search for that. So, here’s my predicate:
let predicate = NSPredicate(format: "SELF MATCHES[c] %@", "wav")
  • SELF represents the object being searched.
  • MATCHES means to match the item exactly
  • [c] indicates the search should be case-insensitive
  • %@ is Objective-C’s replaceable parameter notation for an object (such as a string)
  • ”wav” is the string that will replace the object parameter (%@) in the format string
To test the value of my format string, I used the predicateFormatfunction that NSPredicate provides:
print(predicate.predicateFormat)
The string comes out as:
SELF MATCHES[c] “wav”
At this point, I had the fileArray variable. Now, I needed to create a new array to hold the results from using the predicate:
let wavOnlyFileArray = fileArray.filter { predicate.evaluateWithObject($0.pathExtension) }
  • No ? is needed anywhere here because the original array won’t be empty. The guard statement I used when creating fileArray ensured that for me.
  • Arrays in Swift have a filter function. It lets you use a “closure” (an unnamed function) to process the array in some way. This is where I used the predicate. (Note that the closure needs to be in curly brackets after the filter function.)
  • The predicate’s evaluateWithObject function, because it’s in the closure, will be run on each item in the array. The $0represents the current item.
  • NSURL objects have a pathExtension property. As a result, I can simply append that property, using dot notation, to the $0 - and I’ll get a string representing the current path’s extension as we loop (iterate) through the original array.
  • The final array, wavOnlyFileArray, will contain only the path URLs with a wav extension!
Now we can delete those items! 
for aWavURL in wavOnlyFileArray {
    do {
        try NSFileManager.defaultManager().removeItemAtURL(aWavURL)
    }
    catch {
        print("Unable to remove file: \(aWavURL)")
    }
}
  • This for loop doesn’t need anything special in the way of optionals handling. If wavOnlyFileArray ends up being nil, that’s okay - the code simply won’t iterate.
  • I’m not setting or working with any variables that might result in optional variable handling. Therefore, a guard or if letstatement wouldn’t really be applicable here.
  • NSFileManager’s removeItemAtURL function, though, throwswhen an error occurs. So, instead I used a do - try - catchstatement to handle the throws possibility.
  • The code in the do portion will run happily along as long as there’s no error.
  • I’m trying the removeItemAtURL call because it might throwan error.
  • If an error does occur, then the code will move to the catch. I can respond to the situation there. Otherwise, the code will just end normally.
Note: I used the URL options for handling file paths, rather than the string-based path versions. Apple indicates their preference in the NSFileManager Class Reference:
When specifying the location of files, you can use either NSURL or NSString objects. The use of the NSURL class is generally preferred for specifying file-system items because they can convert path information to a more efficient representation internally.
Where I am aware, I always try to use Apple’s preferred methods. Often, I find their preferred methods are easier, offer more clarity in code reading, and have additional benefits. For example, as you can see from the NSFileManager documentation:
You can also obtain a bookmark from an NSURL object, which is similar to an alias and offers a more sure way of locating the file or directory later.

No comments:

Post a Comment