├── .gitignore ├── ICACloud.h ├── ICACloud.m ├── LICENSE └── README.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 | xcuserdata 13 | profile 14 | *.moved-aside 15 | DerivedData 16 | .idea/ 17 | *.hmap 18 | *.xccheckout 19 | 20 | #CocoaPods 21 | Pods 22 | -------------------------------------------------------------------------------- /ICACloud.h: -------------------------------------------------------------------------------- 1 | // 2 | // ICACloud 3 | // iCloud Access 4 | // 5 | // Created by Drew McCormack on 18/01/14. 6 | // Copyright (c) 2014 Drew McCormack. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | extern NSString *ICAException; 12 | extern NSString *ICAErrorDomain; 13 | 14 | typedef NS_ENUM(NSInteger, ICAErrorCode) { 15 | ICAErrorCodeFileAccessFailed = 100, 16 | ICAErrorCodeFileCoordinatorTimedOut = 101, 17 | ICAErrorCodeAuthenticationFailure = 102, 18 | ICAErrorCodeConnectionError = 103 19 | }; 20 | 21 | 22 | @interface ICACloudFile : NSObject 23 | 24 | @property (readonly, copy) NSString *path; 25 | @property (readonly, copy) NSString *name; 26 | @property (readonly) NSDictionary *fileAttributes; 27 | 28 | @end 29 | 30 | 31 | @interface ICACloud : NSObject 32 | 33 | @property (nonatomic, assign, readonly) BOOL isConnected; 34 | @property (nonatomic, strong, readonly) id identityToken; // Fires KVO Notifications 35 | @property (nonatomic, readonly) NSString *rootDirectoryPath; // Container relative 36 | 37 | - (instancetype)initWithUbiquityContainerIdentifier:(NSString *)newIdentifier; 38 | - (instancetype)initWithUbiquityContainerIdentifier:(NSString *)newIdentifier rootDirectoryPath:(NSString *)newPath; 39 | 40 | - (void)fileExistsAtPath:(NSString *)path completion:(void(^)(BOOL exists, BOOL isDirectory, NSError *error))block; 41 | 42 | - (void)createDirectoryAtPath:(NSString *)path completion:(void(^)(NSError *error))block; 43 | - (void)contentsOfDirectoryAtPath:(NSString *)path completion:(void(^)(NSArray *contents, NSError *error))block; 44 | 45 | - (void)removeItemAtPath:(NSString *)fromPath completion:(void(^)(NSError *error))block; 46 | 47 | - (void)uploadLocalFile:(NSString *)fromPath toPath:(NSString *)toPath completion:(void(^)(NSError *error))block; 48 | - (void)downloadFromPath:(NSString *)fromPath toLocalFile:(NSString *)toPath completion:(void(^)(NSError *error))block; 49 | 50 | - (void)uploadData:(NSData *)data toPath:(NSString *)toPath completion:(void(^)(NSError *error))block; 51 | - (void)downloadDataFromPath:(NSString *)fromPath completion:(void(^)(NSData *data, NSError *error))block; 52 | 53 | @end 54 | -------------------------------------------------------------------------------- /ICACloud.m: -------------------------------------------------------------------------------- 1 | // 2 | // ICACloud 3 | // iCloud Access 4 | // 5 | // Created by Drew McCormack on 18/01/14. 6 | // Copyright (c) 2014 Drew McCormack. All rights reserved. 7 | // 8 | 9 | #import "ICACloud.h" 10 | 11 | NSString *ICAException = @"ICAException"; 12 | NSString *ICAErrorDomain = @"ICAErrorDomain"; 13 | 14 | 15 | 16 | @interface ICACloudFile () 17 | 18 | @property (readwrite, copy) NSString *path; 19 | @property (readwrite, copy) NSString *name; 20 | @property (readwrite) NSDictionary *fileAttributes; 21 | 22 | @end 23 | 24 | 25 | @implementation ICACloudFile 26 | 27 | @synthesize path = path; 28 | @synthesize name = name; 29 | @synthesize fileAttributes = fileAttributes; 30 | 31 | @end 32 | 33 | 34 | @implementation ICACloud { 35 | NSFileManager *fileManager; 36 | NSURL *rootDirectoryURL; 37 | NSMetadataQuery *metadataQuery; 38 | NSOperationQueue *operationQueue; 39 | NSString *ubiquityContainerIdentifier; 40 | dispatch_queue_t timeOutQueue; 41 | id ubiquityIdentityObserver; 42 | } 43 | 44 | @synthesize rootDirectoryPath = rootDirectoryPath; 45 | 46 | // Designated 47 | - (instancetype)initWithUbiquityContainerIdentifier:(NSString *)newIdentifier rootDirectoryPath:(NSString *)newPath 48 | { 49 | self = [super init]; 50 | if (self) { 51 | fileManager = [[NSFileManager alloc] init]; 52 | 53 | rootDirectoryPath = [newPath copy] ? : @""; 54 | 55 | operationQueue = [[NSOperationQueue alloc] init]; 56 | operationQueue.maxConcurrentOperationCount = 1; 57 | 58 | timeOutQueue = dispatch_queue_create("com.mentalfaculty.cloudaccess.queue.icloudtimeout", DISPATCH_QUEUE_SERIAL); 59 | 60 | rootDirectoryURL = nil; 61 | metadataQuery = nil; 62 | ubiquityContainerIdentifier = [newIdentifier copy]; 63 | ubiquityIdentityObserver = nil; 64 | 65 | [self performInitialPreparation:NULL]; 66 | } 67 | return self; 68 | } 69 | 70 | - (instancetype)initWithUbiquityContainerIdentifier:(NSString *)newIdentifier 71 | { 72 | return [self initWithUbiquityContainerIdentifier:newIdentifier rootDirectoryPath:nil]; 73 | } 74 | 75 | - (instancetype)init 76 | { 77 | @throw [NSException exceptionWithName:ICAException reason:@"iCloud initializer requires container identifier" userInfo:nil]; 78 | return nil; 79 | } 80 | 81 | - (void)dealloc 82 | { 83 | [self removeUbiquityContainerNotificationObservers]; 84 | [self stopMonitoring]; 85 | [operationQueue cancelAllOperations]; 86 | } 87 | 88 | #pragma mark - User Identity 89 | 90 | - (id )identityToken 91 | { 92 | return [fileManager ubiquityIdentityToken]; 93 | } 94 | 95 | #pragma mark - Initial Preparation 96 | 97 | - (void)performInitialPreparation:(void(^)(NSError *error))completion 98 | { 99 | if (fileManager.ubiquityIdentityToken) { 100 | [self setupRootDirectory:^(NSError *error) { 101 | [self startMonitoringMetadata]; 102 | [self addUbiquityContainerNotificationObservers]; 103 | dispatch_async(dispatch_get_main_queue(), ^{ 104 | if (completion) completion(error); 105 | }); 106 | }]; 107 | } 108 | else { 109 | [self addUbiquityContainerNotificationObservers]; 110 | dispatch_async(dispatch_get_main_queue(), ^{ 111 | if (completion) completion(nil); 112 | }); 113 | } 114 | } 115 | 116 | #pragma mark - Root Directory 117 | 118 | - (void)setupRootDirectory:(void(^)(NSError *error))completion 119 | { 120 | [operationQueue addOperationWithBlock:^{ 121 | NSURL *newURL = [fileManager URLForUbiquityContainerIdentifier:ubiquityContainerIdentifier]; 122 | newURL = [newURL URLByAppendingPathComponent:rootDirectoryPath]; 123 | rootDirectoryURL = newURL; 124 | if (!rootDirectoryURL) { 125 | NSError *error = [NSError errorWithDomain:ICAErrorDomain code:ICAErrorCodeFileAccessFailed userInfo:@{NSLocalizedDescriptionKey : @"Could not retrieve URLForUbiquityContainerIdentifier. Check container id for iCloud."}]; 126 | [self dispatchCompletion:completion withError:error]; 127 | return; 128 | } 129 | 130 | NSError *error = nil; 131 | __block BOOL fileExistsAtPath = NO; 132 | __block BOOL existingFileIsDirectory = NO; 133 | NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; 134 | [coordinator coordinateReadingItemAtURL:rootDirectoryURL options:NSFileCoordinatorReadingWithoutChanges error:&error byAccessor:^(NSURL *newURL) { 135 | fileExistsAtPath = [fileManager fileExistsAtPath:newURL.path isDirectory:&existingFileIsDirectory]; 136 | }]; 137 | if (error) { 138 | [self dispatchCompletion:completion withError:error]; 139 | return; 140 | } 141 | 142 | if (!fileExistsAtPath) { 143 | [coordinator coordinateWritingItemAtURL:rootDirectoryURL options:0 error:&error byAccessor:^(NSURL *newURL) { 144 | [fileManager createDirectoryAtURL:newURL withIntermediateDirectories:YES attributes:nil error:NULL]; 145 | }]; 146 | } 147 | else if (fileExistsAtPath && !existingFileIsDirectory) { 148 | [coordinator coordinateWritingItemAtURL:rootDirectoryURL options:NSFileCoordinatorWritingForReplacing error:&error byAccessor:^(NSURL *newURL) { 149 | [fileManager removeItemAtURL:newURL error:NULL]; 150 | [fileManager createDirectoryAtURL:newURL withIntermediateDirectories:YES attributes:nil error:NULL]; 151 | }]; 152 | } 153 | 154 | [self dispatchCompletion:completion withError:error]; 155 | }]; 156 | } 157 | 158 | - (void)dispatchCompletion:(void(^)(NSError *error))completion withError:(NSError *)error 159 | { 160 | dispatch_sync(dispatch_get_main_queue(), ^{ 161 | if (completion) completion(error); 162 | }); 163 | } 164 | 165 | - (NSString *)fullPathForPath:(NSString *)path 166 | { 167 | return [rootDirectoryURL.path stringByAppendingPathComponent:path]; 168 | } 169 | 170 | #pragma mark - Notifications 171 | 172 | - (void)removeUbiquityContainerNotificationObservers 173 | { 174 | [[NSNotificationCenter defaultCenter] removeObserver:ubiquityIdentityObserver]; 175 | ubiquityIdentityObserver = nil; 176 | } 177 | 178 | - (void)addUbiquityContainerNotificationObservers 179 | { 180 | [self removeUbiquityContainerNotificationObservers]; 181 | 182 | __weak typeof(self) weakSelf = self; 183 | ubiquityIdentityObserver = [[NSNotificationCenter defaultCenter] addObserverForName:NSUbiquityIdentityDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { 184 | __strong typeof(weakSelf) strongSelf = weakSelf; 185 | [strongSelf stopMonitoring]; 186 | [strongSelf willChangeValueForKey:@"identityToken"]; 187 | [strongSelf didChangeValueForKey:@"identityToken"]; 188 | [self connect]; 189 | }]; 190 | } 191 | 192 | #pragma mark - Connection 193 | 194 | - (BOOL)isConnected 195 | { 196 | return fileManager.ubiquityIdentityToken != nil; 197 | } 198 | 199 | - (void)connect 200 | { 201 | BOOL loggedIn = fileManager.ubiquityIdentityToken != nil; 202 | if (loggedIn) [self performInitialPreparation:NULL]; 203 | } 204 | 205 | #pragma mark - Metadata Query to download new files 206 | 207 | - (void)startMonitoringMetadata 208 | { 209 | [self stopMonitoring]; 210 | 211 | if (!rootDirectoryURL) return; 212 | 213 | // Determine downloading key and set the appropriate predicate. This is OS dependent. 214 | NSPredicate *metadataPredicate = nil; 215 | 216 | #if (__IPHONE_OS_VERSION_MIN_REQUIRED < 70000) && (__MAC_OS_X_VERSION_MIN_REQUIRED < 1090) 217 | metadataPredicate = [NSPredicate predicateWithFormat:@"%K = FALSE AND %K = FALSE AND %K BEGINSWITH %@", 218 | NSMetadataUbiquitousItemIsDownloadedKey, NSMetadataUbiquitousItemIsDownloadingKey, NSMetadataItemPathKey, rootDirectoryURL.path]; 219 | #else 220 | metadataPredicate = [NSPredicate predicateWithFormat:@"%K != %@ AND %K = FALSE AND %K BEGINSWITH %@", 221 | NSMetadataUbiquitousItemDownloadingStatusKey, NSMetadataUbiquitousItemDownloadingStatusCurrent, NSMetadataUbiquitousItemIsDownloadingKey, NSMetadataItemPathKey, rootDirectoryURL.path]; 222 | #endif 223 | 224 | metadataQuery = [[NSMetadataQuery alloc] init]; 225 | metadataQuery.notificationBatchingInterval = 10.0; 226 | metadataQuery.searchScopes = [NSArray arrayWithObject:NSMetadataQueryUbiquitousDataScope]; 227 | metadataQuery.predicate = metadataPredicate; 228 | 229 | NSNotificationCenter *notifationCenter = [NSNotificationCenter defaultCenter]; 230 | [notifationCenter addObserver:self selector:@selector(initiateDownloads:) name:NSMetadataQueryDidFinishGatheringNotification object:metadataQuery]; 231 | [notifationCenter addObserver:self selector:@selector(initiateDownloads:) name:NSMetadataQueryDidUpdateNotification object:metadataQuery]; 232 | 233 | [metadataQuery startQuery]; 234 | } 235 | 236 | - (void)stopMonitoring 237 | { 238 | if (!metadataQuery) return; 239 | 240 | [metadataQuery disableUpdates]; 241 | [metadataQuery stopQuery]; 242 | 243 | [[NSNotificationCenter defaultCenter] removeObserver:self name:NSMetadataQueryDidFinishGatheringNotification object:metadataQuery]; 244 | [[NSNotificationCenter defaultCenter] removeObserver:self name:NSMetadataQueryDidUpdateNotification object:metadataQuery]; 245 | 246 | metadataQuery = nil; 247 | } 248 | 249 | - (void)initiateDownloads:(NSNotification *)notif 250 | { 251 | [metadataQuery disableUpdates]; 252 | 253 | NSUInteger count = [metadataQuery resultCount]; 254 | dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0); 255 | for ( NSUInteger i = 0; i < count; i++ ) { 256 | @autoreleasepool { 257 | NSMetadataItem *item = [metadataQuery resultAtIndex:i]; 258 | [self resolveConflictsForMetadataItem:item]; 259 | 260 | NSURL *url = [item valueForAttribute:NSMetadataItemURLKey]; 261 | dispatch_async(queue, ^{ 262 | NSError *error; 263 | [fileManager startDownloadingUbiquitousItemAtURL:url error:&error]; 264 | }); 265 | } 266 | } 267 | 268 | [metadataQuery enableUpdates]; 269 | } 270 | 271 | - (void)resolveConflictsForMetadataItem:(NSMetadataItem *)item 272 | { 273 | NSURL *fileURL = [item valueForAttribute:NSMetadataItemURLKey]; 274 | BOOL inConflict = [[item valueForAttribute:NSMetadataUbiquitousItemHasUnresolvedConflictsKey] boolValue]; 275 | if (inConflict) { 276 | NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; 277 | 278 | __block BOOL coordinatorExecuted = NO; 279 | __block BOOL timedOut = NO; 280 | 281 | dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, ICAFileCoordinatorTimeOut * NSEC_PER_SEC); 282 | dispatch_after(popTime, timeOutQueue, ^{ 283 | if (!coordinatorExecuted) { 284 | timedOut = YES; 285 | [coordinator cancel]; 286 | } 287 | }); 288 | 289 | NSError *coordinatorError = nil; 290 | [coordinator coordinateWritingItemAtURL:fileURL options:NSFileCoordinatorWritingForDeleting error:&coordinatorError byAccessor:^(NSURL *newURL) { 291 | dispatch_sync(timeOutQueue, ^{ coordinatorExecuted = YES; }); 292 | if (timedOut) return; 293 | [NSFileVersion removeOtherVersionsOfItemAtURL:newURL error:nil]; 294 | }]; 295 | if (timedOut || coordinatorError) return; 296 | 297 | NSArray *conflictVersions = [NSFileVersion unresolvedConflictVersionsOfItemAtURL:fileURL]; 298 | for (NSFileVersion *fileVersion in conflictVersions) { 299 | fileVersion.resolved = YES; 300 | } 301 | } 302 | } 303 | 304 | #pragma mark - File Operations 305 | 306 | static const NSTimeInterval ICAFileCoordinatorTimeOut = 10.0; 307 | 308 | - (NSError *)specializedErrorForCocoaError:(NSError *)cocoaError 309 | { 310 | NSError *error = cocoaError; 311 | if ([cocoaError.domain isEqualToString:NSCocoaErrorDomain] && cocoaError.code == NSUserCancelledError) { 312 | error = [NSError errorWithDomain:ICAErrorDomain code:ICAErrorCodeFileCoordinatorTimedOut userInfo:nil]; 313 | } 314 | return error; 315 | } 316 | 317 | - (NSError *)notConnectedError 318 | { 319 | NSError *error = [NSError errorWithDomain:ICAErrorDomain code:ICAErrorCodeConnectionError userInfo:@{NSLocalizedDescriptionKey : @"Attempted to access iCloud when not connected."}]; 320 | return error; 321 | } 322 | 323 | - (void)fileExistsAtPath:(NSString *)path completion:(void(^)(BOOL exists, BOOL isDirectory, NSError *error))block 324 | { 325 | [operationQueue addOperationWithBlock:^{ 326 | if (!self.isConnected) { 327 | dispatch_async(dispatch_get_main_queue(), ^{ 328 | if (block) block(NO, NO, [self notConnectedError]); 329 | }); 330 | return; 331 | } 332 | 333 | NSError *fileCoordinatorError = nil; 334 | __block NSError *timeoutError = nil; 335 | __block BOOL coordinatorExecuted = NO; 336 | __block BOOL isDirectory = NO; 337 | __block BOOL exists = NO; 338 | 339 | NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; 340 | 341 | dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, ICAFileCoordinatorTimeOut * NSEC_PER_SEC); 342 | dispatch_after(popTime, timeOutQueue, ^{ 343 | if (!coordinatorExecuted) { 344 | [coordinator cancel]; 345 | timeoutError = [NSError errorWithDomain:ICAErrorDomain code:ICAErrorCodeFileCoordinatorTimedOut userInfo:nil]; 346 | } 347 | }); 348 | 349 | NSURL *url = [NSURL fileURLWithPath:[self fullPathForPath:path]]; 350 | [coordinator coordinateReadingItemAtURL:url options:0 error:&fileCoordinatorError byAccessor:^(NSURL *newURL) { 351 | dispatch_sync(timeOutQueue, ^{ coordinatorExecuted = YES; }); 352 | if (timeoutError) return; 353 | exists = [fileManager fileExistsAtPath:newURL.path isDirectory:&isDirectory]; 354 | }]; 355 | 356 | NSError *error = fileCoordinatorError ? : timeoutError ? : nil; 357 | error = [self specializedErrorForCocoaError:error]; 358 | dispatch_async(dispatch_get_main_queue(), ^{ 359 | if (block) block(exists, isDirectory, error); 360 | }); 361 | }]; 362 | } 363 | 364 | - (void)contentsOfDirectoryAtPath:(NSString *)path completion:(void(^)(NSArray *contents, NSError *error))block 365 | { 366 | [operationQueue addOperationWithBlock:^{ 367 | if (!self.isConnected) { 368 | dispatch_async(dispatch_get_main_queue(), ^{ 369 | if (block) block(nil, [self notConnectedError]); 370 | }); 371 | return; 372 | } 373 | 374 | NSError *fileCoordinatorError = nil; 375 | __block NSError *timeoutError = nil; 376 | __block NSError *fileManagerError = nil; 377 | __block BOOL coordinatorExecuted = NO; 378 | 379 | NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; 380 | 381 | dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, ICAFileCoordinatorTimeOut * NSEC_PER_SEC); 382 | dispatch_after(popTime, timeOutQueue, ^{ 383 | if (!coordinatorExecuted) { 384 | [coordinator cancel]; 385 | timeoutError = [NSError errorWithDomain:ICAErrorDomain code:ICAErrorCodeFileCoordinatorTimedOut userInfo:nil]; 386 | } 387 | }); 388 | 389 | __block NSArray *contents = nil; 390 | NSURL *url = [NSURL fileURLWithPath:[self fullPathForPath:path]]; 391 | [coordinator coordinateReadingItemAtURL:url options:0 error:&fileCoordinatorError byAccessor:^(NSURL *newURL) { 392 | dispatch_sync(timeOutQueue, ^{ coordinatorExecuted = YES; }); 393 | if (timeoutError) return; 394 | 395 | NSDirectoryEnumerator *dirEnum = [fileManager enumeratorAtPath:[self fullPathForPath:path]]; 396 | if (!dirEnum) fileManagerError = [NSError errorWithDomain:ICAErrorDomain code:ICAErrorCodeFileAccessFailed userInfo:nil]; 397 | 398 | NSString *filename; 399 | NSMutableArray *mutableContents = [[NSMutableArray alloc] init]; 400 | while ((filename = [dirEnum nextObject])) { 401 | if ([@[@".DS_Store", @".", @".."] containsObject:filename]) continue; 402 | 403 | ICACloudFile *file = [ICACloudFile new]; 404 | file.name = filename; 405 | file.path = [path stringByAppendingPathComponent:filename];; 406 | file.fileAttributes = [dirEnum.fileAttributes copy]; 407 | [mutableContents addObject:file]; 408 | 409 | if ([dirEnum.fileAttributes.fileType isEqualToString:NSFileTypeDirectory]) { 410 | [dirEnum skipDescendants]; 411 | } 412 | } 413 | 414 | if (!fileManagerError) contents = mutableContents; 415 | }]; 416 | 417 | NSError *error = fileCoordinatorError ? : timeoutError ? : fileManagerError ? : nil; 418 | error = [self specializedErrorForCocoaError:error]; 419 | dispatch_async(dispatch_get_main_queue(), ^{ 420 | if (block) block(contents, error); 421 | }); 422 | }]; 423 | 424 | } 425 | 426 | - (void)createDirectoryAtPath:(NSString *)path completion:(void(^)(NSError *error))block 427 | { 428 | [operationQueue addOperationWithBlock:^{ 429 | if (!self.isConnected) { 430 | dispatch_async(dispatch_get_main_queue(), ^{ 431 | if (block) block([self notConnectedError]); 432 | }); 433 | return; 434 | } 435 | 436 | NSError *fileCoordinatorError = nil; 437 | __block NSError *timeoutError = nil; 438 | __block NSError *fileManagerError = nil; 439 | __block BOOL coordinatorExecuted = NO; 440 | 441 | NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; 442 | 443 | dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, ICAFileCoordinatorTimeOut * NSEC_PER_SEC); 444 | dispatch_after(popTime, timeOutQueue, ^{ 445 | if (!coordinatorExecuted) { 446 | [coordinator cancel]; 447 | timeoutError = [NSError errorWithDomain:ICAErrorDomain code:ICAErrorCodeFileCoordinatorTimedOut userInfo:nil]; 448 | } 449 | }); 450 | 451 | NSURL *url = [NSURL fileURLWithPath:[self fullPathForPath:path]]; 452 | [coordinator coordinateWritingItemAtURL:url options:0 error:&fileCoordinatorError byAccessor:^(NSURL *newURL) { 453 | dispatch_sync(timeOutQueue, ^{ coordinatorExecuted = YES; }); 454 | if (timeoutError) return; 455 | [fileManager createDirectoryAtPath:newURL.path withIntermediateDirectories:YES attributes:nil error:&fileManagerError]; 456 | }]; 457 | 458 | NSError *error = fileCoordinatorError ? : timeoutError ? : fileManagerError ? : nil; 459 | error = [self specializedErrorForCocoaError:error]; 460 | dispatch_async(dispatch_get_main_queue(), ^{ 461 | if (block) block(error); 462 | }); 463 | }]; 464 | } 465 | 466 | - (void)removeItemAtPath:(NSString *)path completion:(void(^)(NSError *error))block 467 | { 468 | [operationQueue addOperationWithBlock:^{ 469 | if (!self.isConnected) { 470 | dispatch_async(dispatch_get_main_queue(), ^{ 471 | if (block) block([self notConnectedError]); 472 | }); 473 | return; 474 | } 475 | 476 | NSError *fileCoordinatorError = nil; 477 | __block NSError *timeoutError = nil; 478 | __block NSError *fileManagerError = nil; 479 | __block BOOL coordinatorExecuted = NO; 480 | 481 | NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; 482 | 483 | dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, ICAFileCoordinatorTimeOut * NSEC_PER_SEC); 484 | dispatch_after(popTime, timeOutQueue, ^{ 485 | if (!coordinatorExecuted) { 486 | [coordinator cancel]; 487 | timeoutError = [NSError errorWithDomain:ICAErrorDomain code:ICAErrorCodeFileCoordinatorTimedOut userInfo:nil]; 488 | } 489 | }); 490 | 491 | NSURL *url = [NSURL fileURLWithPath:[self fullPathForPath:path]]; 492 | [coordinator coordinateWritingItemAtURL:url options:NSFileCoordinatorWritingForDeleting error:&fileCoordinatorError byAccessor:^(NSURL *newURL) { 493 | dispatch_sync(timeOutQueue, ^{ coordinatorExecuted = YES; }); 494 | if (timeoutError) return; 495 | [fileManager removeItemAtPath:newURL.path error:&fileManagerError]; 496 | }]; 497 | 498 | NSError *error = fileCoordinatorError ? : timeoutError ? : fileManagerError ? : nil; 499 | error = [self specializedErrorForCocoaError:error]; 500 | dispatch_async(dispatch_get_main_queue(), ^{ 501 | if (block) block(error); 502 | }); 503 | }]; 504 | } 505 | 506 | - (NSString *)temporaryDirectory 507 | { 508 | static NSString *tempDir = nil; 509 | if (!tempDir) { 510 | tempDir = [NSTemporaryDirectory() stringByAppendingString:@"ICACloudTempFiles"]; 511 | BOOL isDir; 512 | if (![fileManager fileExistsAtPath:tempDir isDirectory:&isDir] || !isDir) { 513 | NSError *error; 514 | [fileManager removeItemAtPath:tempDir error:NULL]; 515 | if (![fileManager createDirectoryAtPath:tempDir withIntermediateDirectories:YES attributes:nil error:&error]) { 516 | tempDir = nil; 517 | NSLog(@"Error creating temp dir for ICACloud: %@", error); 518 | } 519 | } 520 | } 521 | return tempDir; 522 | } 523 | 524 | - (void)uploadData:(NSData *)data toPath:(NSString *)toPath completion:(void(^)(NSError *error))completion 525 | { 526 | NSString *tempFile = [[self temporaryDirectory] stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]; 527 | [data writeToFile:tempFile atomically:NO]; 528 | [self uploadLocalFile:tempFile toPath:toPath completion:^(NSError *error) { 529 | [fileManager removeItemAtPath:tempFile error:NULL]; 530 | if (completion) completion(error); 531 | }]; 532 | } 533 | 534 | - (void)uploadLocalFile:(NSString *)fromPath toPath:(NSString *)toPath completion:(void(^)(NSError *error))block 535 | { 536 | [operationQueue addOperationWithBlock:^{ 537 | if (!self.isConnected) { 538 | dispatch_async(dispatch_get_main_queue(), ^{ 539 | if (block) block([self notConnectedError]); 540 | }); 541 | return; 542 | } 543 | 544 | NSError *fileCoordinatorError = nil; 545 | __block NSError *timeoutError = nil; 546 | __block NSError *fileManagerError = nil; 547 | __block BOOL coordinatorExecuted = NO; 548 | 549 | NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; 550 | 551 | dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, ICAFileCoordinatorTimeOut * NSEC_PER_SEC); 552 | dispatch_after(popTime, timeOutQueue, ^{ 553 | if (!coordinatorExecuted) { 554 | [coordinator cancel]; 555 | timeoutError = [NSError errorWithDomain:ICAErrorDomain code:ICAErrorCodeFileCoordinatorTimedOut userInfo:nil]; 556 | } 557 | }); 558 | 559 | NSURL *fromURL = [NSURL fileURLWithPath:fromPath]; 560 | NSURL *toURL = [NSURL fileURLWithPath:[self fullPathForPath:toPath]]; 561 | [coordinator coordinateReadingItemAtURL:fromURL options:0 writingItemAtURL:toURL options:NSFileCoordinatorWritingForReplacing error:&fileCoordinatorError byAccessor:^(NSURL *newReadingURL, NSURL *newWritingURL) { 562 | dispatch_sync(timeOutQueue, ^{ coordinatorExecuted = YES; }); 563 | if (timeoutError) return; 564 | [fileManager removeItemAtPath:newWritingURL.path error:NULL]; 565 | [fileManager copyItemAtPath:newReadingURL.path toPath:newWritingURL.path error:&fileManagerError]; 566 | }]; 567 | 568 | NSError *error = fileCoordinatorError ? : timeoutError ? : fileManagerError ? : nil; 569 | error = [self specializedErrorForCocoaError:error]; 570 | dispatch_async(dispatch_get_main_queue(), ^{ 571 | if (block) block(error); 572 | }); 573 | }]; 574 | } 575 | 576 | - (void)downloadDataFromPath:(NSString *)fromPath completion:(void(^)(NSData *data, NSError *error))completion 577 | { 578 | NSString *tempFile = [[self temporaryDirectory] stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]; 579 | [self downloadFromPath:fromPath toLocalFile:tempFile completion:^(NSError *error) { 580 | NSData *data = nil; 581 | if (!error) data = [NSData dataWithContentsOfFile:tempFile]; 582 | [fileManager removeItemAtPath:tempFile error:NULL]; 583 | if (completion) completion(data, error); 584 | }]; 585 | } 586 | 587 | 588 | - (void)downloadFromPath:(NSString *)fromPath toLocalFile:(NSString *)toPath completion:(void(^)(NSError *error))block 589 | { 590 | [operationQueue addOperationWithBlock:^{ 591 | if (!self.isConnected) { 592 | dispatch_async(dispatch_get_main_queue(), ^{ 593 | if (block) block([self notConnectedError]); 594 | }); 595 | return; 596 | } 597 | 598 | NSError *fileCoordinatorError = nil; 599 | __block NSError *timeoutError = nil; 600 | __block NSError *fileManagerError = nil; 601 | __block BOOL coordinatorExecuted = NO; 602 | 603 | NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; 604 | 605 | dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, ICAFileCoordinatorTimeOut * NSEC_PER_SEC); 606 | dispatch_after(popTime, timeOutQueue, ^{ 607 | if (!coordinatorExecuted) { 608 | [coordinator cancel]; 609 | timeoutError = [NSError errorWithDomain:ICAErrorDomain code:ICAErrorCodeFileCoordinatorTimedOut userInfo:nil]; 610 | } 611 | }); 612 | 613 | NSURL *fromURL = [NSURL fileURLWithPath:[self fullPathForPath:fromPath]]; 614 | NSURL *toURL = [NSURL fileURLWithPath:toPath]; 615 | [coordinator coordinateReadingItemAtURL:fromURL options:0 writingItemAtURL:toURL options:NSFileCoordinatorWritingForReplacing error:&fileCoordinatorError byAccessor:^(NSURL *newReadingURL, NSURL *newWritingURL) { 616 | dispatch_sync(timeOutQueue, ^{ coordinatorExecuted = YES; }); 617 | if (timeoutError) return; 618 | [fileManager removeItemAtPath:newWritingURL.path error:NULL]; 619 | [fileManager copyItemAtPath:newReadingURL.path toPath:newWritingURL.path error:&fileManagerError]; 620 | }]; 621 | 622 | NSError *error = fileCoordinatorError ? : timeoutError ? : fileManagerError ? : nil; 623 | error = [self specializedErrorForCocoaError:error]; 624 | dispatch_async(dispatch_get_main_queue(), ^{ 625 | if (block) block(error); 626 | }); 627 | }]; 628 | } 629 | 630 | @end 631 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Drew McCormack 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | iCloud Access 2 | === 3 | 4 | _Author:_ Drew McCormack
5 | _Created:_ 18th January, 2014
6 | _Last Updated:_ 18th January, 2014 7 | 8 | iCloud Access is a simple class that makes it easier to work with iCloud, hiding details such as file coordination and metadata queries. It is much more like accessing a web service with a Cocoa networking class, which most developers are more used to. 9 | 10 | The class was originally developed as part of the [Ensembles](https://github.com/drewmccormack/ensembles) Core Data Sync framework, and has been extracted for easier integration in projects not using Ensembles. 11 | 12 | #### Install 13 | Just drag the ICACloud.h and ICACloud.m files directly into your Mac or iOS Xcode project. 14 | 15 | #### Using ICACloud 16 | The methods of the class are fairly self explanatory, mirroring `NSFileManager` to some extent. One big difference is that most are asynchronous, with a completion callback block. The completion block includes an error parameter, which should be checked. If it is `nil`, the operation was successful, and if an `NSError` is supplied, it failed. 17 | 18 | Cloud paths are relative to the ubiquity container, but you can optionally supply a relative path to a root directory in the container. This directory, and intermediate directories, will be created automatically if they don't exist. 19 | 20 | You should check the `isConnected` property to make sure the user is logged into iCloud before using the class. 21 | 22 | Here is a simple example of using the `ICACloud` class. 23 | 24 | ICACloud *cloud = [[ICACloud alloc] initWithUbiquityContainerIdentifier:@"XXXXXXXXXX.com.mycompany.cloudtest" 25 | rootDirectoryPath:@"Path/To/Data/Root"]; 26 | if (cloud.isConnected) { 27 | [cloud createDirectoryAtPath:@"Subdirectory" completion:^(NSError *error) { 28 | if (error) { 29 | NSLog(@"Failed to create subdirectory"); 30 | return; 31 | } 32 | 33 | [cloud uploadLocalFile:@"/Users/me/Downloads/LocalImage.png" 34 | toPath:@"Subdirectory/CloudImage.png" 35 | completion:^(NSError *error) { 36 | if (error) NSLog(@"Failed to upload: %@", error); 37 | }]; 38 | }]; 39 | } 40 | --------------------------------------------------------------------------------