Simplificarea utilizării API-ului RESTful și a persistenței datelor pe iOS cu Mantle și Realm
Publicat: 2022-03-11Fiecare dezvoltator iOS este familiarizat cu Core Data, un cadru de grafică obiect și persistență de la Apple. Pe lângă păstrarea datelor la nivel local, cadrul vine cu o serie de funcții avansate, cum ar fi urmărirea și anularea modificărilor obiectelor. Aceste caracteristici, deși utile în multe cazuri, nu vin gratuit. Necesită mult cod standard, iar cadrul în ansamblu are o curbă de învățare abruptă.
În 2014, Realm, o bază de date mobilă, a fost lansată și a luat cu asalt lumea dezvoltării. Dacă tot ce ne trebuie este să persistăm datele la nivel local, Realm este o alternativă bună. La urma urmei, nu toate cazurile de utilizare necesită caracteristicile avansate ale Core Data. Realm este extrem de ușor de utilizat și, spre deosebire de Core Data, necesită foarte puțin cod boilerplate. De asemenea, este sigur pentru fire și se spune că este mai rapid decât cadrul de persistență de la Apple.
În majoritatea aplicațiilor mobile moderne, datele persistente rezolvă jumătate din problemă. De multe ori trebuie să obținem date de la un serviciu de la distanță, de obicei printr-un API RESTful. Aici intervine Mantle. Este un cadru de model open-source pentru Cocoa și Cocoa Touch. Mantle simplifică semnificativ scrierea modelelor de date pentru interacțiunea cu API-urile care folosesc JSON ca format de schimb de date.
În acest articol, vom construi o aplicație iOS care preia o listă de articole împreună cu link-uri către acestea din API-ul de căutare de articole din New York Times v2. Lista va fi preluată folosind o cerere standard HTTP GET, cu modele de cerere și răspuns create folosind Mantle. Vom ajunge să vedem cât de ușor este cu Mantle să gestionezi transformările de valoare (de exemplu, de la NSDate la șir). Odată ce datele sunt preluate, le vom persista local folosind Realm. Toate acestea cu un cod standard minim.
API RESTful - Noțiuni introductive
Să începem prin a crea un nou proiect Xcode „Master-Detail Application” pentru iOS numit „RealmMantleTutorial”. Vom adăuga cadre la acesta folosind CocoaPods. Podfile-ul ar trebui să semene cu următorul:
pod 'Mantle' pod 'Realm' pod 'AFNetworking'
Odată ce pod-urile sunt instalate, putem deschide spațiul de lucru MantleRealmTutorial nou creat. După cum ați observat, a fost instalat și faimosul cadru AFNetworking. Îl vom folosi pentru a efectua solicitări către API.
După cum sa menționat în introducere, New York Times oferă un excelent API de căutare a articolelor. Pentru a-l folosi, trebuie să vă înregistrați pentru a obține o cheie de acces la API. Acest lucru se poate face la http://developer.nytimes.com. Cu cheia API în mână, suntem gata să începem cu codificare.
Înainte de a ne aprofunda în crearea modelelor de date Mantle, trebuie să punem în funcțiune stratul nostru de rețea. Să creăm un nou grup în Xcode și să-l numim Rețea. În acest grup vom crea două clase. Să-l numim pe primul SessionManager și să ne asigurăm că este derivat din AFHTTPSessionManager , care este o clasă de manager de sesiune din AFNetworking , cadrul de rețea încântător. Clasa noastră SessionManager va fi un obiect singleton pe care îl vom folosi pentru a efectua solicitări de obținere către API. Odată ce clasa a fost creată, vă rugăm să copiați codul de mai jos în fișierele de interfață și, respectiv, de implementare.
#import "AFHTTPSessionManager.h" @interface SessionManager : AFHTTPSessionManager + (id)sharedManager; @end
#import "SessionManager.h" static NSString *const kBaseURL = @"http://api.nytimes.com"; @implementation SessionManager - (id)init { self = [super initWithBaseURL:[NSURL URLWithString:kBaseURL]]; if(!self) return nil; self.responseSerializer = [AFJSONResponseSerializer serializer]; self.requestSerializer = [AFJSONRequestSerializer serializer]; return self; } + (id)sharedManager { static SessionManager *_sessionManager = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _sessionManager = [[self alloc] init]; }); return _sessionManager; } @end
Managerul de sesiune este inițializat cu adresa URL de bază definită în variabila statică kBaseURL . De asemenea, va folosi serializatoare de solicitare și răspuns JSON.
Acum, a doua clasă pe care o vom crea în grupul de rețea se va numi APIManager . Acesta va fi derivat din clasa noastră SessionManager nou creată. Odată ce modelele de date necesare sunt create, vom adăuga o metodă la ApiManager care va fi folosită pentru a solicita o listă de articole din API.
Prezentare generală a API-ului pentru căutarea articolelor New York Times
Documentația oficială pentru acest excelent API este disponibilă la http://developer.nytimes.com/…/article_search_api_v2. Ceea ce vom face este să folosim următorul punct final:
http://api.nytimes.com/svc/search/v2/articlesearch
… pentru a prelua articolele găsite folosind un termen de căutare ales de noi, delimitat de un interval de date. De exemplu, ceea ce am putea face este să cerem API să returneze o listă cu toate articolele apărute în New York Times care au avut vreo legătură cu baschetul în primele șapte zile ale lunii iulie 2015. Conform documentației API, pentru a face asta trebuie să setăm următorii parametri în cererea de obținere la acel punct final:
Parametru | Valoare |
q | "baschet" |
start_date | „20150701” |
Data de încheiere | „20150707” |
Răspunsul de la API este destul de complex. Mai jos este răspunsul pentru o solicitare cu parametrii de mai sus limitati la un singur articol (un articol din matricea documentelor) cu numeroase câmpuri omise pentru claritate.
{ "response": { "docs": [ { "web_url": "http://www.nytimes.com/2015/07/04/sports/basketball/robin-lopez-and-knicks-are-close-to-a-deal.html", "lead_paragraph": "Lopez, a 7-foot center, joined Arron Afflalo, a 6-foot-5 guard, as the Knicks' key acquisitions in free agency. He is expected to solidify the Knicks' interior defense.", "abstract": null, "print_page": "1", "source": "The New York Times", "pub_date": "2015-07-04T00:00:00Z", "document_type": "article", "news_desk": "Sports", "section_name": "Sports", "subsection_name": "Pro Basketball", "type_of_material": "News", "_id": "5596e7ac38f0d84c0655cb28", "word_count": "879" } ] }, "status": "OK", "copyright": "Copyright (c) 2013 The New York Times Company. All Rights Reserved." }
Ceea ce primim în esență ca răspuns sunt trei domenii. Primul numit răspuns conține matricea docs , care la rândul său conține articole reprezentând articole. Celelalte două câmpuri sunt statut și drepturi de autor . Acum că știm cum funcționează API-ul, este timpul să creăm modele de date folosind Mantle.
Introducere în Manta
După cum am menționat mai devreme, Mantle este un cadru open-source care simplifică semnificativ scrierea modelelor de date. Să începem prin a crea un model de solicitare a listei de articole. Să numim această clasă ArticleListRequestModel și să ne asigurăm că este derivată din MTLModel , care este o clasă din care ar trebui să fie derivate toate modelele Mantle. În plus, să o facem conformă cu protocolul MTLJSONSerializing . Modelul nostru de solicitare ar trebui să aibă trei proprietăți de tipuri adecvate: interogare, articlesFromDate și articlesToDate . Doar pentru a ne asigura că proiectul nostru este bine organizat, sugerez ca această clasă să fie plasată în grupul Modele .
Iată cum ar trebui să arate fișierul de interfață al ArticleListRequestModel :
#import "MTLModel.h" #import "Mantle.h" @interface ArticleListRequestModel : MTLModel <MTLJSONSerializing> @property (nonatomic, copy) NSString *query; @property (nonatomic, copy) NSDate *articlesFromDate; @property (nonatomic, copy) NSDate *articlesToDate; @end
Acum, dacă căutăm documentele pentru punctul final de căutare a articolelor sau aruncăm o privire la tabelul cu parametrii de solicitare de mai sus, vom observa că numele variabilelor din solicitarea API diferă de cele din modelul nostru de solicitare. Mantle gestionează acest lucru eficient folosind metoda:
+ (NSDictionary *)JSONKeyPathsByPropertyKey.
Iată cum ar trebui implementată această metodă în implementarea modelului nostru de solicitare:
#import "ArticleListRequestModel.h" @implementation ArticleListRequestModel #pragma mark - Mantle JSONKeyPathsByPropertyKey + (NSDictionary *)JSONKeyPathsByPropertyKey { return @{ @"query": @"q", @"articlesFromDate": @"begin_date", @"articlesToDate": @"end_date" }; } @end
Implementarea acestei metode specifică modul în care proprietățile modelului sunt mapate în reprezentările sale JSON. Odată ce metoda JSONKeyPathsByPropertyKey a fost implementată, putem obține o reprezentare în dicționar JSON a modelului cu metoda clasei +[MTLJSONAdapter JSONArrayForModels:]
.
Un lucru care a mai rămas, după cum știm din lista de parametri, este că ambii parametri de dată trebuie să fie în formatul „AAAAMMZZ”. Aici Mantle devine foarte util. Putem adăuga transformarea valorii personalizată pentru orice proprietate prin implementarea metodei opționale +<propertyName>JSONTransformer
. Implementând-o, îi spunem lui Mantle cum ar trebui să fie transformată valoarea unui anumit câmp JSON în timpul deserializării JSON. De asemenea, putem implementa un transformator reversibil care va fi folosit la crearea unui JSON din model. Deoarece trebuie să transformăm un obiect NSDate într-un șir, vom folosi și clasa NSDataFormatter . Iată implementarea completă a clasei ArticleListRequestModel :
#import "ArticleListRequestModel.h" @implementation ArticleListRequestModel + (NSDateFormatter *)dateFormatter { NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; dateFormatter.dateFormat = @"yyyyMMdd"; return dateFormatter; } #pragma mark - Mantle JSONKeyPathsByPropertyKey + (NSDictionary *)JSONKeyPathsByPropertyKey { return @{ @"query": @"q", @"articlesFromDate": @"begin_date", @"articlesToDate": @"end_date" }; } #pragma mark - JSON Transformers + (NSValueTransformer *)articlesToDateJSONTransformer { return [MTLValueTransformer transformerUsingForwardBlock:^id(NSString *dateString, BOOL *success, NSError *__autoreleasing *error) { return [self.dateFormatter dateFromString:dateString]; } reverseBlock:^id(NSDate *date, BOOL *success, NSError *__autoreleasing *error) { return [self.dateFormatter stringFromDate:date]; }]; } + (NSValueTransformer *)articlesFromDateJSONTransformer { return [MTLValueTransformer transformerUsingForwardBlock:^id(NSString *dateString, BOOL *success, NSError *__autoreleasing *error) { return [self.dateFormatter dateFromString:dateString]; } reverseBlock:^id(NSDate *date, BOOL *success, NSError *__autoreleasing *error) { return [self.dateFormatter stringFromDate:date]; }]; } @end
O altă caracteristică excelentă a Mantle este că toate aceste modele sunt conforme cu protocolul NSCoding , precum și implementează metodele isEqual și hash .
După cum am văzut deja, JSON rezultat din apelul API conține o serie de obiecte care reprezintă articole. Dacă dorim să modelăm acest răspuns folosind Mantle, va trebui să creăm două modele de date separate. Unul ar modela obiecte reprezentând articole (elementele matricei docs ), iar celălalt ar modela întregul răspuns JSON, cu excepția elementelor matricei docs. Acum, nu trebuie să mapam fiecare proprietate din JSON primit în modelele noastre de date. Să presupunem că suntem interesați doar de două câmpuri ale obiectelor articol, iar acestea ar fi lead_paragraph și web_url . Clasa ArticleModel este destul de simplu de implementat, așa cum putem vedea mai jos.
#import "MTLModel.h" #import <Mantle/Mantle.h> @interface ArticleModel : MTLModel <MTLJSONSerializing> @property (nonatomic, copy) NSString *leadParagraph; @property (nonatomic, copy) NSString *url; @end
#import "ArticleModel.h" @implementation ArticleModel #pragma mark - Mantle JSONKeyPathsByPropertyKey + (NSDictionary *)JSONKeyPathsByPropertyKey { return @{ @"leadParagraph": @"lead_paragraph", @"url": @"web_url" }; } @end
Acum că modelul articolului a fost definit, putem finaliza definirea modelului de răspuns prin crearea unui model pentru lista de articole. Iată cum va arăta modelul de răspuns al clasei ArticleList.

#import "MTLModel.h" #import <Mantle/Mantle.h> #import "ArticleModel.h" @interface ArticleListResponseModel : MTLModel <MTLJSONSerializing> @property (nonatomic, copy) NSArray *articles; @property (nonatomic, copy) NSString *status; @end
#import "ArticleListResponseModel.h" @class ArticleModel; @implementation ArticleListResponseModel #pragma mark - Mantle JSONKeyPathsByPropertyKey + (NSDictionary *)JSONKeyPathsByPropertyKey { return @{ @"articles" : @"response.docs", @"status" : @"status" }; } #pragma mark - JSON Transformer + (NSValueTransformer *)articlesJSONTransformer { return [MTLJSONAdapter arrayTransformerWithModelClass:ArticleModel.class]; } @end
Această clasă are doar două proprietăți: statut și articole . Dacă îl comparăm cu răspunsul de la punctul final, vom vedea că al treilea atribut JSON drept de autor nu va fi mapat în modelul de răspuns. Dacă ne uităm la metoda articlesJSONTransformer , vom vedea că returnează un transformator de valoare pentru o matrice care conține obiecte din clasa ArticleModel .
De asemenea, este de remarcat faptul că, în metoda JSONKeyPathsByPropertyKey , articolele de proprietate ale modelului corespund documentelor matrice care este imbricată în răspunsul atributului JSON.
Până acum ar trebui să avem trei clase de model implementate: ArticleListRequestModel, ArticleModel și ArticleListResponseModel.
Prima solicitare API
Acum că am implementat toate modelele de date, este timpul să revenim la clasa APIManager pentru a implementa metoda pe care o vom folosi pentru a efectua cereri GET către API. Metoda:
- (NSURLSessionDataTask *) getArticlesWithRequestModel:(ArticleListRequestModel *)requestModel success:(void (^)(ArticleListResponseModel *responseModel))success failure:(void (^)(NSError *error))failure
ia un model de solicitare ArticleListRequestModel ca parametru și returnează un ArticleListResponseModel în caz de succes sau o NSError în caz contrar. Implementarea acestei metode folosește AFNetworking pentru a efectua o solicitare GET către API. Vă rugăm să rețineți că, pentru a face o solicitare API cu succes, trebuie să furnizăm o cheie care poate fi obținută așa cum sa menționat mai devreme, înregistrându-vă la http://developer.nytimes.com.
#import "SessionManager.h" #import "ArticleListRequestModel.h" #import "ArticleListResponseModel.h" @interface APIManager : SessionManager - (NSURLSessionDataTask *)getArticlesWithRequestModel:(ArticleListRequestModel *)requestModel success:(void (^)(ArticleListResponseModel *responseModel))success failure:(void (^)(NSError *error))failure; @end
#import "APIManager.h" #import "Mantle.h" static NSString *const kArticlesListPath = @"/svc/search/v2/articlesearch.json"; static NSString *const kApiKey = @"replace this with your own key"; @implementation APIManager - (NSURLSessionDataTask *)getArticlesWithRequestModel:(ArticleListRequestModel *)requestModel success:(void (^)(ArticleListResponseModel *responseModel))success failure:(void (^)(NSError *error))failure{ NSDictionary *parameters = [MTLJSONAdapter JSONDictionaryFromModel:requestModel error:nil]; NSMutableDictionary *parametersWithKey = [[NSMutableDictionary alloc] initWithDictionary:parameters]; [parametersWithKey setObject:kApiKey forKey:@"api-key"]; return [self GET:kArticlesListPath parameters:parametersWithKey success:^(NSURLSessionDataTask *task, id responseObject) { NSDictionary *responseDictionary = (NSDictionary *)responseObject; NSError *error; ArticleListResponseModel *list = [MTLJSONAdapter modelOfClass:ArticleListResponseModel.class fromJSONDictionary:responseDictionary error:&error]; success(list); } failure:^(NSURLSessionDataTask *task, NSError *error) { failure(error); }]; }
În implementarea acestei metode se întâmplă două lucruri foarte importante. Mai întâi, să aruncăm o privire la această linie:
NSDictionary *parameters = [MTLJSONAdapter JSONDictionaryFromModel:requestModel error:nil];
Ceea ce se întâmplă aici este că folosind metoda oferită de clasa MTLJSONAdapter obținem o reprezentare NSDictionary a modelului nostru de date. Această reprezentare reflectă JSON care va fi trimis către API. Aici se află frumusețea Mantle. După ce am implementat metodele JSONKeyPathsByPropertyKey și +<propertyName>JSONTransformer
în clasa ArticleListRequestModel, putem obține în cel mai scurt timp reprezentarea JSON corectă a modelului nostru de date cu doar o singură linie de cod.
Manta ne permite, de asemenea, să efectuăm transformări și în cealaltă direcție. Și exact asta se întâmplă cu datele primite de la API. NSDictionary pe care îl primim este mapat într-un obiect al clasei ArticleListResponseModel folosind următoarea metodă de clasă:
ArticleListResponseModel *list = [MTLJSONAdapter modelOfClass:ArticleListResponseModel.class fromJSONDictionary:responseDictionary error:&error];
Date persistente cu Realm
Acum că putem prelua date de la un API la distanță, este timpul să le persistem. După cum sa menționat în introducere, o vom face folosind Realm. Realm este o bază de date mobilă și un înlocuitor pentru Core Data și SQLite. După cum vom vedea mai jos, este extrem de ușor de utilizat.
Pentru a salva o bucată de date în Realm, trebuie mai întâi să încapsulăm un obiect care este derivat din clasa RLMObject. Ceea ce trebuie să facem acum este să creăm o clasă model care va stoca date pentru articole individuale. Iată cât de ușor este să creezi o astfel de clasă.
#import "RLMObject.h" @interface ArticleRealm : RLMObject @property NSString *leadParagraph; @property NSString *url; @end
Și asta ar putea fi practic, implementarea acestei clase ar putea rămâne goală. Vă rugăm să rețineți că proprietățile din clasa modelului nu au atribute precum nonatomic, strong sau copy. Realm are grijă de acestea și nu trebuie să ne facem griji pentru ele.
Deoarece articolele pe care le putem obține sunt modelate cu modelul Articol Mante, ar fi convenabil să inițializați obiectele ArticleRealm cu obiecte din clasa Article . Pentru a face asta, vom adăuga metoda initWithMantleModel la modelul nostru Realm. Iată implementarea completă a clasei ArticleRealm .
#import "RLMObject.h" #import "ArticleModel.h" @interface ArticleRealm : RLMObject @property NSString *leadParagraph; @property NSString *url; - (id)initWithMantleModel:(ArticleModel *)articleModel; @end
#import "ArticleRealm.h" @implementation ArticleRealm - (id)initWithMantleModel:(ArticleModel *)articleModel{ self = [super init]; if(!self) return nil; self.leadParagraph = articleModel.leadParagraph; self.url = articleModel.url; return self; } @end
Interacționăm cu baza de date folosind obiecte din clasa RLMRealm . Putem obține cu ușurință un obiect RLMRealm invocând metoda „[RLMRealm defaultRealm]”. Este important să ne amintim că un astfel de obiect este valabil numai în cadrul firului pe care a fost creat și nu poate fi partajat între fire. Scrierea datelor în Realm este destul de simplă. O singură scriere sau o serie de ele trebuie făcută în cadrul unei tranzacții de scriere. Iată un exemplu de scriere în baza de date:
RLMRealm *realm = [RLMRealm defaultRealm]; ArticleRealm *articleRealm = [ArticleRealm new]; articleRealm.leadParagraph = @"abc"; articleRealm.url = @"sampleUrl"; [realm beginWriteTransaction]; [realm addObject:articleRealm]; [realm commitWriteTransaction];
Ce se întâmplă aici este următorul. Mai întâi creăm un obiect RLMRealm pentru a interacționa cu baza de date. Apoi este creat un obiect model ArticleRealm (vă rugăm să rețineți că este derivat din clasa RLMRealm ). În cele din urmă, pentru a-l salva, începe o tranzacție de scriere, obiectul este adăugat la baza de date și odată ce este salvat, tranzacția de scriere este comisă. După cum putem vedea, tranzacțiile de scriere blochează firul pe care sunt invocate. Deși se spune că Realm este foarte rapid, dacă ar fi să adăugăm mai multe obiecte la baza de date într-o singură tranzacție pe firul principal, asta ar putea duce la interfața de utilizare să nu mai răspundă până la finalizarea tranzacției. O soluție naturală pentru aceasta este efectuarea unei astfel de tranzacții de scriere pe un fir de fundal.
Solicitare API și răspuns persistent în domeniul
Acestea sunt toate informațiile de care avem nevoie pentru a persista articolele folosind Realm. Să încercăm să efectuăm o solicitare API folosind metoda
- (NSURLSessionDataTask *) getArticlesWithRequestModel:(ArticleListRequestModel *)requestModel success:(void (^)(ArticleListResponseModel *responseModel))success failure:(void (^)(NSError *error))failure
și modele de cerere și răspuns Mantle pentru a obține articole din New York Times care au avut vreo legătură (ca în exemplul anterior) cu baschetul și au fost publicate în primele șapte zile ale lunii iunie 2015. Odată ce lista acestor articole este disponibilă, vom o va persista în Tărâm. Mai jos este codul care face asta. Este plasat în metoda viewDidLoad a controlerului de vizualizare a tabelului din aplicația noastră.
ArticleListRequestModel *requestModel = [ArticleListRequestModel new]; // (1) requestModel.query = @"Basketball"; requestModel.articlesToDate = [[ArticleListRequestModel dateFormatter] dateFromString:@"20150706"]; requestModel.articlesFromDate = [[ArticleListRequestModel dateFormatter] dateFromString:@"20150701"]; [[APIManager sharedManager] getArticlesWithRequestModel:requestModel // (2) success:^(ArticleListResponseModel *responseModel){ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // (3) @autoreleasepool { RLMRealm *realm = [RLMRealm defaultRealm]; [realm beginWriteTransaction]; [realm deleteAllObjects]; [realm commitWriteTransaction]; [realm beginWriteTransaction]; for(ArticleModel *article in responseModel.articles){ ArticleRealm *articleRealm = [[ArticleRealm alloc] initWithMantleModel:article]; // (4) [realm addObject:articleRealm]; } [realm commitWriteTransaction]; dispatch_async(dispatch_get_main_queue(), ^{ // (5) RLMRealm *realmMainThread = [RLMRealm defaultRealm]; // (6) RLMResults *articles = [ArticleRealm allObjectsInRealm:realmMainThread]; self.articles = articles; // (7) [self.tableView reloadData]; }); } }); } failure:^(NSError *error) { self.articles = [ArticleRealm allObjects]; [self.tableView reloadData]; }];
Mai întâi, se face un apel API (2) cu un model de solicitare (1), care returnează un model de răspuns care conține o listă de articole. Pentru a persista acele articole folosind Realm, trebuie să creăm obiecte model Realm, care are loc în bucla for (4). De asemenea, este important de observat că, deoarece mai multe obiecte sunt persistente într-o singură tranzacție de scriere, acea tranzacție de scriere este efectuată pe un thread de fundal (3). Acum, odată ce toate articolele sunt salvate în Realm, le atribuim proprietății clasei self.articles (7). Deoarece vor fi accesate mai târziu în firul principal în metodele sursei de date TableView, este sigur să le preluați din baza de date Realm și pe firul principal (5). Din nou, pentru a accesa baza de date dintr-un fir nou, trebuie creat un nou obiect RLMRealm (6) pe acel thread.
Dacă obținerea de articole noi din API eșuează din orice motiv, cele existente sunt preluate din stocarea locală în blocul de eșec.
Încheierea
În acest tutorial am învățat cum să configuram Mantle, un cadru model pentru Cocoa și Cocoa Touch, pentru a interacționa cu un API de la distanță. De asemenea, am învățat cum să persistăm local datele preluate sub formă de obiecte model Mantle folosind baza de date mobilă Realm.
În cazul în care doriți să încercați această aplicație, puteți prelua codul sursă din depozitul său GitHub. Va trebui să generați și să furnizați propria cheie API înainte de a rula aplicația.