Managing Files using the iOS 7 UIDocument Class
Previous | Table of Contents | Next |
Preparing an iOS 7 App to use iCloud Storage | Using iCloud Storage in an iOS 7 Application |
Learn SwiftUI and take your iOS Development to the Next Level |
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.
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:
- UIDocumentStateNormal – The document is open and enabled for user editing.
- UIDocumentStateClosed – The document is currently closed. This state can also indicate an error in reading a document.
- UIDocumentStateInConflict – Conflicts have been detected for the document.
- UIDocumentStateSavingError – An error occurred when an attempt was made to save the document.
- UIDocumentStateEditingDisabled – 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 be 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 7 based device or emulator.
To create the project, begin by launching Xcode and create a new product named iCloudStore with a corresponding class prefix using the Single View Application template configured for either the iPhone or iPad.
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 files for the subclass so select the Xcode File -> New -> File… menu option and in the resulting panel select the Objective-C class template and click 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”:
Figure 35-1
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.
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 iCloudStoreViewController.h file. Ctrl-click on the Text View object and drag to a position just below the @interface 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 newly created outlet 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.h file and add a declaration for an NSString object:
#import <UIKit/UIKit.h> @interface MyDocument : UIDocument @property (strong, nonatomic) NSString *userText; @end
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.m file and add the contentsForType method as follows:
-(id)contentsForType:(NSString *)typeName error:(NSError *__autoreleasing *)outError { return [NSData dataWithBytes:[_userText UTF8String] length:[_userText length]]; }
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:
-(BOOL) loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError *__autoreleasing *)outError { if ( [contents length] > 0) { _userText = [[NSString alloc] initWithBytes:[contents bytes] length:[contents length] encoding:NSUTF8StringEncoding]; } else { _userText = @""; } return YES; }
The implementation of the MyDocument class is now complete and it is time to begin implementing the application functionality.
Learn SwiftUI and take your iOS Development to the Next Level |
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 in the iCloudStoreViewController.h file. First, since we will be creating instances of the MyDocument class within the view controller it will be necessary to import the MyDocument.h file. Secondly, 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 iCloudStoreViewController.h selected in the project navigator, modify the file as follows:
#import <UIKit/UIKit.h> #import "MyDocument.h" @interface iCloudStoreViewController : UIViewController @property (strong, nonatomic) IBOutlet UITextView *textView; @property (strong, nonatomic) NSURL *documentURL; @property (strong, nonatomic) MyDocument *document; -(IBAction)saveDocument; @end
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 on iOS 7) and construct a full path to the document which will be named document.doc. 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:
NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *docsDir = dirPaths[0]; NSString *dataFile = [docsDir stringByAppendingPathComponent: @"document.doc"]; _documentURL = [NSURL fileURLWithPath:dataFile]; _document = [[MyDocument alloc] initWithFileURL:_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 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:
NSFileManager *filemgr = [NSFileManager defaultManager]; if ([filemgr fileExistsAtPath: dataFile]) { [_document openWithCompletionHandler: ^(BOOL success) { if (success){ NSLog(@"Opened"); _textView.text = _document.userText; } else { NSLog(@"Not opened"); } }]; } else { [_document saveToURL:_documentURL forSaveOperation: UIDocumentSaveForCreating completionHandler:^(BOOL success) { if (success){ NSLog(@"Created"); } else { NSLog(@"Not created"); } }]; }
Note that for the purposes of debugging, NSLog 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:
- (void)viewDidLoad { [super viewDidLoad]; NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *docsDir = dirPaths[0]; NSString *dataFile = [docsDir stringByAppendingPathComponent: @"document.doc"]; _documentURL = [NSURL fileURLWithPath:dataFile]; _document = [[MyDocument alloc] initWithFileURL:_documentURL]; _document.userText = @""; NSFileManager *filemgr = [NSFileManager defaultManager]; if ([filemgr fileExistsAtPath: dataFile]) { [_document openWithCompletionHandler: ^(BOOL success) { if (success){ NSLog(@"Opened"); _textView.text = _document.userText; } else { NSLog(@"Not opened"); } }]; } else { [_document saveToURL:_documentURL forSaveOperation: UIDocumentSaveForCreating completionHandler:^(BOOL success) { if (success){ NSLog(@"Created"); } else { NSLog(@"Not created"); } }]; } }
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:
- (void)saveDocument:(id)sender { _document.userText = _textView.text; [_document saveToURL:_documentURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:^(BOOL success) { if (success){ NSLog(@"Saved for overwriting"); } else { NSLog(@"Not saved for overwriting"); } }]; }
Testing the Application
All that remains is to test that the application works by clicking on the Run button. Upon execution, any text entered into the text view object should be saved to the document.doc 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 |
Previous | Table of Contents | Next |
Preparing an iOS 7 App to use iCloud Storage | Using iCloud Storage in an iOS 7 Application |