Daniel Tull: Blog

Wrapping URL's Security Scoped Resource Methods

Sunday, 09 September 2018

Apple’s WWDC 2018 session Managing Documents In Your iOS Apps explains best practices for dealing with documents and the topic of a URL’s security scoped resource is discussed:

This URL has a security scoped resource attached to it. You can think of this resource as a permissions token granted to you by the system, and accessing this token would allow your app to access this document. We can start accessing this document using the following URL APIs. When we call startAccessingSecurityScoped Resource, your app gets access to this document, and so you can start displaying or editing the document. And, once you’re done using [the document], you should call the stopAccessingSecurityScoped Resource API on that URL.

In this post I will be talking about the pair of methods startAccessingSecurityScopedResource and stopAccessingSecurityScopedResource on the URL type and how we can use Swift to make their use a little nicer in our apps. I will walk through the modifications I have made to the Particles sample app that Apple provided for the session. If you’d like to see the final result or any of the steps, you can find them on GitHub.

Apple’s Sample Code

The snippet below shows a function from the DocumentBrowserViewController class in the Particles app. In this, if there is a document open, the browser is encoding the URL as bookmark data for state restoration. To get the bookmark data, it needs to call the start and stop accessing security scoped resource methods before and after the URL is accessed respectively.

override func encodeRestorableState(with coder: NSCoder) {

  if let documentViewController = presentedViewController as? DocumentViewController,
     let documentURL = documentViewController.document?.fileURL {
    do {
      // Obtain the bookmark data of the URL of the document that is currently presented, if there is any.
      let didStartAccessing = documentURL.startAccessingSecurityScopedResource()
      defer {
        if didStartAccessing {
          documentURL.stopAccessingSecurityScopedResource()
        }
      }
      let bookmarkData = try documentURL.bookmarkData()

      // Encode it with the coder.
      coder.encode(bookmarkData, forKey: DocumentBrowserViewController.bookmarkDataKey)

    } catch {
      // Make sure to handle the failure appropriately, e.g., by showing an alert to the user
      os_log("Failed to get bookmark data from URL %@: %@", log: OSLog.default, type: .error, documentURL as CVarArg, error as CVarArg)
    }
  }

  super.encodeRestorableState(with: coder)
}

Use guard to reduce complexity

My first port of call was to reduce the noise with the large amount of indentation. Swift’s guard statement allows the if check to be inverted: make sure I have these, otherwise return.

As in the original code, the superclass should still encode its restorable state in the case that these checks fail, so we can use defer, which calls the code inside the defer scope after everything else in the current scope – that of the method – has finished.

This code actually comes out a little safer now, because no matter what happens in the rest of the method, at the end of it all, super will get called.

override func encodeRestorableState(with coder: NSCoder) {

  defer { super.encodeRestorableState(with: coder) }

  guard
    let documentViewController = presentedViewController as? DocumentViewController,
    let documentURL = documentViewController.document?.fileURL
  else {
    return
  }

  do {
    // Obtain the bookmark data of the URL of the document that is currently presented, if there is any.
    let didStartAccessing = documentURL.startAccessingSecurityScopedResource()
    defer {
      if didStartAccessing {
        documentURL.stopAccessingSecurityScopedResource()
      }
    }
    let bookmarkData = try documentURL.bookmarkData()

    // Encode it with the coder.
    coder.encode(bookmarkData, forKey: DocumentBrowserViewController.bookmarkDataKey)

  } catch {
    // Make sure to handle the failure appropriately, e.g., by showing an alert to the user
    os_log("Failed to get bookmark data from URL %@: %@", log: OSLog.default, type: .error, documentURL as CVarArg, error as CVarArg)
  }
}

There are now two calls using defer in this code. Becuase calls to defer run at the end of the current scope, the call to stop accessing the resource happens at the end of the do-block, which happens before super encodes its restorable state at the end of the function.

Extracting the security scoped logic

We can extract the logic that handles calling the start and stop method pair into its own function. We provide a closure which will be run in between the calls to start & stop.

extension URL {

  func accessSecurityScopedResource(_ accessor: (URL) -> Void) {

    let didStartAccessing = startAccessingSecurityScopedResource()

    accessor(self)

    if didStartAccessing {
      stopAccessingSecurityScopedResource()
    }
  }
}

This allows block of code to call the new accessSecurityScopedResource method, making sure anything in the given closure is correctly accessing the security scoped URL.

do {
  // Obtain the bookmark data of the URL of the document that is currently presented, if there is any.

  var bookmarkData: Data!
  documentURL.accessSecurityScopedResource {
    bookmarkData = try! $0.bookmarkData()
  }

  // Encode it with the coder.
  coder.encode(bookmarkData, forKey: DocumentBrowserViewController.bookmarkDataKey)

} catch {
  // Make sure to handle the failure appropriately, e.g., by showing an alert to the user
  os_log("Failed to get bookmark data from URL %@: %@", log: OSLog.default, type: .error, documentURL as CVarArg, error as CVarArg)
}

Remove implictly unwrapped optional

We can introduce a generic parameter to the function so that we can return a value from the accessor closure and that will be the return of the accessSecurityScopedResource function.

As we saw before, we can use defer to call stopAccessingSecurityScopedResource before control returns to the caller of our function, in this case right after the accessor closure is called.

We can’t wrap the defer inside the if-check for didStartAccessing because defers happen at the end of the current scope, which in that case would be the end of the if’s scope1.

extension URL {

  func accessSecurityScopedResource<Value>(_ accessor: (URL) -> Value) -> Value {

    let didStartAccessing = startAccessingSecurityScopedResource()

    defer {
      if didStartAccessing {
        stopAccessingSecurityScopedResource()
      }
    }

    return accessor(self)
  }
}
let bookmarkData = documentURL.accessSecurityScopedResource {
  try! $0.bookmarkData()
}

The generic parameter means that the type information is also kept and passed through without our function needing to know every possible type which could be retrieved in the accessor. Here the compiler can tell that the bookmarkData variable is a Data type.

Throwing through the closure

If we wanted to allow the closure to throw, we may be tempted to create a second throwing variant of accessSecurityScopedResource(), but Swift has quite a neat little trick up its sleeve for handling this.

The keyword rethrows declares that a function which accepts a throwing closure only throws if the closure throws.

That is to say that:

  • If the given closure throws, then the function will be taken as throwing.
  • If the given closure does not throw, then the function will not throw.
extension URL {

  func accessSecurityScopedResource<Value>(_ accessor: (URL) throws -> Value) rethrows -> Value {

    let didStartAccessing = startAccessingSecurityScopedResource()

    defer {
      if didStartAccessing {
        stopAccessingSecurityScopedResource()
      }
    }

    return try accessor(self)
  }
}

This allows us to call the throwing method bookmarkData() in the closure and it will throw to the caller of accessSecurityScopedResource().

let bookmarkData = try documentURL.accessSecurityScopedResource {
  try $0.bookmarkData()
}

However, if we were to use non-throwing code in the closure, such as the following use of Data(contentsOf:) then we don’t need to use try because accessSecurityScopedResource() is not a throwing function for this case.

let data = url.accessSecurityScopedResource { Data(contentsOf: $0) }

Pass the bookmarkData function directly

As an extra step, because all we need to retrieve in the accessor closure is the bookmark data, we can pass URL’s bookmarkData function directly to accessSecurityScopedResource().

let bookmarkData = try documentURL.accessSecurityScopedResource(URL.bookmarkData)

The accessSecurityScopedResource() now uses URL’s bookmarkData function directly without us having to wrap it in a closure. The compiler has all the type information of bookmarkData, so it can reason that it is throwing and returns Data and will still infer the type returned by this call to accessSecurityScopedResource.

Conclusion

Using this code we get a final method implementation as below.

override func encodeRestorableState(with coder: NSCoder) {

  defer { super.encodeRestorableState(with: coder) }

  guard
    let documentViewController = presentedViewController as? DocumentViewController,
    let documentURL = documentViewController.document?.fileURL
  else {
    return
  }

  do {
    // Obtain the bookmark data of the URL of the document that is currently presented, if there is any.
    let bookmarkData = try documentURL.accessSecurityScopedResource(URL.bookmarkData)

    // Encode it with the coder.
    coder.encode(bookmarkData, forKey: DocumentBrowserViewController.bookmarkDataKey)

  } catch {
    // Make sure to handle the failure appropriately, e.g., by showing an alert to the user
    os_log("Failed to get bookmark data from URL %@: %@", log: OSLog.default, type: .error, documentURL as CVarArg, error as CVarArg)
  }
}

In the encoding method, we can see the steps of the code:

  • Call super no matter what at the end of the scope
  • Only go further if there is a document (with a URL) loaded
  • Retrieve the bookmark data
  • Encode the bookmark for state restoration
  • Handle any errors

We can see that security scoping is mentioned, but it doesn’t get in the way of the important details. We can look up the implementation and easily reason that it is correct.

The other really nice thing is that security scoping is abstracted so that we can use our new method in all cases where we would have used the start/stop method pair before. This includes being able to unit test that method separately from any other code that may use it.

Update: A note on defer scoping

In the original version of this article, I had used the following implementation, claiming that the deferred code would be called at the end of the function scope after the accessor was called.

extension URL {

  func accessSecurityScopedResource<Value>(_ accessor: (URL) throws -> Value) rethrows -> Value {

    if startAccessingSecurityScopedResource() {
      defer { stopAccessingSecurityScopedResource() }
    }

    return try accessor(self)
  }
}

Thanks to Alexsander Akers for pointing out that the defer actually occurs at the end of the if scope instead, meaning that when the accessor is called, it would actually be after the call to stopAccessingSecurityScopedResource which is not what we want.