/* |
File: NotesDocumentViewController.m |
Abstract: The view controller used for editing "NotesDocument". |
Version: 1.0 |
|
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple |
Inc. ("Apple") in consideration of your agreement to the following |
terms, and your use, installation, modification or redistribution of |
this Apple software constitutes acceptance of these terms. If you do |
not agree with these terms, please do not use, install, modify or |
redistribute this Apple software. |
|
In consideration of your agreement to abide by the following terms, and |
subject to these terms, Apple grants you a personal, non-exclusive |
license, under Apple's copyrights in this original Apple software (the |
"Apple Software"), to use, reproduce, modify and redistribute the Apple |
Software, with or without modifications, in source and/or binary forms; |
provided that if you redistribute the Apple Software in its entirety and |
without modifications, you must retain this notice and the following |
text and disclaimers in all such redistributions of the Apple Software. |
Neither the name, trademarks, service marks or logos of Apple Inc. may |
be used to endorse or promote products derived from the Apple Software |
without specific prior written permission from Apple. Except as |
expressly stated in this notice, no other rights or licenses, express or |
implied, are granted by Apple herein, including but not limited to any |
patent rights that may be infringed by your derivative works or by other |
works in which the Apple Software may be incorporated. |
|
The Apple Software is provided by Apple on an "AS IS" basis. APPLE |
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION |
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS |
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND |
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. |
|
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL |
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, |
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED |
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), |
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE |
POSSIBILITY OF SUCH DAMAGE. |
|
Copyright (C) 2014 Apple Inc. All Rights Reserved. |
|
*/ |
|
#import "NotesDocumentViewController.h" |
#import "ImageViewController.h" |
#import "NotesDocument.h" |
#import "AppDelegate.h" |
#import "Note.h" |
|
static NSString *TextFieldCellID = @"TextFieldCell"; |
static NSString *ImageViewCellID = @"ImageViewCell"; |
static NSString *TextViewCellID = @"TextViewCell"; |
|
// table view section index values |
#define kDocNameSection 0 |
#define kDocImageSection 1 |
#define kDocNotesSection 2 |
|
// view tags to our UIControls inside each table view cell |
#define kTextFieldViewTag 1 |
#define kTextViewViewTag 2 |
|
|
#pragma mark - |
|
@interface NotesDocumentViewController () <UITextFieldDelegate, |
UITextViewDelegate, |
UINavigationControllerDelegate, |
UIImagePickerControllerDelegate> |
|
@property (nonatomic, strong) NotesDocument *document; |
|
@property (nonatomic, strong) UIImagePickerController *imagePickerController; |
|
// cached references to our table cell controls |
@property (nonatomic, weak) UITextField *textField; // document's name |
@property (nonatomic, weak) UIImageView *imageView; // document's image |
@property (nonatomic, weak) UITextView *textView; // document's notes |
|
// these are our data source pieces to back our UITableView in case user cancels any edit session |
@property (nonatomic, strong) NSString *sourceName; |
@property (nonatomic, strong) NSString *sourceNotes; |
@property (nonatomic, strong) UIImage *sourceImage; |
|
@property (nonatomic, assign) BOOL cancelling; |
|
@end |
|
|
#pragma mark - |
|
@implementation NotesDocumentViewController |
|
// this can be called internally or externally by whoever hosts this UIDocument |
// (in our case RootViewController), so open a new NotesDocument |
// |
- (void)setDocumentURL:(NSURL *)url createNewFile:(BOOL)createNewFile |
{ |
self.document = [[NotesDocument alloc] initWithFileURL:url]; |
if (createNewFile) |
{ |
Note *note = [[Note alloc] init]; |
self.document.note = note; |
[self.document saveToURL:url forSaveOperation:UIDocumentSaveForCreating completionHandler:nil]; |
} |
|
if (createNewFile) |
{ |
// important: calling saveToURL for save operation "UIDocumentSaveForCreating" is |
// critical for new, unsaved documents. Otherwise the next close operation will |
// not complete and no initial save is done! |
// |
[self.document saveToURL:self.document.fileURL |
forSaveOperation:UIDocumentSaveForCreating |
completionHandler:^(BOOL success) { |
// note this block will be called after the document is saved, so there may be some delay |
// |
}]; |
} |
} |
|
- (void)viewDidLoad |
{ |
[super viewDidLoad]; |
|
// add an edit/done button to the navigation bar |
self.navigationItem.rightBarButtonItem = self.editButtonItem; |
|
// opening the document could take a while depending on the document size, |
// so show a activity indicator and block user touches to the table until the open completes |
// |
self.tableView.userInteractionEnabled = NO; |
UIActivityIndicatorView *indView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite]; |
[indView setColor:[self.navigationController.navigationBar tintColor]]; |
[indView startAnimating]; |
UIBarButtonItem *indViewItem = [[UIBarButtonItem alloc] initWithCustomView:indView]; |
self.navigationItem.rightBarButtonItem = indViewItem; |
|
// set our view controller's title and data source to match our document name |
self.title = [[self.document.fileURL lastPathComponent] stringByDeletingPathExtension]; |
|
// open our document and update the table with it's content, |
// we balance out by closing after populating the table |
// |
[self.document openWithCompletionHandler:^(BOOL success) { |
if (success) |
{ |
// we need to populate our table cell data source |
// (we are called here after the UIDocument has been read) |
// |
self.sourceImage = self.document.note.image; |
self.sourceNotes = self.document.note.notes; |
self.sourceName = [[self.document.fileURL lastPathComponent] stringByDeletingPathExtension]; |
|
// re-populate the table with our updated data source |
[self refreshTableSections]; |
} |
|
// done opening and ready to edit, |
// remove the loading indicator, enable editing, allow the table to be used |
// |
self.navigationItem.rightBarButtonItem = self.editButtonItem; |
self.tableView.userInteractionEnabled = YES; |
|
// done populating table, close the document |
[self.document closeWithCompletionHandler:nil]; |
}]; |
|
// listen for when we are suspended to the background |
[[NSNotificationCenter defaultCenter] addObserver:self |
selector:@selector(didEnterBackground:) |
name:UIApplicationDidEnterBackgroundNotification |
object:nil]; |
|
// listen for text field changes |
[[NSNotificationCenter defaultCenter] addObserver:self |
selector:@selector(textFieldDidChange:) |
name:UITextFieldTextDidChangeNotification |
object:nil]; |
} |
|
- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex |
{ |
// user dismissed duplicate alert below, revert the title field to the previous value |
NSString *originalFileName = [[self.document.fileURL lastPathComponent] stringByDeletingPathExtension]; |
self.sourceName = self.textField.text = originalFileName; |
} |
|
- (BOOL)documentNameExists:(NSString *)documentName |
{ |
NSString *newFileName = [documentName stringByAppendingPathExtension:kFileExtension]; |
NSURL *checkFileURL = [[AppDelegate localDocumentsDirectoryURL] URLByAppendingPathComponent:newFileName]; |
|
return [[NSFileManager defaultManager] fileExistsAtPath:[checkFileURL path] isDirectory:NO]; |
} |
|
// signifies whether or not we want to allow the editing operation to be fulfilled |
// (includes both Done (save) and Cancel operations), returns NO only if the user wanted |
// to rename the document that matches one already on disk |
// |
- (BOOL)processEditResults:(BOOL)editing |
{ |
BOOL processEditResults = YES; |
|
if (editing) |
{ |
// user has started editing (tapped Edit button) |
// |
// apply the Cancel button in place of the left back button |
self.navigationItem.leftBarButtonItem = |
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel |
target:self |
action:@selector(cancelAction:)]; |
} |
else |
{ |
// user has finished editing (tapped either Done or Cancel buttons) |
// |
NSString *existingDocName = [[self.document.fileURL lastPathComponent] stringByDeletingPathExtension]; |
|
if (self.cancelling) |
{ |
// user tapped Cancel button, |
// so cancel all work, and revert to the current document's data |
// |
self.sourceImage = self.document.note.image; |
self.sourceName = existingDocName; |
self.sourceNotes = self.document.note.notes; |
} |
else |
{ |
// is the document name on disk match what the user entered |
// |
if (![existingDocName isEqualToString:self.textField.text]) |
{ |
// user chose a different document name, |
// check if there exists another doc with that name |
// |
if ([self documentNameExists:self.textField.text]) |
{ |
// a document with that name already exists, warn the user |
// |
NSString *message = [NSString stringWithFormat:@"Document \"%@\" already exists", self.textField.text]; |
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:message |
message:@"Please choose a different name." |
delegate:self |
cancelButtonTitle:nil |
otherButtonTitles:@"OK", nil]; |
[alert show]; |
|
processEditResults = NO; // don't yet leave edit mode since there's a conflict |
} |
else |
{ |
NSString *newDocName = [self.textField.text stringByAppendingPathExtension:kFileExtension]; |
NSURL *renamedDocumentURL = [[AppDelegate localDocumentsDirectoryURL] URLByAppendingPathComponent:newDocName]; |
|
// save the docoument to the new URL with a new name |
[self.document saveToURL:renamedDocumentURL |
forSaveOperation:UIDocumentSaveForCreating |
completionHandler:^(BOOL success) { |
if (success) |
{ |
// note this block will be called after the document is saved, so there may be some delay |
// |
|
// update the view control's title to reflect the new document name |
self.title = [[renamedDocumentURL lastPathComponent] stringByDeletingPathExtension]; |
|
// remove activity indicator |
|
// since we have renamed the document successfully, notify our |
// delegate (in this case our RootViewController) to update it's table |
// view with the new document name |
// |
if ([self.delegate respondsToSelector:@selector(directoryDidChange)]) |
{ |
[self.delegate directoryDidChange]; |
} |
} |
}]; |
} |
} |
|
if (processEditResults) |
{ |
// user has finished and wants to "commit all changes to disk |
// |
Note *note = self.document.note; |
note.image = self.imageView.image; |
note.notes = self.textView.text; |
|
// update our interim data source (to be reflected in our table cells) |
self.sourceImage = self.imageView.image; |
self.sourceName = self.textField.text; |
self.sourceNotes = self.textView.text; |
|
// trigger autosave of this document bump the change count |
[self.document updateChangeCount:UIDocumentChangeDone]; |
} |
} |
} |
|
return processEditResults; |
} |
|
// invoked by the Edit/Done button to toggle the editing state of the document. |
// |
- (void)setEditing:(BOOL)editing animated:(BOOL)animated |
{ |
if ([self processEditResults:editing]) |
{ |
// start or end editing (used tapped either Cancel or Done) |
// |
// When editing, hide the back button and replace it with 'Cancel' so the user can't |
// navigate away from the view until committing the changes. |
// |
[self.navigationItem setHidesBackButton:editing animated:YES]; |
|
// update the image cell indicator to reflect the edit state |
NSIndexPath *imageCellIndexPath = [NSIndexPath indexPathForRow:0 inSection:kDocImageSection]; |
UITableViewCell *imageCell = [self.tableView cellForRowAtIndexPath:imageCellIndexPath]; |
imageCell.textLabel.text = editing ? @"Add or Edit" : @""; |
|
// update the enabled states of our edit cells to reflect the edit state |
self.textField.enabled = self.textView.editable = editing; |
|
if (self.cancelling || !editing) |
{ |
// we are done editing the document |
|
/* |
When you directly call closeWithCompletionHandler: on the document instance, |
UIDocument saves the document if there are any unsaved changes, by calling your |
UIDocument subclass method: |
|
- (id)contentsForType:(NSString *)typeName error:(NSError **)outError; |
|
So you call "closeWithCompletionHandler" to begin the sequence of method calls that |
saves a document safely and asynchronously. After the save operation concludes, |
the code in completionHandler is executed. |
|
You typically would not override closeWithCompletionHandler. |
The default implementation calls the autosaveWithCompletionHandler: method. |
*/ |
|
if (!(self.document.documentState & UIDocumentStateClosed)) |
{ |
// note this close operation may take some time for bigger documents |
[_document closeWithCompletionHandler:^(BOOL success) { |
if (success) |
{ |
// document was successfully closed |
} |
}]; |
} |
|
// remove the Cancel button on the left |
self.navigationItem.leftBarButtonItem = nil; |
} |
else |
{ |
// start editing means we need to open the document to make changes |
[self.document openWithCompletionHandler:^(BOOL success) { |
if (success) |
{ |
// document was successfully opened |
} |
}]; |
} |
|
// allow the edit session to be honored by calling our super |
[super setEditing:editing animated:animated]; |
} |
} |
|
// refresh the three sections in this table |
- (void)refreshTableSections |
{ |
// force an update of these 3 table rows (more efficient than calling reloadData) |
[self.tableView reloadRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:0 inSection:kDocNameSection], |
[NSIndexPath indexPathForRow:0 inSection:kDocImageSection], |
[NSIndexPath indexPathForRow:0 inSection:kDocNotesSection]] |
withRowAnimation:UITableViewRowAnimationAutomatic]; |
} |
|
- (void)dealloc |
{ |
// we are no longer interested in these notifications |
[[NSNotificationCenter defaultCenter] removeObserver:self |
name:UIApplicationDidEnterBackgroundNotification |
object:nil]; |
[[NSNotificationCenter defaultCenter] removeObserver:self |
name:UITextFieldTextDidChangeNotification |
object:nil]; |
} |
|
|
#pragma mark - Notifications |
|
// we are being notified that this app is being put to the background (suspended) |
- (void)didEnterBackground:(NSNotification *)notif |
{ |
// we are moving to the background, so exit edit mode |
[self setEditing:NO animated:NO]; |
} |
|
// we are being notified when text has changed in our UITextField |
- (void)textFieldDidChange:(NSNotification *)notif |
{ |
// check if the text field has text, keep the Done button disabled until we have valid text |
UITextField *textField = (UITextField *)notif.object; |
|
if (textField == self.textField) |
{ |
self.navigationItem.rightBarButtonItem.enabled = (textField.text.length > 0) ? YES : NO; |
} |
} |
|
|
#pragma mark - Action methods |
|
- (void)cancelAction:(id)sender |
{ |
// flag ourselves that the user is cancelling |
// (used inside "setEditing" method to distinquish between a commit or cancel) |
// |
self.cancelling = YES; |
|
[self setEditing:NO animated:NO]; |
|
self.cancelling = NO; |
|
// since the user cancelled the edit session, restore our table view to it's previous state |
[self refreshTableSections]; |
|
// re-enable the "Edit" button since the user cancelled the last edit |
self.navigationItem.rightBarButtonItem.enabled = YES; |
} |
|
|
#pragma mark - UIImagePickerControllerDelegate |
|
- (UIImage *)normalizedImage:(UIImage *)image |
{ |
UIImage *returnImage = nil; |
if (image != nil) |
{ |
if (image.imageOrientation == UIImageOrientationUp) { return image; } |
|
UIGraphicsBeginImageContextWithOptions(image.size, NO, image.scale); |
[image drawInRect:(CGRect){0, 0, image.size}]; |
returnImage = UIGraphicsGetImageFromCurrentImageContext(); |
UIGraphicsEndImageContext(); |
} |
return returnImage; |
} |
|
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info |
{ |
// use the uncropped - uneditied image |
UIImage *originalImage = [info valueForKey:UIImagePickerControllerOriginalImage]; |
if (originalImage) |
{ |
// adjust its orientation if necesary |
self.sourceImage = [self normalizedImage:originalImage]; |
|
// update just our image cell in the table |
NSIndexPath *imageCellIndexPath = [NSIndexPath indexPathForRow:0 inSection:kDocImageSection]; |
[self.tableView reloadRowsAtIndexPaths:@[imageCellIndexPath] |
withRowAnimation:UITableViewRowAnimationAutomatic]; |
} |
|
[self dismissViewControllerAnimated:YES completion:^ { |
// picker finished closing |
}]; |
} |
|
|
#pragma mark - UITextFieldDelegate |
|
- (void)textFieldDidEndEditing:(UITextField *)textField |
{ |
// user tapped out of the text field, navigated to pick an image or tapped Cancel/Done |
if (!self.cancelling) |
{ |
self.sourceName = textField.text; |
} |
} |
|
- (BOOL)textFieldShouldReturn:(UITextField *)textField |
{ |
// a document name is required to dismiss the keyboard |
if (textField.text.length > 0) |
{ |
// dismiss the keyboard and toggle back to non edit (changes Done button to Edit) |
[textField resignFirstResponder]; |
return YES; |
} |
else |
{ |
return NO; |
} |
} |
|
|
#pragma mark - UITextViewDelegate |
|
- (void)textViewDidChange:(UITextView *)textView |
{ |
if ([textView respondsToSelector:@selector(setTextContainerInset:)]) |
{ |
// keep scrolling to the end, if necessary |
[textView layoutIfNeeded]; |
CGRect caretRect = [textView caretRectForPosition:textView.selectedTextRange.end]; |
caretRect.size.height += textView.textContainerInset.bottom; |
[textView scrollRectToVisible:caretRect animated:NO]; |
} |
} |
|
|
#pragma mark - UITableViewDataSource |
|
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section |
{ |
return 1; |
} |
|
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView |
{ |
return 3; // document name, image, and notes |
} |
|
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section |
{ |
NSString *title = nil; |
switch (section) |
{ |
case kDocNameSection: |
title = @"Name"; |
break; |
case kDocImageSection: |
title = @"Image"; |
break; |
case kDocNotesSection: |
title = @"Notes"; |
break; |
} |
return title; |
} |
|
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath |
{ |
UITableViewCell *cell = nil; |
|
switch (indexPath.section) |
{ |
case kDocNameSection: |
{ |
// the document name edit field |
cell = [self.tableView dequeueReusableCellWithIdentifier:TextFieldCellID forIndexPath:indexPath]; |
_textField = (UITextField *)[cell viewWithTag:kTextFieldViewTag]; |
self.textField.text = self.sourceName; |
break; |
} |
|
case kDocImageSection: |
{ |
// the document image |
cell = [self.tableView dequeueReusableCellWithIdentifier:ImageViewCellID forIndexPath:indexPath]; |
_imageView = cell.imageView; |
cell.imageView.image = self.sourceImage; |
break; |
} |
|
case kDocNotesSection: |
{ |
// the document notes |
cell = [self.tableView dequeueReusableCellWithIdentifier:TextViewCellID forIndexPath:indexPath]; |
_textView = (UITextView *)[cell viewWithTag:kTextViewViewTag]; |
self.textView.text = self.sourceNotes; |
break; |
} |
} |
|
return cell; |
} |
|
|
#pragma mark - UITableViewDelegate |
|
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath |
{ |
CGFloat height = [self.tableView rowHeight]; |
|
switch (indexPath.section) |
{ |
case kDocImageSection: |
height = 90.0; // image section row is bigger |
break; |
case kDocNotesSection: |
height = 167.0; // notes section row is bigger |
break; |
} |
return height; |
} |
|
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath; |
{ |
return NO; |
} |
|
- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath |
{ |
return NO; |
} |
|
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath |
{ |
if (indexPath.section == kDocImageSection) |
{ |
if (self.tableView.editing) |
{ |
// when in edit more, open the image picker |
if (self.imagePickerController == nil) |
{ |
_imagePickerController = [[UIImagePickerController alloc] init]; |
self.imagePickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; |
self.imagePickerController.delegate = self; |
self.imagePickerController.allowsEditing = NO; |
} |
|
[self.navigationController presentViewController:self.imagePickerController animated:YES completion:^ { |
// image picker is done presenting |
}]; |
} |
else if (self.imageView.image != nil) |
{ |
// when not in edit mode, just display the image |
ImageViewController *imageViewController = |
[self.storyboard instantiateViewControllerWithIdentifier:@"imageViewController"]; |
imageViewController.image = self.imageView.image; |
imageViewController.title = @"Image"; |
|
[self.navigationController pushViewController:imageViewController animated:YES]; |
} |
} |
|
[self.tableView deselectRowAtIndexPath:indexPath animated:YES]; |
} |
|
@end |