Created
December 6, 2011 15:52
-
-
Save aquarius/1438664 to your computer and use it in GitHub Desktop.
iCloud Document Controller
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// MNDocumentConflictResolutionViewController.h | |
// MindNodeTouch | |
// | |
// Created by Markus Müller on 15.12.11. | |
// Copyright (c) 2011 __MyCompanyName__. All rights reserved. | |
// | |
#import <UIKit/UIKit.h> | |
@class MNDocumentReference; | |
@interface MNDocumentConflictResolutionViewController : UIViewController | |
- (id)initWithDocumentReference:(MNDocumentReference *)reference; | |
@property(nonatomic,retain) IBOutlet UITableView *tableView; | |
@property(nonatomic,retain) IBOutlet UILabel *descriptionLabel; | |
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// MNDocumentConflictResolutionViewController.m | |
// MindNodeTouch | |
// | |
// Created by Markus Müller on 15.12.11. | |
// Copyright (c) 2011 __MyCompanyName__. All rights reserved. | |
// | |
#import "MNDocumentConflictResolutionViewController.h" | |
#import "MNDocumentReference.h" | |
#import "MNTableViewCell.h" | |
#import "MNFormatter.h" | |
#import "MNDocumentController.h" | |
@interface MNDocumentConflictResolutionViewController () <NSFilePresenter> | |
@property (strong) MNDocumentReference *documentReference; | |
@property (copy) NSArray *documentVersions; | |
@property (nonatomic, strong) IBOutlet UIBarButtonItem *keepButtonItem; | |
@property (strong) NSURL *presentedURL; | |
@property (nonatomic, strong) NSOperationQueue *fileItemOperationQueue; | |
@end | |
@implementation MNDocumentConflictResolutionViewController | |
@synthesize tableView=_tableView; | |
@synthesize descriptionLabel=_descriptionLabel; | |
@synthesize documentReference=_documentReference; | |
@synthesize documentVersions=_documentVersions; | |
@synthesize keepButtonItem = _keepButtonItem; | |
@synthesize presentedURL=_presentedURL; | |
@synthesize fileItemOperationQueue=_fileItemOperationQueue; | |
- (id)initWithDocumentReference:(MNDocumentReference *)reference | |
{ | |
self = [self initWithNibName:nil bundle:nil]; | |
if (!self) return nil; | |
self.title = NSLocalizedStringFromTable(@"Resolve Conflict", @"DocumentConflictResolution", @"Navigation Bar title"); | |
self.documentReference = reference; | |
self.fileItemOperationQueue = [[NSOperationQueue alloc] init]; | |
self.fileItemOperationQueue.maxConcurrentOperationCount = 1; | |
return self; | |
} | |
- (void)didReceiveMemoryWarning | |
{ | |
// Releases the view if it doesn't have a superview. | |
[super didReceiveMemoryWarning]; | |
// Release any cached data, images, etc that aren't in use. | |
} | |
#pragma mark - View lifecycle | |
- (void)viewDidLoad | |
{ | |
[super viewDidLoad]; | |
self.tableView.allowsMultipleSelectionDuringEditing = YES; | |
self.tableView.rowHeight = 100; | |
[self.tableView setEditing:YES]; | |
self.descriptionLabel.text = NSLocalizedStringFromTable(@"Modifications aren't in sync. Choose which documents to keep.", @"DocumentConflictResolution", @"Description Label above table view"); | |
self.keepButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"" | |
style:UIBarButtonItemStylePlain | |
target:self | |
action:@selector(keepButtonItemPressed:)]; | |
[self updateKeepButton]; | |
self.navigationItem.rightBarButtonItem = self.keepButtonItem; | |
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel | |
target:self | |
action:@selector(cancelButtonItemPressed:)]; | |
} | |
- (void)viewDidUnload | |
{ | |
[super viewDidUnload]; | |
self.keepButtonItem = nil; | |
} | |
- (void)viewWillAppear:(BOOL)animated | |
{ | |
[super viewWillAppear:animated]; | |
self.presentedURL = self.documentReference.fileURL; | |
[NSFileCoordinator addFilePresenter:self]; | |
[self _reloadDocumentVersions]; | |
} | |
- (void)viewDidAppear:(BOOL)animated | |
{ | |
[super viewDidAppear:animated]; | |
if ([self.documentVersions count] <= 1) { | |
[self cancelButtonItemPressed:nil]; | |
return; | |
} | |
} | |
- (void)viewWillDisappear:(BOOL)animated | |
{ | |
[super viewWillDisappear:animated]; | |
[NSFileCoordinator removeFilePresenter:self]; | |
} | |
- (void)viewDidDisappear:(BOOL)animated | |
{ | |
[super viewDidDisappear:animated]; | |
} | |
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation | |
{ | |
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { | |
return YES; | |
} else { | |
return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown); | |
} | |
} | |
#pragma mark - NSFileVersion | |
- (void)_reloadDocumentVersions; | |
{ | |
self.documentVersions = nil; | |
NSMutableArray *documentVersions = [NSMutableArray arrayWithObjects:[NSFileVersion currentVersionOfItemAtURL:self.documentReference.fileURL], nil]; | |
[documentVersions addObjectsFromArray:[NSFileVersion unresolvedConflictVersionsOfItemAtURL:self.documentReference.fileURL]]; | |
[documentVersions sortUsingComparator:^NSComparisonResult(NSFileVersion *file1, NSFileVersion *file2) { | |
return [file1.modificationDate compare:file2.modificationDate]; | |
}]; | |
self.documentVersions = documentVersions; | |
[self.tableView reloadData]; | |
} | |
#pragma mark - Table view data source | |
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView | |
{ | |
return 1; | |
} | |
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section | |
{ | |
return [self.documentVersions count]; | |
} | |
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath | |
{ | |
UITableViewCell *cell = [MNTableViewCell cellForTableView:tableView style:UITableViewCellStyleSubtitle]; | |
NSFileVersion *currentVersion = [self.documentVersions objectAtIndex:indexPath.row]; | |
cell.textLabel.text = [NSString stringWithFormat:NSLocalizedStringFromTable(@"Modified on %@", @"DocumentConflictResolution", @"Titel label of cells in the table view, the placeholder will be replaced by the device name"),currentVersion.localizedNameOfSavingComputer]; | |
cell.textLabel.numberOfLines = 2; | |
cell.detailTextLabel.text = [[MNFormatter dateFormatter] stringFromDate:currentVersion.modificationDate]; | |
cell.imageView.image = [MNDocumentReference previewImageForDocumenAtURL:currentVersion.URL]; | |
return cell; | |
} | |
#pragma mark - UITableView delegate methods | |
- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath | |
{ | |
[self updateKeepButton]; | |
} | |
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath | |
{ | |
[self updateKeepButton]; | |
} | |
#pragma mark - Actions | |
- (void)keepButtonItemPressed:(id)sender | |
{ | |
[[UIApplication sharedApplication] beginIgnoringInteractionEvents]; | |
// Create list of versions to keep | |
NSURL *documentURL = self.documentReference.fileURL; | |
NSFileVersion *currentVersion = [NSFileVersion currentVersionOfItemAtURL:documentURL]; | |
NSMutableArray *versionsToKeep = [NSMutableArray arrayWithCapacity:1]; | |
NSUInteger count = [self.documentVersions count]; | |
for (NSIndexPath *currentPath in [self.tableView indexPathsForSelectedRows]) { | |
NSUInteger index = currentPath.row; | |
if (index <= count) { | |
[versionsToKeep addObject:[self.documentVersions objectAtIndex:index]]; | |
} | |
} | |
if ([versionsToKeep count] < 1) { | |
[self dismissModalViewControllerAnimated:YES]; | |
return; | |
} | |
[[MNDocumentController sharedDocumentController] performAsynchronousFileAccessUsingBlock:^{ | |
// we need to replace the current version | |
if (![versionsToKeep containsObject:currentVersion]) { | |
NSFileVersion *newCurrentVersion = [versionsToKeep objectAtIndex:0]; | |
NSError *error = nil; | |
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:self]; | |
[coordinator coordinateWritingItemAtURL:newCurrentVersion.URL options:NSFileCoordinatorWritingForMoving | |
writingItemAtURL:documentURL options:NSFileCoordinatorWritingForReplacing | |
error:&error byAccessor:^(NSURL *newURL1, NSURL *newURL2) { | |
[newCurrentVersion replaceItemAtURL:newURL2 options:NSFileVersionReplacingByMoving error:NULL]; | |
}]; | |
if (error) { | |
NSLog(@"%@",error); | |
} | |
[versionsToKeep removeObject:newCurrentVersion]; | |
} else { | |
[versionsToKeep removeObject:currentVersion]; | |
} | |
// copy all remaining versions to a new file | |
for (NSFileVersion *currentKeepVersion in versionsToKeep) { | |
NSString *fileName = [[MNDocumentController sharedDocumentController] uniqueFileNameForDisplayName:self.documentReference.displayName]; | |
NSURL *destinationURL = [[documentURL URLByDeletingLastPathComponent] URLByAppendingPathComponent:fileName]; | |
NSError *error = nil; | |
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:self]; | |
[coordinator coordinateWritingItemAtURL:currentKeepVersion.URL options:NSFileCoordinatorWritingForDeleting | |
writingItemAtURL:destinationURL options:NSFileCoordinatorWritingForReplacing | |
error:&error byAccessor:^(NSURL *newURL1, NSURL *newURL2) { | |
[currentKeepVersion replaceItemAtURL:newURL2 options:NSFileVersionReplacingByMoving error:NULL]; | |
}]; | |
if (error) { | |
NSLog(@"%@",error); | |
} | |
} | |
// mark all (remaining) versions as resolved and remove them | |
NSArray *conflictVersions = [NSFileVersion unresolvedConflictVersionsOfItemAtURL:self.documentReference.fileURL]; | |
for (NSFileVersion* fileVersion in conflictVersions) { | |
fileVersion.resolved = YES; | |
} | |
[NSFileVersion removeOtherVersionsOfItemAtURL:self.documentReference.fileURL error:nil]; | |
dispatch_async(dispatch_get_main_queue(), ^{ | |
[[UIApplication sharedApplication] endIgnoringInteractionEvents]; | |
[self dismissModalViewControllerAnimated:YES]; | |
}); | |
}]; | |
} | |
- (void)cancelButtonItemPressed:(id)sender | |
{ | |
[self dismissModalViewControllerAnimated:YES]; | |
} | |
- (void)updateKeepButton | |
{ | |
NSArray *selectedRows = [self.tableView indexPathsForSelectedRows]; | |
if (selectedRows) { | |
self.keepButtonItem.title = [NSString stringWithFormat:NSLocalizedStringFromTable(@"Keep (%d)", @"DocumentConflictResolution", @"Keep multiple documents bar button item"), [selectedRows count]]; | |
self.keepButtonItem.enabled = YES; | |
} else { | |
self.keepButtonItem.title = NSLocalizedStringFromTable(@"Keep", @"DocumentConflictResolution", @"Keep 1 document bar button item"); | |
self.keepButtonItem.enabled = NO; | |
} | |
} | |
#pragma mark - NSFilePresenter Protocol | |
- (NSURL *)presentedItemURL; | |
{ | |
return self.presentedURL; | |
} | |
- (NSOperationQueue *)presentedItemOperationQueue; | |
{ | |
return self.fileItemOperationQueue; | |
} | |
- (void)accommodatePresentedItemDeletionWithCompletionHandler:(void (^)(NSError *errorOrNil))completionHandler; | |
{ | |
dispatch_async(dispatch_get_main_queue(), ^{ | |
[self cancelButtonItemPressed:nil]; | |
if (completionHandler) { | |
completionHandler(nil); | |
} | |
}); | |
} | |
- (void)presentedItemDidMoveToURL:(NSURL *)newURL | |
{ | |
self.presentedURL = newURL; | |
// dispatch on main queue to make sure KVO notifications get send on main | |
dispatch_async(dispatch_get_main_queue(), ^{ | |
[self _reloadDocumentVersions]; | |
}); | |
} | |
- (void)presentedItemDidChange; | |
{ | |
dispatch_async(dispatch_get_main_queue(), ^{ | |
[self _reloadDocumentVersions]; | |
}); | |
} | |
- (void)presentedItemDidGainVersion:(NSFileVersion *)version; | |
{ | |
dispatch_async(dispatch_get_main_queue(), ^{ | |
[self _reloadDocumentVersions]; | |
}); | |
} | |
- (void)presentedItemDidLoseVersion:(NSFileVersion *)version; | |
{ | |
dispatch_async(dispatch_get_main_queue(), ^{ | |
[self _reloadDocumentVersions]; | |
}); | |
} | |
- (void)presentedItemDidResolveConflictVersion:(NSFileVersion *)version; | |
{ | |
dispatch_async(dispatch_get_main_queue(), ^{ | |
[self _reloadDocumentVersions]; | |
}); | |
} | |
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// MNDocumentController.h | |
// MindNodeTouch | |
// | |
// Created by Markus Müller on 22.12.08. | |
// Copyright 2008 Markus Müller. All rights reserved. | |
// | |
#import <Foundation/Foundation.h> | |
@class MNDocumentReference; | |
extern NSString *MNDocumentControllerDocumentReferencesKey; | |
@interface MNDocumentController : NSObject | |
+ (MNDocumentController *)sharedDocumentController; | |
#pragma mark - Documents | |
@property (readonly,strong) NSMutableSet *documentReferences; | |
@property (readonly) BOOL documentsInCloud; | |
- (NSArray *)documentNames; | |
#pragma mark - Document Manipulation | |
- (void)createNewDocumentWithCompletionHandler:(void (^)(MNDocumentReference *reference))completionHandler; | |
- (void)deleteDocument:(MNDocumentReference *)document completionHandler:(void (^)(NSError *errorOrNil))completionHandler; | |
- (void)duplicateDocument:(MNDocumentReference *)document completionHandler:(void (^)(NSError *errorOrNil))completionHandler; | |
- (void)renameDocument:(MNDocumentReference *)document toFileName:(NSString *)fileName completionHandler:(void (^)(NSError *errorOrNil))completionHandler; | |
- (void)performAsynchronousFileAccessUsingBlock:(void (^)(void))block; | |
- (BOOL)pendingDocumentTransfers; | |
- (void)copyAllCloudDocumentsToLocalWithCompletionHandler:(void (^)(NSError *errorOrNil))completionHandler; | |
- (void)moveAllCloudDocumentsToLocalWithCompletionHandler:(void (^)(NSError *errorOrNil))completionHandler; | |
- (void)moveAllLocalDocumentsToCloudWithCompletionHandler:(void (^)(void))completionHandler; | |
- (void)importDocumentAtURL:(NSURL *)url completionHandler:(void (^)(MNDocumentReference *reference, NSError *errorOrNil))completionHandler; | |
#pragma mark - Paths | |
+ (NSURL *)localDocumentsURL; | |
+ (NSURL *)ubiquitousContainerURL; | |
+ (NSURL *)ubiquitousDocumentsURL; | |
- (NSString *)uniqueFileNameForDisplayName:(NSString *)displayName; | |
+ (NSString *)uniqueFileNameForDisplayName:(NSString *)displayName extension:(NSString *)extension usedFileNames:(NSSet *)usedFileNames; | |
+ (NSString *)uniqueFileNameForDisplayName:(NSString *)displayName extension:(NSString *)extension inDictionary:(NSURL *)dictionaryURL; | |
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// MNDocumentController.h | |
// MindNodeTouch | |
// | |
// Created by Markus Müller on 22.12.08. | |
// Copyright 2008 Markus Müller. All rights reserved. | |
// | |
#import "MNDocumentController.h" | |
#import "MNDocumentReference.h" | |
#import "MNError.h" | |
#import "MNDefaults.h" | |
#import "MNDocument.h" | |
// Keys | |
NSString *MNDocumentControllerDocumentReferencesKey = @"documentReferences"; | |
typedef void (^MNDequeueBlockForMetadataQueryDidFinish)(); | |
@interface MNDocumentController () | |
@property (readwrite, strong) NSMutableSet *documentReferences; | |
@property (readwrite, strong) NSMutableSet *previousMetaDataQueryResult; | |
@property (readwrite, strong) NSOperationQueue *fileAccessWorkingQueue; | |
@property (readwrite, copy) MNDequeueBlockForMetadataQueryDidFinish dequeueBlockForMetadataQueryDidFinish; | |
@property (strong) NSMetadataQuery *iCloudMetadataQuery; | |
@end | |
@implementation MNDocumentController | |
@synthesize documentReferences = _documentReferences; | |
@synthesize previousMetaDataQueryResult = _previousMetaDataQueryResult; | |
@synthesize iCloudMetadataQuery = _iCloudMetadataQuery; | |
@synthesize fileAccessWorkingQueue = _fileAccessWorkingQueue; | |
@synthesize dequeueBlockForMetadataQueryDidFinish=_dequeueBlockForMetadataQueryDidFinish; | |
#pragma mark - Init | |
+ (MNDocumentController *)sharedDocumentController | |
{ | |
static MNDocumentController *sharedDocumentController = nil; | |
static dispatch_once_t onceToken; | |
dispatch_once(&onceToken, ^{ | |
sharedDocumentController = [MNDocumentController alloc]; | |
sharedDocumentController = [sharedDocumentController init]; | |
}); | |
return sharedDocumentController; | |
} | |
- (id)init | |
{ | |
self = [super init]; | |
if (self == nil) return self; | |
NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; | |
[center addObserver:self selector:@selector(applicationWillTerminate:) name:UIApplicationWillTerminateNotification object:nil]; | |
[center addObserver:self selector:@selector(applicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; | |
[center addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; | |
[center addObserver:self selector:@selector(userDefaultsDidChange:) name:NSUserDefaultsDidChangeNotification object:nil]; | |
NSOperationQueue *queue = [[NSOperationQueue alloc] init]; | |
[queue setName:@"MNDocumentController Working Queue"]; | |
[queue setMaxConcurrentOperationCount:1]; | |
self.fileAccessWorkingQueue = queue; | |
self.documentReferences = [NSMutableSet setWithCapacity:10]; | |
self.previousMetaDataQueryResult = [NSMutableSet setWithCapacity:10]; | |
[self _loadLocalDocuments]; | |
[self _startMetadataQuery]; | |
return self; | |
} | |
- (void)dealloc | |
{ | |
[[NSNotificationCenter defaultCenter] removeObserver:self]; | |
for (MNDocumentReference *currentReference in _documentReferences) { | |
[currentReference disableFilePresenter]; // we need to do this to make sure FilePresenter get unregistered | |
} | |
} | |
#pragma mark - Documents | |
- (BOOL)documentsInCloud | |
{ | |
return [[NSUserDefaults standardUserDefaults] boolForKey:MNDefaultsDocumentsInCloudKey] && [[self class] ubiquitousContainerURL]; | |
} | |
// we have to do this because NSMetaDataQuery returns file wrapper without a tailing '/', however the file system scanner returns URLS with '/' | |
static NSString *_dictionaryKeyFromURL(NSURL *url) | |
{ | |
NSString *pathString = [url path]; | |
if (![pathString hasSuffix:@"/"]) { | |
return pathString; | |
} | |
pathString = [pathString substringToIndex:([pathString length]-1)]; | |
return pathString; | |
} | |
// replaces all documents, also iCloud documents! | |
- (void)_loadLocalDocuments | |
{ | |
NSURL *documentDirectory = [[self class] localDocumentsURL]; | |
if (!documentDirectory) return; | |
// create file coordinator to request folder read access | |
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; | |
NSError *readError = nil; | |
[coordinator coordinateReadingItemAtURL:documentDirectory options:NSFileCoordinatorReadingWithoutChanges error:&readError byAccessor: ^(NSURL *readURL) { | |
NSFileManager *fileManager = [[NSFileManager alloc] init]; | |
NSError *error = nil; | |
NSArray *fileURLs = [fileManager contentsOfDirectoryAtURL:readURL includingPropertiesForKeys:[NSArray arrayWithObject:NSURLIsDirectoryKey] options:0 error:&error]; | |
if (!fileURLs) { | |
NSLog(@"Failed to scan documents."); | |
return; | |
} | |
for (NSURL *currentFileURL in fileURLs) { | |
if ([[currentFileURL pathExtension] isEqualToString:MNDocumentMindNodeExtension]) { | |
// create a new reference | |
NSDate *modificationDate = nil; | |
NSDictionary *attributes = [fileManager attributesOfItemAtPath:[currentFileURL path] error:NULL]; | |
if (attributes) { | |
modificationDate = [attributes fileModificationDate]; | |
} | |
if (!modificationDate) { | |
modificationDate = [NSDate date]; | |
} | |
MNDocumentReference *reference = [[MNDocumentReference alloc] initWithFileURL:currentFileURL modificationDate:modificationDate]; | |
[self addDocumentReferencesObject:reference]; | |
} else { | |
// we only scan for MindNode files at the moment | |
} | |
} | |
}]; | |
[self debugLogCloudFolder]; | |
} | |
- (void)updateFromMetadataQuery:(NSMetadataQuery *)metadataQuery | |
{ | |
if (!metadataQuery) return; | |
[metadataQuery disableUpdates]; | |
// build dictionary with existing documents | |
NSMutableDictionary *existingDocuments = [[NSMutableDictionary alloc ] initWithCapacity:[self.documentReferences count]]; | |
for (MNDocumentReference *currentReference in self.documentReferences) { | |
NSString *key = _dictionaryKeyFromURL(currentReference.fileURL); | |
if (!key) continue; | |
[existingDocuments setObject:currentReference forKey:key]; | |
} | |
// don't use results proxy as it's fast this way | |
NSUInteger metadataCount = [metadataQuery resultCount]; | |
NSMutableSet *resultDocuments = [[NSMutableSet alloc] init]; | |
for (NSUInteger metadataIndex = 0; metadataIndex < metadataCount; metadataIndex++) { | |
NSMetadataItem *metadataItem = [metadataQuery resultAtIndex:metadataIndex]; | |
NSURL *fileURL = [metadataItem valueForAttribute:NSMetadataItemURLKey]; | |
MNDocumentReference *documentReference = nil; | |
documentReference = [existingDocuments objectForKey:_dictionaryKeyFromURL(fileURL)]; | |
if (!documentReference) { | |
NSDate *modificationDate = [metadataItem valueForAttribute:NSMetadataItemFSContentChangeDateKey]; | |
if (!modificationDate) { | |
modificationDate = [NSDate date]; | |
} | |
documentReference = [[MNDocumentReference alloc] initWithFileURL:fileURL modificationDate:modificationDate]; | |
} | |
[resultDocuments addObject:documentReference]; | |
[documentReference updateWithMetadataItem:metadataItem]; | |
} | |
// create a set of new documents | |
for (MNDocumentReference *currentReference in self.previousMetaDataQueryResult) { | |
if (![resultDocuments containsObject:currentReference]) { | |
[self removeDocumentReferencesObject:currentReference]; | |
} | |
} | |
for (MNDocumentReference *currentReference in resultDocuments) { | |
if (![self.documentReferences containsObject:currentReference]) { | |
[self addDocumentReferencesObject:currentReference]; | |
} | |
} | |
[metadataQuery enableUpdates]; | |
} | |
- (NSArray *)documentNames | |
{ | |
NSMutableArray *documentNames = [NSMutableArray arrayWithCapacity:[self.documentReferences count]]; | |
for (MNDocumentReference *currentRef in self.documentReferences) { | |
[documentNames addObject:currentRef.displayName]; | |
} | |
return documentNames; | |
} | |
- (void)performAsynchronousFileAccessUsingBlock:(void (^)(void))block | |
{ | |
[self.fileAccessWorkingQueue addOperationWithBlock:block]; | |
} | |
#pragma mark - Document Manipulation | |
- (void)createNewDocumentWithCompletionHandler:(void (^)(MNDocumentReference *reference))completionHandler; | |
{ | |
NSURL *fileURL = [[[self class] localDocumentsURL] URLByAppendingPathComponent:[self uniqueFileName]]; | |
[MNDocumentReference createNewDocumentWithFileURL:fileURL completionHandler:^(MNDocumentReference *reference) { | |
if (!reference) { | |
completionHandler(nil); | |
return; | |
} | |
[self addDocumentReferencesObject:reference]; | |
if (!self.documentsInCloud) { | |
completionHandler(reference); | |
return; | |
} | |
__unsafe_unretained id blockSelf = self; | |
[self.fileAccessWorkingQueue addOperationWithBlock:^{ | |
if (![blockSelf _moveDocumentToCloud:reference]) { | |
NSLog(@"Failed to move to iCloud!"); | |
}; | |
dispatch_async(dispatch_get_main_queue(), ^{ | |
completionHandler(reference); | |
}); | |
}]; | |
}]; | |
} | |
- (void)deleteDocument:(MNDocumentReference *)document completionHandler:(void (^)(NSError *errorOrNil))completionHandler | |
{ | |
if (![self.documentReferences containsObject:document]) { | |
completionHandler(MNErrorWithCode(MNUnknownError)); | |
return; | |
} | |
[self removeDocumentReferencesObject:document]; | |
[self.fileAccessWorkingQueue addOperationWithBlock:^{ | |
__block NSError *deleteError = nil; | |
NSError *coordinatorError = nil; | |
NSFileCoordinator* fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; | |
[fileCoordinator coordinateWritingItemAtURL:document.fileURL options:NSFileCoordinatorWritingForDeleting error:&coordinatorError byAccessor:^(NSURL* writingURL) { | |
NSFileManager* fileManager = [[NSFileManager alloc] init]; | |
[fileManager removeItemAtURL:writingURL error:&deleteError]; | |
}]; | |
dispatch_async(dispatch_get_main_queue(), ^(){ | |
if (coordinatorError) { | |
completionHandler(coordinatorError); | |
return; | |
} | |
if (deleteError) { | |
completionHandler(deleteError); | |
return; | |
} | |
completionHandler(nil); | |
}); | |
}]; | |
} | |
- (void)duplicateDocument:(MNDocumentReference*)document completionHandler:(void (^)(NSError *errorOrNil))completionHandler; | |
{ | |
if (![self.documentReferences containsObject:document]) { | |
completionHandler(MNErrorWithCode(MNUnknownError)); | |
return; | |
} | |
NSString *fileName = [self uniqueFileNameForDisplayName:document.displayName]; | |
NSURL *sourceURL = document.fileURL; | |
NSURL *destinationURL = [[[self class] localDocumentsURL] URLByAppendingPathComponent:fileName isDirectory:NO]; | |
__block id blockSelf = self; | |
[self.fileAccessWorkingQueue addOperationWithBlock:^{ | |
__block NSError *copyError = nil; | |
__block BOOL success = NO; | |
__block NSURL *newDocumentURL = nil; | |
NSError *coordinatorError = nil; | |
NSFileCoordinator* fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; | |
[fileCoordinator coordinateReadingItemAtURL:sourceURL options:NSFileCoordinatorReadingWithoutChanges writingItemAtURL:destinationURL options:NSFileCoordinatorWritingForReplacing error:&coordinatorError byAccessor:^(NSURL *newReadingURL, NSURL *newWritingURL) { | |
NSFileManager* fileManager = [[NSFileManager alloc] init]; | |
if ([fileManager fileExistsAtPath:[newWritingURL path]]) { | |
return; | |
} | |
[fileManager copyItemAtURL:sourceURL toURL:destinationURL error:©Error]; | |
newDocumentURL = newWritingURL; | |
success = YES; | |
}]; | |
if (!success) { | |
dispatch_async(dispatch_get_main_queue(), ^(){ | |
if (coordinatorError) { | |
completionHandler(coordinatorError); | |
} else if (copyError) { | |
completionHandler(copyError); | |
} else { | |
completionHandler(MNErrorWithCode(MNUnknownError)); | |
} | |
}); | |
return; | |
} | |
MNDocumentReference *reference = [[MNDocumentReference alloc] initWithFileURL:newDocumentURL modificationDate:[NSDate date]]; | |
if (![blockSelf documentsInCloud]) { | |
dispatch_async(dispatch_get_main_queue(), ^(){ | |
[blockSelf addDocumentReferencesObject:reference]; | |
completionHandler(nil); | |
}); | |
return; | |
} | |
if (![blockSelf _moveDocumentToCloud:reference]) { | |
NSLog(@"Failed to move to iCloud!"); | |
} | |
dispatch_async(dispatch_get_main_queue(), ^{ | |
[blockSelf addDocumentReferencesObject:reference]; | |
completionHandler(nil); | |
}); | |
}]; | |
} | |
- (void)renameDocument:(MNDocumentReference *)document toFileName:(NSString *)fileName completionHandler:(void (^)(NSError *errorOrNil))completionHandler | |
{ | |
// check if valid filename | |
if ([fileName length] > 200) { | |
dispatch_async(dispatch_get_main_queue(), ^(){ | |
completionHandler(MNErrorWithCode(MNErrorFileNameTooLong)); | |
}); | |
return; | |
} | |
if (!NSEqualRanges([fileName rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@"/:"]], NSMakeRange(NSNotFound, 0))) { | |
dispatch_async(dispatch_get_main_queue(), ^(){ | |
completionHandler(MNErrorWithCode(MNErrorFileNameNotAllowedCharacters)); | |
}); | |
return; | |
} | |
[self.fileAccessWorkingQueue addOperationWithBlock:^{ | |
NSURL *sourceURL = document.fileURL; | |
NSURL *destinationURL = [[sourceURL URLByDeletingLastPathComponent] URLByAppendingPathComponent:fileName isDirectory:NO]; | |
NSError *writeError = nil; | |
__block NSError *moveError = nil; | |
__block BOOL success = NO; | |
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; | |
[coordinator coordinateWritingItemAtURL: sourceURL options: NSFileCoordinatorWritingForMoving writingItemAtURL: destinationURL options: NSFileCoordinatorWritingForReplacing error: &writeError byAccessor: ^(NSURL *newURL1, NSURL *newURL2) { | |
NSFileManager *fileManager = [[NSFileManager alloc] init]; | |
success = [fileManager moveItemAtURL:newURL1 toURL:newURL2 error:&moveError]; | |
}]; | |
NSError *outError = nil; | |
if (!success) { | |
if (moveError) { | |
MNLogError(moveError); | |
} | |
if (writeError) { | |
MNLogError(writeError); | |
} | |
outError = MNErrorWithCode(MNErrorFileNameAlreadyUsedError); | |
} | |
dispatch_async(dispatch_get_main_queue(), ^(){ | |
completionHandler(outError); | |
}); | |
}]; | |
} | |
- (BOOL)pendingDocumentTransfers | |
{ | |
for (MNDocumentReference *currentDocument in self.documentReferences) { | |
if (!currentDocument.isDownloaded || !currentDocument.isUploaded) { | |
return YES; | |
} | |
} | |
return NO; | |
} | |
- (void)copyAllCloudDocumentsToLocalWithCompletionHandler:(void (^)(NSError *errorOrNil))completionHandler; | |
{ | |
// check if we have pending changes | |
if ([self pendingDocumentTransfers]) { | |
return completionHandler(MNErrorWithCode(MNErrorCloudUnableToMoveAllDocumentsToCloud)); | |
} | |
NSArray *documents = [self.documentReferences copy]; | |
__unsafe_unretained id blockSelf = self; | |
[self.fileAccessWorkingQueue addOperationWithBlock:^{ | |
for (MNDocumentReference *currentDocument in documents) { | |
__block NSError *copyError = nil; | |
__block BOOL success = NO; | |
__block NSURL *newDocumentURL = nil; | |
NSURL *sourceURL = [currentDocument fileURL]; | |
NSURL *destinationURL = [[[blockSelf class] localDocumentsURL] URLByAppendingPathComponent:[sourceURL lastPathComponent] isDirectory:NO]; | |
NSError *coordinatorError = nil; | |
NSFileCoordinator* fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; | |
[fileCoordinator coordinateReadingItemAtURL:sourceURL options:NSFileCoordinatorReadingWithoutChanges writingItemAtURL:destinationURL options:NSFileCoordinatorWritingForReplacing error:&coordinatorError byAccessor:^(NSURL *newReadingURL, NSURL *newWritingURL) { | |
NSFileManager* fileManager = [[NSFileManager alloc] init]; | |
if ([fileManager fileExistsAtPath:[newWritingURL path]]) { | |
return; | |
} | |
[fileManager copyItemAtURL:sourceURL toURL:destinationURL error:©Error]; | |
newDocumentURL = newWritingURL; | |
success = YES; | |
}]; | |
} | |
dispatch_async(dispatch_get_main_queue(), ^{ | |
completionHandler(nil); | |
}); | |
}]; | |
} | |
- (void)moveAllCloudDocumentsToLocalWithCompletionHandler:(void (^)(NSError *errorOrNil))completionHandler; | |
{ | |
// check if we have pending changes | |
if ([self pendingDocumentTransfers]) { | |
return completionHandler(MNErrorWithCode(MNErrorCloudUnableToMoveAllDocumentsToCloud)); | |
} | |
NSArray *documents = [self.documentReferences copy]; | |
__unsafe_unretained id blockSelf = self; | |
[self.fileAccessWorkingQueue addOperationWithBlock:^{ | |
for (MNDocumentReference *currentDocument in documents) { | |
if (![blockSelf _moveDocumentToLocal:currentDocument]) { | |
NSLog(@"Failed to move to iCloud!"); | |
}; | |
} | |
dispatch_async(dispatch_get_main_queue(), ^{ | |
completionHandler(nil); | |
}); | |
}]; | |
} | |
- (void)moveAllLocalDocumentsToCloudWithCompletionHandler:(void (^)(void))completionHandler | |
{ | |
NSArray *documents = [self.documentReferences copy]; | |
__unsafe_unretained id blockSelf = self; | |
void (^moveAllBlock)(void) = ^ { | |
for (MNDocumentReference *currentDocument in documents) { | |
if (![blockSelf _moveDocumentToCloud:currentDocument]) { | |
NSLog(@"Failed to move to iCloud!"); | |
}; | |
} | |
dispatch_async(dispatch_get_main_queue(), ^{ | |
completionHandler(); | |
}); | |
}; | |
if (self.iCloudMetadataQuery.isGathering) { | |
self.dequeueBlockForMetadataQueryDidFinish = moveAllBlock; | |
} else { | |
[self.fileAccessWorkingQueue addOperationWithBlock:moveAllBlock]; | |
} | |
} | |
- (void)importDocumentAtURL:(NSURL *)url completionHandler:(void (^)(MNDocumentReference *reference, NSError *errorOrNil))completionHandler | |
{ | |
NSString *path = [url path]; | |
NSString *extension = [path pathExtension]; | |
if (!extension) { | |
completionHandler(nil,MNErrorWithCode(MNFileImportError)); | |
return; | |
} | |
NSString *filename = [[path lastPathComponent] stringByDeletingPathExtension]; | |
NSURL *fileURL = [[[self class] localDocumentsURL] URLByAppendingPathComponent:[self uniqueFileNameForDisplayName:filename]]; | |
// create a new document | |
MNDocument *document = [[MNDocument alloc] initWithFileURL:fileURL]; | |
if (!document) { | |
completionHandler(nil,MNErrorWithCode(MNFileImportError)); | |
return; | |
} | |
// initialize the new document from the file we need to import | |
NSError *error = nil; | |
if (![document importFromURL:url error:&error]) { | |
completionHandler(nil,MNErrorWithCode(MNFileImportError)); | |
return; | |
} | |
// save the document | |
[document updateChangeCount: UIDocumentChangeDone]; | |
[document closeWithCompletionHandler:^(BOOL success) { | |
if (!success) { | |
completionHandler(nil,MNErrorWithCode(MNFileImportError)); | |
return; | |
} | |
MNDocumentReference *reference = [[MNDocumentReference alloc] initWithFileURL:fileURL modificationDate:[NSDate date]]; | |
[self addDocumentReferencesObject:reference]; | |
if (!self.documentsInCloud) { | |
completionHandler(reference,nil); | |
return; | |
} | |
__unsafe_unretained id blockSelf = self; | |
[self.fileAccessWorkingQueue addOperationWithBlock:^{ | |
if (![blockSelf _moveDocumentToCloud:reference]) { | |
NSLog(@"Failed to move to iCloud!"); | |
}; | |
dispatch_async(dispatch_get_main_queue(), ^{ | |
completionHandler(reference,nil); | |
}); | |
}]; | |
}]; | |
} | |
#pragma mark - Paths | |
+ (NSURL *)localDocumentsURL | |
{ | |
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); | |
NSString *documentsDirectory = [paths objectAtIndex:0]; | |
return [NSURL fileURLWithPath:documentsDirectory]; | |
} | |
+ (NSURL *)ubiquitousContainerURL | |
{ | |
return [[[NSFileManager alloc] init] URLForUbiquityContainerIdentifier:nil]; | |
} | |
+ (NSURL *)ubiquitousDocumentsURL | |
{ | |
NSURL *containerURL = [self ubiquitousContainerURL]; | |
if (!containerURL) return nil; | |
NSURL *documentURL = [containerURL URLByAppendingPathComponent:@"Documents"]; | |
return documentURL; | |
} | |
- (NSString *)uniqueFileName | |
{ | |
NSString *fileName = NSLocalizedStringFromTable(@"Mind Map", @"DocumentPicker", @"Default file name. Don't localize!"); | |
fileName = [self uniqueFileNameForDisplayName:fileName]; | |
return fileName; | |
} | |
- (NSString *)uniqueFileNameForDisplayName:(NSString *)displayName | |
{ | |
NSSet *documents = self.documentReferences; | |
NSUInteger count = [documents count]; | |
// build list of filenames | |
NSMutableSet *useFileNames = [NSMutableSet setWithCapacity:count]; | |
for (MNDocumentReference *currentReference in documents) { | |
// lowercaseString: make sure our name is also unique on a case insensitive file system | |
[useFileNames addObject:[[currentReference.fileURL lastPathComponent] lowercaseString]]; | |
} | |
NSString *fileName = [[self class] uniqueFileNameForDisplayName:displayName extension:MNDocumentMindNodeExtension usedFileNames:useFileNames]; | |
return fileName; | |
} | |
+ (NSString *)uniqueFileNameForDisplayName:(NSString *)displayName extension:(NSString *)extension usedFileNames:(NSSet *)usedFileNames | |
{ // based on code from the OmniGroup Frameworks | |
NSUInteger counter = 0; // starting counter | |
displayName = [displayName stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"/:"]]; | |
if ([displayName length] > 200) displayName = [displayName substringWithRange:NSMakeRange(0, 200)]; | |
while (YES) { | |
NSString *candidateName; | |
if (counter == 0) { | |
candidateName = [[NSString alloc] initWithFormat:@"%@.%@", displayName, extension]; | |
counter = 2; // First duplicate should be "Foo 2". | |
} else { | |
candidateName = [[NSString alloc] initWithFormat:@"%@ %d.%@", displayName, counter, extension]; | |
counter++; | |
} | |
// lowercaseString: make sure our name is also unique on a case insensitive file system | |
if ([usedFileNames member:[candidateName lowercaseString]] == nil) { | |
return candidateName; | |
} | |
} | |
} | |
+ (NSString *)uniqueFileNameForDisplayName:(NSString *)displayName extension:(NSString *)extension inDictionary:(NSURL *)dictionaryURL | |
{ | |
NSUInteger counter = 0; | |
displayName = [displayName stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"/:"]]; | |
if ([displayName length] > 200) displayName = [displayName substringWithRange:NSMakeRange(0, 200)]; | |
NSArray *directoryContents = [[[NSFileManager alloc] init] subpathsOfDirectoryAtPath:[dictionaryURL path] error:NULL]; | |
if (!directoryContents) return nil; | |
while (YES) { | |
NSString *candidateName; | |
if (counter == 0) { | |
candidateName = [[NSString alloc] initWithFormat:@"%@.%@", displayName, extension]; | |
counter = 2; // First duplicate should be "Foo 2". | |
} else { | |
candidateName = [[NSString alloc] initWithFormat:@"%@ %d.%@", displayName, counter, extension]; | |
counter++; | |
} | |
// lowercaseString: make sure our name is also unique on a case insensitive file system | |
NSString *lowerCaseCandidateName = [candidateName lowercaseString]; | |
if ([directoryContents indexOfObjectPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) { | |
return ([lowerCaseCandidateName compare:obj options:NSCaseInsensitiveSearch]); | |
}] == NSNotFound) { | |
return candidateName; | |
} | |
} | |
} | |
#pragma mark - Document Persistance | |
- (void)applicationWillTerminate:(NSNotification *)notification | |
{ | |
[self _stopMetadataQuery]; | |
} | |
- (void)applicationDidEnterBackground:(NSNotification *)notification | |
{ | |
[self _stopMetadataQuery]; | |
} | |
- (void)applicationWillEnterForeground:(NSNotification *)notification | |
{ | |
[self _startMetadataQuery]; | |
} | |
- (void)userDefaultsDidChange:(NSNotification *)notification | |
{ | |
BOOL iCloudEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:MNDefaultsDocumentsInCloudKey]; | |
if (iCloudEnabled) { | |
if (!self.iCloudMetadataQuery) { | |
[self _startMetadataQuery]; | |
} | |
} else { | |
if (self.iCloudMetadataQuery) { | |
[self _stopMetadataQuery]; | |
} | |
} | |
} | |
#pragma mark - KVO Compliance | |
- (void)addDocumentReferencesObject:(MNDocumentReference *)reference | |
{ | |
[reference enableFilePresenter]; | |
[_documentReferences addObject:reference]; | |
} | |
- (void)removeDocumentReferencesObject:(MNDocumentReference *)reference | |
{ | |
[reference disableFilePresenter]; | |
[_documentReferences removeObject:reference]; | |
} | |
#pragma mark - iCloud | |
- (void)_startMetadataQuery | |
{ | |
if (!self.documentsInCloud) return; | |
if (self.iCloudMetadataQuery) return; | |
if (![[self class] ubiquitousContainerURL]) return; // no iCloud | |
NSMetadataQuery *query = [[NSMetadataQuery alloc] init]; | |
[query setSearchScopes:[NSArray arrayWithObjects:NSMetadataQueryUbiquitousDocumentsScope, nil]]; | |
[query setPredicate:[NSPredicate predicateWithFormat:@"%K like '*'", NSMetadataItemFSNameKey]]; | |
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; | |
[notificationCenter addObserver:self selector:@selector(metadataQueryDidStartGatheringNotifiction:) name:NSMetadataQueryDidStartGatheringNotification object:query]; | |
[notificationCenter addObserver:self selector:@selector(metadataQueryDidGatheringProgressNotifiction:) name:NSMetadataQueryGatheringProgressNotification object:query]; | |
[notificationCenter addObserver:self selector:@selector(metadataQueryDidFinishGatheringNotifiction:) name:NSMetadataQueryDidFinishGatheringNotification object:query]; | |
[notificationCenter addObserver:self selector:@selector(metadataQueryDidUpdateNotifiction:) name:NSMetadataQueryDidUpdateNotification object:query]; | |
if(![query startQuery]) { | |
NSLog(@"Unable to start MetadataQuery"); | |
} | |
self.iCloudMetadataQuery = query; | |
} | |
- (void)_stopMetadataQuery | |
{ | |
NSMetadataQuery *query = self.iCloudMetadataQuery; | |
if (query == nil) | |
return; | |
[query stopQuery]; | |
NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; | |
[center removeObserver:self name:NSMetadataQueryDidStartGatheringNotification object:query]; | |
[center removeObserver:self name:NSMetadataQueryGatheringProgressNotification object:query]; | |
[center removeObserver:self name:NSMetadataQueryDidFinishGatheringNotification object:query]; | |
[center removeObserver:self name:NSMetadataQueryDidUpdateNotification object:query]; | |
self.iCloudMetadataQuery = nil; | |
} | |
- (void)metadataQueryDidStartGatheringNotifiction:(NSNotification *)n; | |
{ | |
} | |
- (void)metadataQueryDidGatheringProgressNotifiction:(NSNotification *)n; | |
{ | |
// we don't update the progress as we don't want to add documents incrementally during startup | |
// our folder scan will take care of providing an initial set of documents | |
} | |
- (void)metadataQueryDidFinishGatheringNotifiction:(NSNotification *)n; | |
{ | |
[self updateFromMetadataQuery:self.iCloudMetadataQuery]; | |
if (self.dequeueBlockForMetadataQueryDidFinish) { | |
[self.fileAccessWorkingQueue addOperationWithBlock:^{ | |
self.dequeueBlockForMetadataQueryDidFinish(); | |
self.dequeueBlockForMetadataQueryDidFinish = nil; | |
}]; | |
} | |
} | |
- (void)metadataQueryDidUpdateNotifiction:(NSNotification *)n; | |
{ | |
[self updateFromMetadataQuery:self.iCloudMetadataQuery]; | |
} | |
// This method blocks, make sure to call it on a queue | |
- (BOOL)_moveDocumentToCloud:(MNDocumentReference *)documentReference | |
{ | |
NSURL *sourceURL = documentReference.fileURL; | |
NSURL *targetDocumentURL = [[self class] ubiquitousDocumentsURL]; | |
if (!targetDocumentURL) { | |
return NO; | |
} | |
NSURL *destinationURL = [targetDocumentURL URLByAppendingPathComponent:[sourceURL lastPathComponent] isDirectory:NO]; | |
NSFileManager *fileManager = [[NSFileManager alloc] init]; | |
if ([fileManager fileExistsAtPath:[destinationURL path]]) { | |
NSString *fileName = [self uniqueFileNameForDisplayName:documentReference.displayName]; | |
destinationURL = [targetDocumentURL URLByAppendingPathComponent:fileName isDirectory:NO]; | |
} | |
NSError *error = nil; | |
BOOL success = [fileManager setUbiquitous:YES itemAtURL:sourceURL destinationURL:destinationURL error:&error]; | |
if (error) NSLog(@"%@",error); | |
return success; | |
} | |
// This method blocks, make sure to call it on a queue | |
- (BOOL)_moveDocumentToLocal:(MNDocumentReference *)documentReference | |
{ | |
NSURL *sourceURL = documentReference.fileURL; | |
NSURL *targetDocumentURL = [[self class] localDocumentsURL]; | |
NSURL *destinationURL = [targetDocumentURL URLByAppendingPathComponent:[sourceURL lastPathComponent] isDirectory:NO]; | |
NSFileManager *fileManager = [[NSFileManager alloc] init]; | |
NSError *error = nil; | |
BOOL success = [fileManager setUbiquitous:NO itemAtURL:sourceURL destinationURL:destinationURL error:&error]; | |
if (error) NSLog(@"%@",error); | |
return success; | |
} | |
- (void)debugLogCloudFolder | |
{ | |
NSURL *url = [[self class] ubiquitousDocumentsURL]; | |
if (!url) NSLog(@"Unable to access ubiquitousContainer"); | |
NSError *error = nil; | |
NSFileManager *fileManager = [[NSFileManager alloc] init]; | |
NSArray *fileURLs = [fileManager contentsOfDirectoryAtURL:url includingPropertiesForKeys:[NSArray arrayWithObject:NSURLIsDirectoryKey] options:0 error:&error]; | |
for (NSURL *currentURL in fileURLs) { | |
NSDictionary *attributes = [fileManager attributesOfItemAtPath:[currentURL path] error:NULL]; | |
NSLog(@"%@",[currentURL lastPathComponent]); | |
NSLog(@"%@",attributes); | |
NSLog(@"--"); | |
} | |
} | |
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// MNDocumentReference.h | |
// MindNodeTouch | |
// | |
// Created by Markus Müller on 23.09.10. | |
// Copyright 2010 __MyCompanyName__. All rights reserved. | |
// | |
#import <Foundation/Foundation.h> | |
@class MNDocument; | |
// attributes | |
extern NSString *MNDocumentReferenceDisplayNameKey; | |
extern NSString *MNDocumentReferenceModificationDateKey; | |
extern NSString *MNDocumentReferencePreviewKey; | |
extern NSString *MNDocumentReferenceStatusUpdatedKey; // virtual | |
@interface MNDocumentReference : NSObject <NSFilePresenter> | |
#pragma mark - Init | |
+ (void)createNewDocumentWithFileURL:(NSURL *)fileURL completionHandler:(void (^)(MNDocumentReference *reference))completionHandler; | |
- (id)initWithFileURL:(NSURL *)fileURL modificationDate:(NSDate *)modificationDate; | |
- (void)enableFilePresenter; | |
- (void)disableFilePresenter; | |
#pragma mark - Properties | |
@property (readonly,strong) NSString *displayName; | |
@property (readonly,strong) NSString* displayModificationDate; | |
@property (readonly,strong) NSURL *fileURL; | |
@property (readonly,strong) NSDate *modificationDate; | |
// iCloud state | |
@property (readonly) BOOL isUbiquitous; | |
@property (readonly) BOOL hasUnresolvedConflicts; | |
@property (readonly) BOOL isDownloaded; | |
@property (readonly) BOOL isDownloading; | |
@property (readonly) BOOL isUploaded; | |
@property (readonly) BOOL isUploading; | |
@property (readonly) CGFloat percentDownloaded; | |
@property (readonly) CGFloat percentUploaded; | |
#pragma mark - Document Representation | |
- (MNDocument *)document; | |
#pragma mark - iCloud Support | |
- (void)startDownloading; | |
- (void)updateWithMetadataItem:(NSMetadataItem *)metaDataItem; | |
#pragma mark - Preview Image | |
@property (nonatomic,readonly,strong) UIImage* preview; | |
- (void)previewImageWithCallbackBlock:(void(^)(UIImage *image))callbackBlock; | |
+ (UIImage *)animationImageForDocument:(MNDocument *)document withSize:(CGSize)size; | |
+ (UIImage *)previewImageForDocumenAtURL:(NSURL *)url; | |
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// MNDocumentReference.m | |
// MindNodeTouch | |
// | |
// Created by Markus Müller on 23.09.10. | |
// Copyright 2010 __MyCompanyName__. All rights reserved. | |
// | |
#import "MNDocumentReference.h" | |
#import "MNDocumentController.h" | |
#import "MNDocumentViewController.h" | |
#import "MNDocument.h" | |
#import "MNImageExporter.h" | |
#import "MNFormatter.h" | |
#import "NSString+UUID.h" | |
#import "MNError.h" | |
#import "UIImage+Size.h" | |
// Attributes Keys | |
NSString *MNDocumentReferenceDisplayNameKey = @"displayName"; | |
NSString *MNDocumentReferenceModificationDateKey = @"modificationDate"; | |
NSString *MNDocumentReferencePreviewKey = @"preview"; | |
NSString *MNDocumentReferenceStatusUpdatedKey = @"statusUpdate"; | |
@interface MNDocumentReference () | |
// attributes | |
@property (readwrite,strong) NSString *displayName; | |
@property (readwrite,strong) NSString *displayModificationDate; | |
@property (readwrite,strong) NSURL *fileURL; | |
@property (readwrite,strong) NSDate *modificationDate; | |
@property (nonatomic,readwrite,strong) UIImage* preview; | |
@property (readwrite,strong) NSOperationQueue *fileItemOperationQueue; | |
// iCloud | |
@property (readwrite) BOOL isUbiquitous; | |
@property (readwrite) BOOL hasUnresolvedConflicts; | |
@property (readwrite) BOOL isDownloaded; | |
@property (readwrite) BOOL isDownloading; | |
@property (readwrite) BOOL isUploaded; | |
@property (readwrite) BOOL isUploading; | |
@property (readwrite) CGFloat percentDownloaded; | |
@property (readwrite) CGFloat percentUploaded; | |
@property (readwrite) BOOL startedDownload; | |
@end | |
@implementation MNDocumentReference | |
#pragma mark - | |
#pragma mark Properties | |
@synthesize displayName = _displayName; | |
@synthesize fileURL = _fileURL; | |
@synthesize modificationDate = _modficationDate; | |
@synthesize displayModificationDate = _displayModificationDate; | |
@synthesize fileItemOperationQueue = _fileItemOperationQueue; | |
@synthesize preview = _preview; | |
// iCloud | |
@synthesize isUbiquitous = _isUbiquitous; | |
@synthesize hasUnresolvedConflicts=_hasUnresolvedConflictsKey; | |
@synthesize isDownloaded=_isDownloadedKey; | |
@synthesize isDownloading=_isDownloadingKey; | |
@synthesize isUploaded=_isUploadedKey; | |
@synthesize isUploading=_isUploadingKey; | |
@synthesize percentDownloaded=_percentDownloadedKey; | |
@synthesize percentUploaded=_percentUploadedKey; | |
@synthesize startedDownload=_startedDownload; | |
#pragma mark - Init | |
+ (void)createNewDocumentWithFileURL:(NSURL *)fileURL completionHandler:(void (^)(MNDocumentReference *reference))completionHandler | |
{ | |
MNDocumentReference *reference = [[[self class] alloc] initWithFileURL:fileURL modificationDate:[NSDate date]]; | |
if (!reference) { | |
completionHandler(nil); | |
return; | |
} | |
// create and initialize an empty document | |
MNDocument *document = [[MNDocument alloc] initNewDocumentWithFileURL:fileURL]; | |
[document saveToURL:fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success){ | |
[document closeWithCompletionHandler:^(BOOL success) { | |
completionHandler(reference); | |
}]; | |
}]; | |
} | |
// we don't have a init methode without modification date because we don't want to do a coordinating read in an initilizer | |
- (id)initWithFileURL:(NSURL *)fileURL modificationDate:(NSDate *)modificationDate | |
{ | |
self = [super init]; | |
if (self == nil) return self; | |
self.fileURL = fileURL; | |
self.displayName = [[fileURL lastPathComponent] stringByDeletingPathExtension]; | |
[self _refreshModificationDate:modificationDate]; | |
self.fileItemOperationQueue = [[NSOperationQueue alloc] init]; | |
self.fileItemOperationQueue.name = @"MNDocumentReference"; | |
[self.fileItemOperationQueue setMaxConcurrentOperationCount:1]; | |
// iCloud | |
NSNumber* numberValue; | |
if ([fileURL getResourceValue:&numberValue forKey:NSURLIsUbiquitousItemKey error:nil]) { | |
self.isUbiquitous = [numberValue boolValue]; | |
} | |
self.hasUnresolvedConflicts = NO; | |
self.isDownloaded = YES; | |
self.isDownloading = NO; | |
self.isUploaded = YES; | |
self.isUploading = NO; | |
self.percentDownloaded = 0; | |
self.percentUploaded = 100; | |
self.startedDownload = NO; | |
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; | |
return self; | |
} | |
- (void)enableFilePresenter | |
{ | |
[NSFileCoordinator addFilePresenter:self]; | |
} | |
- (void)disableFilePresenter | |
{ | |
[NSFileCoordinator removeFilePresenter:self]; | |
} | |
- (void) dealloc | |
{ | |
[[NSNotificationCenter defaultCenter] removeObserver:self]; | |
} | |
- (NSString *)description; | |
{ | |
return [NSString stringWithFormat:@"Name: '%@' Date: '%@'",self.displayName, self.modificationDate]; | |
} | |
#pragma mark - Document Representation | |
- (MNDocument *)document | |
{ | |
if (!self.isDownloaded || self.hasUnresolvedConflicts) return nil; | |
return [[MNDocument alloc] initWithFileURL:self.fileURL]; | |
} | |
- (void)_refreshModificationDate:(NSDate*)date | |
{ | |
self.displayModificationDate = [[MNFormatter dateFormatter] stringFromDate:date]; | |
self.modificationDate = date; | |
} | |
#pragma mark - Preview Image | |
- (void)didReceiveMemoryWarning:(NSNotification*)n | |
{ | |
self.preview = nil; | |
} | |
- (void)previewImageWithCallbackBlock:(void(^)(UIImage *image))callbackBlock | |
{ | |
if (self.preview) { | |
if (callbackBlock) { | |
callbackBlock(self.preview); | |
} | |
return; | |
} | |
__block id blockSelf = self; | |
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ | |
[blockSelf _reloadPreviewImageWithCallbackBlock:^(UIImage *image) { | |
callbackBlock(image); | |
}]; | |
}); | |
} | |
- (void)_reloadPreviewImageWithCallbackBlock:(void(^)(UIImage *image))callbackBlock | |
{ | |
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ | |
NSURL *imageURL = [[self.fileURL URLByAppendingPathComponent:MNDocumentQuickLookFolderName isDirectory:YES] URLByAppendingPathComponent:MNDocumentQuickLookPreviewFileName isDirectory:NO]; | |
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:self]; | |
NSError *readError = nil; | |
__block UIImage *image; | |
[coordinator coordinateReadingItemAtURL:imageURL options:NSFileCoordinatorReadingWithoutChanges error:&readError byAccessor: ^(NSURL *readURL){ | |
image = [UIImage mn_thumbnailImageAtURL:imageURL withMaxSize:230]; | |
}]; | |
if (!image) return; | |
dispatch_async(dispatch_get_main_queue(), ^{ | |
[self willChangeValueForKey:MNDocumentReferencePreviewKey]; | |
self.preview = image; | |
[self didChangeValueForKey:MNDocumentReferencePreviewKey]; | |
if (callbackBlock) { | |
callbackBlock(image); | |
} | |
}); | |
}); | |
} | |
+ (UIImage *)animationImageForDocument:(MNDocument *)document withSize:(CGSize)size | |
{ | |
id viewState = document.viewState; | |
if (!viewState) return nil; | |
id zoomLevelNumber = [viewState objectForKey: MNDocumentViewStateZoomScaleKey]; | |
if (![zoomLevelNumber isKindOfClass:[NSNumber class]]) return nil; | |
CGFloat zoomLevel = [zoomLevelNumber doubleValue]; | |
if (zoomLevel == 0) zoomLevel = 1; | |
// scroll point | |
id offsetString = [viewState objectForKey: MNDocumentViewStateScrollCenterPointKey]; | |
if (![offsetString isKindOfClass:[NSString class]]) return nil; | |
CGPoint centerPoint = CGPointFromString(offsetString); | |
CGRect drawRect = CGRectMake(centerPoint.x, centerPoint.y, 0, 0); | |
drawRect = CGRectInset(drawRect, -size.width/zoomLevel/2, -size.height/zoomLevel/2); | |
MNImageExporter *exporter = [MNImageExporter exporterWithDocument:document]; | |
return [exporter imageRepresentationFromRect:drawRect]; | |
} | |
+ (UIImage *)previewImageForDocumenAtURL:(NSURL *)url | |
{ | |
NSURL *imageURL = [[url URLByAppendingPathComponent:MNDocumentQuickLookFolderName isDirectory:YES] URLByAppendingPathComponent:MNDocumentQuickLookPreviewFileName isDirectory:NO]; | |
UIImage *image = [UIImage mn_thumbnailImageAtURL:imageURL withMaxSize:230]; | |
return image; | |
} | |
#pragma mark - iCloud | |
- (void)startDownloading | |
{ | |
NSFileManager *fm = [[NSFileManager alloc] init]; | |
NSError *error = nil; | |
if (![fm startDownloadingUbiquitousItemAtURL:self.fileURL error:&error]) { | |
NSLog(@"%@",error); | |
} | |
self.startedDownload = YES; | |
} | |
- (void)updateWithMetadataItem:(NSMetadataItem *)metadataItem; | |
{ | |
BOOL didUpdate = NO; | |
if (!self.isUbiquitous) { | |
self.isUbiquitous = YES; | |
didUpdate = YES; | |
} | |
NSDate *date = [metadataItem valueForAttribute:NSMetadataItemFSContentChangeDateKey]; | |
if ((date && ![date isEqualToDate:self.modificationDate])) { | |
[self _refreshModificationDate:date]; | |
didUpdate = YES; | |
} | |
NSNumber *metadataValue = [metadataItem valueForAttribute:NSMetadataUbiquitousItemHasUnresolvedConflictsKey]; | |
BOOL value = [metadataValue boolValue]; | |
if (metadataValue && (value!=self.hasUnresolvedConflicts)) { | |
self.hasUnresolvedConflicts = value; | |
didUpdate = YES; | |
} | |
metadataValue = [metadataItem valueForAttribute:NSMetadataUbiquitousItemIsDownloadedKey]; | |
value = [metadataValue boolValue]; | |
if (metadataValue && (value!=self.isDownloaded)) { | |
self.isDownloaded = value; | |
if (!value) self.percentDownloaded = 0; | |
didUpdate = YES; | |
} | |
metadataValue = [metadataItem valueForAttribute:NSMetadataUbiquitousItemIsDownloadingKey]; | |
value = [metadataValue boolValue]; | |
if (metadataValue && (value!=self.isDownloading)) { | |
self.isDownloading = value; | |
if (value) self.percentDownloaded = 0; | |
didUpdate = YES; | |
} | |
if (!self.isDownloaded && !self.isDownloading && !self.startedDownload) { | |
[self startDownloading]; | |
} | |
metadataValue = [metadataItem valueForAttribute:NSMetadataUbiquitousItemIsUploadedKey]; | |
value = [metadataValue boolValue]; | |
if (metadataValue && (value!=self.isUploaded)) { | |
self.isUploaded = value; | |
if (!value) self.percentUploaded = 0; | |
didUpdate = YES; | |
} | |
metadataValue = [metadataItem valueForAttribute:NSMetadataUbiquitousItemIsUploadingKey]; | |
value = [metadataValue boolValue]; | |
if (metadataValue && (value!=self.isUploading)) { | |
self.isUploading = value; | |
if (value) self.percentUploaded = 0; | |
didUpdate = YES; | |
} | |
metadataValue = [metadataItem valueForAttribute:NSMetadataUbiquitousItemPercentDownloadedKey]; | |
double doubleValue = [metadataValue doubleValue]; | |
if (metadataValue && (doubleValue!=self.percentDownloaded)) { | |
self.percentDownloaded = doubleValue; | |
didUpdate = YES; | |
} | |
metadataValue = [metadataItem valueForAttribute:NSMetadataUbiquitousItemPercentUploadedKey]; | |
doubleValue = [metadataValue doubleValue]; | |
if (metadataValue && (doubleValue!=self.percentUploaded)) { | |
self.percentUploaded = doubleValue; | |
didUpdate = YES; | |
} | |
if (didUpdate) { | |
[self willChangeValueForKey:MNDocumentReferenceStatusUpdatedKey]; | |
[self didChangeValueForKey:MNDocumentReferenceStatusUpdatedKey]; | |
} | |
} | |
#pragma mark - NSFilePresenter Protocol | |
- (NSURL *)presentedItemURL; | |
{ | |
return self.fileURL; | |
} | |
- (NSOperationQueue *)presentedItemOperationQueue; | |
{ | |
return self.fileItemOperationQueue; | |
} | |
- (void)presentedItemDidMoveToURL:(NSURL *)newURL | |
{ | |
self.fileURL = newURL; | |
// dispatch on main queue to make sure KVO notifications get send on main | |
dispatch_async(dispatch_get_main_queue(), ^{ | |
self.displayName = [[newURL lastPathComponent] stringByDeletingPathExtension]; | |
}); | |
} | |
- (void)presentedItemDidChange; | |
{ | |
// this call can happen on any thread, make sure we coordinate the read | |
NSFileCoordinator *fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:self]; | |
[fileCoordinator coordinateReadingItemAtURL:self.fileURL options:NSFileCoordinatorReadingWithoutChanges error:NULL byAccessor:^(NSURL *newURL) { | |
NSFileManager *fileManager = [[NSFileManager alloc] init]; | |
NSDate *modificationDate = nil; | |
NSDictionary *attributes = [fileManager attributesOfItemAtPath:[newURL path] error:NULL]; | |
if (attributes) { | |
modificationDate = [attributes fileModificationDate]; | |
} | |
if (modificationDate && ![modificationDate isEqualToDate:self.modificationDate]) { | |
// dispatch on main queue to make sure KVO notifications get send on main | |
dispatch_async(dispatch_get_main_queue(), ^{ | |
[self _refreshModificationDate:modificationDate]; | |
}); | |
} | |
}]; | |
if (self.preview) { | |
[self _reloadPreviewImageWithCallbackBlock:NULL]; | |
} | |
} | |
- (void)presentedItemDidGainVersion:(NSFileVersion *)version; | |
{ | |
} | |
- (void)presentedItemDidLoseVersion:(NSFileVersion *)version; | |
{ | |
} | |
- (void)presentedItemDidResolveConflictVersion:(NSFileVersion *)version; | |
{ | |
} | |
@end |
btw this is ARC code…
Updated to my current code and added my Conflict Resolution controller.
I released the current version of MNDocumentController here: https://github.com/IdeasOnCanvas/MNDocumentController
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is the current version of the MindNode touch document controller code. As I keep running into iCloud issues, I'm posting this to gather feedback from other developer. Parts of this code are influenced by https://github.com/omnigroup/OmniGroup
How would you change this design?
Where are you seeing possible issues like deadlocks?
Thank you
Markus