Managing iPhone Files using the iOS 5 UIDocument Class

From Techotopia
Revision as of 20:11, 27 October 2016 by Neil (Talk | contribs) (Text replacement - "<table border="0" cellspacing="0"> " to "<table border="0" cellspacing="0" width="100%">")

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search
PreviousTable of ContentsNext
Preparing an iOS 5 iPhone App to use iCloud StorageUsing iCloud Storage in an iOS 5 iPhone 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 5 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 5 based iPhone device. Since the application requires an iCloud enabled provisioning profile it is essential that the steps outlined in the previous chapter of this book be performed to create a profile and install it into the Xcode development environment.

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. On the project options page ensure that the Company Identifier matches that specified when the iCloud enabled provisioning profile for this project was created. Since storyboards are not required, also make sure this option is not selected before clicking the Next button and creating the new project in a suitable file system location.

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 -> 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 NSObject, click Next and save the new class as MyDocument.

At this point two new files (named MyDocument.h and MyDocument.m respectively) will have been added to the project. The new class needs to be modified so that it subclasses UIDocument instead of NSObject. To do so, select the MyDocument.h file and modify it as follows:

#import <Foundation/Foundation.h>

@interface MyDocument : UIDocument

@end

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

Declaring the Outlets and Actions

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. As such, the view controller will require an outlet to the text view object and an action method to be called by the button. With these requirements in mind, select the iCloudStoreViewController.h file and modify it to add the action and outlet:

#import <UIKit/UIKit.h>

@interface iCloudStoreViewController : UIViewController
{
    UITextView *textView;
}
@property (strong, nonatomic) IBOutlet UITextView *textView;
-(IBAction)saveDocument;
@end

Next, select the iCloudStoreViewController.m implementation file and add the @synthesize directive for the outlet, the declaration of the action method and the code to ensure the outlet gets released when no longer needed:

#import "iCloudStoreViewController.h"

@implementation iCloudStoreViewController
@synthesize textView;

- (void)saveDocument
{
}
.
.
- (void)viewDidUnload
{
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
    self.textView = nil;
}
.
.
@end

Now that the action and outlet are declared now is a good time to design the user interface of the application and make the appropriate connections.

Designing the User Interface

As previously stated, the user interface is going to consist of a UITextView and a UIButton. Select iCloudStoreViewController.xib and display the Interface Builder Object Library (View -> Utilities -> Show Object Library). Drag and drop the Text View and Round Rect Button objects into the view canvas, resizing the text view so that occupies only the upper area of the view. Double click on the button object and change the label text to “Save”:


The user interface for an example iOS 5 iPhone UIDocument application

Figure 27-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 Attribute Inspector panel.

With the user interface designed it is now time to connect the action and outlet. Ctrl-click on the button object and drag the resulting line to the File’s Owner entry in the center panel. From the resulting menu select the saveDocument action method. To connect the outlet, Ctrl-click on the File’s Owner icon and drag the resulting line to the Text View object. Release the line and select textView from the menu.

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 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 <Foundation/Foundation.h>

@interface MyDocument : UIDocument
{
    NSString *userText;
}
@property (strong, nonatomic) NSString *userText;
@end

Edit the MyDocument.m file and add the corresponding @synthesize directive:

#import "MyDocument.h"

@implementation MyDocument
@synthesize 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 contentForType method as follows:

-(id)contentsForType:(NSString *)typeName 
error:(NSError *__autoreleasing *)outError
{
    return [NSData dataWithBytes:[self.userText UTF8String] 
       length:[self.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) {
        self.userText = [[NSString alloc] 
        initWithBytes:[contents bytes] 
        length:[contents length] 
        encoding:NSUTF8StringEncoding];
    } else {
        self.userText = @"";
    }
    return YES;
} 

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 in the iCloudStoreViewController.h file. Firstly, 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
{
    MyDocument *document;
    NSURL *documentURL;
    UITextView *textView;
}
@property (strong, nonatomic) IBOutlet UITextView *textView;
@property (strong, nonatomic) NSURL *documentURL;
@property (strong, nonatomic) MyDocument *document;
-(IBAction)saveDocument;
@end

Next, edit the iCloudStoreViewController.m file and modify the @synthesize directive to add a reference to documentURL and document:

#import "iCloudStoreViewController.h"

@implementation iCloudStoreViewController
@synthesize textView, documentURL, document;
.
.
@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 iPhone iOS 5 Filesystem Directories) 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 objectAtIndex:0];
NSString *dataFile = 
   [docsDir stringByAppendingPathComponent: @"document.doc"];
self.documentURL = [NSURL fileURLWithPath:dataFile];
self.document = [[MyDocument alloc] initWithFileURL:documentURL];
self.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 objectAtIndex:0];
    NSString *dataFile = 
      [docsDir stringByAppendingPathComponent: 
        @"document.doc"];

    self.documentURL = [NSURL fileURLWithPath:dataFile];
    self.document = [[MyDocument alloc] initWithFileURL:documentURL];
    self.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
{
    self.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. At present the UIDocument class does not function within the iOS Simulator environment so a provisioned iPhone device must be connected to the computer on which Xcode is running and selected as the target device before 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 iPhone 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 5 iPhone App to use iCloud StorageUsing iCloud Storage in an iOS 5 iPhone Application