Hver iOS-utvikler er kjent med Core Data, et objektgrafikk og utholdenhetsrammeverk fra Apple. Bortsett fra vedvarende data lokalt, kommer rammeverket med en rekke avanserte funksjoner, for eksempel sporing av objektendring og angre. Disse funksjonene, selv om de er nyttige i mange tilfeller, kommer ikke gratis. Det krever mye kokerplatekode, og rammeverket som helhet har en bratt læringskurve.
I 2014, Rike , en mobil database, ble utgitt og tok utviklingsverdenen med storm. Hvis alt vi trenger er å vedvare data lokalt, er Realm et godt alternativ. Tross alt krever ikke alle brukssaker de avanserte funksjonene i kjernedata. Realm er ekstremt enkel å bruke, og i motsetning til Core Data, krever det svært lite kjelekode. Det er også trådsikkert og sies å være raskere enn utholdenhetsrammen fra Apple.
I de fleste moderne mobilapplikasjoner løser vedvarende data halvparten av problemet. Vi trenger ofte å hente data fra en ekstern tjeneste, vanligvis gjennom en RESTful API. Dette er hvor Mantel kommer inn i bildet. Det er et open source-modellrammeverk for Cocoa og Cocoa Touch. Mantle forenkler signifikant skriving av datamodeller for interaksjon med APIer som bruker JSON som deres datautvekslingsformat.
I denne artikkelen skal vi bygge et iOS-program som henter en liste over artikler sammen med lenker til dem fra New York Times Article Search API v2. Listen skal hentes ved hjelp av en standard HTTP GET-forespørsel, med forespørsels- og responsmodeller opprettet ved hjelp av Mantle. Vi får se hvor enkelt det er med Mantle å håndtere verditransformasjoner (f.eks. Fra NSDate til streng). Når dataene er hentet, vil vi vedvare dem lokalt ved hjelp av Realm. Alt dette med minimal kjelekode.
La oss starte med å lage et nytt 'Master-Detail Application' Xcode-prosjekt for iOS med navnet 'RealmMantleTutorial'. Vi vil legge til rammer for det ved hjelp av CocoaPods. Podfilen skal ligne på følgende:
pod 'Mantle' pod 'Realm' pod 'AFNetworking'
Når podene er installert, kan vi åpne den nyopprettede MantleRealmTutorial arbeidsområdet. Som du har lagt merke til, er det berømte AFNetworking-rammeverket også installert. Vi bruker den til å utføre forespørsler til API.
Som nevnt innledningsvis gir New York Times et utmerket API for artikkelsøk. For å kunne bruke den, må man registrere seg for å få en tilgangsnøkkel til API. Dette kan gjøres på http://developer.nytimes.com . Med API-nøkkelen i hånden er vi klare til å komme i gang med koding.
Før vi går i dybden med å lage Mantle-datamodeller, må vi få nettverkslaget i gang. La oss opprette en ny gruppe i Xcode og kalle den Network. I denne gruppen skal vi lage to klasser. La oss ringe den første SessionManager og sørg for at den kommer fra AFHTTPSessionManager som er en øktlederklasse fra AFNetworking , det herlige nettverksrammeverket. Våre SessionManager klasse vil være et enkelt objekt som vi vil bruke til å utføre få forespørsler til API. Når klassen er opprettet, kan du kopiere koden nedenfor i henholdsvis grensesnitt- og implementeringsfiler.
#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
Øktestyreren initialiseres med den grunnleggende URL-en definert i det statiske kBaseURL variabel. Det vil også bruke JSON-forespørsels- og svarserialiserer.
Nå er den andre klassen vi skal lage i Nettverk gruppe vil bli kalt APIManager . Det skal stamme fra vårt nyopprettede SessionManager klasse. Når de nødvendige datamodellene er opprettet, vil vi legge til en metode for ApiManager som vil bli brukt til å be om en liste over artikler fra API.
Den offisielle dokumentasjonen for denne utmerkede API-en er tilgjengelig på http://developer.nytimes.com/…/article_search_api_v2 . Det vi skal gjøre er å bruke følgende endepunkt:
http://api.nytimes.com/svc/search/v2/articlesearch
... for å hente artikler som er funnet ved hjelp av et søkeord vi velger, begrenset av et datointervall. Det vi for eksempel kan gjøre er å be APIen om å returnere en liste over alle artiklene som dukket opp i New York Times som hadde noe med basketball å gjøre de første syv dagene i juli 2015. Ifølge API dokumentasjon , for å gjøre det må vi sette følgende parametere i get-forespørselen til det endepunktet:
Parameter | Verdi |
hva | “BasketBall” |
start_dato | '20150701' |
sluttdato | '20150707' |
Svaret fra API er ganske komplisert. Nedenfor er svaret på en forespørsel med de ovennevnte parametrene begrenset til bare en artikkel (ett element i docs-array) med mange felt utelatt for klarhetens skyld.
{ '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.' }
Det vi i utgangspunktet får som svar er tre felt. Den første ringte respons inneholder matrisen dokumenter , som igjen inneholder gjenstander som representerer artikler. De to andre feltene er status og opphavsrett . Nå som vi vet hvordan API-et fungerer, er det på tide å lage datamodeller ved hjelp av Mantle.
Som nevnt tidligere er Mantle et open source-rammeverk som forenkler skrivemodellmodeller betydelig. La oss starte med å lage en modell for forespørsel om artikkellister. La oss kalle denne klassen ArticleListRequestModel og sørg for at den kommer fra MTLModell , som er en klasse som alle Mantle-modeller skal være avledet fra. I tillegg skal vi gjøre det i samsvar med MTLJSON Serialisering protokoll. Vår forespørselsmodell bør ha tre egenskaper av passende typer: spørring, artiklerFromDate , og artiklerToDate . Bare for å sikre at prosjektet vårt er godt organisert, foreslår jeg at denne klassen plasseres i Modeller gruppe.
Mantle forenkler skriving av datamodeller, reduserer kokerplatekoden. kvitringSlik gjør grensesnittfilen til ArticleListRequestModel skal se ut:
#import 'MTLModel.h' #import 'Mantle.h' @interface ArticleListRequestModel : MTLModel @property (nonatomic, copy) NSString *query; @property (nonatomic, copy) NSDate *articlesFromDate; @property (nonatomic, copy) NSDate *articlesToDate; @end
Nå hvis vi ser opp dokumentene for vårt endepunkt for artikkelsøk eller ser på tabellen med forespørselsparametere ovenfor, vil vi legge merke til at navnene på variablene i API-forespørselen er forskjellige fra de i vår forespørselsmodell. Mantle håndterer dette effektivt ved hjelp av metoden:
+ (NSDictionary *)JSONKeyPathsByPropertyKey.
Slik skal denne metoden implementeres i implementeringen av forespørselsmodellen vår:
#import 'ArticleListRequestModel.h' @implementation ArticleListRequestModel #pragma mark - Mantle JSONKeyPathsByPropertyKey + (NSDictionary *)JSONKeyPathsByPropertyKey { return @{ @'query': @'q', @'articlesFromDate': @'begin_date', @'articlesToDate': @'end_date' }; } @end
Implementeringen av denne metoden spesifiserer hvordan egenskapene til modellen er kartlagt i JSON-representasjonene. Når metoden JSONKeyPathsByPropertyKey har blitt implementert, kan vi få en JSON-ordbokrepresentasjon av modellen med klassemetoden +[MTLJSONAdapter JSONArrayForModels:]
.
En ting som fortsatt er igjen, som vi vet fra listen over parametere, er at begge dataparametrene kreves i formatet 'ÅÅÅÅMMDD'. Det er her Mantle blir veldig nyttig. Vi kan legge til tilpasset verditransformasjon for alle eiendommer ved å implementere den valgfrie metoden +JSONTransformer
. Ved å implementere det forteller vi Mantle hvordan verdien til et bestemt JSON-felt skal transformeres under JSON-deserialisering. Vi kan også implementere en reversibel transformator som skal brukes når du lager en JSON fra modellen. Siden vi trenger å transformere en NSDate objekt i en streng, vil vi også bruke NSDataFormatter klasse. Her er den fullstendige implementeringen av ArticleListRequestModel klasse:
#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
En annen flott funksjon ved Mantle er at alle disse modellene samsvarer med NSCoding protokoll, samt implementere er lik og hasj metoder.
Som vi allerede har sett, inneholder den resulterende JSON fra API-anropet en rekke objekter som representerer artikler. Hvis vi vil modellere dette svaret ved hjelp av Mantle, må vi lage to separate datamodeller. Man vil modellere gjenstander som representerer artikler ( dokumenter array-elementer), og den andre vil modellere hele JSON-svaret bortsett fra elementene i docs-arrayet. Nå trenger vi ikke å kartlegge hver eneste eiendom fra den innkommende JSON i datamodellene våre. La oss anta at vi bare er interessert i to felt med gjenstandsobjekter, og det ville være føre_avsnitt og web_url . De ArticleModel klasse er ganske grei å implementere, som vi kan se nedenfor.
#import 'MTLModel.h' #import @interface ArticleModel : MTLModel @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
Nå som artikkelmodellen er definert, kan vi fullføre definisjonen av responsmodellen ved å lage en modell for artikkellisten. Slik ser klassen ArticleList-responsmodell ut.
#import 'MTLModel.h' #import #import 'ArticleModel.h' @interface ArticleListResponseModel : MTLModel @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
Denne klassen har bare to egenskaper: status og artikler . Hvis vi sammenligner det med svaret fra endepunktet, vil vi se at det tredje JSON-attributtet copyright ikke blir kartlagt i responsmodellen. Hvis vi ser på artiklerJSONTransformator metode, vil vi se at den returnerer en verditransformator for en matrise som inneholder objekter i klassen ArticleModel .
Det er også verdt å merke seg det i metoden JSONKeyPathsByPropertyKey , modellegenskapen artikler samsvarer med matrisedokumentene som er nestet i JSON-attributtet respons .
Nå skal vi ha tre modellklasser implementert: ArticleListRequestModel, ArticleModel og ArticleListResponseModel.
Nå som vi har implementert alle datamodellene, er det på tide å komme tilbake til klassen APIManager for å implementere metoden som vi vil bruke til å utføre GET-forespørsler til API. Metoden:
- (NSURLSessionDataTask *) getArticlesWithRequestModel:(ArticleListRequestModel *)requestModel success:(void (^)(ArticleListResponseModel *responseModel))success failure:(void (^)(NSError *error))failure
tar en ArticleListRequestModel forespørselsmodell som parameter og returnerer en ArticleListResponseModel i tilfelle suksess eller NSE-feil på annen måte. Implementeringen av denne metoden bruker AFNetworking for å utføre en GET-forespørsel til API. Vær oppmerksom på at for å kunne utføre en vellykket API-forespørsel, må vi oppgi en nøkkel som kan fås som nevnt tidligere, ved å registrere oss på 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); }]; }
Det er to veldig viktige ting som skjer i implementeringen av denne metoden. La oss først se på denne linjen:
NSDictionary *parameters = [MTLJSONAdapter JSONDictionaryFromModel:requestModel error:nil];
Det som skjer her er at bruk av metoden som tilbys av MTLJSONAdapter klasse får vi en NSDictionary representasjon av datamodellen vår. Denne representasjonen speiler JSON som skal sendes til API. Dette er hvor skjønnheten til Mantle ligger. Etter å ha implementert JSONKeyPathsByPropertyKey og +JSONTransformer
metoder i klassen ArticleListRequestModel, kan vi få riktig JSON-representasjon av datamodellen vår på kort tid med bare en enkelt kodelinje.
Mantle lar oss også utføre transformasjoner i den andre retningen. Og det er akkurat det som skjer med dataene som mottas fra API. NSDictionary som vi mottar er kartlagt til et objekt av ArticleListResponseModel-klassen ved hjelp av følgende klassemetode:
ArticleListResponseModel *list = [MTLJSONAdapter modelOfClass:ArticleListResponseModel.class fromJSONDictionary:responseDictionary error:&error];
Nå som vi kan hente data fra et eksternt API, er det på tide å vedvare det. Som nevnt i innledningen, vil vi gjøre det ved hjelp av Realm. Realm er en mobil database og en erstatning for Core Data og SQLite. Som vi vil se nedenfor, er det ekstremt enkelt å bruke.
Realm, den ultimate mobile databasen, er en perfekt erstatning for Core Data og SQLite. kvitringFor å lagre et stykke data i Realm må vi først kapsle inn et objekt som er avledet fra RLMObject-klassen. Det vi trenger å gjøre nå er å lage en modellklasse som vil lagre data for enkeltartikler. Her er hvor enkelt det er å lage en slik klasse.
#import 'RLMObject.h' @interface ArticleRealm : RLMObject @property NSString *leadParagraph; @property NSString *url; @end
Og dette kan i utgangspunktet være det, implementeringen av denne klassen kan forbli tom. Vær oppmerksom på at egenskapene i modellklassen ikke har noen attributter som ikke-atomisk, sterk eller kopi. Rike tar seg av dem, og vi trenger ikke bekymre oss for dem.
Siden artiklene vi kan få er modellert med Mante-modellen Artikkel det ville være praktisk å initialisere ArticleRealm gjenstander med gjenstander av klasse Artikkel . For å gjøre det vil vi legge til initWithMantleModel metode til vår Realm-modell. Her er den fullstendige implementeringen av ArticleRealm klasse.
#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
Vi samhandler med databasen ved hjelp av objekter i klassen RLMRealm . Vi kan lett få en RLMRealm objekt ved å påkalle metoden “[RLMRealm defaultRealm]”. Det er viktig å huske at et slikt objekt bare er gyldig innenfor tråden det ble opprettet på og ikke kan deles på tvers av tråder. Å skrive data til Realm er ganske grei. En enkelt skriving, eller en serie av dem, må gjøres innenfor en skrivetransaksjon. Her er et eksempel på skriving til databasen:
RLMRealm *realm = [RLMRealm defaultRealm]; ArticleRealm *articleRealm = [ArticleRealm new]; articleRealm.leadParagraph = @'abc'; articleRealm.url = @'sampleUrl'; [realm beginWriteTransaction]; [realm addObject:articleRealm]; [realm commitWriteTransaction];
Det som skjer her er følgende. Først lager vi en RLMRealm objekt for å samhandle med databasen. Så en ArticleRealm modellobjekt er opprettet (husk at det er avledet fra RLMRealm klasse). Til slutt for å lagre den, begynner en skrivetransaksjon, objektet legges til i databasen, og når den er lagret, blir skrivetransaksjonen begått. Som vi kan se, blokkerer skrivetransaksjoner tråden de påberopes på. Mens Realm sies å være veldig rask, kan vi føre til at brukergrensesnittet ikke svarer til transaksjonen er ferdig hvis vi legger til flere objekter i databasen innen en enkelt transaksjon på hovedtråden. En naturlig løsning på det er å utføre en slik skrivetransaksjon på en bakgrunnstråd.
Dette er all informasjonen vi trenger for å vedvare artikler som bruker Realm. La oss prøve å utføre en API-forespørsel ved hjelp av metoden
- (NSURLSessionDataTask *) getArticlesWithRequestModel:(ArticleListRequestModel *)requestModel success:(void (^)(ArticleListResponseModel *responseModel))success failure:(void (^)(NSError *error))failure
og Mantle-forespørsels- og responsmodeller for å få New York Times-artikler som hadde noe å gjøre (som i det tidligere eksemplet) med basketball og ble publisert de første sju dagene i juni 2015. Når listen over slike artikler er tilgjengelig, vil vedvare det i Realm. Nedenfor er koden som gjør det. Den er plassert i viewDidLoad metoden til tabellvisningskontrolleren i appen vår.
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]; }];
Først foretas en API-samtale (2) med en forespørselsmodell (1), som returnerer en svarmodell som inneholder en liste over artikler. For å fortsette disse artiklene ved hjelp av Realm, må vi lage Realm-modellobjekter, som foregår i for loop (4). Det er også viktig å legge merke til at siden flere objekter vedvarer i en enkelt skrivetransaksjon, blir den skrivetransaksjonen utført på en bakgrunnstråd (3). Når alle artiklene er lagret i Realm, tilordner vi dem til klasseegenskapen selv. partikler (7). Siden de kommer til senere på hovedtråden i TableView-datakildemetoder, er det trygt å hente dem fra Realm-databasen på hovedtråden også (5). Igjen, for å få tilgang til databasen fra en ny tråd, må det opprettes et nytt RLMRealm-objekt (6) på den tråden.
Hvis det ikke henter nye artikler fra API-en av en eller annen grunn, blir de eksisterende hentet fra den lokale lagringen i feilblokken.
I denne opplæringen lærte vi hvordan vi konfigurerer Mantle, et modellrammeverk for Cocoa og Cocoa Touch, for å samhandle med en fjernkontroll BRANN . Vi lærte også hvordan man lokalt vedvarer data hentet i form av Mantle-modellobjekter ved hjelp av Realm mobile database.
Hvis du vil prøve dette programmet, kan du hente kildekoden fra GitHub-depotet . Du må generere og oppgi din egen API-nøkkel før du kjører applikasjonen.