Active Filters: Files

Topics

#321: Opening Files from the Files App 📂

Topics

Apple's Files.app is a great way to interact with files on iOS.
Today we'll begin taking a look at how our app can better integrate with it. First up, we're going to learn how to allow users to open files in our app, from the Files.app. Let's get started.

We'll begin by creating a new project with the Single View App template in Xcode. In the future, we'll look at how Document Picker apps work, but for simplicity, we'll use just a plain single view app this time.

Then, we'll head into our AppDelegate.swift file, and add a new function:

func application(
  _ app: UIApplication, 
  open inputURL: URL, 
  options: [UIApplication.OpenURLOptionsKey : Any] = [:]
) -> Bool {
  // TODO
}

This application(_:open:options:) function will be called when the user selects a file inside the Files app and presses the Share (  ) button on it, and selects our app in the UIActivityViewController that appears.

Pro Tip: We can also access this by long-pressing a file in the Files app and selecting Share from the menu:

Next we'll need to tell Xcode that our app supports the type of files we want to our app to be able to open. For this, we'll choose Text Files.

We'll click on our project at the top of the project navigator and click Info at the top. We'll find this screen:

We want that area labeled Document Types (0). We'll click the + button and enter in Text for the Name and public.text for Types.

(This public.text thing is part of a system called Uniform Type Identifiers, Apple uses these to identify specific kinds of file types. Read more here).

Then, we'll add a new key to our Info.plist to make our app work more smoothly. We'll right-click on the keys at the top and select Add Row, then add the key LSSupportsOpeningDocumentsInPlace. We'll make it a Boolean and set its value to YES:

This allows our app to work with the files users open, without needing to first copy them into its own sandbox directory.

Finally, we'll head back to our AppDelegate.swift file and fill out that function we added earlier:

func application(
  _ app: UIApplication, 
  open url: URL, 
  options: [UIApplication.OpenURLOptionsKey : Any] = [:]
) -> Bool {
  let needTo = url.startAccessingSecurityScopedResource()

  do {
    let data = try Data(contentsOf: url)
    let contents = String(
      data: data,
      encoding: String.Encoding.utf8
    )

    print(contents)
  } catch(let error) { print(error) }

  if needTo {
    url.stopAccessingSecurityScopedResource()
  }

  return true
}

First we setup a do/catch block so we can catch and print any errors that might happen during all of this. Then we call a special function on our inputURL.

Since, we're opening in place, iOS has gives us what Apple calls a "security scoped" URL. (Read more about them here).

First we have to tell the system when we begin using a URL, saving the Bool value that url.startAccessingSecurityScopedResource() returns. If that Bool is true, then we'll also need to call stopAccessingSecurityScopedResource on our URL once we're finished with it.

After that we simply read the Data at the URL, then put it into a String so we can print it out.

Now we can Build & Run the app on our device, open the Files.app, choose an .txt file, share it, and our app appears in the Share sheet:

Tapping our app's icon opens it, and runs our code to print out the contents of the file, neat!

That's all for today. Shout out to friend of the show Casey for inspiring the idea for this Bite!

Have an idea for a Bite? Send it in!

Topics

#46: NSFileManager 📂

Topics

NSFileManager is one of the primary ways you interact with the file system on iOS and OS X. Today we’ll look at both how to use it, as well as how to put some of the new Swift 2 features we’ve been learning about to real world use.

guard let documentsPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).first else { return }

let logPath = documentsPath + "spaceship.log"
guard let logExists = fm.fileExistsAtPath(logPath) else { return }

do {
  let attributes = try fm.attributesOfItemAtPath(logPath)
  let createdAt = attributes[NSFileCreationDate]

  // > 5 minutes (in seconds) ago?
  if let created = createdAt where fabs(created.timeIntervalSinceNow) > 300 {
    try fm.removeItemAtPath(logPath)

      "[BEGIN SPACESHIP LOG]".writeToFile(
      logPath,
      atomically: false,
      encoding: NSUTF8StringEncoding,
      error: nil
    )
  }
} catch let error as NSError {
    print("Error: \(error.localizedDescription)")
}

Here we use NSFileManager to wipe out a log file if it's more than 5 minutes old:

First we use guard to make sure our documents directory is available.

Then, we create a path for our log file, and check that it exists.

Many of NSFileManager's functions are marked as throws, so we need a set of do/catch blocks.

Next we check when the log file was created.

We use another new Swift 2 feature, pattern matching, to do our age check on the optional inside the if statement.

Finally we try to remove the file, and replace it with a fresh, blank log.

We then use the catch statement to capture and print any errors that have been thrown.