Beginning iCloud
development
Rocchi Cesare




    @_funkyboy   studiomagnolia.com
Outline


 What is iCloud?
 How does it work?
 Are there alternatives?
Who am I?
UX designer and developer
mnml
< is >
execution matters
lean approach
1000 details coming together
Giveaway
1 of the Wenderlich’s


                raywenderlich.com
ARC                     GameCenter API

           Storyboards

                            News Stand
  iCloud




Turn Based Gaming           OpenGL ES 2.0
twitter.com/_funkyboy
cesare@studiomagnolia.com
Giveaway
Giveaway
(yes, another)
www.icloudfordevelopers.com
UIDocument

                              Key-Value store
 Conflict
Resolution

                                       CoreData

  Custom
 Documents

         www.icloudfordevelopers.com
twitter.com/_funkyboy
cesare@studiomagnolia.com




     www.icloudfordevelopers.com
Who are you?
What is iCloud?
6028 Startown Rd, Maiden, NC
Stores and synchs stuff
It just works ...
... when it works
Seamlessness can be a limit
Pros (for devs)


 No server setup
 No costs
 No rumination on synch
Cons (for devs)


 Stick to a synch model
 No http API
 No control on upload
Pros and Cons for users



Expectation
Under the hood
Daemon


Monitors changes
Works on metadata
Shreds files
Special folder, synched
Synched when “appropriate”
Appropriate


Which OS?
Which connection?
Battery status?
Placeholders
Information Structure


 Document
 Key-value
 CoreData
UIDocument
UIDocument


NSFilePresenter
Non-blocking read/write
-(void) openWithCompletionHandler:^(BOOL success) { }
- (BOOL)loadFromContents:(id)contents
                  ofType:(NSString *)typeName
                   error:(NSError **)outError { }
@interface SMNote : UIDocument
@implementation SMNote

- (BOOL)loadFromContents:(id)contents
                  ofType:(NSString *)typeName
                   error:(NSError **)outError {


    if ([contents length] > 0) {
        self.myContent = [[NSString alloc]
                            initWithBytes:[contents bytes]
                            length:[contents length]
                            encoding:NSUTF8StringEncoding];
    } else {

        // Default content
        self.myContent = @"Empty";

    }

    return YES;

}
- (BOOL) saveToURL:(NSURL *)url
  forSaveOperation:UIDocumentSaveForOverwriting
 completionHandler:^(BOOL success) { }
- (id)contentsForType:(NSString *)typeName
                error:(NSError **)outError {}
- (id)contentsForType:(NSString *)typeName
                error:(NSError **)outError {


    return [NSData dataWithBytes:[self.myContent UTF8String]
                          length:[self.myContent length]];


}
Autosave


updateChangeCount:
use the methods of the undoManager
@implementation SMNote

@synthesize noteContent;

// Called whenever the application reads data
- (BOOL)loadFromContents:(id)contents
                  ofType:(NSString *)typeName
                   error:(NSError **)outError {

}

// Called whenever the application (auto)saves the content
- (id)contentsForType:(NSString *)typeName
                error:(NSError **)outError {


}
Opening a document
Opening a document


Build and run a query
Wait for results
Unfold results
#import "SMNote.h"

@interface SMAppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) SMViewController *viewController;

@property (strong) SMNote *doc;
@property (strong) NSMetadataQuery *query;

- (void)loadDocument;

@end
NSMetadataQuery
- (void)loadDocument {

    NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
    _query = query;

    [query setSearchScopes:[NSArray arrayWithObject:
                     NSMetadataQueryUbiquitousDocumentsScope]];

}
- (void)loadDocument {

    NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
    _query = query;

    [query setSearchScopes:[NSArray arrayWithObject:
                     NSMetadataQueryUbiquitousDocumentsScope]];

    NSPredicate *pred = [NSPredicate predicateWithFormat:
                         @"%K == %@",
                         NSMetadataItemFSNameKey, kFILENAME];

    [query setPredicate:pred];

}
- (void)loadDocument {

    NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
    _query = query;

    [query setSearchScopes:[NSArray arrayWithObject:
                     NSMetadataQueryUbiquitousDocumentsScope]];

    NSPredicate *pred = [NSPredicate predicateWithFormat:
                         @"%K == %@",
                         NSMetadataItemFSNameKey, kFILENAME];

    [query setPredicate:pred];

    [[NSNotificationCenter defaultCenter]
      addObserver:self
         selector:@selector(queryDidFinish:)
              name:NSMetadataQueryDidFinishGatheringNotification
           object:query];

    [query startQuery];

}
NSPredicate *pred = [NSPredicate predicateWithFormat:
                                    @"%K like 'Note_*'",
                                    NSMetadataItemFSNameKey];
Asynchronous!
- (void)queryDidFinish:(NSNotification *)notification {

    NSMetadataQuery *query = [notification object];
    [query disableUpdates];
    [query stopQuery];

    [[NSNotificationCenter defaultCenter]
    removeObserver:self
              name:NSMetadataQueryDidFinishGatheringNotification
            object:query];

    _query = nil;

!   [self loadData:query];

}
- (void)loadData:(NSMetadataQuery *)query {

    if ([query resultCount] == 1) {

        NSMetadataItem *item = [query resultAtIndex:0];

        NSURL *url = [item valueForAttribute:NSMetadataItemURLKey];

        SMNote *doc = [[SMNote alloc] initWithFileURL:url];

    }

}
- (void)loadData:(NSMetadataQuery *)query {

    if ([query resultCount] == 1) {

        NSMetadataItem *item = [query resultAtIndex:0];

        NSURL *url = [item valueForAttribute:NSMetadataItemURLKey];

        self.doc = [[SMNote alloc] initWithFileURL:url];

        [self.doc openWithCompletionHandler:^(BOOL success) {

               if (success) {
                   NSLog(@"iCloud document opened");
               } else {
                   NSLog(@"failed opening document from iCloud");
               }
         }];

    }

}
else {

    NSURL *ubiq = [[NSFileManager defaultManager]
                    URLForUbiquityContainerIdentifier:nil];

    NSURL *ubiquitousPackage =
          [[ubiq URLByAppendingPathComponent: @"Documents"]
                    URLByAppendingPathComponent:kFILENAME];

    SMNote *doc = [[SMNote alloc]
                        initWithFileURL:ubiquitousPackage];
}
else {

     NSURL *ubiq = [[NSFileManager defaultManager]
                     URLForUbiquityContainerIdentifier:nil];

     NSURL *ubiquitousPackage =
           [[ubiq URLByAppendingPathComponent: @"Documents"]
                     URLByAppendingPathComponent:kFILENAME];

     SMNote *doc = [[SMNote alloc]
                         initWithFileURL:ubiquitousPackage];
     self.doc = doc;

   [doc saveToURL: [doc fileURL]
 forSaveOperation:UIDocumentSaveForCreating
completionHandler:^(BOOL success) {

        if (success) {
            [doc openWithCompletionHandler:^(BOOL success) {
                  NSLog(@"new document opened from iCloud");
             }];
          }
      }];

 }
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    ...
    [self.window makeKeyAndVisible];

    NSURL *ubiq = [[NSFileManager defaultManager]
                            URLForUbiquityContainerIdentifier:nil];

    if (ubiq) {

        NSLog(@"iCloud access at %@", ubiq);
        [self loadDocument];

    } else {

        NSLog(@"No iCloud access");

    }

    return YES;
}
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    ...
    [self.window makeKeyAndVisible];

    NSURL *ubiq = [[NSFileManager defaultManager]
                            URLForUbiquityContainerIdentifier:nil];

    if (ubiq) {

        NSLog(@"iCloud access at %@", ubiq);
        [self loadDocument];

    } else {

        NSLog(@"No iCloud access");

    }

    return YES;
}
- (void)viewDidLoad {

    [super viewDidLoad];

    [[NSNotificationCenter defaultCenter]
                     addObserver:self
                        selector:@selector(dataReloaded:)
                            name:@"noteModified"
                          object:nil];
}




- (void)dataReloaded:(NSNotification *)notification {

    self.doc = notification.object;
    self.noteView.text = self.doc.noteContent;

}
Switching on/off
- (NSURL *) localNotesURL {
    return [[[NSFileManager defaultManager]
             URLsForDirectory:NSDocumentDirectory
             inDomains:NSUserDomainMask] lastObject];
}



- (NSURL *) ubiquitousNotesURL {
    return [[[NSFileManager defaultManager]
             URLForUbiquityContainerIdentifier:nil]
            URLByAppendingPathComponent:@"Documents"];
}
- (void) setNoteUbiquity {

    NSURL *baseUrl = [self localNotesURL];

    if (_useiCloud)
        baseUrl = [self ubiquitousNotesURL];


    NSURL *destUrl = [baseUrl URLByAppendingPathComponent:
                          [note.fileURL lastPathComponent]];

    [[NSFileManager defaultManager] setUbiquitous:_useiCloud
                                        itemAtURL:note.fileURL
                                   destinationURL:destUrl
                                            error:NULL];

}


                             Don’t call it on the main thread!
- (void) startMigration {

    NSOperationQueue *iCloudQueue = [NSOperationQueue new];
    NSInvocationOperation *op =
       [[NSInvocationOperation alloc]
                     initWithTarget:self
                           selector:@selector(setNoteUbiquity)
                             object:nil];

    [iCloudQueue addOperation:op];

}
Custom documents
SMNotesDocument



  SMNote      SMNote   SMNote
                                ...
@interface SMNote : NSObject <NSCoding>

@property   (copy, nonatomic) NSString   *noteId;
@property   (copy, nonatomic) NSString   *noteContent;
@property   (strong, nonatomic) NSDate   *createdAt;
@property   (strong, nonatomic) NSDate   *updatedAt;

@end
#import "SMNote.h"

@interface SMNotesDocument : UIDocument

@property (nonatomic, strong) NSMutableArray *entries;
@property (nonatomic, strong) NSFileWrapper *fileWrapper;

@end
#import "SMNote.h"

@interface SMNotesDocument : UIDocument

@property (nonatomic, strong) NSMutableArray *entries;
@property (nonatomic, strong) NSFileWrapper *fileWrapper;

@end
- (id)contentsForType:(NSString *)typeName
                error:(NSError **)outError {

    NSMutableDictionary *w = [NSMutableDictionary dictionary];
    NSMutableData *data = [NSMutableData data];

    NSKeyedArchiver *arch = [[NSKeyedArchiver alloc]
                             initForWritingWithMutableData:data];
    [arch encodeObject:_entries forKey:@"entries"];
    [arch finishEncoding];

}
- (id)contentsForType:(NSString *)typeName
                error:(NSError **)outError {

    NSMutableDictionary *w = [NSMutableDictionary dictionary];
    NSMutableData *data = [NSMutableData data];

    NSKeyedArchiver *arch = [[NSKeyedArchiver alloc]
                             initForWritingWithMutableData:data];
    [arch encodeObject:_entries forKey:@"entries"];
    [arch finishEncoding];
    NSFileWrapper *entriesWrapper = [[NSFileWrapper alloc]
                               initRegularFileWithContents:data];

    [w setObject:entriesWrapper forKey:@"notes.dat"];
    // add other wrappers if you like

    NSFileWrapper *res = [[NSFileWrapper alloc]
                                initDirectoryWithFileWrappers:w];

    return res;

}
- (BOOL)loadFromContents:(id)contents
                  ofType:(NSString *)typeName
                   error:(NSError **)outError {

    NSFileWrapper *wrapper = (NSFileWrapper *)contents;
    NSDictionary *d = [wrapper fileWrappers];

    NSFileWrapper *entriesWrap = [d objectForKey:@"notes.dat"];

}
- (BOOL)loadFromContents:(id)contents
                  ofType:(NSString *)typeName
                   error:(NSError **)outError {

    NSFileWrapper *wrapper = (NSFileWrapper *)contents;
    NSDictionary *d = [wrapper fileWrappers];

    NSFileWrapper *entriesWrap = [d objectForKey:@"notes.dat"];
    NSData *data = [entriesWrap regularFileContents];

    NSKeyedUnarchiver *arch = [[NSKeyedUnarchiver alloc]
                                    initForReadingWithData:data];
    _entries = [arch decodeObjectForKey:@"entries"];

    // Notify the view

}
Uniform Type Identifier
Key-value
Key-value
   1Mb
Key-value
    1Mb
 was 64Kb !
- (void) saveNoteAsCurrent {

    [[NSUbiquitousKeyValueStore defaultStore]
               setString:self.currentNote.noteId
                  forKey:@"com.studiomagnolia.currentNote"];

    [[NSUbiquitousKeyValueStore defaultStore] synchronize];

}
- (void) saveNoteAsCurrent {

      [[NSUbiquitousKeyValueStore defaultStore]
                 setString:self.currentNote.noteId
                    forKey:@"com.studiomagnolia.currentNote"];

      [[NSUbiquitousKeyValueStore defaultStore] synchronize];

}




    NSString *currentNoteId =
        [[NSUbiquitousKeyValueStore defaultStore] stringForKey:
         @"com.studiomagnolia.currentNote"];
NSUbiquitousKeyValueStore* store =
    [NSUbiquitousKeyValueStore defaultStore];

[[NSNotificationCenter defaultCenter]
 addObserver:self
    selector:@selector(updateCurrentNoteIfNeeded:)
        name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification
        object:store];

[store synchronize];
Conflict Resolution
Conflict Resolution


Up to the dev
documentState
DocumentStates

UIDocumentStateNormal
UIDocumentStateClosed
UIDocumentStateInConflict
UIDocumentStateSavingError
UIDocumentStateEditingDisabled
UIDocumentStateChangedNotification
[[NSNotificationCenter defaultCenter]
                   addObserver:self
                      selector:@selector(noteHasChanged:)
                          name:UIDocumentStateChangedNotification
                        object:nil];
UIDocumentState s = [n documentState];

switch (s) {

    case UIDocumentStateNormal:
        NSLog(@"Everything is fine");
        break;

    case UIDocumentStateInConflict:
        NSLog(@"There is a conflict");
        break;

    ...

    default:
        NSLog(@"Unknown state");
        break;

}
UI conflict
      vs
iCloud conflict
Resolution policy


 last wins
 prompt user
 automatic merge
Resolution policy

 last wins
 prompt user
 automatic merge
 NSFileVersion
NSError *err;

NSURL *url = [[NSFileManager defaultManager]
   URLForPublishingUbiquitousItemAtURL:[self.currentNote fileURL]
                        expirationDate:&expirationInOneHourSinceNow
                                 error:&err];
Tips & Tricks
Patience!
Test on wireless & 3G
Regenerate provisioning
Delete previous data
Restart device
API throttle!
App policy


Be gentle with storage
<App_home>/tmp
<App_home>/Library/Caches/
App policy


Documents is backed up
mark files as “do not backup”
// iOS 5.0.1

#import <sys/xattr.h>

- (BOOL)addSkipBackupAttributeToItemAtURL:(NSURL *)URL {

    const char* filePath = [[URL path] fileSystemRepresentation];

    const char* attrName = "com.apple.MobileBackup";
    u_int8_t attrValue = 1;

    int result = setxattr(filePath, attrName, &attrValue,
                          sizeof(attrValue), 0, 0);
    return result == 0;

}
// iOS 5.1

- (BOOL)addSkipBackupAttributeToItemAtURL:(NSURL *)URL {

    NSError *error = nil;

    BOOL success = [URL setResourceValue: [NSNumber numberWithBool:YES]
                                  forKey: NSURLIsExcludedFromBackupKey
                                   error: &error];
    if(!success){

        NSLog(@"Error excluding %@ from backup %@",
                           [URL lastPathComponent],
                           error);
    }

    return success;
}
“To iCloud or not to iCloud?”
Alternatives
Alternatives

 dropbox
 parse.com
 cloudmine
 stackmob
 custom
Dropbox


documents
authentication
no notifications
Dropbox


other platforms
no CR (revision #)
expectation
Parse
Parse


ORM approach
Recently released
No cost of infrastructure
Parse


Pay as you use
Limit of calls/mo
PFObject *note = [PFObject objectWithClassName:@"Note"];

[note setObject:@"Ciao"
         forKey:@"title"];

[note setObject:@"Note on Parse"
         forKey:@"content"];

[note save];
//[note saveInBackground];
//[note saveEventually];
[note saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error)
{
    if (error) {

          NSLog(@"Note not saved");

      } else {

          NSLog(@"Note saved successfully");

      }
}];
Parse

Other platforms
REST API
Push notifications
Object browser
curl -X POST 
-H "X-Parse-Application-Id: ${APPLICATION_ID}" 
-H "X-Parse-REST-API-Key: ${REST_API_KEY}" 
-H "Content-Type: application/json" 
-d '{"note": 001,
     "title": "Ciao",
     "content": “Note on parse” }' 
https://api.parse.com/1/classes/GameScore
PFObject *note = [PFObject objectWithClassName:@"Note"];

[note setObject:@"Ciao"
           forKey:@"title"];

[note setObject:@"Note on parse"
           forKey:@"content"];

PFObject *myTag = [PFObject objectWithClassName:@"Tag"];

[myTag setObject:@"important"
          forKey:@"tagName"];

// Add a relation
[note setObject:myTag forKey:@"tag"];

// Saves both
[note saveInBackground];
Recap


UIDocument
Key-Value store
Alternatives
“You can’t always get what
           you want
but if you try sometime, you
      just might find ...”
“You can’t always get what
           you want
but if you try sometime, you
      just might find ...”
                      Rolling Stones
Contact


twitter.com/_funkyboy
cesare@studiomagnolia.com
http://studiomagnolia.com

Beginning icloud development - Cesare Rocchi - WhyMCA