Skip to content

Instantly share code, notes, and snippets.

Revisions

  1. Markus Müller created this gist Dec 6, 2011.
    46 changes: 46 additions & 0 deletions MNDocumentController.h
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,46 @@
    //
    // 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;
    - (void)updateDocuments;
    - (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;


    #pragma mark - Paths

    + (NSString *)localDocumentsPath;
    + (NSURL *)localDocumentsURL;
    + (NSURL *)ubiquitousContainerURL;
    + (NSURL *)ubiquitousDocumentsURL;
    - (NSString *)uniqueFileNameForDisplayName:(NSString *)displayName;
    + (NSString *)uniqueFileNameForDisplayName:(NSString *)displayName extension:(NSString *)extension usedFileNames:(NSSet *)usedFileNames;


    @end
    670 changes: 670 additions & 0 deletions MNDocumentController.m
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,670 @@
    //
    // 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";

    @interface MNDocumentController ()

    #pragma mark - Documents

    @property (readwrite, strong) NSMutableArray *documentReferences;
    @property (readwrite,strong) NSOperationQueue *fileAccessWorkingQueue;
    @property (readwrite) BOOL didInitialDirectoryScan;
    - (void)_scanDirectory:(NSURL *)directory existingDocuments:(NSMutableDictionary *)existingDocuments completionHandler:(void(^)(NSSet *foundDocuments))completionHandler;

    #pragma mark - Paths

    - (NSString *)uniqueFileName;

    #pragma mark - KVO Compliance

    - (void)addDocumentReferencesObject:(MNDocumentReference *)reference;
    - (void)removeDocumentReferencesObject:(MNDocumentReference *)reference;
    - (void)addDocumentReferences:(NSSet *)set;
    - (void)removeDocumentReferences:(NSSet *)set;

    #pragma mark - iCloud

    @property (strong) NSMetadataQuery *iCloudMetadataQuery;
    - (void)_startMetadataQuery;
    - (void)_stopMetadataQuery;
    - (BOOL)_moveDocumentToiCloud:(MNDocumentReference *)documentReference;
    - (BOOL)_moveDocumentToLocal:(MNDocumentReference *)documentReference;


    @end




    @implementation MNDocumentController

    @synthesize documentReferences = _documentReferences;
    @synthesize iCloudMetadataQuery = _iCloudMetadataQuery;
    @synthesize didInitialDirectoryScan=_didInitialDirectoryScan;
    @synthesize fileAccessWorkingQueue = _fileAccessWorkingQueue;

    #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];

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue setName:@"MNDocumentController Working Queue"];
    [queue setMaxConcurrentOperationCount:1];
    self.fileAccessWorkingQueue = queue;

    self.documentReferences = [NSMutableSet setWithCapacity:10];
    self.didInitialDirectoryScan = NO;
    [self updateDocuments];
    [self _startMetadataQuery];

    return self;
    }

    - (void)dealloc
    {
    [[NSNotificationCenter defaultCenter] removeObserver:self];

    for (MNDocumentReference *currentReference in _documentReferences) {
    [currentReference invalidateReference]; // we need to do this to make sure FilePresenter get unregistered
    }
    }


    #pragma mark - Documents

    - (BOOL)documentsInCloud
    {
    return [[NSUserDefaults standardUserDefaults] boolForKey:MNDefaultsDocumentsInCloud];
    }

    - (void)updateDocuments
    {
    NSURL *documentDirectory = nil;
    if (self.documentsInCloud) {
    // on launch we scan the ubiquitous folder so we don't launch with an empty grid view
    if (!self.didInitialDirectoryScan) {
    documentDirectory = [[self class] ubiquitousDocumentsURL];
    if (!documentDirectory) {
    // we weren't able to locate the ubiquitous folder, scan the local folder instead
    documentDirectory = [[self class] localDocumentsURL];
    }
    }
    } else {
    documentDirectory = [[self class] localDocumentsURL];
    }

    if (!documentDirectory) return;

    NSMutableDictionary *existingDocuments = [[NSMutableDictionary alloc] initWithCapacity:[self.documentReferences count]];
    for (MNDocumentReference *currentReference in self.documentReferences) {
    NSString *key = [currentReference.fileURL absoluteString];
    if (!key) continue;
    [existingDocuments setObject:currentReference forKey:key];
    }


    [self _scanDirectory:documentDirectory existingDocuments:existingDocuments completionHandler: ^(NSSet *foundDocuments) {
    if (!foundDocuments) return;
    self.didInitialDirectoryScan = YES;
    if (self.documentsInCloud && !self.iCloudMetadataQuery.isGathering) {
    // our metadata query already finished, no need to update with the folder content
    return;
    }

    // added documents, use manual KVO so we only send one notification
    [self willChangeValueForKey:MNDocumentControllerDocumentReferencesKey];
    [self.documentReferences intersectSet:foundDocuments];
    [self.documentReferences unionSet:foundDocuments];
    [self didChangeValueForKey:MNDocumentControllerDocumentReferencesKey];
    }];
    }


    - (void)_scanDirectory:(NSURL *)directory existingDocuments:(NSMutableDictionary *)existingDocuments completionHandler:(void(^)(NSSet *foundDocuments))completionHandler
    {
    [self.fileAccessWorkingQueue addOperationWithBlock:^{

    NSMutableSet *foundDocuments = [NSMutableSet set];

    // create file coordinator to request folder read access
    NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
    NSError *readError = nil;

    [coordinator coordinateReadingItemAtURL:directory 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]) {

    MNDocumentReference *documentReference = nil;
    documentReference = [existingDocuments objectForKey:[currentFileURL absoluteString]];
    if (documentReference) {
    [foundDocuments addObject:documentReference];
    continue;
    }

    // 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];
    [foundDocuments addObject:reference];

    continue;
    } else {
    // we only scan for MindNode files at the moment
    }
    }
    }];

    dispatch_async(dispatch_get_main_queue(), ^(){
    if (readError) {
    completionHandler(nil);
    }
    completionHandler(foundDocuments);
    });
    }];
    }


    - (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 = [currentReference.fileURL absoluteString];
    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:[fileURL absoluteString]];
    if (documentReference) {
    [resultDocuments addObject:documentReference];
    } else {
    NSDate *modificationDate = [metadataItem valueForAttribute:NSMetadataItemFSContentChangeDateKey];
    if (!modificationDate) {
    modificationDate = [NSDate date];
    }

    documentReference = [[MNDocumentReference alloc] initWithFileURL:fileURL modificationDate:modificationDate];
    [resultDocuments addObject:documentReference];
    }
    [documentReference updateWithMetadataItem:metadataItem];
    }

    [metadataQuery enableUpdates];

    // added documents, use manual KVO so we only send one notification
    [self willChangeValueForKey:MNDocumentControllerDocumentReferencesKey];
    [self.documentReferences intersectSet:resultDocuments];
    [self.documentReferences unionSet:resultDocuments];
    [self didChangeValueForKey:MNDocumentControllerDocumentReferencesKey];
    }


    - (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 _moveDocumentToiCloud: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;
    }

    [document invalidateReference];
    [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 absoluteString]]) {
    return;
    }
    [fileManager copyItemAtURL:sourceURL toURL:destinationURL error:&copyError];
    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 _moveDocumentToiCloud: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:sourceURL toURL:destinationURL 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);
    });
    }];
    }


    #pragma mark - Paths


    + (NSString *)localDocumentsPath
    {
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    return documentsDirectory;
    }

    + (NSURL *)localDocumentsURL
    {
    NSString *documentsDirectory = [self localDocumentsPath];
    return [NSURL fileURLWithPath:documentsDirectory];

    }

    + (NSURL *)ubiquitousContainerURL
    {
    return [[NSFileManager defaultManager] 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) {
    [useFileNames addObject:[currentReference.fileURL lastPathComponent]];
    }

    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++;
    }

    if ([usedFileNames member:candidateName] == nil) {
    return candidateName;
    }
    }
    }


    #pragma mark -
    #pragma mark Document Persistance

    - (void)applicationWillTerminate:(NSNotification *)notification
    {
    [self _stopMetadataQuery];
    }

    - (void)applicationDidEnterBackground:(NSNotification *)notification
    {
    [self _stopMetadataQuery];
    }

    - (void)applicationWillEnterForeground:(NSNotification *)notification
    {
    [self updateDocuments];
    [self _startMetadataQuery];
    }


    #pragma mark - KVO Compliance


    - (void)addDocumentReferencesObject:(MNDocumentReference *)reference
    {
    [_documentReferences addObject:reference];
    }

    - (void)removeDocumentReferencesObject:(MNDocumentReference *)reference
    {
    [_documentReferences removeObject:reference];
    }

    - (void)addDocumentReferences:(NSSet *)set
    {
    [_documentReferences unionSet:set];
    }

    - (void)removeDocumentReferences:(NSSet *)set
    {
    [_documentReferences minusSet:set];
    }


    #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];

    [query startQuery];
    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];
    }

    - (void)metadataQueryDidUpdateNotifiction:(NSNotification *)n;
    {
    [self updateFromMetadataQuery:self.iCloudMetadataQuery];
    }


    // This method blocks, make sure to call it on a queue
    - (BOOL)_moveDocumentToiCloud:(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];
    NSError *error = nil;
    BOOL success = [fileManager setUbiquitous:YES itemAtURL:sourceURL destinationURL:destinationURL error:&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];

    return success;
    }

    @end
    59 changes: 59 additions & 0 deletions MNDocumentReference.h
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,59 @@
    //
    // 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)invalidateReference; // MUST BE CALLED, OTHERWISE WE WON'T DEALLOC!!!!

    #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 hasUnresolvedConflictsKey;
    @property (readonly) BOOL isDownloadedKey;
    @property (readonly) BOOL isDownloadingKey;
    @property (readonly) BOOL isUploadedKey;
    @property (readonly) BOOL isUploadingKey;
    @property (readonly) double percentDownloadedKey;
    @property (readonly) double percentUploadedKey;


    #pragma mark - Document Representation

    - (MNDocument *)document;

    #pragma mark - iCloud Support

    - (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;

    @end
    320 changes: 320 additions & 0 deletions MNDocumentReference.m
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,320 @@
    //
    // 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 "ZipArchive.h"

    #import "MNFormatter.h"

    #import "NSString+UUID.h"
    #import "UIImage+RoundColorDot.h"
    #import "NSArray+Convenience.h"
    #import "UIAlertView+Error.h"
    #import "MNError.h"
    #import "UIImage+Size.h"

    // Attributes Keys
    NSString *MNDocumentReferenceDisplayNameKey = @"displayName";
    NSString *MNDocumentReferenceModificationDateKey = @"modificationDate";
    NSString *MNDocumentReferencePreviewKey = @"preview";
    NSString *MNDocumentReferenceStatusUpdatedKey = @"statusUpdate";


    @interface MNDocumentReference ()


    @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;

    - (void) _refreshModificationDate:(NSDate*)date;

    // iCloud
    @property (readwrite) BOOL hasUnresolvedConflictsKey;
    @property (readwrite) BOOL isDownloadedKey;
    @property (readwrite) BOOL isDownloadingKey;
    @property (readwrite) BOOL isUploadedKey;
    @property (readwrite) BOOL isUploadingKey;
    @property (readwrite) BOOL percentDownloadedKey;
    @property (readwrite) BOOL percentUploadedKey;


    @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 hasUnresolvedConflictsKey=_hasUnresolvedConflictsKey;
    @synthesize isDownloadedKey=_isDownloadedKey;
    @synthesize isDownloadingKey=_isDownloadingKey;
    @synthesize isUploadedKey=_isUploadedKey;
    @synthesize isUploadingKey=_isUploadingKey;
    @synthesize percentDownloadedKey=_percentDownloadedKey;
    @synthesize percentUploadedKey=_percentUploadedKey;




    #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){
    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];
    [NSFileCoordinator addFilePresenter:self];

    // iCloud
    self.hasUnresolvedConflictsKey = NO;
    self.isDownloadedKey = YES;
    self.isDownloadingKey = NO;
    self.isUploadedKey = YES;
    self.isUploadingKey = NO;
    self.percentDownloadedKey = 100;
    self.percentUploadedKey = 100;

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];

    return self;
    }

    - (void)invalidateReference
    {
    [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.isDownloadedKey || self.hasUnresolvedConflictsKey) 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) {
    callbackBlock(self.preview);
    }

    [[MNDocumentController sharedDocumentController] performAsynchronousFileAccessUsingBlock:^(){
    NSURL *imageURL = [[self.fileURL URLByAppendingPathComponent:MNDocumentQuickLookFolderName isDirectory:YES] URLByAppendingPathComponent:MNDocumentQuickLookPreviewFileName isDirectory:NO];
    NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
    NSError *readError = nil;
    __block UIImage *image;
    [coordinator coordinateReadingItemAtURL:imageURL options:NSFileCoordinatorReadingWithoutChanges error:&readError byAccessor: ^(NSURL *readURL){
    image = [UIImage mn_thumbnailImageAtURL:imageURL withMaxSize:230];
    }];

    dispatch_async(dispatch_get_main_queue(), ^{
    self.preview = image;
    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];
    }


    #pragma mark - File System IO

    - (void)updateWithMetadataItem:(NSMetadataItem *)metadataItem;
    {
    NSDate *date = [metadataItem valueForAttribute:NSMetadataItemFSContentChangeDateKey];
    if (!date) {
    date = [NSDate date];
    }
    [self _refreshModificationDate:date];

    NSNumber *metadataValue = [metadataItem valueForAttribute:NSMetadataUbiquitousItemHasUnresolvedConflictsKey];
    self.hasUnresolvedConflictsKey = metadataValue ? [metadataValue boolValue] : NO;

    metadataValue = [metadataItem valueForAttribute:NSMetadataUbiquitousItemIsDownloadedKey];
    self.isDownloadedKey = metadataValue ? [metadataValue boolValue] : YES;

    metadataValue = [metadataItem valueForAttribute:NSMetadataUbiquitousItemIsDownloadingKey];
    self.isDownloadingKey = metadataValue ? [metadataValue boolValue] : NO;
    if (!self.isDownloadedKey && !self.isDownloadingKey) {
    NSFileManager *fm = [[NSFileManager alloc] init];
    if (![fm startDownloadingUbiquitousItemAtURL:self.fileURL error:NULL]) {
    NSLog(@"Unable to start download");
    } else {
    NSLog(@"Started download");
    }
    }

    metadataValue = [metadataItem valueForAttribute:NSMetadataUbiquitousItemIsUploadedKey];
    self.isUploadedKey = metadataValue ? [metadataValue boolValue] : YES;

    metadataValue = [metadataItem valueForAttribute:NSMetadataUbiquitousItemIsUploadingKey];
    self.isUploadingKey = metadataValue ? [metadataValue boolValue] : NO;

    metadataValue = [metadataItem valueForAttribute:NSMetadataUbiquitousItemPercentDownloadedKey];
    self.percentDownloadedKey = metadataValue ? [metadataValue doubleValue] : 100;

    metadataValue = [metadataItem valueForAttribute:NSMetadataUbiquitousItemPercentUploadedKey];
    self.percentUploadedKey = metadataValue ? [metadataValue doubleValue] : 100;

    [self willChangeValueForKey:MNDocumentReferenceStatusUpdatedKey];
    [self didChangeValueForKey:MNDocumentReferenceStatusUpdatedKey];
    }


    #pragma mark - NSFilePresenter Protocol

    - (NSURL *)presentedItemURL;
    {
    return self.fileURL;
    }

    - (NSOperationQueue *)presentedItemOperationQueue;
    {
    return self.fileItemOperationQueue;
    }

    - (void)presentedItemDidMoveToURL:(NSURL *)newURL
    {
    // dispatch on main queue to make sure KVO notifications get send on main
    dispatch_async(dispatch_get_main_queue(), ^{
    self.fileURL = newURL;
    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];
    self.preview = nil;
    });
    }
    }];
    }

    - (void)presentedItemDidGainVersion:(NSFileVersion *)version;
    {
    }

    - (void)presentedItemDidLoseVersion:(NSFileVersion *)version;
    {
    }

    - (void)presentedItemDidResolveConflictVersion:(NSFileVersion *)version;
    {
    }

    @end