Waiting for File Write Completion on iOS

During investment time, I’ve been writing an iOS client to pass(1). As part of the app’s onboarding, the user is asked to upload, via iTunes, their PGP private key to the application’s documents directory.

I want the application to detect when this file is written to the device, then read the file and add its content to the keychain. Apple has helpfully published some code relating to watching for changes in the documents directory, which I happily implemented… leading to a runtime crash of my application. It seems the file was not ready for reading when I attempted to read it.

Here’s a class with the problematic code:

import Foundation

fileprivate let queue = DispatchQueue(label: "FileMonitorQueue", target: .main)

final class FileWatcher {
  let fsSource: DispatchSourceFileSystemObject
  var readSource: DispatchSourceRead! // explained later

  init(in directory: URL, filename: String) {
    let path = directory.path.cString(using: .ascii)
    let fileDescriptor = open(path!, O_EVTONLY)

    fsSource = DispatchSource.makeFileSystemObjectSource(
      fileDescriptor: fileDescriptor,
      eventMask: .write,
      queue: queue
    )

    fsSource.setEventHandler {
      /* do whatever you like here */
    }

    fsSource.setCancelHandler {
      close(fileDescriptor)
    }

    fsSource.resume()
  }

  deinit {
    fsSource.cancel()
  }
}

(n.b. some error handling has been left out for brevity; see this gist for a more complete version)

The problem is that the blocked passed to setEventHandler fires as soon as writing begins, not when the write completes.

To remedy that, we can create a DispatchSourceRead which has an event handler that fires when a file becomes readable.

fsSource.setEventHandler {
  do {
    let fileHandle = try FileHandle(forReadingFrom: documentsDirectory.appendingPathComponent(filename))

    self.readSource = DispatchSource.makeReadSource(fileDescriptor: fileHandle.fileDescriptor, queue: queue)
    self.readSource.setEventHandler {
        fileHandle.readToEndOfFileInBackgroundAndNotify()
    }
    self.readSource.resume()

    self.readSource.setCancelHandler {
        fileHandle.closeFile()
    }
  } catch {
    /* error handling here */
  }
}

Now we can listen to NSFileHandleReadToEndOfFileCompletion using NotificationCenter:

import Foundation

final class FileListener {
  init() {
    NotificationCenter.default.addObserver(
      self,
      selector: #selector(handler),
      name: .NSFileHandleReadToEndOfFileCompletion,
      object: nil
    )
  }

  @objc func handler() {
    /* file is available for reading here */
  }
}

And voila: