Managing Files using the iOS 8 UIDocument Class

From Techotopia
Revision as of 20:39, 1 February 2016 by Neil (Talk | contribs) (Text replacement - "<google>BUY_IOS8</google>" to "<htmlet>ios9_upgrade</htmlet>")

Jump to: navigation, search
PreviousTable of ContentsNext
Preparing an iOS 8 App to use iCloud StorageUsing iCloud Storage in an iOS 8 Application


Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book


Use of iCloud to store files requires a basic understanding of the UIDocument class. Introduced as part of the iOS 5 SDK, the UIDocument class is the recommended mechanism for working with iCloud based file and document storage.

The objective of this chapter is to provide a brief overview of the UIDocument class before working through a simple example demonstrating the use of UIDocument to create and perform read and write operations on a document on the local device file system. Once these basics have been covered the next chapter will extend the example to store the document using the iCloud document storage service.


Contents


An Overview of the UIDocument Class

The iOS UIDocument class is designed to provide an easy to use interface for the creation and management of documents and content. Whilst primarily intended to ease the process of storing files using iCloud, UIDocument also provides additional benefits in terms of file handling on the local file system such as reading and writing data asynchronously on a background queue, handling of version conflicts on a file (a more likely possibility when using iCloud) and automatic document saving.

Subclassing the UIDocument Class

UIDocument is an abstract class, in that it cannot be directly instantiated from within code. Instead applications must create a subclass of UIDocument and, at a minimum, override two methods:

  • contentsForType(_:error:) - This method is called by the UIDocument subclass instance when data is to be written to the file or document. The method is responsible for gathering the data to be written and returning it in the form of an NSData or NSFileWrapper object.
  • loadFromContents(_:ofType:error:) - Called by the subclass instance when data is being read from the file or document. The method is passed the content that has been read from the file by the UIDocument subclass and is responsible for loading that data into the application’s internal data model.

Conflict Resolution and Document States

Storage of documents using iCloud means that multiple instances of an application can potentially access the same stored document consecutively. This considerably increases the risk of a conflict occurring when application instances simultaneously make different changes to the same document. One option is to simply let the most recent save operation overwrite any changes made by the other application instances. A more user friendly alternative, however, is to implement conflict detection code in the application and present the user with the option to resolve the conflict. Such resolution options will be application specific but might include presenting the file differences and letting the user choose which one to save, or allowing the user to merge the conflicting file versions.

The current state of a UIDocument subclass object may be identified by accessing the object’s documentState property. At any given time this property will be set to one of the following constants:

  • UIDocumentState.Normal – The document is open and enabled for user editing.
  • UIDocumentState.Closed – The document is currently closed. This state can also indicate an error in reading a document.
  • UIDocumentState.InConflict – Conflicts have been detected for the document.
  • UIDocumentState.SavingError – An error occurred when an attempt was made to save the document.
  • UIDocumentState.EditingDisabled – The document is busy and is not currently safe for editing.

Clearly one option for detecting conflicts is to periodically check the documentState property for a UIDocumentStateInConflict value. That said, it only really makes sense to check for this state when changes have actually been made to the document. This can be achieved by registering an observer on the UIDocumentStateChangedNotification notification. When the notification is received that the document state has changed the code will need to check the documentState property for the presence of a conflict and act accordingly.

The UIDocument Example Application

The remainder of this chapter will focus on the creation of an application designed to demonstrate the use of the UIDocument class to read and write a document locally on an iOS 8 based device or simulator.

To create the project, begin by launching Xcode and create a new product named iCloudStore using the Single View Application template, the Swift programming language and configured with the Universal device setting.

Creating a UIDocument Subclass

As previously discussed, UIDocument is an abstract class that cannot be directly instantiated. It is necessary, therefore, to create a subclass and to implement some methods in that subclass before using the features that UIDocument provides. The first step in this project is to create the source file for the subclass so select the Xcode File -> New -> File… menu option and in the resulting panel select the Source category listed under iOS in the left hand panel and the Cocoa Touch Class template before clicking on Next. On the options panel, set the Subclass of menu to UIDocument, name the class MyDocument and click Next to create the new class.

With the basic outline of the subclass created the next step is to begin implementing the user interface and the corresponding outlets and actions.

Designing the User Interface

The finished application is going to consist of a user interface comprising a UITextView and UIButton. The user will enter text into the text view and initiate the saving of that text to a file by touching the button.

Select the Main.storyboard file and display the Interface Builder Object Library (View -> Utilities -> Show Object Library). Drag and drop the Text View and Button objects into the view canvas, resizing the text view so that it occupies only the upper area of the view. Double click on the button object and change the label text to “Save”:


Ios 8 icloud store ui.png

Figure 37-1


Click on the Text View so that it is selected and use the Auto Layout Pin menu to add Spacing to nearest neighbor constraints on the top, left and right hand edges of the view with the Constrain to margins option switched on. Before adding the nearest neighbor constraints, also enable the Height constraint so that the height of the view is preserved at runtime.

Having configured the constraints for the Text View, select the Button view and use the Auto Layout Align menu to configure a Horizontal Center in Container constraint. With the Button view still selected, display the Pin menu and add a Spacing to nearest neighbor constraint on the top edge of the view using the current value and with the Constrain to margins option switched off. Remove the example Latin text from the text view object by selecting it in the view canvas and deleting the value from the Text property in the Attributes Inspector panel. Replace the text with a string which reads “Sample Text”.

With the user interface designed it is now time to connect the action and outlet. Select the Text View object in the view canvas, display the Assistant Editor panel and verify that the editor is displaying the contents of the ViewController.swift file. Ctrl-click on the Text View object and drag to a position just below the “class ViewController” declaration line in the Assistant Editor. Release the line and in the resulting connection dialog establish an outlet connection named textView.

Finally, Ctrl-click on the button object and drag the line to the area immediately beneath the viewDidLoad method declaration in the Assistant Editor panel. Release the line and, within the resulting connection dialog, establish an Action method on the Touch Up Inside event configured to call a method named saveDocument.

Implementing the Application Data Structure

So far we have created and partially implemented a UIDocument subclass named MyDocument and designed the user interface of the application together with corresponding actions and outlets. As previously discussed, the MyDocument class will require two methods that are responsible for interfacing between the MyDocument object instances and the application’s data structures. Before we can implement these methods, however, we first need to implement the application data structure. In the context of this application the data simply consists of the string entered by the user into the text view object. Given the simplicity of this example we will declare the data structure, such as it is, within the MyDocument class where it can be easily accessed by the contentsForType and loadFromContents methods. To implement the data structure, albeit a single data value, select the MyDocument.swift file and add a declaration for a String object:

import UIKit

class MyDocument: UIDocument {

    var userText: String? = "Some Sample Text"
}

Now that the data model is defined it is now time to complete the MyDocument class implementation.

Implementing the contentsForType Method

The MyDocument class is a subclass of UIDocument. When an instance of MyDocument is created and the appropriate method is called on that instance to save the application’s data to a file, the class makes a call to its contentsForType instance method. It is the job of this method to collect the data to be stored in the document and to pass it back to the MyDocument object instance in the form of an NSData object. The content of the NSData object will then be written to the document. Whilst this may sound complicated most of the work is done for us by the parent UIDocument class. All the method needs to do, in fact, is get the current value of the userText NSString object, put it into an NSData object and return it.

Select the MyDocument.swift file and add the contentsForType method as follows:

override func contentsForType(typeName: String,
           error outError: NSErrorPointer) -> AnyObject {

    if let content = userText {

        var length =
              content.lengthOfBytesUsingEncoding(NSUTF8StringEncoding)
        return NSData(bytes:content, length: length)

    } else {
        return NSData()
    }
} 

Implementing the loadFromContents Method

The loadFromContents instance method is called by an instance of MyDocument when the object is instructed to read the contents of a file. This method is passed an NSData object containing the content of the document and is responsible for updating the application’s internal data structure accordingly. All this method needs to do, therefore, is convert the NSData object contents to a string and assign it to the userText object:

override func loadFromContents(contents: AnyObject,
   ofType typeName: String, error outError: NSErrorPointer) -> Bool {

    if let userContent = contents as? NSData {
        userText = NSString(bytes: contents.bytes,
                   length: userContent.length,
                   encoding: NSUTF8StringEncoding) as? String
    }
    return true
} 

The implementation of the MyDocument class is now complete and it is time to begin implementing the application functionality.

Loading the Document at App Launch

The ultimate goal of the application is to save any text in the text view to a document on the local file system of the device. When the application is launched it needs to check if the document exists and, if so, load the contents into the text view object. If, on the other hand, the document does not yet exist it will need to be created. As is usually the case, the best place to perform these tasks is the viewDidLoad method of the view controller.

Before implementing the code for the viewDidLoad method we first need to perform some preparatory work. First, both the viewDidLoad and saveDocument methods will need access to an NSURL object containing a reference to the document and also an instance of the MyDocument class, so these need to be declared in the view controller implementation file. With the ViewController.swift file selected in the project navigator, modify the file as follows:

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var textView: UITextView!

    var document: MyDocument?
    var documentURL: NSURL?
.
.
.
}

The first task for the viewDidLoad method is to identify the path to the application’s Documents directory (a task outlined in Working with Directories in Swift on iOS 8) and construct a full path to the document which will be named savefile.txt. The method will then need to create an NSURL object based on the path to the document and use it to create an instance of the MyDocument class. The code to perform these tasks can be implemented as outlined in the following code fragment:

let dirPaths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, 
			.UserDomainMask, true)

let docsDir = dirPaths[0] as! String
let dataFile = docsDir.stringByAppendingPathComponent("savefile.txt")

documentURL = NSURL(fileURLWithPath: dataFile)
document = MyDocument(fileURL: documentURL!)
document!.userText = ""

The next task for the method is to create an NSFileManager instance and use it to identify whether the file exists. In the event that it does, the openWithCompletionHandler method of the MyDocument instance object is called to open the document and load the contents (thereby automatically triggering a call to the loadFromContents method created earlier in the chapter).

The openWithCompletionHandler method allows for a code block to be written to which is passed a Boolean value indicating the success or otherwise of the file opening and reading process. On a successful read operation this handler code simply needs to assign the value of the userText property of the MyDocument instance (which has been updated with the document contents by the loadFromContents method) to the text property of the textView object, thereby making it visible to the user.

In the event that the document does not yet exist, the saveToURL method of the MyDocument class will be called using the argument to create a new file:

if filemgr.fileExistsAtPath(dataFile) {

    document?.openWithCompletionHandler({(success: Bool) -> Void in
        if success {
            println("File open OK")
            self.textView.text = self.document?.userText
        } else {
            println("Failed to open file")   
        }
      })
    } else {
       document?.saveToURL(documentURL!, 
		forSaveOperation: .ForCreating, 
		completionHandler: {(success: Bool) -> Void in
         if success {
            println("File created OK")
         } else {
            println("Failed to create file")
         }
    })
}

Note that for the purposes of debugging, println calls have been made at key points in the process. These can be removed once the application is verified to be working correctly.

Bringing the above code fragments together results in the following, fully implemented viewDidLoad method:

override func viewDidLoad() {
    super.viewDidLoad()

    let dirPaths = 
          NSSearchPathForDirectoriesInDomains(.DocumentDirectory,
                            .UserDomainMask, true)

    let docsDir = dirPaths[0] as! String

    let dataFile = 
             docsDir.stringByAppendingPathComponent("savefile.txt")
    
    documentURL = NSURL(fileURLWithPath: dataFile)
    document = MyDocument(fileURL: documentURL!)
    document!.userText = ""

    let filemgr = NSFileManager.defaultManager()

    if filemgr.fileExistsAtPath(dataFile) {

        document?.openWithCompletionHandler({(success: Bool) -> Void in
            if success {
                println("File open OK")
                self.textView.text = self.document?.userText
            } else {
                println("Failed to open file")               }
        })
    } else {
        document?.saveToURL(documentURL!, forSaveOperation: 
		.ForCreating,
                 completionHandler: {(success: Bool) -> Void in
            if success {
                println("File created OK")
            } else {
                println("Failed to create file ")
            }
        })
    }
} 

Saving Content to the Document

When the user touches the application’s save button the content of the text view object needs to be saved to the document. An action method has already been connected to the user interface object for this purpose and it is now time to write the code for this method.

Since the viewDidLoad method has already identified the path to the document and initialized the document object, all that needs to be done is to call that object’s saveToURL method using the UIDocumentSaveForOverwriting option. The saveToURL method will automatically call the contentsForType method implemented previously in this chapter. Prior to calling the method, therefore, it is important that the userText property of the document object be set to the current text of the textView object.

Bringing this all together results in the following implementation of the saveDocument method:

@IBAction func saveDocument(sender: AnyObject) {
    document!.userText = textView.text

    document?.saveToURL(documentURL!, 
	   forSaveOperation: .ForOverwriting, 
           completionHandler: {(success: Bool) -> Void in
        if success {
            println("File overwrite OK")
        } else {
            println("File overwrite failed")
        }
    })
}  

Testing the Application

All that remains is to test that the application works by clicking on the Xcode run button. Upon execution, any text entered into the text view object should be saved to the savefile.txt file when the Save button is touched. Once some text has been saved, click on the stop button located in the Xcode toolbar. On subsequently restarting the application the text view should be populated with the previously saved text.

Summary

Whilst the UIDocument class is the cornerstone of document storage using the iCloud service it is also of considerable use and advantage in terms of using the local file system storage of an iOS device. As an abstract class, UIDocument must be subclassed and two mandatory methods implemented within the subclass in order to operate. This chapter worked through an example of using UIDocument to save and load content using a locally stored document.

The next chapter will look at using UIDocument to perform cloud based document storage and retrieval.


Learn SwiftUI and take your iOS Development to the Next Level
SwiftUI Essentials – iOS 16 Edition book is now available in Print ($39.99) and eBook ($29.99) editions. Learn more...

Buy Print Preview Book



PreviousTable of ContentsNext
Preparing an iOS 8 App to use iCloud StorageUsing iCloud Storage in an iOS 8 Application