22 Kwiecień

“Obietnice” na przykładzie PromiseKit w Objective-C, cz.1

Każdy programista, prędzej czy później natrafia na problem synchronicznego wykonania po sobie pewnych czynności. Przykładowo: pobierz informację o użytkowniku z API, sparsuj odpowiedź serwera, zapisz dane do bazy, zaktualizuj widok i wiele innych. Do tego wszystkiego dochodzi jeszcze obsługa błędów na części z tych etapów. Pierwsze co nasuwa się na myśl i wydaje się być najbardziej rozsądne oraz “na czasie” to użycie bloków. Jak mógłby wyglądać taki kod?

// W tym przykładzie pominąłem implementację poniższych metod, ponieważ są to klasyczne metody z arguementami blokowymi

[self downloadUserInfo:^(id result, NSError *error) {
        if (!error) {
            [self parseUserInfo:result completion:^(NSDictionary *userInfo, NSError *error) {
                if (!error) {
                    [self saveUserInfoToDB:userInfo completion:^(BOOL success, NSError *error) {
                        if (!error) {
                            [self updateView];
                        }
                        else {
                            // zrób coś z błędem
                        }
                    }];
                }
                else {
                    // zrób coś z błędem
                }
            }];
        }
        else {
            // zrób coś z błędem
        }
    }];

Mimo tak prostej operacji uzyskujemy spory “spaghetty code”, co nie wygląda najlepiej. W takich sytuacjach dobrze jest skorzystać z “obietnicy”, czyli w naszym przypadku z PromiseKit dla Objective-C. Zobaczmy, jak będzie wyglądał nasz kod, kiedy użyjemy PromiseKit’a.

- (PMKPromise *)downloadUserInfo
{
    return [PMKPromise new:^(PMKFulfiller fulfill, PMKRejecter reject) {
        NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
        [session dataTaskWithURL:[NSURL URLWithString:@"adres_api.com"]
               completionHandler:^(NSData *_Nullable data, NSURLResponse *_Nullable response, NSError *_Nullable error) {
                   if (error) {
                       reject(error);
                   }
                   else {
                       fulfill(data);
                   }
               }];
    }];
}

- (PMKPromise *)parseUserInfo:(id)responseObject
{
    return [PMKPromise new:^(PMKFulfiller fulfill, PMKRejecter reject) {
        NSError *error = nil;
        NSDictionary *dictObj =
            [NSJSONSerialization JSONObjectWithData:responseObject options:NSJSONReadingMutableContainers error:&error];
        if (error) {
            reject(error);
        }
        else {
            fulfill(dictObj);
        }
    }];
}

- (PMKPromise *)saveUserInfoToDB:(NSDictionary *)userInfo
{
    return [PMKPromise new:^(PMKFulfiller fulfill, PMKRejecter reject) {
        NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        NSManagedObject *user = [NSEntityDescription insertNewObjectForEntityForName:@"User" inManagedObjectContext:context];
        [user setValue:userInfo[@"user_name"] forKey:@"userName"];
        // ...
        NSError *error = nil;
        [context save:&error];
        if (error) {
            reject(error);
        }
        else {
            fulfill(user.objectID);
        }
    }];
}

// Przykład wywołania zdefiniowanych powyżej obietnic

[self downloadUserInfo]
        .then(^(id responseObject) {
            return [self parseUserInfo:responseObject];
        })
        .then(^(NSDictionary *userDict) {
            return [self saveUserInfoToDB:userDict];
        })
        .then(^(NSManagedObject *user) {
            [self updateView];
        })
        .catch(^(NSError *error) {
            NSLog(@"%@", error);
            // obsłuż błąd
        })
        .finally(^{
            // wyłącz activity indictor itp
        });

Od razu widać, że kod jest bardziej przejrzysty i czytelny. Dodatkowo zyskujemy możliwość obsługi błędu w jednym miejscu. Oczywiście nic nie stoi na przeszkodzie, żeby “catch” użyć po dowolnym “then”, wtedy błąd rzucony za pomocą “reject(error)” zostanie złapany przez pierwszy napotkany “catch” w łańcuchu. Można w nim obsłużyć błąd i podobnie, jak w “then” zwrócić kolejny promise, by kontynuować wywołania poszczególnych “ogniw” łańcucha. Moduł finally zawsze umieszczany jest na końcu i wtedy też wykonywany (nawet jeśli “catch” nie zwraca nic). Nie zwraca on żadnej wartości, dlatego dobrze jest użyć go np. do ukrycia progresu ładowania danych.

Powyższy przykład to zaledwie część tego, co oferuje nam PromiseKit, dlatego zachęcam do zapoznania się z tą biblioteką. Została ona bardzo dobrze opisana na oficjalnej stronie: www.promisekit.org. W części drugiej opiszę przykład “opakowania” delegacji w Objective-C za pomocą PromiseKit.