Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save steipete/1438679 to your computer and use it in GitHub Desktop.
Save steipete/1438679 to your computer and use it in GitHub Desktop.
iCloud Document Controller
//
// 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
//
// 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
//
// 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
//
// 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