├── .gitignore ├── MNDocumentController ├── MNDocumentController+Convenience.h ├── MNDocumentController+Convenience.m ├── MNDocumentReference.h ├── MNDocumentController.h ├── MNDocumentReference.m └── MNDocumentController.m ├── README.md └── LICENSE.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | .DS_Store 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | *.xcworkspace 13 | !default.xcworkspace 14 | xcuserdata 15 | profile 16 | *.moved-aside 17 | DerivedData 18 | .idea/ 19 | -------------------------------------------------------------------------------- /MNDocumentController/MNDocumentController+Convenience.h: -------------------------------------------------------------------------------- 1 | // 2 | // MNDocumentController+Convenience.h 3 | // MindNodeTouch 4 | // 5 | // Created by Markus Müller on 03.04.12. 6 | // Copyright (c) 2012 IdeasOnCanvas GmbH. All rights reserved. 7 | // 8 | // A collection of methodes that sit ontop of MNDocumentController and only use it's public interface 9 | 10 | #import "MNDocumentController.h" 11 | 12 | @interface MNDocumentController (Convenience) 13 | 14 | - (MNDocumentReference *)documentReferenceFromFileURL:(NSURL *)fileURL; 15 | - (MNDocumentReference *)documentReferenceFromFilename:(NSString *)filename; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MNDocumentController 2 | 3 | MNDocumentController is a controller that manages on disk representations of UIDocuments. It supports either local or iCloud documents and performs all operations coordinated and asynchronous. 4 | 5 | MNDocumentController is not a drop in replacement for your current solution and adapting it for your project will require a considerable amount of work. As several helper classes are missing it won't even compile on your system! 6 | 7 | I'm releasing this code to help other who are struggling with adding iCloud to their own iOS apps. iCloud support in MNDocumentController is not perfect (for example folders are missing), but it's shipping code that is working reasonable well in my own app [MindNode](http://www.mindnode.com/). 8 | 9 | # License 10 | This code is licensed under the MT License. See LICENSE.md for more information. -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013 Markus Mueller 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /MNDocumentController/MNDocumentController+Convenience.m: -------------------------------------------------------------------------------- 1 | // 2 | // MNDocumentController+Convenience.m 3 | // MindNodeTouch 4 | // 5 | // Created by Markus Müller on 03.04.12. 6 | // Copyright (c) 2012 IdeasOnCanvas GmbH. All rights reserved. 7 | // 8 | 9 | #import "MNDocumentController+Convenience.h" 10 | #import "MNDocumentReference.h" 11 | #import "MNDocument.h" 12 | 13 | @implementation MNDocumentController (Convenience) 14 | 15 | 16 | - (MNDocumentReference *)documentReferenceFromFileURL:(NSURL *)fileURL 17 | { 18 | if (!fileURL) return nil; 19 | id fileURLID; 20 | [fileURL getResourceValue:&fileURLID forKey:NSURLFileResourceIdentifierKey error:NULL]; 21 | 22 | for (MNDocumentReference *currentReference in [MNDocumentController sharedDocumentController].documentReferences) { 23 | NSURL *currentURL = currentReference.fileURL; 24 | id currentFileURLID; 25 | [currentURL getResourceValue:¤tFileURLID forKey:NSURLFileResourceIdentifierKey error:NULL]; 26 | 27 | if ([currentFileURLID isEqual:fileURLID]) { 28 | return currentReference; 29 | } 30 | 31 | if ([currentURL isEqual:fileURL]) { 32 | return currentReference; 33 | } 34 | } 35 | return nil; 36 | } 37 | 38 | 39 | - (MNDocumentReference *)documentReferenceFromFilename:(NSString *)filename 40 | { 41 | if (!filename) return nil; 42 | if (![filename hasSuffix:MNDocumentMindNodeExtension]) { 43 | filename = [filename stringByAppendingPathExtension:MNDocumentMindNodeExtension]; 44 | } 45 | 46 | for (MNDocumentReference *currentReference in [MNDocumentController sharedDocumentController].documentReferences) { 47 | NSURL *currentURL = currentReference.fileURL; 48 | 49 | if ([[[currentURL lastPathComponent] lowercaseString] isEqualToString:filename]) { 50 | return currentReference; 51 | } 52 | } 53 | return nil; 54 | } 55 | 56 | 57 | @end 58 | -------------------------------------------------------------------------------- /MNDocumentController/MNDocumentReference.h: -------------------------------------------------------------------------------- 1 | // 2 | // MNDocumentReference.h 3 | // MindNodeTouch 4 | // 5 | // Created by Markus Müller on 23.09.10. 6 | // Copyright 2010 IdeasOnCanvas GmbH. All rights reserved. 7 | // 8 | 9 | #import 10 | @class MNDocument; 11 | 12 | // attributes 13 | extern NSString *MNDocumentReferenceDisplayNameKey; 14 | extern NSString *MNDocumentReferenceModificationDateKey; 15 | extern NSString *MNDocumentReferencePreviewKey; 16 | 17 | extern NSString *MNDocumentReferenceStatusUpdatedKey; // virtual 18 | 19 | extern CGFloat MNDocumentReferencePreviewWidthPhone; 20 | extern CGFloat MNDocumentReferencePreviewWidthPad; 21 | 22 | 23 | @interface MNDocumentReference : NSObject 24 | 25 | #pragma mark - Init 26 | 27 | + (void)createNewDocumentWithFileURL:(NSURL *)fileURL completionHandler:(void (^)(MNDocument *document, MNDocumentReference *reference))completionHandler; 28 | - (id)initWithFileURL:(NSURL *)fileURL modificationDate:(NSDate *)modificationDate; 29 | - (void)enableFilePresenter; 30 | - (void)disableFilePresenter; 31 | - (void)loadDocumentWithCompletionHandler:(void (^)(MNDocument *document))completionHandler; 32 | 33 | #pragma mark - Properties 34 | 35 | @property (readonly,strong) NSString *displayName; 36 | @property (readonly,strong) NSString *displayModificationDate; 37 | @property (readonly,strong) NSURL *fileURL; 38 | @property (readonly,strong) NSDate *modificationDate; 39 | 40 | // iCloud state 41 | @property (readonly) BOOL isUbiquitous; 42 | @property (readonly) BOOL hasUnresolvedConflicts; 43 | @property (readonly) BOOL isDownloaded; 44 | @property (readonly) BOOL isDownloading; 45 | @property (readonly) BOOL isUploaded; 46 | @property (readonly) BOOL isUploading; 47 | @property (readonly) CGFloat percentDownloaded; 48 | @property (readonly) CGFloat percentUploaded; 49 | 50 | 51 | #pragma mark - iCloud Support 52 | 53 | - (void)startDownloading; 54 | - (void)updateWithMetadataItem:(NSMetadataItem *)metaDataItem; 55 | - (void)updateMetadataFromURL; 56 | 57 | 58 | #pragma mark - Preview Image 59 | 60 | @property (atomic,readonly,strong) UIImage *preview; 61 | - (void)previewImageWithCallbackBlock:(void(^)(UIImage *image))callbackBlock; 62 | - (void)previewImageWithWidth:(CGFloat)width withCallbackBlock:(void(^)(UIImage *image))callbackBlock; 63 | + (UIImage *)animationImageForDocument:(MNDocument *)document withSize:(CGSize)size; 64 | + (UIImage *)previewImageForDocumenAtURL:(NSURL *)url withMaxSize:(CGFloat)size; 65 | 66 | @end 67 | -------------------------------------------------------------------------------- /MNDocumentController/MNDocumentController.h: -------------------------------------------------------------------------------- 1 | // 2 | // MNDocumentController.h 3 | // MindNodeTouch 4 | // 5 | // Created by Markus Müller on 22.12.08. 6 | // Copyright 2008 IdeasOnCanvas GmbH. All rights reserved. 7 | // 8 | 9 | #import 10 | @class MNDocumentReference; 11 | @class MNDocument; 12 | 13 | extern NSString *MNDocumentControllerDocumentReferencesKey; 14 | extern NSString *MNDocumentControllerDidChangeStateNotification; 15 | 16 | 17 | 18 | enum { 19 | MNDocumentControllerStateNormal = 0, 20 | MNDocumentControllerStateLoading = 1 << 0 21 | }; 22 | typedef NSInteger MNDocumentControllerState; 23 | 24 | 25 | 26 | @interface MNDocumentController : NSObject 27 | 28 | + (MNDocumentController *)sharedDocumentController; 29 | 30 | 31 | #pragma mark - Documents 32 | 33 | @property(readonly) MNDocumentControllerState controllerState; 34 | @property (readonly) BOOL documentsInCloud; 35 | @property (readonly,strong) NSMutableSet *documentReferences; 36 | - (void)reloadLocalDocuments; 37 | 38 | 39 | #pragma mark - Document Manipulation 40 | 41 | - (void)createNewDocumentWithCompletionHandler:(void (^)(MNDocument *document, MNDocumentReference *reference))completionHandler; 42 | - (void)deleteDocument:(MNDocumentReference *)document completionHandler:(void (^)(NSError *errorOrNil))completionHandler; 43 | - (void)duplicateDocument:(MNDocumentReference *)document completionHandler:(void (^)(NSError *errorOrNil))completionHandler; 44 | - (void)renameDocument:(MNDocumentReference *)document toFileName:(NSString *)fileName completionHandler:(void (^)(NSError *errorOrNil))completionHandler; 45 | - (void)performAsynchronousFileAccessUsingBlock:(void (^)(void))block; 46 | - (BOOL)pendingDocumentTransfers; 47 | - (void)disableiCloudAndCopyAllCloudDocumentsToLocalWithCompletionHandler:(void (^)(NSError *errorOrNil))completionHandler; 48 | - (void)moveAllCloudDocumentsToLocalWithCompletionHandler:(void (^)(NSError *errorOrNil))completionHandler; 49 | - (void)moveAllLocalDocumentsToCloudWithCompletionHandler:(void (^)(void))completionHandler; 50 | - (void)importDocumentAtURL:(NSURL *)url completionHandler:(void (^)(MNDocumentReference *reference, NSError *errorOrNil))completionHandler; 51 | - (void)evictAllCloudDocumentsWithCompletionHandler:(void (^)(void))completionHandler progressUpdateHandler:(void (^)(CGFloat progress))progressUpdateHandler; 52 | 53 | #pragma mark - Paths 54 | 55 | + (NSURL *)localDocumentsURL; 56 | + (NSURL *)ubiquitousContainerURL; 57 | + (NSURL *)ubiquitousDocumentsURL; 58 | - (NSString *)uniqueFileNameForDisplayName:(NSString *)displayName; 59 | + (NSString *)uniqueFileNameForDisplayName:(NSString *)displayName extension:(NSString *)extension usedFileNames:(NSSet *)usedFileNames; 60 | + (NSString *)uniqueFileNameForDisplayName:(NSString *)displayName extension:(NSString *)extension inDirectory:(NSURL *)directionaryURL; 61 | 62 | 63 | @end 64 | -------------------------------------------------------------------------------- /MNDocumentController/MNDocumentReference.m: -------------------------------------------------------------------------------- 1 | // 2 | // MNDocumentReference.m 3 | // MindNodeTouch 4 | // 5 | // Created by Markus Müller on 23.09.10. 6 | // Copyright 2010 IdeasOnCanvas GmbH. All rights reserved. 7 | // 8 | 9 | #import "MNDocumentReference.h" 10 | #import "MNDocumentController.h" 11 | #import "MNDocument.h" 12 | #import "MNMindMap.h" 13 | #import "MNMindMapMetadata.h" 14 | 15 | #import "MNImageExporter.h" 16 | 17 | #import "MNDateFormatter.h" 18 | 19 | #import "NSString+UUID.h" 20 | #import "MNError.h" 21 | #import "UIImage+Size.h" 22 | 23 | // Attributes Keys 24 | NSString *MNDocumentReferenceDisplayNameKey = @"displayName"; 25 | NSString *MNDocumentReferenceModificationDateKey = @"modificationDate"; 26 | NSString *MNDocumentReferencePreviewKey = @"preview"; 27 | NSString *MNDocumentReferenceStatusUpdatedKey = @"statusUpdate"; 28 | 29 | CGFloat MNDocumentReferencePreviewWidthPhone = 120; 30 | CGFloat MNDocumentReferencePreviewWidthPad = 210; 31 | 32 | @interface MNDocumentReference () 33 | 34 | // attributes 35 | @property (readwrite,strong) NSString *displayName; 36 | @property (readwrite,strong) NSString *displayModificationDate; 37 | @property (readwrite,strong) NSURL *fileURL; 38 | @property (readwrite,strong) NSDate *modificationDate; 39 | @property (atomic,readwrite,strong) UIImage* preview; 40 | 41 | @property (readwrite,strong) NSOperationQueue *fileItemOperationQueue; 42 | 43 | // iCloud 44 | @property (readwrite) BOOL isUbiquitous; 45 | @property (readwrite) BOOL hasUnresolvedConflicts; 46 | @property (readwrite) BOOL isDownloaded; 47 | @property (readwrite) BOOL isDownloading; 48 | @property (readwrite) BOOL isUploaded; 49 | @property (readwrite) BOOL isUploading; 50 | @property (readwrite) CGFloat percentDownloaded; 51 | @property (readwrite) CGFloat percentUploaded; 52 | @property (readwrite) BOOL startedDownload; 53 | 54 | 55 | @end 56 | 57 | 58 | @implementation MNDocumentReference 59 | 60 | 61 | 62 | #pragma mark - Init 63 | 64 | + (void)createNewDocumentWithFileURL:(NSURL *)fileURL completionHandler:(void (^)(MNDocument *document, MNDocumentReference *reference))completionHandler 65 | { 66 | MNDocumentReference *reference = [[[self class] alloc] initWithFileURL:fileURL modificationDate:[NSDate date]]; 67 | if (!reference) { 68 | completionHandler(nil,nil); 69 | return; 70 | } 71 | 72 | // create and initialize an empty document 73 | MNDocument *document = [[MNDocument alloc] initNewDocumentWithFileURL:fileURL]; 74 | 75 | [document updateChangeCount: UIDocumentChangeDone]; 76 | 77 | [document saveToURL:fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success){ 78 | completionHandler(document,reference); 79 | }]; 80 | } 81 | 82 | // we don't have a init methode without modification date because we don't want to do a coordinating read in an initilizer 83 | - (id)initWithFileURL:(NSURL *)fileURL modificationDate:(NSDate *)modificationDate 84 | { 85 | self = [super init]; 86 | if (self == nil) return self; 87 | 88 | self.fileURL = fileURL; 89 | self.displayName = [[fileURL lastPathComponent] stringByDeletingPathExtension]; 90 | 91 | [self _refreshModificationDate:modificationDate]; 92 | 93 | self.fileItemOperationQueue = [[NSOperationQueue alloc] init]; 94 | self.fileItemOperationQueue.name = @"MNDocumentReference"; 95 | [self.fileItemOperationQueue setMaxConcurrentOperationCount:1]; 96 | 97 | // iCloud 98 | NSNumber* numberValue; 99 | if ([fileURL getResourceValue:&numberValue forKey:NSURLIsUbiquitousItemKey error:nil]) { 100 | self.isUbiquitous = [numberValue boolValue]; 101 | } 102 | 103 | self.hasUnresolvedConflicts = NO; 104 | self.isDownloaded = YES; 105 | self.isDownloading = NO; 106 | self.isUploaded = YES; 107 | self.isUploading = NO; 108 | self.percentDownloaded = 0; 109 | self.percentUploaded = 100; 110 | self.startedDownload = NO; 111 | 112 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; 113 | 114 | return self; 115 | } 116 | 117 | - (void)enableFilePresenter 118 | { 119 | [NSFileCoordinator addFilePresenter:self]; 120 | } 121 | 122 | 123 | - (void)disableFilePresenter 124 | { 125 | [NSFileCoordinator removeFilePresenter:self]; 126 | } 127 | 128 | - (void) dealloc 129 | { 130 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 131 | } 132 | 133 | - (NSString *)description; 134 | { 135 | return [NSString stringWithFormat:@"Name: '%@' Date: '%@'",self.displayName, self.modificationDate]; 136 | } 137 | 138 | 139 | #pragma mark - Document Representation 140 | 141 | - (void)loadDocumentWithCompletionHandler:(void (^)(MNDocument *document))completionHandler 142 | { 143 | if (!self.isDownloaded || self.hasUnresolvedConflicts) { 144 | if (completionHandler) completionHandler(nil); 145 | return; 146 | } 147 | 148 | MNDocument *document = [[MNDocument alloc] initWithFileURL:self.fileURL]; 149 | [document openWithCompletionHandler:^(BOOL success) { 150 | if (!success) { 151 | if (completionHandler) completionHandler(nil); 152 | return; 153 | } 154 | NSString *mindMapTitle = self.displayName; 155 | if (mindMapTitle && [mindMapTitle length] != 0) { 156 | document.mindMap.metadata.title = mindMapTitle; 157 | } 158 | if (completionHandler) completionHandler(document); 159 | }]; 160 | } 161 | 162 | 163 | - (void)_refreshModificationDate:(NSDate *)date 164 | { 165 | self.displayModificationDate = [[MNDateFormatter modificationDateFormatter] stringFromDate:date]; 166 | self.modificationDate = date; 167 | } 168 | 169 | 170 | #pragma mark - Preview Image 171 | 172 | static dispatch_queue_t _MNDocumentReferencePreviewGenerationQueue = NULL; 173 | 174 | static dispatch_queue_t MNDocumentReferenceSharedPreviewGenerationQueue(void) 175 | { 176 | static dispatch_once_t queueCreationPredicate = 0; 177 | dispatch_once(&queueCreationPredicate, ^{ 178 | _MNDocumentReferencePreviewGenerationQueue = dispatch_queue_create("com.mindnode.documentReferences.sharedPreviewGenerationQueue", 0); 179 | }); 180 | return _MNDocumentReferencePreviewGenerationQueue; 181 | } 182 | 183 | 184 | - (void)didReceiveMemoryWarning:(NSNotification*)n 185 | { 186 | self.preview = nil; 187 | } 188 | 189 | 190 | - (void)previewImageWithCallbackBlock:(void(^)(UIImage *image))callbackBlock 191 | { 192 | if (self.preview) { 193 | if (callbackBlock) callbackBlock(self.preview); 194 | return; 195 | } 196 | 197 | __weak id blockSelf = self; 198 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 199 | [blockSelf _reloadPreviewImageWithCallbackBlock:^(UIImage *image) { 200 | if (callbackBlock) callbackBlock(image); 201 | }]; 202 | }); 203 | } 204 | 205 | - (void)previewImageWithWidth:(CGFloat)width withCallbackBlock:(void(^)(UIImage *image))callbackBlock 206 | { 207 | if (!self.isDownloaded) { 208 | return; 209 | } 210 | 211 | dispatch_async(MNDocumentReferenceSharedPreviewGenerationQueue(), ^{ 212 | NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:self]; 213 | NSError *readError = nil; 214 | __block UIImage *image; 215 | [coordinator coordinateReadingItemAtURL:self.fileURL options:NSFileCoordinatorReadingWithoutChanges error:&readError byAccessor: ^(NSURL *readURL){ 216 | NSURL *imageURL = [[readURL URLByAppendingPathComponent:MNDocumentQuickLookFolderName isDirectory:YES] URLByAppendingPathComponent:MNDocumentQuickLookPreviewFileName isDirectory:NO]; 217 | 218 | image = [UIImage mn_thumbnailImageAtURL:imageURL withMaxSize:width]; 219 | }]; 220 | 221 | dispatch_async(dispatch_get_main_queue(), ^{ 222 | if (callbackBlock) { 223 | callbackBlock(image); 224 | } 225 | }); 226 | }); 227 | } 228 | 229 | - (void)_reloadPreviewImageWithCallbackBlock:(void(^)(UIImage *image))callbackBlock 230 | { 231 | if (!self.isDownloaded) { 232 | return; 233 | } 234 | dispatch_async(MNDocumentReferenceSharedPreviewGenerationQueue(), ^{ 235 | NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:self]; 236 | NSError *readError = nil; 237 | __block UIImage *image; 238 | [coordinator coordinateReadingItemAtURL:self.fileURL options:NSFileCoordinatorReadingWithoutChanges error:&readError byAccessor: ^(NSURL *readURL){ 239 | NSURL *imageURL = [[readURL URLByAppendingPathComponent:MNDocumentQuickLookFolderName isDirectory:YES] URLByAppendingPathComponent:MNDocumentQuickLookPreviewFileName isDirectory:NO]; 240 | image = [UIImage mn_thumbnailImageAtURL:imageURL withMaxSize:(UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad ? MNDocumentReferencePreviewWidthPad : MNDocumentReferencePreviewWidthPhone)]; 241 | }]; 242 | 243 | if (!image) return; 244 | dispatch_async(dispatch_get_main_queue(), ^{ 245 | [self willChangeValueForKey:MNDocumentReferencePreviewKey]; 246 | self.preview = image; 247 | [self didChangeValueForKey:MNDocumentReferencePreviewKey]; 248 | if (callbackBlock) { 249 | callbackBlock(image); 250 | } 251 | }); 252 | }); 253 | } 254 | 255 | 256 | + (UIImage *)animationImageForDocument:(MNDocument *)document withSize:(CGSize)size 257 | { 258 | id viewState = document.viewState; 259 | if (!viewState) return nil; 260 | 261 | id zoomLevelNumber = viewState[MNDocumentViewStateZoomScaleKey]; 262 | if (![zoomLevelNumber isKindOfClass:[NSNumber class]]) return nil; 263 | CGFloat zoomLevel = [zoomLevelNumber doubleValue]; 264 | if (zoomLevel == 0) zoomLevel = 1; 265 | 266 | 267 | // scroll point 268 | id offsetString = viewState[MNDocumentViewStateScrollCenterPointKey]; 269 | if (![offsetString isKindOfClass:[NSString class]]) return nil; 270 | CGPoint centerPoint = CGPointFromString(offsetString); 271 | 272 | CGRect drawRect = CGRectMake(centerPoint.x, centerPoint.y, 0, 0); 273 | drawRect = CGRectInset(drawRect, -size.width/zoomLevel/2, -size.height/zoomLevel/2); 274 | 275 | 276 | MNImageExporter *exporter = [MNImageExporter exporterWithDocument:document]; 277 | return [exporter imageRepresentationFromRect:drawRect]; 278 | } 279 | 280 | + (UIImage *)previewImageForDocumenAtURL:(NSURL *)url withMaxSize:(CGFloat)size 281 | { 282 | NSURL *imageURL = [[url URLByAppendingPathComponent:MNDocumentQuickLookFolderName isDirectory:YES] URLByAppendingPathComponent:MNDocumentQuickLookPreviewFileName isDirectory:NO]; 283 | UIImage *image = [UIImage mn_thumbnailImageAtURL:imageURL withMaxSize:size]; 284 | 285 | if (!image) { 286 | imageURL = [[NSBundle mainBundle] URLForResource:@"MNTempDocumentPreview" withExtension:@"png"]; 287 | image = [UIImage mn_thumbnailImageAtURL:imageURL withMaxSize:size]; 288 | } 289 | 290 | return image; 291 | } 292 | 293 | #pragma mark - iCloud 294 | 295 | - (void)startDownloading 296 | { 297 | NSFileManager *fm = [[NSFileManager alloc] init]; 298 | NSError *error = nil; 299 | if (![fm startDownloadingUbiquitousItemAtURL:self.fileURL error:&error]) { 300 | NSLog(@"%@",error); 301 | } 302 | 303 | self.startedDownload = YES; 304 | } 305 | 306 | 307 | - (void)updateMetadataFromURL 308 | { 309 | NSURL *url = self.fileURL; 310 | NSDictionary *attributes = [url resourceValuesForKeys:@[NSURLIsUbiquitousItemKey, NSURLUbiquitousItemHasUnresolvedConflictsKey, NSURLUbiquitousItemIsDownloadedKey, NSURLUbiquitousItemIsDownloadingKey, NSURLUbiquitousItemIsUploadedKey, NSURLUbiquitousItemIsUploadingKey, NSURLUbiquitousItemPercentDownloadedKey, NSURLUbiquitousItemPercentUploadedKey] error:NULL]; 311 | 312 | NSMutableDictionary *resultAttributes = [NSMutableDictionary dictionaryWithCapacity:10]; 313 | [attributes enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { 314 | if ([key isEqualToString:NSURLIsUbiquitousItemKey]) { 315 | resultAttributes[NSMetadataItemIsUbiquitousKey] = obj; 316 | 317 | } else if ([key isEqualToString:NSURLUbiquitousItemHasUnresolvedConflictsKey]) { 318 | resultAttributes[NSMetadataUbiquitousItemHasUnresolvedConflictsKey] = obj; 319 | 320 | } else if ([key isEqualToString:NSURLUbiquitousItemIsDownloadedKey]) { 321 | resultAttributes[NSMetadataUbiquitousItemIsDownloadedKey] = obj; 322 | } else if ([key isEqualToString:NSURLUbiquitousItemIsDownloadingKey]) { 323 | resultAttributes[NSMetadataUbiquitousItemIsDownloadingKey] = obj; 324 | } else if ([key isEqualToString:NSURLUbiquitousItemPercentDownloadedKey]) { 325 | resultAttributes[NSMetadataUbiquitousItemPercentDownloadedKey] = obj; 326 | 327 | 328 | } else if ([key isEqualToString:NSURLUbiquitousItemIsUploadedKey]) { 329 | resultAttributes[NSMetadataUbiquitousItemIsUploadedKey] = obj; 330 | } else if ([key isEqualToString:NSURLUbiquitousItemIsUploadingKey]) { 331 | resultAttributes[NSMetadataUbiquitousItemIsUploadingKey] = obj; 332 | } else if ([key isEqualToString:NSURLUbiquitousItemPercentUploadedKey]) { 333 | resultAttributes[NSMetadataUbiquitousItemPercentUploadedKey] = obj; 334 | } 335 | }]; 336 | 337 | 338 | [self _updateStateFromMetadataDictionary:resultAttributes]; 339 | 340 | } 341 | 342 | - (void)updateWithMetadataItem:(NSMetadataItem *)metadataItem 343 | { 344 | NSDictionary *attributes = [metadataItem valuesForAttributes:@[NSMetadataItemFSContentChangeDateKey,NSMetadataUbiquitousItemHasUnresolvedConflictsKey,NSMetadataUbiquitousItemIsDownloadedKey,NSMetadataUbiquitousItemIsDownloadingKey,NSMetadataUbiquitousItemPercentDownloadedKey,NSMetadataUbiquitousItemIsUploadedKey,NSMetadataUbiquitousItemIsUploadingKey,NSMetadataUbiquitousItemPercentUploadedKey]]; 345 | [self _updateStateFromMetadataDictionary:attributes]; 346 | 347 | } 348 | 349 | 350 | 351 | - (void)_updateStateFromMetadataDictionary:(NSDictionary *)dictionary 352 | { 353 | BOOL didUpdate = NO; 354 | 355 | if (!self.isUbiquitous) { 356 | self.isUbiquitous = YES; 357 | didUpdate = YES; 358 | } 359 | 360 | 361 | NSDate *date = [dictionary valueForKey:NSMetadataItemFSContentChangeDateKey]; 362 | if ((date && ![date isEqualToDate:self.modificationDate])) { 363 | dispatch_async(dispatch_get_main_queue(), ^{ 364 | [self _refreshModificationDate:date]; 365 | self.preview = nil; 366 | }); 367 | didUpdate = YES; 368 | } 369 | 370 | NSNumber *metadataValue = [dictionary valueForKey:NSMetadataUbiquitousItemHasUnresolvedConflictsKey]; 371 | BOOL value = [metadataValue boolValue]; 372 | if (metadataValue && (value!=self.hasUnresolvedConflicts)) { 373 | self.hasUnresolvedConflicts = value; 374 | didUpdate = YES; 375 | } 376 | 377 | // Download 378 | metadataValue = [dictionary valueForKey:NSMetadataUbiquitousItemIsDownloadedKey]; 379 | value = [metadataValue boolValue]; 380 | if (metadataValue && (value!=self.isDownloaded)) { 381 | self.isDownloaded = value; 382 | if (!value) self.percentDownloaded = 0; 383 | didUpdate = YES; 384 | } 385 | 386 | if (self.isDownloaded) { 387 | self.isDownloading = NO; 388 | self.percentDownloaded = 100.f; 389 | } else { 390 | metadataValue = [dictionary valueForKey:NSMetadataUbiquitousItemIsDownloadingKey]; 391 | value = [metadataValue boolValue]; 392 | if (metadataValue && (value!=self.isDownloading)) { 393 | self.isDownloading = value; 394 | didUpdate = YES; 395 | } 396 | 397 | metadataValue = [dictionary valueForKey:NSMetadataUbiquitousItemPercentDownloadedKey]; 398 | double doubleValue = [metadataValue doubleValue]; 399 | if (metadataValue && (doubleValue!=self.percentDownloaded)) { 400 | self.percentDownloaded = doubleValue; 401 | if (self.percentDownloaded == 100) { 402 | self.isDownloading = NO; 403 | self.isDownloaded = YES; 404 | } 405 | didUpdate = YES; 406 | } 407 | } 408 | 409 | if (!self.isDownloaded && !self.isDownloading && !self.startedDownload) { 410 | [self startDownloading]; 411 | } 412 | 413 | // Upload 414 | metadataValue = [dictionary valueForKey:NSMetadataUbiquitousItemIsUploadedKey]; 415 | value = [metadataValue boolValue]; 416 | if (metadataValue && (value!=self.isUploaded)) { 417 | self.isUploaded = value; 418 | if (!value) self.percentUploaded = 0; 419 | didUpdate = YES; 420 | } 421 | 422 | if (self.isUploaded) { 423 | self.isUploading = NO; 424 | self.percentUploaded = 100.f; 425 | } else { 426 | metadataValue = [dictionary valueForKey:NSMetadataUbiquitousItemIsUploadingKey]; 427 | value = [metadataValue boolValue]; 428 | if (metadataValue && (value!=self.isUploading)) { 429 | self.isUploading = value; 430 | didUpdate = YES; 431 | } 432 | 433 | metadataValue = [dictionary valueForKey:NSMetadataUbiquitousItemPercentUploadedKey]; 434 | double doubleValue = [metadataValue doubleValue]; 435 | if (metadataValue && (doubleValue!=self.percentUploaded)) { 436 | self.percentUploaded = doubleValue; 437 | if (self.percentUploaded == 100) { 438 | self.isUploaded = YES; 439 | self.isUploading = NO; 440 | } 441 | didUpdate = YES; 442 | } 443 | } 444 | 445 | if (didUpdate) { 446 | dispatch_async(dispatch_get_main_queue(), ^{ 447 | [self willChangeValueForKey:MNDocumentReferenceStatusUpdatedKey]; 448 | [self didChangeValueForKey:MNDocumentReferenceStatusUpdatedKey]; 449 | }); 450 | } 451 | } 452 | 453 | 454 | #pragma mark - NSFilePresenter Protocol 455 | 456 | - (NSURL *)presentedItemURL; 457 | { 458 | return self.fileURL; 459 | } 460 | 461 | - (NSOperationQueue *)presentedItemOperationQueue; 462 | { 463 | return self.fileItemOperationQueue; 464 | } 465 | 466 | - (void)presentedItemDidMoveToURL:(NSURL *)newURL 467 | { 468 | if ([self.fileURL isEqual:newURL]) return; // as we sometimes send it manually, make sure it's only evaluated once 469 | self.fileURL = newURL; 470 | 471 | // dispatch on main queue to make sure KVO notifications get send on main 472 | dispatch_async(dispatch_get_main_queue(), ^{ 473 | self.displayName = [[newURL lastPathComponent] stringByDeletingPathExtension]; 474 | }); 475 | } 476 | 477 | - (void)presentedItemDidChange; 478 | { 479 | // this call can happen on any thread, make sure we coordinate the read 480 | NSFileCoordinator *fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:self]; 481 | [fileCoordinator coordinateReadingItemAtURL:self.fileURL options:NSFileCoordinatorReadingWithoutChanges error:NULL byAccessor:^(NSURL *newURL) { 482 | NSFileManager *fileManager = [[NSFileManager alloc] init]; 483 | 484 | NSDate *modificationDate = nil; 485 | NSDictionary *attributes = [fileManager attributesOfItemAtPath:[newURL path] error:NULL]; 486 | if (attributes) { 487 | modificationDate = [attributes fileModificationDate]; 488 | } 489 | if (modificationDate && ![modificationDate isEqualToDate:self.modificationDate]) { 490 | // dispatch on main queue to make sure KVO notifications get send on main 491 | dispatch_async(dispatch_get_main_queue(), ^{ 492 | [self _refreshModificationDate:modificationDate]; 493 | }); 494 | } 495 | }]; 496 | if (self.preview) { 497 | // dispatch on main queue to make sure KVO notifications get send on main 498 | dispatch_async(dispatch_get_main_queue(), ^{ 499 | [self willChangeValueForKey:MNDocumentReferencePreviewKey]; 500 | self.preview = nil; // we don't want to reload all in memory image when enabling or disalbing iCloud 501 | [self didChangeValueForKey:MNDocumentReferencePreviewKey]; 502 | }); 503 | } 504 | } 505 | 506 | - (void)_logURLState 507 | { 508 | NSURL *url = self.fileURL; 509 | NSDictionary *attributes = [url resourceValuesForKeys:@[NSURLIsUbiquitousItemKey, NSURLUbiquitousItemHasUnresolvedConflictsKey, NSURLUbiquitousItemIsDownloadedKey, NSURLUbiquitousItemIsDownloadingKey, NSURLUbiquitousItemIsUploadedKey, NSURLUbiquitousItemIsUploadingKey, NSURLUbiquitousItemPercentDownloadedKey, NSURLUbiquitousItemPercentUploadedKey] error:NULL]; 510 | 511 | NSLog(@"--Attributes of URL:%@--",url); 512 | [attributes enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { 513 | NSLog(@"Key: %@ Value: %@",key,obj); 514 | }]; 515 | 516 | 517 | } 518 | @end 519 | -------------------------------------------------------------------------------- /MNDocumentController/MNDocumentController.m: -------------------------------------------------------------------------------- 1 | // 2 | // MNDocumentController.h 3 | // MindNodeTouch 4 | // 5 | // Created by Markus Müller on 22.12.08. 6 | // Copyright 2008 IdeasOnCanvas GmbH. All rights reserved. 7 | // 8 | 9 | #import "MNDocumentController.h" 10 | #import "MNDocumentReference.h" 11 | #import "MNError.h" 12 | #import "MNDefaults.h" 13 | #import "MNDocument.h" 14 | #import "MNDocumentController+Convenience.h" 15 | #import "NSString+UUID.h" 16 | 17 | // Keys 18 | NSString *MNDocumentControllerDocumentReferencesKey = @"documentReferences"; 19 | NSString *MNDocumentControllerDidChangeStateNotification = @"MNDocumentControllerDidChangeStateNotification"; 20 | typedef void (^MNDequeueBlockForMetadataQueryDidFinish)(); 21 | 22 | @interface MNDocumentController () 23 | 24 | @property(readwrite) MNDocumentControllerState controllerState; 25 | @property(readwrite, strong) NSMutableSet *documentReferences; 26 | @property(readwrite, strong) NSOperationQueue *fileAccessWorkingQueue; 27 | @property(readwrite, copy) MNDequeueBlockForMetadataQueryDidFinish dequeueBlockForMetadataQueryDidFinish; 28 | @property(strong) NSMetadataQuery *iCloudMetadataQuery; 29 | 30 | @end 31 | 32 | @interface MNDocumentReference () 33 | @property (readwrite,strong) NSURL *fileURL; 34 | @end 35 | 36 | 37 | @implementation MNDocumentController 38 | 39 | #pragma mark - Init 40 | 41 | + (MNDocumentController *)sharedDocumentController 42 | { 43 | static MNDocumentController *sharedDocumentController = nil; 44 | static dispatch_once_t onceToken; 45 | dispatch_once(&onceToken, ^{ 46 | sharedDocumentController = [MNDocumentController alloc]; 47 | sharedDocumentController = [sharedDocumentController init]; 48 | }); 49 | 50 | return sharedDocumentController; 51 | } 52 | 53 | 54 | - (id)init 55 | { 56 | self = [super init]; 57 | if (self == nil) return self; 58 | 59 | NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; 60 | [center addObserver:self selector:@selector(applicationWillTerminate:) name:UIApplicationWillTerminateNotification object:nil]; 61 | [center addObserver:self selector:@selector(applicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; 62 | [center addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; 63 | [center addObserver:self selector:@selector(userDefaultsDidChange:) name:NSUserDefaultsDidChangeNotification object:nil]; 64 | 65 | NSOperationQueue *queue = [[NSOperationQueue alloc] init]; 66 | [queue setName:@"MNDocumentController Working Queue"]; 67 | [queue setMaxConcurrentOperationCount:1]; 68 | self.fileAccessWorkingQueue = queue; 69 | 70 | self.documentReferences = [NSMutableSet setWithCapacity:10]; 71 | [self reloadLocalDocuments]; 72 | [self _startMetadataQuery]; 73 | 74 | self.controllerState = (self.documentsInCloud) ? MNDocumentControllerStateLoading : MNDocumentControllerStateNormal; 75 | 76 | return self; 77 | } 78 | 79 | - (void)dealloc 80 | { 81 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 82 | 83 | for (MNDocumentReference *currentReference in _documentReferences) { 84 | [currentReference disableFilePresenter]; // we need to do this to make sure FilePresenter get unregistered 85 | } 86 | } 87 | 88 | 89 | #pragma mark - Documents 90 | 91 | - (BOOL)documentsInCloud 92 | { 93 | return [[NSUserDefaults standardUserDefaults] boolForKey:MNDefaultsDocumentsInCloudKey] && [[self class] ubiquitousContainerURL]; 94 | } 95 | 96 | 97 | // replaces all documents, also iCloud documents! 98 | - (void)reloadLocalDocuments 99 | { 100 | for (MNDocumentReference *currentReference in [self.documentReferences copy]) { 101 | [self removeDocumentReferencesObject:currentReference]; 102 | } 103 | [self.documentReferences removeAllObjects]; 104 | 105 | for (MNDocumentReference *currentReference in [self _localDocumentReferences]) { 106 | [self addDocumentReferencesObject:currentReference]; 107 | } 108 | } 109 | 110 | 111 | 112 | - (NSSet *)_localDocumentReferences 113 | { 114 | NSMutableSet *results = [NSMutableSet setWithCapacity:0]; 115 | 116 | NSURL *documentDirectory = [[self class] localDocumentsURL]; 117 | if (!documentDirectory) return results; 118 | 119 | // create file coordinator to request folder read access 120 | NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; 121 | NSError *readError = nil; 122 | 123 | [coordinator coordinateReadingItemAtURL:documentDirectory options:NSFileCoordinatorReadingWithoutChanges error:&readError byAccessor: ^(NSURL *readURL) { 124 | 125 | NSFileManager *fileManager = [[NSFileManager alloc] init]; 126 | NSError *error = nil; 127 | NSArray *fileURLs = [fileManager contentsOfDirectoryAtURL:readURL includingPropertiesForKeys:@[NSURLIsDirectoryKey] options:0 error:&error]; 128 | if (!fileURLs) { 129 | NSLog(@"Failed to scan documents."); 130 | return; 131 | } 132 | 133 | for (NSURL *currentFileURL in fileURLs) { 134 | 135 | if ([[currentFileURL pathExtension] isEqualToString:MNDocumentMindNodeExtension]) { 136 | 137 | // create a new reference 138 | NSDate *modificationDate = nil; 139 | NSDictionary *attributes = [fileManager attributesOfItemAtPath:[currentFileURL path] error:NULL]; 140 | if (attributes) { 141 | modificationDate = [attributes fileModificationDate]; 142 | } 143 | if (!modificationDate) { 144 | modificationDate = [NSDate date]; 145 | } 146 | 147 | MNDocumentReference *reference = [[MNDocumentReference alloc] initWithFileURL:currentFileURL modificationDate:modificationDate]; 148 | [results addObject:reference]; 149 | } else { 150 | 151 | // this is a work around for a bug in the document migration in 2.1 152 | // check if folders without the MindNode extension have a content xml 153 | NSNumber *isDirectory = nil; 154 | NSError *resourceError = nil; 155 | if ([currentFileURL getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:&resourceError] && [isDirectory boolValue]) { 156 | NSArray *folderFileURLs = [fileManager contentsOfDirectoryAtURL:currentFileURL includingPropertiesForKeys:nil options:0 error:&error]; 157 | BOOL isDocument = NO; 158 | for (NSURL *currentFolderURL in folderFileURLs) { 159 | if ([[currentFolderURL lastPathComponent] isEqualToString:@"contents.xml"]){ 160 | isDocument = YES; 161 | break; 162 | } 163 | } 164 | 165 | if (isDocument) { 166 | // don't do a coordinated write 167 | NSURL *destinationURL = [currentFileURL URLByAppendingPathExtension:MNDocumentMindNodeExtension]; 168 | BOOL success = [fileManager moveItemAtURL:currentFileURL toURL:destinationURL error:NULL]; 169 | if (!success) { 170 | destinationURL = [[currentFileURL URLByDeletingLastPathComponent] URLByAppendingPathComponent:[NSString mn_stringWithUUID]]; 171 | destinationURL = [destinationURL URLByAppendingPathExtension:MNDocumentMindNodeExtension]; 172 | [fileManager moveItemAtURL:currentFileURL toURL:destinationURL error:NULL]; 173 | } 174 | 175 | // create a new reference 176 | NSDate *modificationDate = nil; 177 | NSDictionary *attributes = [fileManager attributesOfItemAtPath:[destinationURL path] error:NULL]; 178 | if (attributes) { 179 | modificationDate = [attributes fileModificationDate]; 180 | } 181 | if (!modificationDate) { 182 | modificationDate = [NSDate date]; 183 | } 184 | 185 | MNDocumentReference *reference = [[MNDocumentReference alloc] initWithFileURL:destinationURL modificationDate:modificationDate]; 186 | [results addObject:reference]; 187 | 188 | } 189 | } 190 | } 191 | } 192 | }]; 193 | 194 | return results; 195 | } 196 | 197 | 198 | - (void)updateFromMetadataQuery:(NSMetadataQuery *)metadataQuery 199 | { 200 | if (!metadataQuery) return; 201 | [metadataQuery disableUpdates]; 202 | 203 | // don't use results proxy as it's fast this way 204 | NSUInteger metadataCount = [metadataQuery resultCount]; 205 | NSMutableSet *resultDocuments = [[NSMutableSet alloc] init]; 206 | for (NSUInteger metadataIndex = 0; metadataIndex < metadataCount; metadataIndex++) { 207 | NSMetadataItem *metadataItem = [metadataQuery resultAtIndex:metadataIndex]; 208 | 209 | NSURL *fileURL = [metadataItem valueForAttribute:NSMetadataItemURLKey]; 210 | 211 | MNDocumentReference *documentReference = nil; 212 | documentReference = [self documentReferenceFromFileURL:fileURL]; 213 | 214 | if (!documentReference) { 215 | NSDate *modificationDate = [metadataItem valueForAttribute:NSMetadataItemFSContentChangeDateKey]; 216 | if (!modificationDate) { 217 | modificationDate = [NSDate date]; 218 | } 219 | 220 | documentReference = [[MNDocumentReference alloc] initWithFileURL:fileURL modificationDate:modificationDate]; 221 | } 222 | 223 | [resultDocuments addObject:documentReference]; 224 | [documentReference updateWithMetadataItem:metadataItem]; 225 | 226 | } 227 | 228 | // create a set of new documents 229 | for (MNDocumentReference *currentReference in [self.documentReferences copy]) { 230 | if (![resultDocuments containsObject:currentReference]) { 231 | [self removeDocumentReferencesObject:currentReference]; 232 | } 233 | } 234 | 235 | for (MNDocumentReference *currentReference in resultDocuments) { 236 | if (![self.documentReferences containsObject:currentReference]) { 237 | [self addDocumentReferencesObject:currentReference]; 238 | } 239 | } 240 | 241 | 242 | [metadataQuery enableUpdates]; 243 | } 244 | 245 | 246 | - (void)performAsynchronousFileAccessUsingBlock:(void (^)(void))block 247 | { 248 | [self.fileAccessWorkingQueue addOperationWithBlock:block]; 249 | } 250 | 251 | #pragma mark - Document Manipulation 252 | 253 | - (void)createNewDocumentWithCompletionHandler:(void (^)(MNDocument *document, MNDocumentReference *reference))completionHandler; 254 | { 255 | NSURL *fileURL = [[[self class] localDocumentsURL] URLByAppendingPathComponent:[self uniqueFileName]]; 256 | 257 | [MNDocumentReference createNewDocumentWithFileURL:fileURL completionHandler:^(MNDocument *document, MNDocumentReference *reference) { 258 | if (!reference) { 259 | completionHandler(nil,nil); 260 | return; 261 | } 262 | [self addDocumentReferencesObject:reference]; 263 | 264 | if (!self.documentsInCloud) { 265 | completionHandler(document,reference); 266 | return; 267 | } 268 | 269 | __weak id blockSelf = self; 270 | [self.fileAccessWorkingQueue addOperationWithBlock:^{ 271 | if (![blockSelf _moveDocumentToCloud:reference]) { 272 | NSLog(@"Failed to move to iCloud!"); 273 | }; 274 | dispatch_async(dispatch_get_main_queue(), ^{ 275 | completionHandler(document,reference); 276 | }); 277 | }]; 278 | }]; 279 | } 280 | 281 | 282 | - (void)deleteDocument:(MNDocumentReference *)document completionHandler:(void (^)(NSError *errorOrNil))completionHandler 283 | { 284 | if (![self.documentReferences containsObject:document]) { 285 | completionHandler(MNErrorWithCode(MNUnknownError)); 286 | return; 287 | } 288 | __weak id blockSelf = self; 289 | [self.fileAccessWorkingQueue addOperationWithBlock:^{ 290 | __block NSError *deleteError = nil; 291 | NSError *coordinatorError = nil; 292 | NSFileCoordinator* fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; 293 | [fileCoordinator coordinateWritingItemAtURL:document.fileURL options:NSFileCoordinatorWritingForDeleting error:&coordinatorError byAccessor:^(NSURL* writingURL) { 294 | NSFileManager* fileManager = [[NSFileManager alloc] init]; 295 | [fileManager removeItemAtURL:writingURL error:&deleteError]; 296 | }]; 297 | dispatch_async(dispatch_get_main_queue(), ^(){ 298 | if (coordinatorError) { 299 | completionHandler(coordinatorError); 300 | return; 301 | } 302 | if (deleteError) { 303 | completionHandler(deleteError); 304 | return; 305 | } 306 | completionHandler(nil); 307 | [blockSelf removeDocumentReferencesObject:document]; 308 | }); 309 | }]; 310 | } 311 | 312 | 313 | - (void)duplicateDocument:(MNDocumentReference*)document completionHandler:(void (^)(NSError *errorOrNil))completionHandler; 314 | { 315 | if (![self.documentReferences containsObject:document]) { 316 | completionHandler(MNErrorWithCode(MNUnknownError)); 317 | return; 318 | } 319 | 320 | 321 | NSString *fileName = [self uniqueFileNameForDisplayName:document.displayName]; 322 | NSURL *sourceURL = document.fileURL; 323 | NSURL *destinationURL = [[[self class] localDocumentsURL] URLByAppendingPathComponent:fileName isDirectory:NO]; 324 | 325 | __weak id blockSelf = self; 326 | [self.fileAccessWorkingQueue addOperationWithBlock:^{ 327 | __block NSError *copyError = nil; 328 | __block BOOL success = NO; 329 | __block NSURL *newDocumentURL = nil; 330 | NSError *coordinatorError = nil; 331 | NSFileCoordinator* fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; 332 | [fileCoordinator coordinateReadingItemAtURL:sourceURL options:NSFileCoordinatorReadingWithoutChanges writingItemAtURL:destinationURL options:NSFileCoordinatorWritingForReplacing error:&coordinatorError byAccessor:^(NSURL *newReadingURL, NSURL *newWritingURL) { 333 | NSFileManager* fileManager = [[NSFileManager alloc] init]; 334 | if ([fileManager fileExistsAtPath:[newWritingURL path]]) { 335 | return; 336 | } 337 | [fileManager copyItemAtURL:sourceURL toURL:destinationURL error:©Error]; 338 | newDocumentURL = newWritingURL; 339 | success = YES; 340 | }]; 341 | 342 | if (!success) { 343 | dispatch_async(dispatch_get_main_queue(), ^(){ 344 | if (coordinatorError) { 345 | completionHandler(coordinatorError); 346 | } else if (copyError) { 347 | completionHandler(copyError); 348 | } else { 349 | completionHandler(MNErrorWithCode(MNUnknownError)); 350 | } 351 | }); 352 | return; 353 | } 354 | MNDocumentReference *reference = [[MNDocumentReference alloc] initWithFileURL:newDocumentURL modificationDate:[NSDate date]]; 355 | 356 | if (![blockSelf documentsInCloud]) { 357 | dispatch_async(dispatch_get_main_queue(), ^(){ 358 | [blockSelf addDocumentReferencesObject:reference]; 359 | completionHandler(nil); 360 | }); 361 | return; 362 | } 363 | 364 | if (![blockSelf _moveDocumentToCloud:reference]) { 365 | NSLog(@"Failed to move to iCloud!"); 366 | } 367 | 368 | dispatch_async(dispatch_get_main_queue(), ^{ 369 | [blockSelf addDocumentReferencesObject:reference]; 370 | completionHandler(nil); 371 | }); 372 | }]; 373 | } 374 | 375 | 376 | - (void)renameDocument:(MNDocumentReference *)document toFileName:(NSString *)fileName completionHandler:(void (^)(NSError *errorOrNil))completionHandler 377 | { 378 | // check if valid filename 379 | if (!fileName || ([fileName length] > 200) || ([fileName length] == 0)) { 380 | dispatch_async(dispatch_get_main_queue(), ^(){ 381 | if (completionHandler) completionHandler(MNErrorWithCode(MNErrorFileNameTooLong)); 382 | }); 383 | return; 384 | } 385 | 386 | if (!NSEqualRanges([fileName rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@"/\\?%*|\"<>"]], NSMakeRange(NSNotFound, 0))) { 387 | dispatch_async(dispatch_get_main_queue(), ^(){ 388 | if (completionHandler) completionHandler(MNErrorWithCode(MNErrorFileNameNotAllowedCharacters)); 389 | }); 390 | return; 391 | } 392 | 393 | // check for case insensitivity 394 | NSMutableSet *useFileNames = [NSMutableSet setWithCapacity:[self.documentReferences count]]; 395 | for (MNDocumentReference *currentReference in self.documentReferences) { 396 | [useFileNames addObject:[[currentReference.fileURL lastPathComponent] lowercaseString]]; 397 | } 398 | if ([useFileNames member:[fileName lowercaseString]] != nil) { 399 | dispatch_async(dispatch_get_main_queue(), ^(){ 400 | if (completionHandler) completionHandler(MNErrorWithCode(MNErrorFileNameAlreadyUsedError)); 401 | }); 402 | return; 403 | } 404 | 405 | 406 | [self.fileAccessWorkingQueue addOperationWithBlock:^{ 407 | NSURL *sourceURL = document.fileURL; 408 | NSURL *destinationURL = [[sourceURL URLByDeletingLastPathComponent] URLByAppendingPathComponent:fileName isDirectory:NO]; 409 | 410 | NSError *writeError = nil; 411 | __block NSError *moveError = nil; 412 | __block BOOL success = NO; 413 | __block NSURL *finalDocumentURL = destinationURL; // finalDocumentURL might be different than destinationURL when entering the coordinator block 414 | NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; 415 | [coordinator coordinateWritingItemAtURL: sourceURL options: NSFileCoordinatorWritingForMoving writingItemAtURL: destinationURL options: NSFileCoordinatorWritingForReplacing error: &writeError byAccessor: ^(NSURL *newURL1, NSURL *newURL2) { 416 | NSFileManager *fileManager = [[NSFileManager alloc] init]; 417 | success = [fileManager moveItemAtURL:newURL1 toURL:newURL2 error:&moveError]; 418 | if (success) { 419 | [coordinator itemAtURL:newURL1 didMoveToURL:newURL2]; 420 | finalDocumentURL = newURL2; 421 | } 422 | }]; 423 | 424 | NSError *outError = nil; 425 | if (success) { 426 | if (!self.documentsInCloud) { 427 | // sometimes when renaming documents, the file presenter is not correctly informed 428 | // this is not a problem when using iCloud as the next metadata query will take care of this 429 | // we don't use the same code for iCloud as sometimes the next metadata query will return the old name and the following query the new name 430 | // this caused the reappearing of the old name, followed by the new name 431 | [document presentedItemDidMoveToURL:finalDocumentURL]; 432 | } 433 | 434 | } else { 435 | if (moveError) { 436 | MNLogError(moveError); 437 | } 438 | if (writeError) { 439 | MNLogError(writeError); 440 | } 441 | outError = MNErrorWithCode(MNErrorFileNameAlreadyUsedError); 442 | } 443 | dispatch_async(dispatch_get_main_queue(), ^(){ 444 | if (completionHandler) completionHandler(outError); 445 | }); 446 | }]; 447 | } 448 | 449 | - (BOOL)pendingDocumentTransfers 450 | { 451 | for (MNDocumentReference *currentDocument in self.documentReferences) { 452 | if (!currentDocument.isDownloaded || !currentDocument.isUploaded) { 453 | return YES; 454 | } 455 | } 456 | return NO; 457 | } 458 | 459 | - (void)disableiCloudAndCopyAllCloudDocumentsToLocalWithCompletionHandler:(void (^)(NSError *errorOrNil))completionHandler; 460 | { 461 | // check if we have pending changes 462 | if ([self pendingDocumentTransfers]) { 463 | return completionHandler(MNErrorWithCode(MNErrorCloudUnableToMoveAllDocumentsToCloud)); 464 | } 465 | 466 | NSArray *documents = [self.documentReferences copy]; 467 | 468 | __weak id blockSelf = self; 469 | [self.fileAccessWorkingQueue addOperationWithBlock:^{ 470 | for (MNDocumentReference *currentDocument in documents) { 471 | __block NSError *copyError = nil; 472 | __block BOOL success = NO; 473 | __block NSURL *newDocumentURL = nil; 474 | NSURL *sourceURL = [currentDocument fileURL]; 475 | NSURL *destinationURL = [[[blockSelf class] localDocumentsURL] URLByAppendingPathComponent:[sourceURL lastPathComponent] isDirectory:NO]; 476 | NSError *coordinatorError = nil; 477 | NSFileCoordinator* fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; 478 | [fileCoordinator coordinateReadingItemAtURL:sourceURL options:NSFileCoordinatorReadingWithoutChanges writingItemAtURL:destinationURL options:NSFileCoordinatorWritingForReplacing error:&coordinatorError byAccessor:^(NSURL *newReadingURL, NSURL *newWritingURL) { 479 | NSFileManager* fileManager = [[NSFileManager alloc] init]; 480 | if ([fileManager fileExistsAtPath:[newWritingURL path]]) { 481 | return; 482 | } 483 | [fileManager copyItemAtURL:sourceURL toURL:destinationURL error:©Error]; 484 | newDocumentURL = newWritingURL; 485 | success = YES; 486 | }]; 487 | } 488 | 489 | dispatch_async(dispatch_get_main_queue(), ^{ 490 | [[NSUserDefaults standardUserDefaults] setBool:NO forKey:MNDefaultsDocumentsInCloudKey]; 491 | completionHandler(nil); 492 | }); 493 | }]; 494 | } 495 | 496 | 497 | 498 | 499 | - (void)moveAllCloudDocumentsToLocalWithCompletionHandler:(void (^)(NSError *errorOrNil))completionHandler; 500 | { 501 | // check if we have pending changes 502 | if ([self pendingDocumentTransfers]) { 503 | return completionHandler(MNErrorWithCode(MNErrorCloudUnableToMoveAllDocumentsToCloud)); 504 | } 505 | 506 | 507 | NSArray *documents = [self.documentReferences copy]; 508 | __weak id blockSelf = self; 509 | [self.fileAccessWorkingQueue addOperationWithBlock:^{ 510 | for (MNDocumentReference *currentDocument in documents) { 511 | if (![blockSelf _moveDocumentToLocal:currentDocument]) { 512 | NSLog(@"Failed to move to iCloud!"); 513 | }; 514 | } 515 | 516 | dispatch_async(dispatch_get_main_queue(), ^{ 517 | completionHandler(nil); 518 | }); 519 | }]; 520 | } 521 | 522 | - (void)moveAllLocalDocumentsToCloudWithCompletionHandler:(void (^)(void))completionHandler 523 | { 524 | 525 | NSArray *documents = [self.documentReferences copy]; 526 | __weak id blockSelf = self; 527 | 528 | void (^moveAllBlock)(void) = ^ { 529 | for (MNDocumentReference *currentDocument in documents) { 530 | if (![blockSelf _moveDocumentToCloud:currentDocument]) { 531 | NSLog(@"Failed to move to iCloud!"); 532 | }; 533 | } 534 | 535 | dispatch_async(dispatch_get_main_queue(), ^{ 536 | completionHandler(); 537 | }); 538 | }; 539 | 540 | if (self.iCloudMetadataQuery.isGathering) { 541 | self.dequeueBlockForMetadataQueryDidFinish = moveAllBlock; 542 | } else { 543 | [self.fileAccessWorkingQueue addOperationWithBlock:moveAllBlock]; 544 | } 545 | 546 | } 547 | 548 | 549 | - (void)importDocumentAtURL:(NSURL *)documentURL completionHandler:(void (^)(MNDocumentReference *reference, NSError *errorOrNil))completionHandler 550 | { 551 | NSString *path = [documentURL path]; 552 | NSString *extension = [path pathExtension]; 553 | if (!extension) { 554 | completionHandler(nil,MNErrorWithCode(MNFileImportError)); 555 | return; 556 | } 557 | 558 | NSString *filename = [[path lastPathComponent] stringByDeletingPathExtension]; 559 | if ([[[filename pathExtension] lowercaseString] isEqualToString:MNDocumentMindNodeExtension]) { 560 | // when we import a Document.mindnode.zip file, we also need to trim the .mindnode extension 561 | filename = [filename stringByDeletingPathExtension]; 562 | } 563 | 564 | // When importing we use the current file extension and not the mindnode extension. It will get set during first saving by MNDocument 565 | NSURL *newDocumentURL = [[[self class] localDocumentsURL] URLByAppendingPathComponent:[self uniqueFileNameForDisplayName:filename]]; 566 | 567 | 568 | // create a new document 569 | MNDocument *document = [[MNDocument alloc] initWithFileURL:documentURL]; 570 | if (!document) { 571 | completionHandler(nil,MNErrorWithCode(MNFileImportError)); 572 | return; 573 | } 574 | 575 | // initialize the new document from the file we need to import 576 | [document openWithCompletionHandler:^(BOOL success) { 577 | if (!success) { 578 | completionHandler(nil,MNErrorWithCode(MNFileImportError)); 579 | return; 580 | } 581 | 582 | [document updateChangeCount: UIDocumentChangeDone]; // mark the document as dirty 583 | 584 | [document saveToURL:newDocumentURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) { 585 | if (!success) { 586 | [document closeWithCompletionHandler:nil]; 587 | completionHandler(nil,MNErrorWithCode(MNFileImportError)); 588 | return; 589 | } 590 | [document closeWithCompletionHandler:^(BOOL success) { 591 | MNDocumentReference *reference = [[MNDocumentReference alloc] initWithFileURL:newDocumentURL modificationDate:[NSDate date]]; 592 | [self addDocumentReferencesObject:reference]; 593 | 594 | if (!self.documentsInCloud) { 595 | if (completionHandler) completionHandler(reference,nil); 596 | return; 597 | } 598 | 599 | __weak id blockSelf = self; 600 | [self.fileAccessWorkingQueue addOperationWithBlock:^{ 601 | if (![blockSelf _moveDocumentToCloud:reference]) { 602 | NSLog(@"Failed to move to iCloud!"); 603 | }; 604 | dispatch_async(dispatch_get_main_queue(), ^{ 605 | completionHandler(reference,nil); 606 | }); 607 | }]; 608 | 609 | }]; 610 | }]; 611 | }]; 612 | } 613 | 614 | - (void)evictAllCloudDocumentsWithCompletionHandler:(void (^)(void))completionHandler progressUpdateHandler:(void (^)(CGFloat progress))progressUpdateHandler; 615 | { 616 | [self.fileAccessWorkingQueue addOperationWithBlock:^{ 617 | 618 | NSUInteger count = [self.documentReferences count]; 619 | NSUInteger currentItem = 0; 620 | 621 | NSFileManager *fm = [[NSFileManager alloc] init]; 622 | for (MNDocumentReference *currentReference in self.documentReferences) { 623 | NSURL *url = currentReference.fileURL; 624 | [fm evictUbiquitousItemAtURL:url error:NULL]; 625 | 626 | currentItem++; 627 | if (progressUpdateHandler) { 628 | dispatch_async(dispatch_get_main_queue(), ^{ 629 | progressUpdateHandler(currentItem / ((CGFloat)count)); 630 | }); 631 | } 632 | } 633 | dispatch_async(dispatch_get_main_queue(), ^{ 634 | if (completionHandler) completionHandler(); 635 | }); 636 | }]; 637 | } 638 | 639 | 640 | #pragma mark - Paths 641 | 642 | 643 | + (NSURL *)localDocumentsURL 644 | { 645 | NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); 646 | NSString *documentsDirectory = paths[0]; 647 | return [NSURL fileURLWithPath:documentsDirectory]; 648 | } 649 | 650 | + (NSURL *)ubiquitousContainerURL 651 | { 652 | return [[[NSFileManager alloc] init] URLForUbiquityContainerIdentifier:nil]; 653 | } 654 | 655 | + (NSURL *)ubiquitousDocumentsURL 656 | { 657 | NSURL *containerURL = [self ubiquitousContainerURL]; 658 | if (!containerURL) return nil; 659 | 660 | NSURL *documentURL = [containerURL URLByAppendingPathComponent:@"Documents"]; 661 | return documentURL; 662 | } 663 | 664 | 665 | - (NSString *)uniqueFileName 666 | { 667 | NSString *fileName = NSLocalizedStringFromTable(@"Mind Map", @"DocumentPicker", @"Default file name. Don't localize!"); 668 | fileName = [self uniqueFileNameForDisplayName:fileName]; 669 | return fileName; 670 | } 671 | 672 | - (NSString *)uniqueFileNameForDisplayName:(NSString *)displayName 673 | { 674 | NSSet *documents = self.documentReferences; 675 | NSUInteger count = [documents count]; 676 | 677 | // build list of filenames 678 | NSMutableSet *useFileNames = [NSMutableSet setWithCapacity:count]; 679 | for (MNDocumentReference *currentReference in documents) { 680 | // lowercaseString: make sure our name is also unique on a case insensitive file system 681 | [useFileNames addObject:[[currentReference.fileURL lastPathComponent] lowercaseString]]; 682 | } 683 | 684 | NSString *fileName = [[self class] uniqueFileNameForDisplayName:displayName extension:MNDocumentMindNodeExtension usedFileNames:useFileNames]; 685 | return fileName; 686 | } 687 | 688 | + (NSString *)uniqueFileNameForDisplayName:(NSString *)displayName extension:(NSString *)extension usedFileNames:(NSSet *)usedFileNames 689 | { // based on code from the OmniGroup Frameworks 690 | NSUInteger counter = 0; // starting counter 691 | displayName = [displayName stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"/:"]]; 692 | if ([displayName length] > 200) displayName = [displayName substringWithRange:NSMakeRange(0, 200)]; 693 | 694 | while (YES) { 695 | NSString *candidateName; 696 | if (counter == 0) { 697 | candidateName = [[NSString alloc] initWithFormat:@"%@.%@", displayName, extension]; 698 | counter = 2; // First duplicate should be "Foo 2". 699 | } else { 700 | candidateName = [[NSString alloc] initWithFormat:@"%@ %d.%@", displayName, counter, extension]; 701 | counter++; 702 | } 703 | 704 | // lowercaseString: make sure our name is also unique on a case insensitive file system 705 | if ([usedFileNames member:[candidateName lowercaseString]] == nil) { 706 | return candidateName; 707 | } 708 | } 709 | } 710 | 711 | + (NSString *)uniqueFileNameForDisplayName:(NSString *)displayName extension:(NSString *)extension inDirectory:(NSURL *)directionaryURL 712 | { 713 | NSUInteger counter = 0; 714 | displayName = [displayName stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"/:"]]; 715 | if ([displayName length] > 200) displayName = [displayName substringWithRange:NSMakeRange(0, 200)]; 716 | 717 | NSArray *directoryContents = [[[NSFileManager alloc] init] contentsOfDirectoryAtPath:[directionaryURL path] error:NULL]; 718 | if (!directoryContents) return nil; 719 | 720 | while (YES) { 721 | NSString *candidateName; 722 | if (counter == 0) { 723 | candidateName = [[NSString alloc] initWithFormat:@"%@.%@", displayName, extension]; 724 | counter = 2; // First duplicate should be "Foo 2". 725 | } else { 726 | candidateName = [[NSString alloc] initWithFormat:@"%@ %d.%@", displayName, counter, extension]; 727 | counter++; 728 | } 729 | 730 | // lowercaseString: make sure our name is also unique on a case insensitive file system 731 | NSString *lowercaseName = [candidateName lowercaseString]; 732 | NSUInteger foundIndex = [directoryContents indexOfObjectPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) { 733 | return ([[obj lowercaseString] isEqualToString:lowercaseName]); 734 | }]; 735 | if (foundIndex == NSNotFound) { 736 | return candidateName; 737 | } 738 | } 739 | } 740 | 741 | 742 | 743 | #pragma mark - Document Persistance 744 | 745 | - (void)applicationWillTerminate:(NSNotification *)notification 746 | { 747 | [self _stopMetadataQuery]; 748 | } 749 | 750 | - (void)applicationDidEnterBackground:(NSNotification *)notification 751 | { 752 | [self _stopMetadataQuery]; 753 | } 754 | 755 | - (void)applicationWillEnterForeground:(NSNotification *)notification 756 | { 757 | if (self.documentsInCloud) { 758 | [self _startMetadataQuery]; 759 | } else { 760 | if ([[NSUserDefaults standardUserDefaults] boolForKey:MNDefaultsDocumentsInCloudKey]) { 761 | [self reloadLocalDocuments]; // iCloud was available last time, reload local docs. 762 | } 763 | } 764 | } 765 | 766 | - (void)userDefaultsDidChange:(NSNotification *)notification 767 | { 768 | BOOL iCloudEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:MNDefaultsDocumentsInCloudKey]; 769 | if (iCloudEnabled) { 770 | if (!self.iCloudMetadataQuery) { 771 | [self _startMetadataQuery]; 772 | } 773 | } else { 774 | if (self.iCloudMetadataQuery) { 775 | [self _stopMetadataQuery]; 776 | [self reloadLocalDocuments]; 777 | } 778 | } 779 | } 780 | 781 | #pragma mark - KVO Compliance 782 | 783 | 784 | - (void)addDocumentReferencesObject:(MNDocumentReference *)reference 785 | { 786 | [reference enableFilePresenter]; 787 | [_documentReferences addObject:reference]; 788 | } 789 | 790 | - (void)removeDocumentReferencesObject:(MNDocumentReference *)reference 791 | { 792 | [reference disableFilePresenter]; 793 | [_documentReferences removeObject:reference]; 794 | } 795 | 796 | 797 | #pragma mark - iCloud 798 | 799 | - (void)_startMetadataQuery 800 | { 801 | if (!self.documentsInCloud) return; 802 | if (self.iCloudMetadataQuery) return; 803 | if (![[self class] ubiquitousContainerURL]) return; // no iCloud 804 | 805 | NSMetadataQuery *query = [[NSMetadataQuery alloc] init]; 806 | [query setSearchScopes:@[NSMetadataQueryUbiquitousDocumentsScope]]; 807 | [query setPredicate:[NSPredicate predicateWithFormat:@"%K like '*'", NSMetadataItemFSNameKey]]; 808 | NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; 809 | [notificationCenter addObserver:self selector:@selector(metadataQueryDidStartGatheringNotifiction:) name:NSMetadataQueryDidStartGatheringNotification object:query]; 810 | [notificationCenter addObserver:self selector:@selector(metadataQueryDidGatheringProgressNotifiction:) name:NSMetadataQueryGatheringProgressNotification object:query]; 811 | [notificationCenter addObserver:self selector:@selector(metadataQueryDidFinishGatheringNotifiction:) name:NSMetadataQueryDidFinishGatheringNotification object:query]; 812 | [notificationCenter addObserver:self selector:@selector(metadataQueryDidUpdateNotifiction:) name:NSMetadataQueryDidUpdateNotification object:query]; 813 | 814 | if(![query startQuery]) { 815 | NSLog(@"Unable to start MetadataQuery"); 816 | } 817 | self.iCloudMetadataQuery = query; 818 | } 819 | 820 | - (void)_stopMetadataQuery 821 | { 822 | NSMetadataQuery *query = self.iCloudMetadataQuery; 823 | if (query == nil) 824 | return; 825 | 826 | [query stopQuery]; 827 | 828 | NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; 829 | [center removeObserver:self name:NSMetadataQueryDidStartGatheringNotification object:query]; 830 | [center removeObserver:self name:NSMetadataQueryGatheringProgressNotification object:query]; 831 | [center removeObserver:self name:NSMetadataQueryDidFinishGatheringNotification object:query]; 832 | [center removeObserver:self name:NSMetadataQueryDidUpdateNotification object:query]; 833 | 834 | self.iCloudMetadataQuery = nil; 835 | } 836 | 837 | - (void)metadataQueryDidStartGatheringNotifiction:(NSNotification *)n; 838 | { 839 | self.controllerState = MNDocumentControllerStateLoading; 840 | [[NSNotificationCenter defaultCenter] postNotificationName:MNDocumentControllerDidChangeStateNotification object:self]; 841 | } 842 | 843 | - (void)metadataQueryDidGatheringProgressNotifiction:(NSNotification *)n; 844 | { 845 | // we don't update the progress as we don't want to add documents incrementally during startup 846 | // our folder scan will take care of providing an initial set of documents 847 | } 848 | 849 | - (void)metadataQueryDidFinishGatheringNotifiction:(NSNotification *)n; 850 | { 851 | [self updateFromMetadataQuery:self.iCloudMetadataQuery]; 852 | self.controllerState = MNDocumentControllerStateNormal; 853 | [[NSNotificationCenter defaultCenter] postNotificationName:MNDocumentControllerDidChangeStateNotification object:self]; 854 | if (self.dequeueBlockForMetadataQueryDidFinish) { 855 | [self.fileAccessWorkingQueue addOperationWithBlock:^{ 856 | self.dequeueBlockForMetadataQueryDidFinish(); 857 | self.dequeueBlockForMetadataQueryDidFinish = nil; 858 | }]; 859 | } else { 860 | // move local documents to iCloud 861 | // sometimes moving documents to iCloud failes, we don't show those documents in the picker, but we try to move them to iCloud everytime we finish gathering 862 | // (hopefully a document won't fail to move to iCloud forever) 863 | __weak id blockSelf = self; 864 | [self.fileAccessWorkingQueue addOperationWithBlock:^{ 865 | NSSet *references = [blockSelf _localDocumentReferences]; 866 | for (MNDocumentReference *currentReference in references) { 867 | [blockSelf _moveDocumentToCloud:currentReference]; 868 | } 869 | }]; 870 | } 871 | } 872 | 873 | - (void)metadataQueryDidUpdateNotifiction:(NSNotification *)n; 874 | { 875 | [self updateFromMetadataQuery:self.iCloudMetadataQuery]; 876 | } 877 | 878 | 879 | // This method blocks, make sure to call it on a queue 880 | - (BOOL)_moveDocumentToCloud:(MNDocumentReference *)documentReference 881 | { 882 | NSURL *sourceURL = documentReference.fileURL; 883 | NSURL *targetDocumentURL = [[self class] ubiquitousDocumentsURL]; 884 | if (!targetDocumentURL) return NO; 885 | NSURL *destinationURL = [targetDocumentURL URLByAppendingPathComponent:[sourceURL lastPathComponent] isDirectory:NO]; 886 | 887 | NSFileManager *fileManager = [[NSFileManager alloc] init]; 888 | if ([fileManager fileExistsAtPath:[destinationURL path]]) { 889 | NSString *fileName = [[self class] uniqueFileNameForDisplayName:documentReference.displayName extension:MNDocumentMindNodeExtension inDirectory:targetDocumentURL]; 890 | destinationURL = [targetDocumentURL URLByAppendingPathComponent:fileName isDirectory:NO]; 891 | } 892 | 893 | NSError *error = nil; 894 | BOOL success = [fileManager setUbiquitous:YES itemAtURL:sourceURL destinationURL:destinationURL error:&error]; 895 | if (!success && error) NSLog(@"%@",error); 896 | 897 | return success; 898 | } 899 | 900 | // This method blocks, make sure to call it on a queue 901 | - (BOOL)_moveDocumentToLocal:(MNDocumentReference *)documentReference 902 | { 903 | NSURL *sourceURL = documentReference.fileURL; 904 | NSURL *targetDocumentURL = [[self class] localDocumentsURL]; 905 | NSURL *destinationURL = [targetDocumentURL URLByAppendingPathComponent:[sourceURL lastPathComponent] isDirectory:NO]; 906 | 907 | NSFileManager *fileManager = [[NSFileManager alloc] init]; 908 | if ([fileManager fileExistsAtPath:[destinationURL path]]) { 909 | NSString *fileName = [[self class] uniqueFileNameForDisplayName:documentReference.displayName extension:MNDocumentMindNodeExtension inDirectory:targetDocumentURL]; 910 | destinationURL = [targetDocumentURL URLByAppendingPathComponent:fileName isDirectory:NO]; 911 | } 912 | 913 | NSError *error = nil; 914 | BOOL success = [fileManager setUbiquitous:NO itemAtURL:sourceURL destinationURL:destinationURL error:&error]; 915 | if (!success && error) NSLog(@"%@",error); 916 | return success; 917 | } 918 | 919 | 920 | - (void)debugLogCloudFolder 921 | { 922 | NSURL *url = [[self class] ubiquitousDocumentsURL]; 923 | if (!url) NSLog(@"Unable to access ubiquitousContainer"); 924 | 925 | NSError *error = nil; 926 | NSFileManager *fileManager = [[NSFileManager alloc] init]; 927 | NSArray *fileURLs = [fileManager contentsOfDirectoryAtURL:url includingPropertiesForKeys:@[NSURLIsDirectoryKey] options:0 error:&error]; 928 | 929 | for (NSURL *currentURL in fileURLs) { 930 | NSDictionary *attributes = [fileManager attributesOfItemAtPath:[currentURL path] error:NULL]; 931 | NSLog(@"%@",[currentURL lastPathComponent]); 932 | NSLog(@"%@",attributes); 933 | NSLog(@"--"); 934 | } 935 | 936 | } 937 | @end 938 | --------------------------------------------------------------------------------