Opening and Saving Custom Document Types from a Swift Cocoa Application

The Cocoa application framework provides extensive support for Document-based applications where most of the file type handling implementation details are handled by the application's First Responder. For non-Document-based applications there are a number of steps that you need to follow to add file handling capabilities. In this article, we will build a simple View Controller based application that has simple file handling capabilities including:

opening a custom file type and displaying it's contents in the application's View
saving the application's View content to the same custom file type
implementing the custom file type association so that in response to double-click of the file, the application opens its custom file type and displays the file's contents in the application's view

Step 1: Set up the application

Create a new Swift Cocoa MacOS storyboard application called OpenSave in XCode.

Open the storyboard and add:

  • A Label and Text Field for the file's name
  • A Label and Text View for the file's data (the Text View will be automatically embedded in a Scroll View)
  • A Button called Open
  • A Button called Save

Open the View Controller and add the following Outlets

@IBOutlet weak var nameField : NSTextField!

@IBOutlet weak var dataScrollView : NSScrollView!

Add the following Actions to ViewController.swift

@IBAction func openData(sender : AnyObject) {

}

@IBAction func saveData(sender : AnyObject) {

}

Back in the Storyboard, select View Controller Scene > View Controller and switch to the Connections Inspector (Option-Command-6)

Connect the openData action to the "Open" button
Connect the saveData action to the "Save" button
Connect the nameField Outlet to the Name Text Field
Connect the dataScrollView to the Scroll View that contains the TextView

Save and run the project, it should look similar to this:

Step 2: Implement the Open Action

In ViewController.swift implement the openData action

@IBAction func openData(sender : AnyObject) {
  // If the sender is a notification sent by the Application Delegate
  // handle the notification
  // Otherwise this action was triggered by the Open Button
  if let notification = sender as? NSNotification {
    // the file path will be the object sent by the notification
    let fUrl = notification.object as! String

    // unarchive the dictionary contained at the file path
    let d = NSKeyedUnarchiver.unarchiveObjectWithFile(fUrl) as! Dictionary<String, String>

    // populate the name and data fields with the unarchived data
    nameField.stringValue = d["name"]!
    let docDataTextView = dataScrollView.contentView.documentView as! NSTextView
    docDataTextView.string = d["data"]
} else {
    // We're responding to a click of the Open button, so show an Open Panel
    let dlg = NSOpenPanel()
    // run the open panel and handle an OK selection
    if (dlg.runModal() == NSFileHandlingPanelOKButton) {
        // the the URL of the selected file
        let openUrl = dlg.URL

        // unarchive the dictionary contained in the selected file
        let d = NSKeyedUnarchiver.unarchiveObjectWithFile(openUrl!.path!) as! Dictionary<String, String>

        // populate the name and data fields with the unarchived data
        nameField.stringValue = d["name"]!
        let docDataTextView = dataScrollView.contentView.documentView as! NSTextView
        docDataTextView.string = d["data"]
    }
  }
}

This action will first determine if it's being triggered by a click of the Open button or in response to double-clicking on a file. Then it will display an Open Panel (if trigged by a click of the open button) and read the data out of the selected file and update the fields of the View.

Step 3: Implement the Save Action

@IBAction func saveData(sender : AnyObject) {
  // get the values from the name and data fields
  let docName = nameField.stringValue
  let docDataTextView = dataScrollView.contentView.documentView as! NSTextView
  let docData = docDataTextView.string!

  // create a dictionary with the values
  let doc = ["name" : docName, "data" : docData]

  // create a Save Panel to choose a file path to save to
  let dlg = NSSavePanel()
  // use the name fields value to suggest a name for the file
  dlg.nameFieldStringValue = docName
  // run the Save Panel and handle an OK selection
  if (dlg.runModal() == NSFileHandlingPanelOKButton) {
    // get the URL of the selected file path
    let saveUrl = dlg.URL

    // archive the dictionary and save to the file path
    let fileUrlWithExt = saveUrl?.URLByAppendingPathExtension("bposdoc")
    NSKeyedArchiver.archiveRootObject(doc, toFile: fileUrlWithExt!.path!)
  }
}    

Step 4: Configure the application to handle the custom file type.

Use Command-1 to display the Project Navigator and select the SaveOpen project. This will display the General tab of Project properties. Select the Info tab and expand Document Types.

Add a new Document Type by click on the + button.

Set the following properties for the Document Type:

  • Name: opensavedoc
  • Extensions: opensavedoc
  • Role: Editor

Under Additional document type properties add a property as:

  • Key: LSHandlerRank
  • Type: String
  • Value: Owner

This registers the file extension .opensavedoc as being owned by the OpenSave application. When you double-click on a file with this extension, Mac OS will launch this application and hand-off the path of the clicked file to the application.

The application can't yet handle opening the custom file type when you double-click on a .opensavedoc file. To do this, we need to update AppDelegate.swift to tell the OpenSave what to do with the opened file.

Step 5: Add file opening to the Application Delegate

Open AppDelegate.swift and add the following function:

func application(sender: NSApplication, openFile filename: String) -> Bool {
    NSNotificationCenter.defaultCenter().postNotificationName("com.bpos.openfile", object: filename)
    return true
}

This function responds to a file being opened in the Finder. Since the application is registered as the Owner of .opensavedoc files, Finder will launch the application and hand-off the path to the file to the Application Delegate.

In this implementation we take the path to the opened file and post a notification with the path as the notification's object.

Step 6: Handling the openfile Notification

Open ViewController.swift and modify the viewDidLoad function:

override func viewDidLoad() {
  super.viewDidLoad()
      NSNotificationCenter.defaultCenter().addObserver(self, selector: "openData:", name: "com.bpos.openfile", object: nil)
}

This adds the View Controller as an observer of the "com.bpos.openfile" notification. When this Notification is posted by the Application Delegate the View Controller will pass the notification to the openData Action.

You can now test out the application by entering a name and some data in the fields and click on the save button. Quit and restart the application and try out the Open button. Now quit the application and double-click on the file you saved in Finder to open the file with the OpenSave application.

Conclusion

Custom file type handling can be added to your application quite simply by following these steps.

By using NSKeyedArchiver, you can save quite complex data types. In this example we modelled a [String : String] dictionary. But you can model any plist-compliant data types.

So, for example, you could model a more complex data structure like:

[
  [String : String, 
    String : [
      [String : String]
    ]
  ]
] 

(An array of dictionaries containing a string value and an embedded array of dictionaries)

The contents of the .opensavedoc file is in binary plist format. However, this format is human readable using many text editors (including TextEdit). You will need to employ your own obfuscation or encryption method if the contents of your file should be kept private.