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().contentsOfDirectoryAtURL
could 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 nil
there.)
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 predicateFormat
function 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 $0
represents 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 let
statement wouldn’t really be applicable here.
NSFileManager
’s removeItemAtURL
function, though, throws
when an error occurs. So, instead I used a do - try - catch
statement to handle the throws
possibility.
- The code in the
do
portion will run happily along as long as there’s no error.
- I’m
try
ing the removeItemAtURL
call because it might throw
an 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.
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()
let dirPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as String
let pathURLs = NSFileManager.defaultManager().URLsForDirectory(NSSearchPathDirectory.DocumentDirectory, inDomains: .UserDomainMask)
if pathURLs.count > 0 {
let dirURL = pathURLs[0]
print(dirURL)
}
file:///Users/myName/Library/Developer/CoreSimulator/Devices/5BE7BEE7-1AA7-4BB8-BAE3-7E444441D4F3/data/Containers/Data/Application/0D401AA5-19CC-4F50-B956-34F89D671FA9/Documents/
let fileArray = try? NSFileManager.defaultManager().contentsOfDirectoryAtURL(dirURL, includingPropertiesForKeys: nil, options: .SkipsHiddenFiles)
try?
is necessary because the results could be nil
or NSFileManager.defaultManager().contentsOfDirectoryAtURL
could 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]
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).contentsOfDirectoryAtURL
is the path to search. That was my dirURL
variable.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.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 nil
there.)A handy guide to using NSPredicates
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 stringpredicateFormat
function that NSPredicate
provides:print(predicate.predicateFormat)
SELF MATCHES[c] “wav”
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) }
?
is needed anywhere here because the original array won’t be empty. The guard
statement I used when creating fileArray
ensured that for me.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.)evaluateWithObject
function, because it’s in the closure, will be run on each item in the array. The $0
represents 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.wavOnlyFileArray
, will contain only the path URLs with a wav
extension!for aWavURL in wavOnlyFileArray {
do {
try NSFileManager.defaultManager().removeItemAtURL(aWavURL)
}
catch {
print("Unable to remove file: \(aWavURL)")
}
}
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.guard
or if let
statement wouldn’t really be applicable here.NSFileManager
’s removeItemAtURL
function, though, throws
when an error occurs. So, instead I used a do - try - catch
statement to handle the throws
possibility.do
portion will run happily along as long as there’s no error.try
ing the removeItemAtURL
call because it might throw
an error.catch
. I can respond to the situation there. Otherwise, the code will just end normally.
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.
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