Chapter 10. Working with Files and File Types

At the moment, the iOS app can work with the text content of note documents, but doesn’t really know anything about attachments that might have been added through the OS X app.

In this chapter, we’ll add support for working with attachments to the iOS app, as well as make its handling of note documents more robust. We’ll do this by adding—you guessed it—more user interface to:

  • display any attachments.

  • handle conflict resolution, for when a file is synced from multiple devices.

  • add Quick Look support, to display a thumbnail preview of attachments.

Setting Up the Interface for Attachments

First, we’ll update the interface for the document view controller to support showing the list of attachments. This will involve reworking everything, as well as some reasonably complex constraints, so it’s easier to start from scratch.

  1. Open Main.storyboard.

  2. Delete the text view from the document view controller’s interface. We’ll be reconstructing the interface, with room for the attachments to be displayed, so it’s easier to remove everything than it is to rearrange.

  3. It’ll be easier to do this without the top bar in the way, so select the document view controller, and in the Simulated Metrics section of the Inspector, change Top Bar from Inferred to None (Figure 10-1).

    lesw 1001
    Figure 10-1. Setting the mode of the top bar
  4. Drag a UIScrollView into the interface; this will enable us to display content larger than the view it’s currently in (see Figure 10-2).

    We want the scroll view to fill the entire screen. By default, constraints are made relative to the margins, and to the layout guides at the top and the bottom. However, because the contents of the entire screen need to scroll, we want to take up all of the space. This means that we need to add constraints differently.

  5. Add constraints to the scroll view by selecting the scroll view and clicking the Pin button at the bottom right of the window. Turn off “Constrain to margins” and set all four of the numbers that appear to 0. Change Update Frames to Items of New Constraints, and click Add 4 Constraints. The scroll view will now completely fill the screen.

    We’ll now add controls inside it. In particular, we’ll be adding a stack view, which will contain the text editor and the collection view that will show the list of attachments. A stack view handles most of the work of laying out views in a horizontal or vertical stack. If all you care about is “these views should be next to each other,” and you don’t want to have to deal with more complex layouts, then stack views are exactly what you want.

    lesw 1002
    Figure 10-2. The empty document view controller interface
  6. Drag a vertical UIStackView into the scroll view.

  7. With the stack view selected, click the Pin button, and set the top, leading, trailing, and bottom space to 0.

  8. Next, resize the stack view so that it’s the same width as the scroll view.

  9. Hold down the Control key and drag from the stack view to the scroll view. A list of possible constraints will appear; choose Equal Widths.

    Note

    It’s important to make it the same width as the scroll view. This ensures that the scroll view doesn’t collapse it to 0.

  10. Inside the Attribute Inspector, ensure that the stack view’s Alignment and Distribution are both set to Fill. This means that the stack view will make the size of its child views sufficient to fill up the stack view’s boundaries.

  11. Drag a UICollectionView into the stack view.

  12. Hold down the Control key and drag from the collection view to the collection view itself. Choose Height from the menu that appears.

  13. Select the collection view’s cell and resize the cell size to 88 by 88.

  14. Set the collection view’s background color to 90% white (very slightly gray) in the Attributes Inspector.

    Next, we’ll add (back) the text view, just like we did in the previous chapter.

  15. Add a UITextView to the stack view.

    It needs no constraints, since the stack view will size and position it. Setting the height to 88 for the collection view, and adding no other constraints, will make the stack view do two things: position the collection view at the very top and make it fill the width of the screen, and make other views expand their height to fill the remaining space.

  16. Connect the document view controller’s textView outlet to this text view.

    Tip

    The textView property has the type UITextView, which means that the connection can only be made to a text view. The interface builder won’t let you connect to any other type of view.

  17. Make the text view use the document view controller as its delegate, by Control-dragging from the text view onto the document view controller in the outline.

  18. Select the text view, and go to the Attributes Inspector. Set the text view to use attributed text and then turn Scrolling Enabled off—it’s not necessary, because it’s already contained inside a scroll view (see Figure 10-3).

    lesw 1003
    Figure 10-3. Disabling scrolling on the text view
  19. Run the app; the text now appears underneath the collection view.

Listing Attachments

Now that the interface is set up, we’ll add support for storing attachments in the iOS Document class.

  1. Open Document.swift.

  2. Add the following code to add the attachmentsDirectoryWrapper property, which returns the NSFileWrapper representing the folder where attachments are stored. If it doesn’t exist, it creates it:

      private var attachmentsDirectoryWrapper : NSFileWrapper? {
    
          // Ensure that we can actually work with this document
          guard let fileWrappers = self.documentFileWrapper.fileWrappers else {
              NSLog("Attempting to access document's contents, but none found!")
              return nil
          }
    
          // Try to get the attachments directory
          var attachmentsDirectoryWrapper =
              fileWrappers[NoteDocumentFileNames.AttachmentsDirectory.rawValue]
    
          // If it doesn't exist...
          if attachmentsDirectoryWrapper == nil {
    
              // Create it
              attachmentsDirectoryWrapper =
                  NSFileWrapper(directoryWithFileWrappers: [:])
              attachmentsDirectoryWrapper?.preferredFilename =
                  NoteDocumentFileNames.AttachmentsDirectory.rawValue
    
              // And then add it
              self.documentFileWrapper
              .addFileWrapper(attachmentsDirectoryWrapper!)
    
              // We made a change to the file, so record that
              self.updateChangeCount(UIDocumentChangeKind.Done)
          }
    
          // Either way, return it
          return attachmentsDirectoryWrapper
      }

    The attachmentsDirectoryWrapper computed property first checks to make sure the Document’s file wrapper actually has a usable array of file wrappers to access. Generally, this is always true, but if it’s not, we can’t continue.

    Next, we attempt to get the file wrapper for the Attachments directory. If that doesn’t exist, then we first create it, and add it to the document’s file wrapper. Either way, by the end of the method, we’ve got an Attachments directory to use, which we then return.

  3. Add the attachedFiles property, which returns an array of NSFileWrappers, each of which represents an attached file:

      dynamic var attachedFiles : [NSFileWrapper]? {
    
          // Get the contents of the attachments directory
          guard let attachmentsFileWrappers =
              attachmentsDirectoryWrapper?.fileWrappers else {
              NSLog("Can't access the attachments directory!")
              return nil
          }
    
          // attachmentsFileWrappers is a dictionary mapping filenames
          // to NSFileWrapper objects; we only care about the
          // NSFileWrappers, so return that as an array
          return Array(attachmentsFileWrappers.values)
    
      }

    To return the list of all attachments, we first ensure that we have an attachments directory to use. Next, we need to do a little bit of conversion. The fileWrappers property on NSFileWrapper objects returns a dictionary, in which strings are mapped to other NSFileWrappers. If we don’t care about the filenames, and only care about the file wrappers, we need to ask the dictionary for its values value, and then ask Swift to convert it to an Array, which we then return.

  4. Add the addAttachmentAtURL method, which adds an attachment to the document by copying it in:

      func addAttachmentAtURL(url:NSURL) throws -> NSFileWrapper {
    
          // Ensure that we have a place to put attachments
          guard attachmentsDirectoryWrapper != nil else {
              throw err(.CannotAccessAttachments)
          }
    
          // Create the new attachment with this file, or throw an error
          let newAttachment = try NSFileWrapper(URL: url,
              options: NSFileWrapperReadingOptions.Immediate)
    
          // Add it to the Attachments directory
          attachmentsDirectoryWrapper?.addFileWrapper(newAttachment)
    
          // Mark ourselves as needing to save
          self.updateChangeCount(UIDocumentChangeKind.Done)
    
          return newAttachment
      }

    Adding an attachment to the Document class works almost identically to the Mac version of the same method (seen in “Storing and Managing Attachments”). We first check to ensure that we have a file wrapper that we can place our attachments in, and then attempt to create a new file wrapper for the attachment. It’s then added to the Attachments directory, and we record the fact that the document changed.

Determining Types of Attachments

To show attachments in the list, we need a way to visually represent them. This means that we need to show some kind of thumbnail image. We’ll start by adding the default File image, which will be used as the fallback for when the app doesn’t have special support for a type of file.

  1. Open the Assets.xcassets file.

  2. Drag the File.pdf image from the resources we provided on our website into the list of images to add it to the collection.

    Next, we’ll implement a way for the document to determine the type of the attachment, and a method to generate a thumbnail for the attachment. We’ll do this by adding methods to the NSFileWrapper class that allow it to determine its file type and to return a UIImage that’s appropriate for the type.

  3. Open Document.swift.

  4. Import the MobileCoreServices framework by adding this to the top of the file:

      import MobileCoreServices
  5. Add a new extension to NSFileWrapper by adding the following code to Document.swift. We’ll be putting our extra methods for NSFileWrapper into it:

      extension NSFileWrapper {
    
      }
  6. Next, add the fileExtension property and the conformsToType method to this extension, which determines the file type:

      var fileExtension : String? {
          return self.preferredFilename?
              .componentsSeparatedByString(".").last
      }
    
      func conformsToType(type: CFString) -> Bool {
    
          // Get the extension of this file
          guard let fileExtension = fileExtension else {
              // If we can't get a file extension, assume that
              // it doesn't conform
              return false
          }
    
          // Get the file type of the attachment based on its extension.
          guard let fileType = UTTypeCreatePreferredIdentifierForTag(
              kUTTagClassFilenameExtension, fileExtension, nil)?
              .takeRetainedValue() else {
              // If we can't figure out the file type from the extension,
              // it also doesn't conform
              return false
          }
    
          // Ask the system if this file type conforms to the provided type
          return UTTypeConformsTo(fileType, type)
      }

    The fileExtension property simply splits the file extension’s preferredFilename wherever a . appears, and takes the last item from that array. This has the effect of getting the file extension.

    The conformsToType method takes a UTI, stored in a CFString, and asks the type system to give us the UTI that applies to our file extension (using the fileExtension property we just added). If that UTI conforms to the UTI that was passed in as a parameter, then we return true.

  1. Finally, we’ll add the method thumbnailImage to the extension, which uses the information from conformsToType to figure out and return the image:

      func thumbnailImage() -> UIImage? {
    
          if self.conformsToType(kUTTypeImage) {
              // If it's an image, return it as a UIImage
    
              // Ensure that we can get the contents of the file
              guard let attachmentContent = self.regularFileContents else {
                  return nil
              }
    
              // Attempt to convert the file's contents to text
              return UIImage(data: attachmentContent)
          }
    
          // We don't know what type it is, so return nil
          return nil
      }

    The thumbnailImage property is one that we’ll be adding to over time, as we continue to add support for additional types of attachments. At the moment, it simply checks to see if the file wrapper is an image file; if it is, it returns a UIImage based on the content of the file.

Displaying Attachment Cells

Now that attachments are capable of providing an image, we’ll make the attachments collection view show cells. We’ll show one cell for each attachment, plus an additional “add new attachment” cell, which will add a new attachment when tapped.

First, we’ll add the image for this “add attachment” cell, and then we’ll connect up the collection view to the document view controller.

  1. Open the Assets.xcassets file.

  2. Add the AddAttachment.pdf image to the list of images. Next, we’ll define the class that powers the collection view cells that represent each attachment.

  3. In DocumentViewController.swift, add AttachmentCell. It’s a subclass of UICollectionViewCell that has an outlet for an image view and for a label:

      class AttachmentCell : UICollectionViewCell {
    
          @IBOutlet weak var imageView : UIImageView?
    
          @IBOutlet weak var extensionLabel : UILabel?
    
      }

Next, let’s make the view controller use this new class to show the list of all attachments.

  1. Open DocumentViewController.swift.

  2. Add an outlet for a UICollectionView called attachmentsCollectionView:

      @IBOutlet weak var attachmentsCollectionView : UICollectionView!
  3. Create an extension on DocumentViewController that conforms to UICollectionViewDataSource and UICollectionViewDelegate:

      extension DocumentViewController : UICollectionViewDataSource,
          UICollectionViewDelegate {
    
      }
  4. Implement the numberOfItemsInSection method in this extension, which returns the number of attachments the document has, plus an additional cell (for the “add attachment” cell):

      func collectionView(collectionView: UICollectionView,
          numberOfItemsInSection section: Int) -> Int {
    
          // No cells if the document is closed or if it doesn't exist
          if self.document!.documentState.contains(.Closed) {
              return 0
          }
    
          guard let attachments = self.document?.attachedFiles else {
              // No cells if we can't access the attached files list
              return 0
          }
    
          // Return as many cells as we have, plus the add cell
          return attachments.count + 1
      }

    To figure out how many items need to exist in the attachments list, we need to first check to see if the document is closed; if it is, then we can’t display any attachments, or the “add” cell. (This will be the case when the view controller has appeared on screen, but the document hasn’t finished opening yet.) We then ask for the document’s attachedFiles array and return its length, plus one. This additional cell will be the “add attachment” cell.

  5. Implement the cellForItemAtIndexPath method:

      func collectionView(collectionView: UICollectionView,
          cellForItemAtIndexPath indexPath: NSIndexPath) ->
          UICollectionViewCell {
    
          // Work out how many cells we need to display
          let totalNumberOfCells =
              collectionView.numberOfItemsInSection(indexPath.section)
    
          // Figure out if we're being asked to configure the add cell,
          // or any other cell. If we're the last cell, it's the add cell.
          let isAddCell = (indexPath.row == (totalNumberOfCells - 1))
    
          // The place to store the cell. By making it 'let', we're
          // ensuring that we never accidentally fail to give it a
          // value - the compiler will call us out.
          let cell : UICollectionViewCell
    
          // Create and return the 'add' cell if we need to
          if isAddCell {
              cell = collectionView.dequeueReusableCellWithReuseIdentifier(
                  "AddAttachmentCell", forIndexPath: indexPath)
          } else {
    
              // This is a regular attachment cell
    
              // Get the cell
              let attachmentCell = collectionView
                  .dequeueReusableCellWithReuseIdentifier("AttachmentCell",
                      forIndexPath: indexPath) as! AttachmentCell
    
              // Get a thumbnail image for the attachment
              let attachment = self.document?.attachedFiles?[indexPath.row]
              var image = attachment?.thumbnailImage()
    
              // Give it to the cell
              if image == nil {
    
                  // We don't know what it is, so use a generic image
                  image = UIImage(named: "File")
    
                  // Also set the label
                  attachmentCell.extensionLabel?.text =
                      attachment?.fileExtension?.uppercaseString
    
              } else {
                  // We know what it is, so ensure that the label is empty
                  attachmentCell.extensionLabel?.text = nil
              }
              attachmentCell.imageView?.image = image
    
              // Use this cell
              cell = attachmentCell
          }
    
          return cell
    
      }

    The cellForItemAtIndexPath method is very similar to its counterpart in the DocumentListViewController: the collection view will provide an index path, and we use it to grab a thumbnail image for the attachment, which is displayed in the cell. The only significant twist in this method is that if the index path refers to the last item in the collection view, we don’t display an attachment but instead display the AddAttachmentCell.

We’ll now create the interface for the attachment cells.

  1. Open Main.storyboard and select the collection view.

  2. Go to the Attributes Inspector, change the number of Items from 1 to 2, and set the Scroll Direction to Horizontal (Figure 10-4).

    lesw 1004
    Figure 10-4. Updating the collection view’s settings
  3. Select the first cell and set its Identifier to AttachmentCell.

  4. Go to the Identity Inspector, and set the class of this cell to AttachmentCell.

  5. Select the second cell and set its Identifier to AddAttachmentCell.

  6. Drag a UIImageView into both of these cells.

  7. Make them both fill their cells—that is, resize them to fill the cell, and add constraints that pin the distances from all edges to 0.

  8. Select the image view that you just added to the first cell—that is, AttachmentCell, and go to the Attributes Inspector. Set its Mode to Aspect Fill. This will make the image fill all of the image view.

  9. Add a label to the first cell. Place it near the bottom of the cell, and resize it to fill the width.

    • Reduce the font size to 13.

    • Set its text alignment to Center.

    • Add constraints that pin the label to the bottom of the cell, and to the left and right edges.

  10. Next, select the image view in the second cell (AddAttachmentCell). Set its Mode to Center. This will center the image in the middle of the view, without scaling.

  11. Set the AddAttachmentCell’s image view’s Image property to AddAttachment, as shown in Figure 10-5.

    lesw 1005
    Figure 10-5. Setting the image view’s image

    The collection view’s cells should now look like Figure 10-6.

    lesw 1006
    Figure 10-6. The configured collection view
  12. Open DocumentViewController.swift in the Assistant.

  13. Connect the empty image view in AttachmentCell to the imageView outlet of AttachmentCell.

  14. Connect the label to the extensionLabel outlet.

  15. Connect the attachmentsCollectionView outlet of the DocumentViewController class to the collection view.

  16. Hold down the Control key and drag from the collection view to the view controller, and then choose “data source” from the menu that appears.

  17. Repeat the process, this time choosing “delegate” from the menu.

  18. Open DocumentViewController.swift, and add the following code to the code in viewWillAppear:

      // If this document is not already open, open it
      if document.documentState.contains(UIDocumentState.Closed) {
          document.openWithCompletionHandler { (success) -> Void in
              if success == true {
                  self.textView?.attributedText = document.text
    
    >             self.attachmentsCollectionView?.reloadData()
    
              }

    This code makes the view controller reload the contents of the collection view once the document is opened. This ensures that the list of attachments actually contains content.

  19. Finally, add the following code to the end of viewWillAppear to make the attachments list refresh even if the document wasn’t just freshly opened:

      // And reload our list of attachments, in case it changed
      // while we were away
      self.attachmentsCollectionView?.reloadData()
  20. Run the app. You’ll see the list of attachments, plus an add cell!

Dealing with Conflicts

This is now a good point to address conflict resolution in the files. When you’re making an application that uses iCloud—or, for that matter, any app that deals with files that can be opened by multiple entities at the same time—you need to handle situations in which a file is changed from two places at once.

Consider the following situation: you’re about to board your flight, and you’re editing a note. Your flight is called, so you hit Save and close your laptop. As a result, your file doesn’t get saved to iCloud yet. On board the flight, you pull out your phone, and open your document. You make some changes and put your phone away. You later get off the plane, and your phone syncs its changes to iCloud. You then get home and open up your laptop, which finally has a chance to send your changes to iCloud. Suddenly, there’s a problem: the file was changed from two places at once, which means that the file is in conflict. Which version of the file is correct? The file on your laptop, or the file on your phone? Or both?

It’s up to your app to decide what to do. There are three main methods for resolving a conflict:

Our app will pick the third option: if a document ends up in a conflicted state, then we’ll simply show the list of possible options to the user, and let him or her decide. The advantage to doing this is that it’s simple to think about, and generally what the user wants; the downside is that it will always involve discarding data.

  1. Add the following property to the DocumentViewController class:

      var stateChangedObserver : AnyObject?
  2. Add the following code to viewWillAppear:

      // If this document is not already open, open it
      if document.documentState.contains(UIDocumentState.Closed) {
          document.openWithCompletionHandler { (success) -> Void in
              if success == true {
                  self.textView?.attributedText = document.text
    
    >             // Register for state change notifications
    >             self.stateChangedObserver = NSNotificationCenter
    >                 .defaultCenter().addObserverForName(
    >                     UIDocumentStateChangedNotification,
    >                     object: document,
    >                     queue: nil,
    >                     usingBlock: { (notification) -> Void in
    >                     self.documentStateChanged()
    >                 })
    >
    >             self.documentStateChanged()
    
              }

    This code registers a closure with the system, which will be run every time iOS receives a notification that the document’s state has changed. In this case, all it will do is call the documentStateChanged method, which will handle conflicts for us.

    Currently, the view controller will close the document when the view controller disappears. This can happen for a number of reasons, and we don’t want the document to be closed except when the user taps the back button to go back to the document list. We therefore need to add some code to support this.

  3. Add the following property to DocumentViewController to keep track of whether we should close the document when viewWillDisappear is called:

      private var shouldCloseOnDisappear = true

    We’ll use a UIAlertController to present the list of possible actions the user can take. We’ve used UIAlertControllers before to present a message and possible actions for the user to take, but they’ve all been presented as dialog boxes—small windows that appear with buttons underneath. When you could have multiple options for the user to select from, or when the options might be quite wide, then an action sheet is better. Action sheets slide up from the bottom of the window and provide you room for multiple options. Functionally, there’s no difference; the only way they differ is in their presentation.

  4. Add the following method to DocumentViewController:

      func documentStateChanged() {
          if let document = self.document
              where document.documentState.contains(UIDocumentState.InConflict) {
    
              // Gather all conflicted versions
              guard var conflictedVersions = NSFileVersion
                  .unresolvedConflictVersionsOfItemAtURL(document.fileURL) else {
                  fatalError("The document is in conflict, but no " +
                      "conflicting versions were found. This should not happen.")
              }
              let currentVersion
                  = NSFileVersion.currentVersionOfItemAtURL(document.fileURL)!
    
              // And include our own local version
              conflictedVersions += [currentVersion]
    
              // Prepare a chooser
              let title = "Resolve conflicts"
              let message = "Choose a version of this document to keep."
    
              let picker = UIAlertController(title: title, message: message,
                  preferredStyle: UIAlertControllerStyle.ActionSheet)
    
              let dateFormatter = NSDateFormatter()
              dateFormatter.dateStyle = .ShortStyle
              dateFormatter.timeStyle = .ShortStyle
    
              // We'll use this multiple times, so save it as a variable
              let cancelAndClose = { (action:UIAlertAction) -> Void in
                  // Give up and return
                  self.navigationController?.popViewControllerAnimated(true)
              }
    
              // For each version, offer it as an option
              for version in conflictedVersions {
                  let description = "Edited on " +
                  "\(version.localizedNameOfSavingComputer!) at " +
                  "\(dateFormatter.stringFromDate(version.modificationDate!))"
    
                  let action = UIAlertAction(title: description,
                                             style: UIAlertActionStyle.Default,
                                             handler: { (action) -> Void in
    
                      // If it was selected, use this version
                      do {
    
                          if version != currentVersion {
                              try version.replaceItemAtURL(document.fileURL,
                                  options: NSFileVersionReplacingOptions
                                  .ByMoving)
    
                              try NSFileVersion.
                                  removeOtherVersionsOfItemAtURL(
                                  document.fileURL
                                                                )
                          }
    
                          document.revertToContentsOfURL(document.fileURL,
                              completionHandler: { (success) -> Void in
    
                              self.textView.attributedText = document.text
                              self.attachmentsCollectionView?.reloadData()
    
                          })
    
                          for version in conflictedVersions{
                              version.resolved = true
                          }
    
                      } catch let error as NSError {
                          // If there was a problem, let the user know and
                          // close the document
                          let errorView = UIAlertController(title: "Error",
                              message: error.localizedDescription,
                              preferredStyle: UIAlertControllerStyle.Alert)
    
                          errorView.addAction(UIAlertAction(title: "Done",
                              style: UIAlertActionStyle.Cancel,
                              handler: cancelAndClose))
    
                          self.shouldCloseOnDisappear = false
                          self.presentViewController(errorView,
                              animated: true,
                              completion: nil)
                      }
    
                  })
                  picker.addAction(action)
              }
    
              // Add a "choose later" option
              picker.addAction(UIAlertAction(title: "Choose Later",
                  style: UIAlertActionStyle.Cancel, handler: cancelAndClose))
    
              self.shouldCloseOnDisappear = false
    
              // Finally, show the picker
              self.presentViewController(picker, animated: true, completion: nil)
          }
      }

    First, this method asks if the document is in a conflicted state. If it is, we’ve got some problems to solve! We ask the system to provide us with a list of all of the possible versions of this file. We then add the local device’s current version of this file to the list.

    We then create a closure, called cancelAndClose, which bails on the whole operation and returns to the document list view controller. This is kept in a variable, because it’s used both for the Choose Later option (which we’ll add in a moment), as well as for when there’s a problem resolving the conflict.

    Once this is done, we create a UIAlertAction, and, for each version of the file, we create a new action. This action displays the name of the computer that created the conflicting version, as well as the date and time that the version was created. When the action is selected, the app indicates to the system that we should use the action’s associated version of the file and discard every other version.

    If there’s a problem, we present a separate alert controller, indicating to the user that something’s gone wrong. This alert controller only has a single action, which, when tapped, runs the cancelAndClose code.

    Finally, we add a final option, labeled Choose Later, which simply runs the cancelAndClose code (see Figure 10-7). The action sheet is then presented, letting the user choose what to do.

    lesw 1007
    Figure 10-7. The interface that appears when resolving conflicts
  5. Add the following code to viewWillDisappear to use the shouldCloseOnDisappear property to determine whether the document should be closed or not. Additionally, we’ll clear the state changed observer:

      override func viewWillDisappear(animated: Bool) {
    
    >     if shouldCloseOnDisappear == false {
    >         return
    >     }
    
    >     self.stateChangedObserver = nil
    
          self.document?.closeWithCompletionHandler(nil)
      }
  6. Add the following code to the very end of viewWillAppear to reset the flag to true when the view controller reappears:

      // We may be reappearing after having presented an attachment,
      // which means that our "don't close on disappear" flag has been set.
      // Regardless, clear that flag.
      self.shouldCloseOnDisappear = true

You can now test to see if it worked:

  1. Open a document in the Mac application and make some changes. Don’t save the changes yet.

  2. Open the same document in the iOS application, ideally on a real device, and make some different changes to the ones you made on the Mac app.

  3. Save and close the document in the Mac application, and then close the document in the iOS app. This will cause both of the apps to save their own versions, which will conflict with each other.

  4. Wait a little bit of time—30 seconds or so—for both of the changes to be uploaded to iCloud and synchronized to the different apps.

  5. Open the document one last time in the iOS app. Because it’s in conflict, you’ll see the UI that you just created!

Creating the Quick Look Thumbnail

Now that we can access the attachments, we’ll add support for Quick Look in the iOS app.

We’ll add a method to the Document class that generates an NSData containing a PNG-encoded image that can be used for the app. This will generate the same kind of image as used in the Mac app (which we added in “Adding QuickLook”); the difference being that we need to use the iOS methods for drawing.

  1. Add the following method to the Document class:

      func iconImageDataWithSize(size: CGSize) -> NSData? {
          UIGraphicsBeginImageContext(size)
          defer {
              UIGraphicsEndImageContext()
          }
    
          let entireImageRect = CGRect(origin: CGPoint.zero, size: size)
    
          // Fill the background with white
          let backgroundRect = UIBezierPath(rect: entireImageRect)
          UIColor.whiteColor().setFill()
          backgroundRect.fill()
    
          if self.attachedFiles?.count >= 1 {
              // Render our text and the first attachment
              let attachmentImage = self.attachedFiles?[0].thumbnailImage()
    
              var firstHalf : CGRect = CGRectZero
              var secondHalf : CGRect = CGRectZero
    
              CGRectDivide(entireImageRect, &firstHalf, &secondHalf,
                  entireImageRect.size.height / 2.0, CGRectEdge.MinYEdge)
    
              self.text.drawInRect(firstHalf)
              attachmentImage?.drawInRect(secondHalf)
          } else {
              // Just render our text
              self.text.drawInRect(entireImageRect)
          }
    
          let image = UIGraphicsGetImageFromCurrentImageContext()
          return UIImagePNGRepresentation(image)
      }

    To create the image in iOS, we first call UIGraphicsBeginImageContext to indicate that we’d like to start drawing in a canvas with the specified size. In addition, we need to be sure to tell iOS that we’re done with this drawing once we are finished; to ensure that we don’t forget, we’ll use the defer keyword.

    When you use defer, any code that you put in its associated block of code will be run when you exit the current scope. In this case, it means that just before we return from this method, we’ll call UIGraphicsEndImageContext. defer is a great way to ensure that you clean up after yourself while keeping your clean-up code close to the code that actually creates the mess in the first place.

    When we’re drawing this icon, we use the UIBezierPath and UIColor classes to paint the entire canvas white. We then do the exact same thing as in the Mac version: if we have at least one attachment, we get its thumbnail image and draw it in the top half of the canvas while drawing the text in the lower half. If we don’t have any attachments, we just draw the text.

    Finally, we get the image from iOS by calling UIGraphicsGetImageFromCurrentImageContext, and convert it to an NSData containing the PNG-encoded image by calling UIImagePNGRepresentation.

  2. Add the following code to the contentsForType method to add the Quick Look files to the document package:

      override func contentsForType(typeName: String) throws -> AnyObject {
    
          let textRTFData = try self.text.dataFromRange(
              NSRange(0..<self.text.length),
              documentAttributes:
                  [NSDocumentTypeDocumentAttribute: NSRTFTextDocumentType])
    
          if let oldTextFileWrapper = self.documentFileWrapper
              .fileWrappers?[NoteDocumentFileNames.TextFile.rawValue] {
              self.documentFileWrapper.removeFileWrapper(oldTextFileWrapper)
          }
    
    >     // Create the QuickLook folder
    >
    >     let thumbnailImageData =
    >         self.iconImageDataWithSize(CGSize(width: 512, height: 512))!
    >
    >     let thumbnailWrapper =
    >         NSFileWrapper(regularFileWithContents: thumbnailImageData)
    >
    >     let quicklookPreview =
    >         NSFileWrapper(regularFileWithContents: textRTFData)
    >
    >     let quickLookFolderFileWrapper =
    >         NSFileWrapper(directoryWithFileWrappers: [
    >         NoteDocumentFileNames.QuickLookTextFile.rawValue: quicklookPreview,
    >         NoteDocumentFileNames.QuickLookThumbnail.rawValue: thumbnailWrapper
    >         ])
    >     quickLookFolderFileWrapper.preferredFilename =
    >         NoteDocumentFileNames.QuickLookDirectory.rawValue
    >
    >     // Remove the old QuickLook folder if it existed
    >     if let oldQuickLookFolder = self.documentFileWrapper
    >         .fileWrappers?[NoteDocumentFileNames.QuickLookDirectory.rawValue] {
    >             self.documentFileWrapper.removeFileWrapper(oldQuickLookFolder)
    >     }
    >
    >     // Add the new QuickLook folder
    >     self.documentFileWrapper.addFileWrapper(quickLookFolderFileWrapper)
    
          self.documentFileWrapper.addRegularFileWithContents(textRTFData,
              preferredFilename: NoteDocumentFileNames.TextFile.rawValue)
    
          return self.documentFileWrapper
      }

    Again, this is almost identical to the code seen in the Mac version: we create a file wrapper for the QuickLook folder, as well as file wrappers for both the thumbnail and preview file. We then remove the old QuickLook folder, if it exists, and add the new one to the document.

  3. Run the app. When you close a document, it will update its Quick Look thumbnail.

1 Dropbox doesn’t throw away the other versions; instead, it sticks Jon’s conflicted copy to the end of them, so that you can later decide what you want to do.