6

FileManager Class Tutorial for macOS: Getting Started with the File System

 3 years ago
source link: https://www.raywenderlich.com/666-filemanager-class-tutorial-for-macos-getting-started-with-the-file-system
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Home macOS Tutorials

FileManager Class Tutorial for macOS: Getting Started with the File System

In this tutorial, learn to use the FileManager class in a macOS app. Work with files, folders and navigate the file system while creating a working app.

By Sarah Reichelt Apr 19 2017 · Article (30 mins) · Beginner

3.7/5 7 Ratings

Version

The file system in macOS underlies every app — and the FileManager class has a lot to do with that. Your app is stored in the Applications folder, users store documents in their Documents folder, and preferences and support files are stored in the users Library folder.

With files and data spread all over the filesystem, how can your app find out about files and folders, work with file and folder paths, and even read and write data to a file?

The FileManager class — that’s how!

In this tutorial, you’ll learn how to manage directory paths, work with URLs, use common file and folder dialogs, display file and folder information, and much more!

Getting Started

For this tutorial, you’ll start with a playground and move on to an app later once you’ve learned the basics.

macOS uses a hierarchical file system: files & folders inside folders, inside folders. This means that finding a particular file can be complicated. Each file has its own address; the structure that defines this address is named URL.

Open Xcode and click Get started with a playground in the Welcome to Xcode window, or choose File/New/Playground… Set the name of the playground to Files, make sure the Platform is set to macOS and click Next.

Select your Desktop and click Create to save the playground.

New Playground

Note: For this tutorial, the name of the playground must be Files and it must be located on your Desktop. If you gave it a different name or saved it somewhere else, please rename it and move it before proceeding, or else the code below just won’t work!

Once the starting playground is open, delete all the lines except for import Cocoa.

Add the following line to your playground, but don’t worry about changing the username for now:

let completePath = "/Users/sarah/Desktop/Files.playground"

completePath now contains the address, or path, of this playground file. Since macOS is Unix-based, this is how Unix (and all its variants) describe file paths. The first slash indicates the root directory, which in this case is your startup disk. After that, every slash delimits a new folder or file. So Files.playground is in the Desktop folder in the sarah folder in the Users folder on the startup drive.

While this string describes the full path to this file, it isn’t the best way to handle addresses. Instead, you are going to convert the address into a URL by adding:

let completeUrl = URL(fileURLWithPath: completePath)

In the results panel of the playground, you now see: file:///Users/sarah/Desktop/Files.playground

Create a URL

“Wait a minute!” you cry. “I thought a URL was a web address like https://www.raywenderlich.com, not a directory path!”

Well, yes…and yes!

URL stands for Uniform Resource Locator — which also can point to local files and folders. Instead of https://, local file URLs start with file://. In the results panel, it looks like there are 3 slashes, but that is because the path itself starts with a slash.

FileManager Class

You’ve used a String to specify a file path and converted it to a URL. But while this is a valid URL, it won’t work — unless your user name also happens to be sarah. Therefore, the next step is to create a URL that works on anyone’s computer.

To do this, you’ll use the FileManager class, which provides methods to handle most file-related actions in macOS.

The first task is to identify your Home folder and replace sarah with your own user name.

Add the following line to your playground:

let home = FileManager.default.homeDirectoryForCurrentUser

default returns the FileManager class singleton instance, and homeDirectoryForCurrentUser contains the URL for the home folder of the current user.

Now that you have a URL pointing to your home folder, you can derive the path to the playground by adding the following code:

let playgroundPath = "Desktop/Files.playground"
let playgroundUrl = home.appendingPathComponent(playgroundPath)

The results panel should show you the URL for your own home folder.

Add these lines to the playground to query various URL properties:

playgroundUrl.path
playgroundUrl.absoluteString
playgroundUrl.absoluteURL
playgroundUrl.baseURL
playgroundUrl.pathComponents
playgroundUrl.lastPathComponent
playgroundUrl.pathExtension
playgroundUrl.isFileURL
playgroundUrl.hasDirectoryPath

The pathComponents property is interesting, as it separates out all folder and file names into an array. The lastPathComponent and pathExtension properties are both quite useful in practice.

Here’s what you should have in your playground:

URL Components

Note: Note that the playground file has the property hasDirectoryPath set to true. This marks the URL as representing a directory. But why is the playground file marked as a directory?

That’s because .playground files are folder bundles, just like .app files. Right-click on the playground file and select Show Package Contents to see what’s inside.

The URL class makes it easy to edit URLs.

Add the following code to your playground:

var urlForEditing = home
urlForEditing.path

urlForEditing.appendPathComponent("Desktop")
urlForEditing.path

urlForEditing.appendPathComponent("Test file")
urlForEditing.path

urlForEditing.appendPathExtension("txt")
urlForEditing.path

urlForEditing.deletePathExtension()
urlForEditing.path

urlForEditing.deleteLastPathComponent()
urlForEditing.path

Note how you show the path property each time so it’s easy to see what’s changed.

While those commands edited the URL in place, you can also create a new URL from an existing one.

To see how to do this, insert the following commands into your playground:

let fileUrl = home
    .appendingPathComponent("Desktop")
    .appendingPathComponent("Test file")
    .appendingPathExtension("txt")
fileUrl.path

let desktopUrl = fileUrl.deletingLastPathComponent()
desktopUrl.path

These methods return new URLs, so chaining them into a sequence works well.

The three appending methods could have been shortened to just one, but I’ve broken them out here to make the individual steps clear to you.

Here’s what the playground should look like:

Append Path to a URL

Checking for Files and Folders

NSString has a lot of file path manipulation methods, but Swift’s String struct doesn’t. Instead, you should use URLs when working with file paths. Working with paths in this manner will become even more important as Apple transitions to the new Apple File System (APFS).

However, there is one case where you still have to use a string representation of a file URL: checking to see if a file or folder exists. The best way to get a string version of a URL is through the path property.

Add the following code to your playground:

let fileManager = FileManager.default
fileManager.fileExists(atPath: playgroundUrl.path)

let missingFile = URL(fileURLWithPath: "this_file_does_not_exist.missing")
fileManager.fileExists(atPath: missingFile.path)

Checking whether a folder exists is slightly more obscure, as you have to check if the URL points to a valid resource that is also a folder.

This requires what I consider a very un-Swifty mechanism of using an inout Objective-C version of a Bool. Add the following:

var isDirectory: ObjCBool = false
fileManager.fileExists(atPath: playgroundUrl.path, isDirectory: &isDirectory)
isDirectory.boolValue

Your playground should look something like this:

FileManager Class Check For File Exists

A fully-annotated version of the playground can be downloaded here.

Now that you understand how to use URL to identify files and folders, close the playground. It’s time to build an app!

File Spy

In this part of the tutorial, you’re going to build the File Spy app, which lets you select a folder and view a listing of every file or folder inside. Selecting any item will give you more details about it.

FileManager Class File Information

Download the starter app project, open it in Xcode and click the Play button in the toolbar, or press Command-R to build and run. The UI is already set up, but you’ll need to add the file management bits.

Your first task is to let the user select a folder and then list its contents. You’ll add some code behind the Select Folder button and use the NSOpenPanel class to select a folder.

In ViewController.swift, find selectFolderClicked in the Actions section and insert the following:

// 1
guard let window = view.window else { return }

// 2
let panel = NSOpenPanel()
panel.canChooseFiles = false
panel.canChooseDirectories = true
panel.allowsMultipleSelection = false

// 3
panel.beginSheetModal(for: window) { (result) in
  if result == NSFileHandlingPanelOKButton {
    // 4
    self.selectedFolder = panel.urls[0]
    print(self.selectedFolder)
  }
}

Here’s what’s going on in the code above:

  1. Check that you can get a reference to the window, since that’s where the NSOpenPanel will be displayed.
  2. Create a new NSOpenPanel and set some properties to only permit a single selection which must be a folder.
  3. Display the NSOpenPanel modally in the window and use a closure to wait for the result.
  4. If the result shows that the user clicked the OK button (the displayed button will have a different label depending on your locale), get the selected URL and set a specific ViewController property. For a quick temporary test, you print the selected URL to the console. Ignore the warning on this line for now.

Build and run, click the Select Folder button and choose a folder. Confirm that the URL for the selected folder prints in the console.

Click the button again to open the dialog,but this time click Cancel. This will not print a selected URL.

Selecting a Folder

Quit the app and delete the temporary print statement.

Folder Contents

Now that you can select a folder, your next job is to find the contents of that folder and display it.

The previous section of code populated a property named selectedFolder. Scroll to the top of the ViewController definition and check out the selectedFolder property. It’s using a didSet property observer to run code whenever its value is set.

The key line here is the one that calls contentsOf(folder:). Scroll down to the stub of this method, which is currently returning an empty array. Replace the entire function with the following:

func contentsOf(folder: URL) -> [URL] {
  // 1
  let fileManager = FileManager.default

  // 2
  do {
    // 3
    let contents = try fileManager.contentsOfDirectory(atPath: folder.path)

    // 4
    let urls = contents.map { return folder.appendingPathComponent($0) }
    return urls
  } catch {
    // 5
    return []
  }
}

Stepping through what the code does:

  1. Get the FileManager class singleton, just as before.
  2. Since the FileManager method can throw errors, you use a do...catch block.
  3. Try to find the contents of the folder contentsOfDirectory(atPath:) and return an array of file and folder names inside.
  4. Process the returned array using map to convert each name into a complete URL with its parent folder. Then return the array.
  5. Return an empty array if contentsOfDirectory(atPath:) throws an error.

The selectedFolder property sets the filesList property to the contents of the selected folder, but since you use a table view to show the contents, you need to define how to display each item.

Scroll down to the NSTableViewDataSource extension. Note that numberOfRows already returns the number of URLs in the filesList array. Now scroll to NSTableViewDelegate and note that tableView(_:viewFor:row:) returns nil. You need to change that before anything will appear in the table.

Replace the method with:

func tableView(_ tableView: NSTableView, viewFor
  tableColumn: NSTableColumn?, row: Int) -> NSView? {
  // 1
  let item = filesList[row]

  // 2
  let fileIcon = NSWorkspace.shared().icon(forFile: item.path)

  // 3
  if let cell = tableView.make(withIdentifier: "FileCell", owner: nil) 
  	as? NSTableCellView {
    // 4
    cell.textField?.stringValue = item.lastPathComponent
    cell.imageView?.image = fileIcon
    return cell
  }

  // 5
  return nil
}

Here’s what you do in this code:

  1. Get the URL matching the row number.
  2. Get the icon for this URL. NSWorkspace is another useful singleton; this method returns the Finder icon for any URL.
  3. Get a reference to the cell for this table. The FileCell identifier was set in the Storyboard.
  4. If the cell exists, set its text field to show the file name and its image view to show the file icon.
  5. If no cell exists, return nil.

Now build and run, select a folder and you should see a list of files and folders appear — hurray!

Show Folder Contents

But clicking on a file or folder gives no useful information yet, so on to the next step.

Getting File Information

Open up the Finder and press Command-I to open a window with information about the file: creation date, modification date, size, permissions and so on. All that information, and more, is available to you through the FileManager class.

File Information

Back in the app, still in ViewController.swift, look for tableViewSelectionDidChange. This sets the property of the ViewController: selectedItem.

Scroll back to the top and look at where selectedItem is defined. As with selectedFolder, a didSet observer is watching for changes to this property. When the property changes, and if the new value is not nil, the observer calls infoAbout(url:). This is where you will retrieve the information for display.

Find infoAbout, which currently returns a boring static string, and replace it with the following:

func infoAbout(url: URL) -> String {
  // 1
  let fileManager = FileManager.default

  // 2
  do {
    // 3
    let attributes = try fileManager.attributesOfItem(atPath: url.path)
    var report: [String] = ["\(url.path)", ""]

    // 4
    for (key, value) in attributes {
      // ignore NSFileExtendedAttributes as it is a messy dictionary
      if key.rawValue == "NSFileExtendedAttributes" { continue }
      report.append("\(key.rawValue):\t \(value)")
    }
    // 5
    return report.joined(separator: "\n")
  } catch {
    // 6
    return "No information available for \(url.path)"
  }
}

There are a few different things happening here, so take them one at a time:

  1. As usual, get a reference to the FileManager shared instance.
  2. Use do...catch to trap any errors.
  3. Use the FileManager Class’ attributesOfItem(atPath:) method to try to get the file information. If this succeeds, it returns a dictionary of type [FileAttributeKey: Any] FileAttributeKeys, which are members of a struct with a String rawValue.
  4. Assemble the key names & values into an array of tab-delimited strings. Ignore the NSFileExtendedAttributes key as it contains a messy dictionary that isn’t really useful.
  5. Join these array entries into a single string & return it.
  6. If the try throws an error, return a default report.

Build and run again, select a folder as before, then click on any file or folder in the list:

Folder Information

You now get a lot of useful details about the file or folder. But there’s still more you can do!

More Features

The app is getting better, but it’s still missing a few things:

  • Clicking on Show Invisible Files doesn’t change anything.
  • Double-clicking on a folder should drill into its contents.
  • The Move Up button needs to move back up the folder hierarchy.
  • Save Info should record the selected file’s details to a file.

You’ll tackle these next.

Handling Invisible Files

In Unix systems, files and folders whose name starts with a period are invisible. You’ll add code to handle this case.

Go to contentsOf(folder:) and replace the line containing map with the following:

let urls = contents
  .filter { return showInvisibles ? true : $0.characters.first != "." }
  .map { return folder.appendingPathComponent($0) }

The above adds a filter that rejects hidden items if the showInvisibles property is not true. Otherwise the filter returns every item, including hidden items.

Find the toggleShowInvisibles method of ViewController and insert this into the function:

// 1
showInvisibles = (sender.state == NSOnState)

// 2
if let selectedFolder = selectedFolder {
  filesList = contentsOf(folder: selectedFolder)
  selectedItem = nil
  tableView.reloadData()
}

Here is what this code does:

  1. Sets the showInvisibles property based on the sender’s state. Since the sender is an NSButton, it has either NSOnState or NSOffState. Because this is a checkbox button, NSOnState means checked.
  2. If there is a currently selectedFolder, regenerate the filesList and update the UI.

Build and run, select a folder and check and un-check the Show Invisible Files button. Depending on the folder you’re viewing, you may see files starting with a period when Show Invisible Files is checked.

Show Invisible Files

Handling Double-Clicking on a Folder

In the storyboard, the table view has been assigned a doubleAction that calls tableViewDoubleClicked. Find tableViewDoubleClicked and replace it with the following:

@IBAction func tableViewDoubleClicked(_ sender: Any) {
  // 1
  if tableView.selectedRow < 0 { return }

  // 2
  let selectedItem = filesList[tableView.selectedRow]
  // 3
  if selectedItem.hasDirectoryPath {
    selectedFolder = selectedItem
  }
}

Taking the above code comment-by-comment:

  1. Check to see whether the double-click occurred on a populated row. Clicking in a blank part of the table sets the tableView's selectedRow to -1.
  2. Get the matching URL from filesList.
  3. If the URL is a folder, set the ViewController's selectedFolder property. Just like when you select a folder using the Select Folder button, setting this property triggers the property observer to read the contents of the folder and update the UI. If the URL is not a folder, nothing happens.

Build and run, select a folder containing other folders, and then double-click a folder in the list to drill down into it.

Handle the Move Up Button

Once you have implemented double-click to drill down, the next obvious step is to move back up the tree.

Find the empty moveUpClicked method and replace it with the following:

@IBAction func moveUpClicked(_ sender: Any) {
  if selectedFolder?.path == "/" { return }
  selectedFolder = selectedFolder?.deletingLastPathComponent()
}

This first checks to see whether the selectedFolder is the root folder. If so, you can’t go any higher. If not, use a URL method to strip the last segment off the URL. Editing selectedFolder will trigger the update as before.

Build and run again; confirm that you can select a folder, double-click to move down into a sub-folder and click Move Up to go back up the folder hierarchy. You can move up even before double-clicking a folder, as long as you are not already at the root level.

Move Up The Folder Tree

Note: As you've seen, using property observers (didSet) can be incredibly useful. All the code for updating the display is in an observer, so no matter what method or UI element changes an observed property, the update happens with no need to do anything else. Sweet!

Saving Information

There are two main ways to save data: user-initiated saves and automatic saves. For user-initiated saves, your app should prompt the user for a location to save the data, then write the data to that location. For automatic saves, the app has to figure out where to save the data.

In this section, you are going to handle the case when the user clicks the Save Info button to initiate a save.

You used NSOpenPanel to prompt the user to select a folder. This time, you are going to use NSSavePanel. Both NSOpenPanel and NSSavePanel are subclasses of NSPanel, so they have a lot in common.

Replace the empty saveInfoClicked method with the following:

@IBAction func saveInfoClicked(_ sender: Any) {
  // 1
  guard let window = view.window else { return }
  guard let selectedItem = selectedItem else { return }

  // 2
  let panel = NSSavePanel()
  // 3
  panel.directoryURL = FileManager.default.homeDirectoryForCurrentUser
  // 4
  panel.nameFieldStringValue = selectedItem
    .deletingPathExtension()
    .appendingPathExtension("fs.txt")
    .lastPathComponent

  // 5
  panel.beginSheetModal(for: window) { (result) in
    if result == NSFileHandlingPanelOKButton,
      let url = panel.url {
      // 6
      do {
        let infoAsText = self.infoAbout(url: selectedItem)
        try infoAsText.write(to: url, atomically: true, encoding: .utf8)
      } catch {
        self.showErrorDialogIn(window: window,
                               title: "Unable to save file",
                               message: error.localizedDescription)
      }
    }
  }
}

Taking each numbered comment in turn:

  1. Confirm that everything you need is available: a window for displaying the panel and the URL whose info you are going to save.
  2. Create an NSSavePanel.
  3. Set the directoryURL property which dictates the initial folder shown in the panel.
  4. Set the nameFieldStringValue property to supply a default name of the file.
  5. Show the panel and wait in a closure for the user to finish.
  6. If the user selects a valid path for the data file (a valid URL) and clicks the OK button, get the file information and write it to the selected file. If there is an error, show a dialog. Note that if the user clicks Cancel on the save dialog, you simply ignore the operation.

write(to:atomically:encoding) is a String method that writes the string to the provided URL. The atomically option means that the string will be written to a temporary file and then renamed, ensuring that you won’t end up with a corrupt file — even if the system crashes during the write. The encoding for the text in this file is set to UTF8, which is a commonly used standard.

Build and run, select a file or folder from the table and click Save Info. Select a save location, and click Save. You will end up with a text file that looks similar to the following:

File Info

Note: One neat feature of using NSSavePanel is that if you try to overwrite a file that already exists, your app will automatically display a confirmation dialog asking if you want to replace that file.

That closes off the list of features for this app, but there is one more feature I think would be a nice addition: recording the selected folder and item so that when the app restarts, the last selected folder is re-displayed.

Saving App State

Normally, I would store app-state data in UserDefaults, which is saved automatically for you in the Preferences folder. But that doesn't allow you to do anything fancy with the file system. Instead, you will save this data to a dedicated app folder inside the Application Support folder.

Scroll down to the end of ViewController.swift and you’ll see an extension dedicated to saving and restoring the user's selections.

I’ve provided the functions that do the actual writing and reading. Writing uses the same write(to:atomically:encoding) method used when saving the info file. Reading uses a String initializer to create a String from a URL.

The really interesting thing here is how to decide where to save the data. You’ll do that in urlForDataStorage, which is returning nil at the moment.

Replace urlForDataStorage with the following:

private func urlForDataStorage() -> URL? {
  // 1
  let fileManager = FileManager.default

  // 2
  guard let folder = fileManager.urls(for: .applicationSupportDirectory,
                                      in: .userDomainMask).first else {
                                        return nil
  }

  // 3
  let appFolder = folder.appendingPathComponent("FileSpy")
  var isDirectory: ObjCBool = false
  let folderExists = fileManager.fileExists(atPath: appFolder.path, 
  			                    isDirectory: &isDirectory)
  if !folderExists || !isDirectory.boolValue {
    do {
      // 4
      try fileManager.createDirectory(at: appFolder,
                                      withIntermediateDirectories: true,
                                      attributes: nil)
    } catch {
      return nil
    }
  }

  // 5
  let dataFileUrl = appFolder.appendingPathComponent("StoredState.txt")
  return dataFileUrl
}

What is all this code doing?

  1. It’s your old friend FileManager class to the rescue again. :]
  2. The FileManager class has a method for returning a list of appropriate URLs for specific uses. In this case, you are looking for the applicationSupportDirectory in the current user's directory. It is unlikely to return more than one URL, but you only want to take the first one. You can use this method with different parameters to locate many different folders.
  3. As you did in the playground, append a path component to create an app-specific folder URL and check to see if it exists.
  4. If the folder does not exist, try to create it and any intermediate folders along the path, returning nil if this fails.
  5. Append another path component to create the full URL for the data file and return that.
Note: .applicationSupportDirectory is a short way to say FileManager.SearchPathDirectory.applicationSupportDirectory. .userDomainMask refers to FileManager.SearchPathDomainMask.userDomainMask. While the shorthand is much easier to type and read, it is useful to know where these come from, so you can find them in the documentation if you ever need to look them up.

Build and run, select a folder, then click on a folder or file. Use the Quit menu item or Command-Q to close the app. Don’t quit via Xcode, or the lifecycle methods won’t trigger a save. Run the app again and notice it opens up to the file or folder you were viewing when you quit.

Saved App State

Note: If you want to see the stored-state data file, hold down Option while clicking on Finder's Go menu and select Library. In the new Finder window, open Application Support and look for the FileSpy folder. You should see the StoredState.txt file with your selected folder and item.

Where to Go From Here?

You can download the final sample project here.

In this FileManager class tutorial:

  1. You learned how URLs can represent local files and folders and can show many properties available to you about a file or folder.
  2. You learned how you can add and remove path components to a URL.
  3. You explored the FileManager class which gives you access properties like homeDirectoryForCurrentUser, the applicationSupportDirectory and even attributesOfItem with detailed information about a file or folder.
  4. You learned how to save information to a file.
  5. You learned how to check if a file or folder exists.

For more information, check out Apple's FileManager API Reference Documentation which shows many more of the available methods in the FileManager class.

You are now ready to begin incorporating the use of files and folders in your own apps.

If you have any questions or comments please join the forum discussion below!

raywenderlich.com Weekly

The raywenderlich.com newsletter is the easiest way to stay up-to-date on everything you need to know as a mobile developer.

Get a weekly digest of our tutorials and courses, and receive a free in-depth email course as a bonus!

Average Rating

3.7/5

Add a rating for this content

Sign in to add a rating
7 ratings

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK