Einführung in CloudKit (Teil 2)

Im vorherigen Beitrag habe ich einen Überblick zu möglichen Anwendungsfällen, dem Aufbau und den Vor- und Nachteilen von CloudKit gegeben. Des Weiteren wurde gezeigt, wie CloudKit und ein iCloud Container für eine App ID erstellt sowie in einem Xcode-Projekt aktiviert wurde.

Im Folgenden werde ich anhand einer kleinen Beispielanwendung die Einbindung von CloudKit zeigen.

Beispielanwendung

Die Beispielanwendung soll eine einfache Notizenverwaltung darstellen. Dazu kann der Nutzer neue Notizen, bestehend aus einem Titel und einer Beschreibung erstellen. Diese wird dann mittels CloudKit in die private Standard-Datenbank des Nutzer im iCloud Account gespeichert.
Notizen können in der App editiert und auch wieder gelöscht werden. Änderungen und gelöschte Notizen werden umgehend in der iCloud gespeichert.

Nicht betrachtet werden in diesem Beispiel mögliche Konflikte durch Änderung eines NotesItems über z.B. durch eine Desktop-App oder auf einem anderen iOS Device des Nutzers. In diesem Beispiel soll die einfache Integration von CloudKit und das grundsätzliche Verfahren erläutert werden.
Ein Ausblick für weitere Ideen zur App als auch Lesestoff zu CloudKit werden am Ende gegeben.

Zugriff auf CloudKit

Um auf die iCloud zugreifen zu können muss ein eingeloggter iCloud Account auf dem Gerät hinterlegt sein. Es ist möglich aus einer App heraus den Login für iCloud zu erfragen, dies soll aber nicht Bestandteil dieses Beispiels sein.
Bevor der Zugriff auf Daten in der iCloud geschehen kann sollte geprüft werden, ob die App berechtigt ist auf iCloud zugreifen zu dürfen.

- (void)requestDiscoverabilityPermission:(void (^)(BOOL discoverable)) completionHandler {
    
    [self.container requestApplicationPermission:CKApplicationPermissionUserDiscoverability
                               completionHandler:^(CKApplicationPermissionStatus applicationPermissionStatus, NSError *error) {
                                   if (error) {
                                       // In your app, handle this error really beautifully.
                                       NSLog(@"An error occured in %@: %@", NSStringFromSelector(_cmd), error);
                                       abort();
                                   } else {
                                       dispatch_async(dispatch_get_main_queue(), ^{
                                           completionHandler(applicationPermissionStatus == CKApplicationPermissionStatusGranted);
                                       });
                                   }
                               }];
}

Hat eine App keinen Zugriff auf die iCloud kann CloudKit keine Daten verarbeiten. Zugriff kann über Settings > iCloud > iCloud Drive für jede App separat verwaltet werden.

Ist der Zugriff gestattet kann der Standardcontainer als auch die private Standarddatenbank referenziert werden:

- (id)init {
    self = [super init];
    if (self) {
        _container = [CKContainer defaultContainer];
        _privateDB = [_container privateCloudDatabase];
    }
    
    return self;
}

 

Notiz anlegen / ändern

Die Detailansicht zum Anlegen und Ändern einer Notiz besteht aus zwei Eingabefeldern und einem Button zum Speichern.
Beim Erstellen einer Notiz wird ein leeres CKRecord Objekt erstellt, wobei dann die entsprechenden Keys mit den Werten (Values) aus den Textfeldern gefüllt werden. Wird eine bestehende Notiz editiert, wird die im Controller existierende CKRecord (self.record) Instanz weiterverwendet und die entsprechenden Einträge aktualisiert.

- (IBAction)add:(id)sender {
    
    if (self.tfTitle.text.length < 1) {
        [self.tfTitle resignFirstResponder];
    } else {
        CKRecord *newRecord;
        
        if(self.record) {
            newRecord = self.record;
        } else {
            newRecord = [[CKRecord alloc] initWithRecordType:ReferenceItemRecordName];
        }
        
        newRecord[@"title"] = self.tfTitle.text;
        newRecord[@"desc"] = self.tfDesc.text;

        [self.tfDesc resignFirstResponder];
        
        [self.cloudStore saveRecord:newRecord completionHandler:^(NSError *error) {
            if(error) {
                
            } else {
                [self.navigationController popViewControllerAnimated:YES];

            }
        }];
    }
}

Da das Speichern einen Moment dauern kann, wird mittels completionHandler:^() auf das erfolgreiche Abspeichern gewartet, eh, bei Erfolg, automatisch zur Übersichtsseite zurückgesprungen wird.
Sollte beim Anlegen bzw. Editieren der Notiz ein Fehler auftreten kann der NSError entsprechend verarbeitet werden. Dies wurde im vorliegenden Beispiel nur angedeutet.

Das eigentliche Speichern einer CKRecord Instanz vom Typ NotesItems findet gekapselt in einer UCDCloudStore Instanz statt. Hierbei wird das CKRecord Objekt in der privaten Datenbank in der iCloud gespeichert:

- (void)saveRecord:(CKRecord *)record completionHandler:(void (^)(NSError *error))theCompletionHandler {
    
    [self.privateDB saveRecord:record completionHandler:^(CKRecord *record, NSError *error) {
        if (error) {
            // In your app, handle this error awesomely.
            NSLog(@"An error occured in %@: %@", NSStringFromSelector(_cmd), error);
            abort();
        } else {
            NSLog(@"Successfully saved record");
        }
        
        dispatch_async(dispatch_get_main_queue(), ^(void){
            theCompletionHandler(error);
        });
    }];
}

 

Alle Notizen laden

Um alle Notizen zu laden, muss ein CKQuery erstellt werden. Dieser kann, ähnlich wie bei CoreData Anwendungen, mittels NSPredicate gefiltert und / oder mittels NSSortDescriptor sortiert werden.

Ein Hinweis zur Sortierung: standardmäßig werden für jeden Record Type die Attribute “ID”, “Created By”, “Date created”, “Date modified” und “Modified by” angelegt. Um nach diesen sortieren zu können, muss dies explizit für den Record Type erlaubt sein. Dies ist über die Eigenschaft Metadata Indexes möglich:

Festlegung, nach welchen Attributen u.a. sortiert werden darf
Festlegung, nach welchen Attributen u.a. sortiert werden darf

In der Beispielanwendung werden alle NotesItems nach dem creationDate sortiert, es findet aber keine Filterung statt:

- (void)fetchRecordsWithType:(NSString *)recordType completionHandler:(void (^)(NSArray *records))completionHandler {
    
    NSPredicate *truePredicate = [NSPredicate predicateWithValue:YES];
    CKQuery *query = [[CKQuery alloc] initWithRecordType:recordType predicate:truePredicate];
    query.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]];
    
    CKQueryOperation *queryOperation = [[CKQueryOperation alloc] initWithQuery:query];
    NSMutableArray *results = [[NSMutableArray alloc] init];
    
    queryOperation.recordFetchedBlock = ^(CKRecord *record) {
        [results addObject:record];
    };
    
    queryOperation.queryCompletionBlock = ^(CKQueryCursor *cursor, NSError *error) {
        if (error) {
            // In your app, this error needs love and care.
            NSLog(@"An error occured in %@: %@", NSStringFromSelector(_cmd), error);
            //abort();
        } else {
            
            NSLog(@"fetchRecordsWithType Finished!");
            
            dispatch_async(dispatch_get_main_queue(), ^(void){
                completionHandler(results);
            });
        }
    };
    
    [self.privateDB addOperation:queryOperation];
}

 

Notizen darstellen und löschen

Alle Notizen werden in einer einfachen UITableView dargestellt. Das Anwählen einer Notiz öffnet die Ansicht zum Editieren der Notiz. Durch einen horizentalen Swipe kann eine Notiz in der Übersicht gelöscht werden.

Die vollständige Umsetzung ist im finalen Projekt zu sehen, welches über github bezogen werden kann: https://github.com/soraxdesign/CloudKit-Test

Ausblick

Die App kann relativ leicht noch um die Möglichkeit zur Einbindung von Bildern und weiteren Attributen einer Notiz sowie Funktionen zur Verwaltung ergänzt werden (z.B. Gruppierung, Sortieren nach Änderungsdatum, Suche, Caching etc.).

Spannend wird die Umsetzung mit CloudKit im Zusammenspiel mit einem Desktopclient oder einer Installation der App auf einem zweiten Device mit Zugang zum gleichen iCloud Account. Dann werden Herausforderungen wie das Auflösen von Konflikten und zusammenführen von Informationen relevant. Ebenso sollte die Möglichkeit bestehen, die Daten offline zu bearbeiten und das spätere Übertragen in die iCloud bei verfügbarer Netzverbindung.

Apple bietet einen guten Einstieg in CloudKit an:
https://developer.apple.com/library/ios/documentation/Miscellaneous/Conceptual/CloudKitQuickStart/
Introduction/Introduction.html#//apple_ref/doc/uid/TP40014987-CH1-SW1

Das Video der WWDC 2014 zu CloudKit ist ebenso empfehlenswert:
https://developer.apple.com/videos/wwdc/2014/#231