Skip to content

Instantly share code, notes, and snippets.

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

aquarius commented Dec 6, 2011

This is the current version of the MindNode touch document controller code. As I keep running into iCloud issues, I'm posting this to gather feedback from other developer. Parts of this code are influenced by https://github.com/omnigroup/OmniGroup

How would you change this design?
Where are you seeing possible issues like deadlocks?

Thank you
Markus

@aquarius
Copy link
Author

aquarius commented Dec 6, 2011

btw this is ARC code…

@aquarius
Copy link
Author

aquarius commented Mar 6, 2012

Updated to my current code and added my Conflict Resolution controller.

@aquarius
Copy link
Author

I released the current version of MNDocumentController here: https://github.com/IdeasOnCanvas/MNDocumentController

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment