PackagedDocument/RootViewController.m
/* |
Copyright (C) 2016 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
The main root view controller of this app. |
*/ |
#import "AppDelegate.h" |
#import "RootViewController.h" |
#import "NotesDocument.h" |
#import "NotesDocumentViewController.h" |
#import "FileRepresentation.h" |
// segue identifiers that both navigate to NotesDocumentViewController |
static NSString *kSegueIDForShowDocument = @"ShowDocument"; |
static NSString *kSegueIDForNewDocument = @"NewDocument"; |
@interface RootViewController () <NotesDocumentDelegate> |
@property (nonatomic, strong) NSMutableArray *documentList; |
@end |
#pragma mark - |
@implementation RootViewController |
- (void)viewDidLoad |
{ |
// configure the view by setting up the Edit/Done button, and create the list of documents |
[super viewDidLoad]; |
self.tableView.allowsSelectionDuringEditing = NO; |
self.navigationItem.leftBarButtonItem = self.editButtonItem; |
// while in table edit mode, we allow for "Delete My Photos" feature in the bottom toolbar |
UIBarButtonItem *clearAllButton = [[UIBarButtonItem alloc] initWithTitle:@"Delete All" |
style:UIBarButtonItemStylePlain |
target:self |
action:@selector(deleteAllAction:)]; |
[self setToolbarItems:@[clearAllButton] animated:NO]; |
[self populateTableWithDirectoryContents]; |
} |
- (void)viewWillAppear:(BOOL)animated |
{ |
[super viewWillAppear:animated]; |
// whenever we come back to this view controller update the enable state of the "Edit" button |
self.navigationItem.leftBarButtonItem.enabled = self.documentList.count > 0 ? YES : NO; |
} |
- (void)setEditing:(BOOL)editing animated:(BOOL)animated |
{ |
[super setEditing:editing animated:animated]; |
// change the enable state of the '+' button, |
// don't allow adding documents while our table is being edited for document removals |
// |
self.navigationItem.rightBarButtonItem.enabled = !editing; |
// after editing starts or ends, make sure to update the enable state of the "Edit" button |
self.navigationItem.leftBarButtonItem.enabled = self.documentList.count > 0 ? YES : NO; |
// hide/show toolbar on edit toggle |
[self.navigationController setToolbarHidden:!editing animated:YES]; |
} |
// called to populate or re-populate our table with the conts in the Documents folder, |
// called upon initialiation and when we are notified as NotesDocumentDelegate |
// |
- (void)populateTableWithDirectoryContents |
{ |
self.documentList = [[NSMutableArray alloc] init]; |
// enumerate the contents of the Documents directory: |
// for each file in the directory, create a FileRepresentation object and add it to |
// the documentList array |
// |
NSError *error = nil; |
NSArray *localDocuments = |
[[NSFileManager defaultManager] contentsOfDirectoryAtURL:[AppDelegate localDocumentsDirectoryURL] |
includingPropertiesForKeys:nil |
options:NSDirectoryEnumerationSkipsPackageDescendants |
error:&error]; |
NSAssert(error == nil, ([NSString stringWithFormat:@"Error contentsOfDirectoryAtURL: %@", error])); |
for (NSURL *documentURL in localDocuments) |
{ |
// only add documents to the table that match our extension |
if ([documentURL.pathExtension isEqualToString:kFileExtension]) |
{ |
FileRepresentation *fileRepresentation = [[FileRepresentation alloc] initWithURL:documentURL]; |
[self.documentList addObject:fileRepresentation]; |
} |
} |
[self.tableView reloadData]; |
} |
#pragma mark - Creating a new document |
// called when the '+' button is tapped |
- (IBAction)createDocument:(id)sender |
{ |
// display an alert to ask the user to enter the document name |
UIAlertController *createDocumentAlertController = |
[UIAlertController alertControllerWithTitle:@"New Document" |
message:@"Enter document name" |
preferredStyle:UIAlertControllerStyleAlert]; |
// add the text field for entering the new document name, |
// note: we want to block certain characters when naming the new document |
// by listening for UITextFieldTextDidChangeNotification |
// |
__weak RootViewController *weakSelf = self; |
[createDocumentAlertController addTextFieldWithConfigurationHandler:^(UITextField *textField) { |
[[NSNotificationCenter defaultCenter] addObserver:weakSelf |
selector:@selector(editFieldChanged:) |
name:UITextFieldTextDidChangeNotification |
object:textField]; |
}]; |
UIAlertAction *OKAction = [UIAlertAction actionWithTitle:@"OK" |
style:UIAlertActionStyleDefault |
handler:^(UIAlertAction *act) { |
/* |
The user decided to create a new document. |
First check for a duplicate document already exists to avoid an overwrite. |
Then add a new file item to the local file list and add a new row in the table view to display it. |
Perform the "CreateNewDocument" segue to display the new empty file for editing. |
*/ |
// the alert view contains the name of the document |
UITextField *docNameField = createDocumentAlertController.textFields[0]; |
// check if a document already exists with that name, to avoid an overwrite |
NSString *fileName = [docNameField.text stringByAppendingPathExtension:kFileExtension]; |
NSURL *fileURL = [[AppDelegate localDocumentsDirectoryURL] URLByAppendingPathComponent:fileName]; |
BOOL isDirectory; |
if ([[NSFileManager defaultManager] fileExistsAtPath:fileURL.path isDirectory:&isDirectory]) |
{ |
// we could a document with the same name, alert the user |
NSString *message = [NSString stringWithFormat:@"Document \"%@\" already exists", docNameField.text]; |
UIAlertController *dupeAlert = [UIAlertController alertControllerWithTitle:message |
message:@"Please choose a different name." |
preferredStyle:UIAlertControllerStyleAlert]; |
UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"OK" |
style:UIAlertActionStyleDefault |
handler:nil]; |
[dupeAlert addAction:okAction]; |
[self presentViewController:dupeAlert animated:YES completion:^ { |
// (do more potential work here after the alert is presented) |
}]; |
} |
else |
{ |
// no document found with the same name |
// add it to our data soruce |
FileRepresentation *fileRepresentation = [[FileRepresentation alloc] initWithURL:fileURL]; |
[self.documentList addObject:fileRepresentation]; |
// insert a table row for this document |
NSIndexPath *newFileIndexPath = [NSIndexPath indexPathForRow:(self.documentList).count - 1 inSection:0]; |
[self.tableView insertRowsAtIndexPaths:@[newFileIndexPath] withRowAnimation:UITableViewRowAnimationNone]; |
[self.tableView selectRowAtIndexPath:newFileIndexPath animated:NO scrollPosition:UITableViewScrollPositionNone]; |
// push navigate to the NotesDocumentViewcontroller for editing |
[self performSegueWithIdentifier:kSegueIDForNewDocument sender:self]; |
// resort the list of document since one was added |
[self.documentList sortUsingComparator:^NSComparisonResult(FileRepresentation *fileRep1, FileRepresentation *fileRep2) { |
return [fileRep1.URL.lastPathComponent localizedStandardCompare:fileRep2.URL.lastPathComponent]; |
}]; |
[self.tableView reloadData]; |
} |
[[NSNotificationCenter defaultCenter] removeObserver:self |
name:UITextFieldTextDidChangeNotification |
object:docNameField]; |
}]; |
[createDocumentAlertController addAction:OKAction]; |
createDocumentAlertController.preferredAction = OKAction; |
OKAction.enabled = NO; // no docunemt name entered yet, don't allow OK dismissal |
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Cancel" |
style:UIAlertActionStyleCancel |
handler:^(UIAlertAction *act) { |
UITextField *docNameField = createDocumentAlertController.textFields[0]; |
[[NSNotificationCenter defaultCenter] removeObserver:self |
name:UITextFieldTextDidChangeNotification |
object:docNameField]; |
}]; |
[createDocumentAlertController addAction:cancelAction]; |
[self presentViewController:createDocumentAlertController animated:YES completion:^ { |
// (do more potential work here after the alert is presented) |
}]; |
} |
// called each time the user types a character in the UIAlertController's text field |
- (void)editFieldChanged:(NSNotification *)aNotification |
{ |
if (self.presentedViewController != nil && [self.presentedViewController isKindOfClass:[UIAlertController class]]) |
{ |
UIAlertController *alertController = (UIAlertController *)self.presentedViewController; |
UITextField *nameField = alertController.textFields.firstObject; |
if ([aNotification object] == nameField) // is it the edit field we expect? |
{ |
NSString *valueStr = nameField.text; |
// enable the OK button if we have a non-empty name |
UIAlertAction *okAction = alertController.preferredAction; |
okAction.enabled = valueStr.length > 0; |
if (valueStr.length > 0) |
{ |
// don't allow files that start with "." |
if ([valueStr hasPrefix:@"."]) |
{ |
valueStr = [valueStr stringByReplacingCharactersInRange:NSMakeRange(0,1) withString:@"-"]; |
} |
else |
{ |
// filter out "/" and ":" characters |
valueStr = [valueStr stringByReplacingOccurrencesOfString:@"/" withString:@"-"]; |
valueStr = [valueStr stringByReplacingOccurrencesOfString:@":" withString:@"-"]; |
} |
} |
nameField.text = valueStr; |
} |
} |
} |
#pragma mark - Deleting a document |
- (void)deleteDocumentWithIndex:(NSUInteger)index |
{ |
// deleta a particular document by index |
NSURL *documentURL = [(self.documentList)[index] URL]; |
NSFileCoordinator* fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; |
[fileCoordinator coordinateWritingItemAtURL:documentURL |
options:NSFileCoordinatorWritingForDeleting |
error:nil |
byAccessor:^(NSURL *writingURL) |
{ |
NSFileManager* fileManager = [[NSFileManager alloc] init]; |
[fileManager removeItemAtURL:writingURL error:nil]; |
// update the document list and remove the row from the table view |
[self.documentList removeObjectAtIndex:index]; |
// update the table |
NSIndexPath *indexPathToDelete = [NSIndexPath indexPathForRow:index inSection:0]; |
[self.tableView deleteRowsAtIndexPaths:@[indexPathToDelete] withRowAnimation:UITableViewRowAnimationAutomatic]; |
}]; |
} |
- (void)deleteAllAction:(id)sender |
{ |
UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil |
message:@"Are you sure you want to delete all documents?" |
preferredStyle:UIAlertControllerStyleActionSheet]; |
UIAlertAction *OKAction = [UIAlertAction actionWithTitle:@"OK" |
style:UIAlertActionStyleDefault |
handler:^(UIAlertAction *action) { |
// OK button action |
while (self.documentList.count > 0) |
{ |
[self deleteDocumentWithIndex:0]; |
} |
// delete all operation means we exit edit mode |
[self setEditing:NO animated:YES]; |
// disable Edit button if we have no records |
self.editButtonItem.enabled = (self.documentList.count > 0); |
}]; |
[alert addAction:OKAction]; |
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Cancel" |
style:UIAlertActionStyleDefault |
handler:nil]; |
[alert addAction:cancelAction]; |
[self presentViewController:alert animated:YES completion:nil]; |
} |
#pragma mark - UITableViewDataSource |
// Data for the first section comes from the documentList array; elements in each array |
// are instances of FileRepresentation class. |
// |
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section |
{ |
return self.documentList.count; |
} |
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath |
{ |
static NSString *CellIdentifier = @"TableCell"; |
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; |
// configure the cell (just display the filename without the extension) |
NSUInteger row = indexPath.row; |
FileRepresentation *fileRep = (self.documentList)[row]; |
NSString *fileName = (fileRep.URL).lastPathComponent.stringByDeletingPathExtension; |
cell.textLabel.text = fileName; |
return cell; |
} |
#pragma mark - UITableViewDelegate |
- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath |
{ |
return NO; // no reordering, we display alphabetically |
} |
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath |
{ |
// which edit action? |
if (editingStyle == UITableViewCellEditingStyleDelete) |
{ |
// user wants to delete a particular document |
[self deleteDocumentWithIndex:indexPath.row]; |
// if no documents left exit edit mode here |
[self setEditing:!(self.documentList.count == 0)]; |
} |
} |
#pragma mark - Segues |
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender |
{ |
// get the view controller the segue will navigate to |
NotesDocumentViewController *notesDocumentViewController = segue.destinationViewController; |
notesDocumentViewController.delegate = self; // so we can be notified if the document was renamed |
// Get the currently-selected row in the table view (the one the user tapped on) and use |
// its row to get the corresponding file representation object and from that the URL of |
// the document the user wants to open. |
// |
NSIndexPath *selectionPath = (self.tableView).indexPathForSelectedRow; |
NSUInteger row = selectionPath.row; |
FileRepresentation *fileRep = (self.documentList)[row]; |
NSURL *selectedDocumentURL = fileRep.URL; |
// determine which segue is being performed, and configure the destination view controller |
// by invoking setDocumentURL:createNewFile: with the appropriate arguments |
// |
if ([segue.identifier isEqualToString:kSegueIDForShowDocument]) |
{ |
// we are navigating to view the document |
[notesDocumentViewController setDocumentURL:selectedDocumentURL createNewFile:NO]; |
} |
else if ([segue.identifier isEqualToString:kSegueIDForNewDocument]) |
{ |
// we are navigating to create the new document |
[notesDocumentViewController setDocumentURL:selectedDocumentURL createNewFile:YES]; |
} |
} |
#pragma mark - NotesDocumentDelegate |
- (void)directoryDidChange |
{ |
// we are notified as a delegate that a document name has changed (from NotesDocumentViewController) |
// so re-populate our table view content. |
// |
[self populateTableWithDirectoryContents]; |
} |
@end |
Copyright © 2016 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2016-02-04