Forked from aquarius/MNDocumentConflictResolutionViewController.h
Created
December 6, 2011 15:56
-
-
Save steipete/1438679 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
// | |
// 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 |
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"; | |
@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:©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 _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 |
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)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 |
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 "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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment