I migliori approcci architetturali per la creazione di applicazioni di rete iOS (client REST)


323

Sono uno sviluppatore iOS con una certa esperienza e questa domanda è davvero interessante per me. Ho visto molte risorse e materiali diversi su questo argomento, ma sono ancora confuso. Qual è la migliore architettura per un'applicazione di rete iOS? Intendo un quadro astratto di base, modelli, che si adatteranno a qualsiasi applicazione di rete sia che si tratti di una piccola app che ha solo poche richieste del server o di un client REST complesso. Apple consiglia di utilizzare MVCcome approccio architettonico di base per tutte le applicazioni iOS, ma MVCné i MVVMmodelli più moderni spiegano dove inserire il codice logico di rete e come organizzarlo in generale.

Devo sviluppare qualcosa di simile a MVCS( Sfor Service) e in questo Servicelivello mettere tutte le APIrichieste e altre logiche di rete, che in prospettiva potrebbero essere davvero complesse? Dopo aver fatto qualche ricerca ho trovato due approcci di base per questo. Qui è stato consigliato di creare una classe separata per ogni richiesta di rete al servizio web API(come LoginRequestclasse o PostCommentRequestclasse e così via) che eredita dalla classe astratta della richiesta di base AbstractBaseRequeste oltre a creare un gestore di rete globale che incapsuli il codice di rete comune e altre preferenze (può essere AFNetworkingpersonalizzazione oRestKittuning, se disponiamo di mappature e persistenza di oggetti complessi, o persino di un'implementazione della comunicazione di rete con API standard). Ma questo approccio mi sembra un sovraccarico. Un altro approccio è quello di avere un po 'di Singleton APIdispatcher o classe Manager come nel primo approccio, ma non creare classi per ogni richiesta e invece di incapsulare ogni richiesta come metodo di istanza pubblica di questa classe dirigente come: fetchContacts, loginUsermetodi, ecc Quindi, cosa è il modo migliore e corretto? Ci sono altri approcci interessanti che non conosco ancora?

E dovrei creare un altro livello per tutte queste cose di rete come Service, o NetworkProviderlivello o qualsiasi altra cosa al di sopra della mia MVCarchitettura, o questo livello dovrebbe essere integrato (iniettato) in MVClivelli esistenti , ad esempio Model?

So che esistono approcci meravigliosi, o in che modo quindi mostri mobili come il client di Facebook o il client di LinkedIn gestiscono una complessità esponenzialmente crescente della logica di rete?

So che non esiste una risposta esatta e formale al problema. L'obiettivo di questa domanda è quello di raccogliere gli approcci più interessanti da sviluppatori iOS esperti . L'approccio migliore suggerito sarà contrassegnato come accettato e premiato con un premio di reputazione, altri saranno votati. È principalmente una questione teorica e di ricerca. Voglio capire l'approccio architettonico di base, astratto e corretto per le applicazioni di rete in iOS. Spero in una spiegazione dettagliata da sviluppatori esperti.


14
Non è una domanda "lista della spesa"? Ho appena avuto una domanda votata al diavolo e chiusa perché è stato affermato che "qual è la migliore" domande di tipo scatenano un dibattito troppo costruttivo. Cosa rende questa lista della spesa una buona domanda degna di voti e una generosità mentre gli altri vengono chiusi?
Alvin Thompson,

1
In genere la logica di rete andava nel controller, che alterava un oggetto modello e avvisava qualsiasi delegato o osservatore.
Quellish,

1
Domande e risposte molto interessanti. Dopo 4 anni di codifica iOS e cercando di trovare il modo più bello per aggiungere un livello di rete all'app. Quale classe dovrebbe avere la responsabilità di gestire una richiesta di rete? Le risposte di seguito sono davvero pertinenti. Grazie
darksider,

@JoeBlow questo non è vero. Il settore delle app mobili si basa ancora molto sulle comunicazioni server-client.
Scord

Risposte:


327

I want to understand basic, abstract and correct architectural approach for networking applications in iOS: non esiste un approccio "il migliore" o "il più corretto" per la creazione di un'architettura applicativa. È un lavoro molto creativo. Dovresti sempre scegliere l'architettura più semplice ed estensibile, che sarà chiara per qualsiasi sviluppatore, che inizi a lavorare sul tuo progetto o per altri sviluppatori nel tuo team, ma sono d'accordo che ci può essere un "buono" e un "cattivo" " architettura.

Hai detto: collect the most interesting approaches from experienced iOS developersnon penso che il mio approccio sia il più interessante o corretto, ma l'ho usato in diversi progetti e ne sono soddisfatto. È un approccio ibrido di quelli che hai menzionato sopra, e anche con miglioramenti dai miei sforzi di ricerca. Sono interessante per i problemi di costruzione di approcci, che combinano diversi schemi e modi di dire noti. Penso che molti dei modelli aziendali di Fowler possano essere applicati con successo alle applicazioni mobili. Ecco un elenco di quelli più interessanti, che possiamo applicare per creare un'architettura di applicazione iOS ( a mio avviso ): Livello di servizio , Unità di lavoro , Facciata remota , Oggetto trasferimento dati ,Gateway , supertipo di livello , caso speciale , modello di dominio . Dovresti sempre progettare correttamente un livello di modello e non dimenticare sempre la persistenza (può aumentare significativamente le prestazioni della tua app). Puoi usarlo Core Dataper questo. Ma non dovresti dimenticare, che Core Datanon è un ORM o un database, ma un gestore di grafici a oggetti con persistenza come una buona opzione di esso. Quindi, molto spesso Core Datapuò essere troppo pesante per le tue esigenze e puoi guardare a nuove soluzioni come Realm e Couchbase Lite o creare il tuo livello di mappatura / persistenza degli oggetti leggeri, basato su SQLite grezzo o LevelDB. Inoltre ti consiglio di familiarizzare con Domain Driven Design e CQRS .

All'inizio, penso, dovremmo creare un altro livello per il networking, perché non vogliamo controller di grasso o modelli pesanti e sopraffatti. Non credo in quelle fat model, skinny controllercose. Ma io credo nel skinny everythingmetodo, perché nessuna classe deve essere grasso, mai. Tutte le reti possono essere generalmente astratte come logica aziendale, di conseguenza dovremmo avere un altro livello, dove possiamo metterlo. Il livello di servizio è ciò di cui abbiamo bisogno:

It encapsulates the application's business logic,  controlling transactions 
and coordinating responses in the implementation of its operations.

Nel nostro MVCregno Service Layerc'è qualcosa come un mediatore tra modello di dominio e controller. C'è una variazione piuttosto simile di questo approccio chiamato MVCS in cui a Storeè in realtà il nostro Servicelivello. Storemodello di istanze vends e gestisce la rete, la cache ecc. Voglio menzionare che non dovresti scrivere tutta la tua rete e la logica di business nel tuo livello di servizio. Anche questo può essere considerato come un cattivo design. Per ulteriori informazioni, guarda i modelli di dominio Anemic e Rich . Alcuni metodi di servizio e la logica di business possono essere gestiti nel modello, quindi sarà un modello "ricco" (con comportamento).

Uso sempre ampiamente due librerie: AFNetworking 2.0 e ReactiveCocoa . Penso che sia un must per qualsiasi applicazione moderna che interagisce con la rete e i servizi Web o contiene una complessa logica dell'interfaccia utente.

ARCHITETTURA

All'inizio creo una APIClientclasse generale , che è una sottoclasse di AFHTTPSessionManager . Questo è un cavallo di battaglia di tutte le reti nell'applicazione: tutte le classi di servizio delegano ad esso richieste REST effettive. Contiene tutte le personalizzazioni del client HTTP, di cui ho bisogno nella specifica applicazione: pinning SSL, elaborazione degli errori e creazione di NSErroroggetti semplici con motivi dettagliati di errore e descrizioni di tutti APIe errori di connessione (in tal caso il controller sarà in grado di mostrare i messaggi corretti per l'utente), l'impostazione di serializzatori di richieste e risposte, intestazioni http e altri elementi relativi alla rete. Poi ho logicamente dividere tutti gli richieste API in sottoservizi o, più correttamente, microservices : UserSerivces, CommonServices, SecurityServices,FriendsServicese così via, secondo la logica aziendale che implementano. Ognuno di questi microservizi è una classe separata. Insieme formano un Service Layer. Queste classi contengono metodi per ogni richiesta API, elaborano modelli di dominio e restituiscono sempre un RACSignalmodello di risposta analizzato o NSErroral chiamante.

Voglio menzionare che se si dispone di una logica di serializzazione del modello complessa, quindi creare un altro livello per esso: qualcosa come Data Mapper ma più generale, ad es. JSON / XML -> Model maper. Se si dispone di cache: crearla anche come livello / servizio separato (non è necessario combinare la logica di business con la cache). Perché? Perché il corretto livello di memorizzazione nella cache può essere piuttosto complesso con i propri gotcha. Le persone implementano una logica complessa per ottenere una cache valida e prevedibile come, ad esempio, la memorizzazione nella cache monoideale con proiezioni basate su profunctor. Puoi leggere di questa bellissima biblioteca chiamata Carlos per capire di più. E non dimenticare che i Core Data possono davvero aiutarti con tutti i problemi di cache e ti permetteranno di scrivere meno logica. Inoltre, se si dispone di una logica tra NSManagedObjectContexti modelli di richieste del server e, è possibile utilizzareModello di repository , che separa la logica che recupera i dati e li mappa al modello di entità dalla logica aziendale che agisce sul modello. Pertanto, consiglio di utilizzare il modello di repository anche quando si dispone di un'architettura basata su dati di base. Repository può cose astratte, come NSFetchRequest, NSEntityDescription, NSPredicatee così via per i metodi semplici come geto put.

Dopo tutte queste azioni nel livello di servizio, il chiamante (controller di visualizzazione) può eseguire alcune complesse operazioni asincrone con la risposta: manipolazioni del segnale, concatenamento, mappatura, ecc. Con l'aiuto di ReactiveCocoaprimitive o semplicemente iscriversi e mostrare i risultati nella vista . Inietto con il Dependency Injection in tutte queste classi di servizio le mie APIClient, che si tradurrà una particolare chiamata di servizio in corrispondenti GET, POST, PUT, DELETE, ecc richiesta al endpoint REST. In questo caso APIClientviene passato in modo implicito a tutti i controller, è possibile renderlo esplicito con un parametro su APIClientclassi di servizio. Questo può avere senso se si desidera utilizzare diverse personalizzazioni diAPIClientper particolari classi di servizio, ma se per qualche motivo non vuoi copie extra o sei sicuro di usare sempre un'istanza particolare (senza personalizzazioni) di APIClient- rendila un singleton, ma NON, per favore DON Preparo lezioni di servizio come singoli.

Quindi ogni controller di visualizzazione con il DI inietta la classe di servizio di cui ha bisogno, chiama i metodi di servizio appropriati e compone i risultati con la logica dell'interfaccia utente. Per l'iniezione di dipendenza mi piace usare BloodMagic o un Typhoon framework più potente . Non uso mai singoli, APIManagerWhateverclasse di Dio o altre cose sbagliate. Perché se chiami la tua classe WhateverManager, questo indica che non conosci il suo scopo ed è una cattiva scelta nel design . Singletons è anche un anti-pattern, e nella maggior parte dei casi (tranne quelli rari) è una soluzione sbagliata . Singleton dovrebbe essere considerato solo se tutti e tre i seguenti criteri sono soddisfatti:

  1. La proprietà della singola istanza non può essere ragionevolmente assegnata;
  2. L'inizializzazione lenta è desiderabile;
  3. L'accesso globale non è altrimenti previsto.

Nel nostro caso la proprietà della singola istanza non è un problema e inoltre non abbiamo bisogno dell'accesso globale dopo aver diviso il nostro god manager in servizi, perché ora solo uno o più controller dedicati hanno bisogno di un servizio particolare (ad es. UserProfileEsigenze del controller UserServicese così via) .

Dobbiamo sempre rispettare i Sprincipi di SOLID e utilizzare la separazione delle preoccupazioni , quindi non mettere tutti i metodi di servizio e le chiamate di rete in una classe, perché è pazzesco, soprattutto se si sviluppa un'applicazione aziendale di grandi dimensioni. Ecco perché dovremmo considerare l'iniezione di dipendenza e l'approccio ai servizi. Considero questo approccio moderno e post-OO . In questo caso abbiamo diviso la nostra applicazione in due parti: logica di controllo (controller ed eventi) e parametri.

Un tipo di parametri sarebbero normali parametri di "dati". Questo è ciò che passiamo intorno a funzioni, manipolazione, modifica, persistenza, ecc. Queste sono entità, aggregati, raccolte, classi di casi. L'altro tipo sarebbe parametri di "servizio". Si tratta di classi che incapsulano la logica aziendale, consentono la comunicazione con sistemi esterni, forniscono l'accesso ai dati.

Ecco un flusso di lavoro generale della mia architettura per esempio. Supponiamo di avere un FriendsViewController, che mostra l'elenco degli amici dell'utente e abbiamo un'opzione per rimuoverlo dagli amici. Creo un metodo nella mia FriendsServicesclasse chiamato:

- (RACSignal *)removeFriend:(Friend * const)friend

dove si Friendtrova un oggetto modello / dominio (o può essere solo un Useroggetto se hanno attributi simili). Underhood questo metodo analizza Frienda NSDictionaryparametri JSON friend_id, name, surname, friend_request_ide così via. Uso sempre la libreria Mantle per questo tipo di boilerplate e per il mio livello di modello (analizzando avanti e indietro, gestendo gerarchie di oggetti nidificati in JSON e così via). Dopo l'analisi si chiama APIClient DELETEmetodo per fare una richiesta di un riposo effettivo e ritorna Responsea RACSignalal chiamante ( FriendsViewControllernel nostro caso) per visualizzare un messaggio appropriato per l'utente o qualsiasi altra cosa.

Se la nostra applicazione è molto grande, dobbiamo separare la nostra logica ancora più chiara. Ad esempio, non è sempre buono mescolare Repositoryo modellare la logica con Serviceuna. Quando ho descritto il mio approccio, avevo detto che il removeFriendmetodo dovrebbe essere nel Servicelivello, ma se saremo più pedanti possiamo notare che appartiene meglio Repository. Ricordiamo cos'è il repository. Eric Evans ha dato una descrizione precisa nel suo libro [DDD]:

Un repository rappresenta tutti gli oggetti di un certo tipo come un insieme concettuale. Funziona come una raccolta, tranne con una capacità di query più elaborata.

Quindi, a Repositoryè essenzialmente una facciata che utilizza la semantica in stile Collection (Aggiungi, Aggiorna, Rimuovi) per fornire accesso a dati / oggetti. Ecco perché quando si ha qualcosa come: getFriendsList, getUserGroups, removeFriendè possibile inserirlo in Repository, poiché la raccolta-come la semantica è abbastanza chiaro qui. E codice come:

- (RACSignal *)approveFriendRequest:(FriendRequest * const)request;

è sicuramente una logica aziendale, perché va oltre le CRUDoperazioni di base e collega due oggetti di dominio ( Friende Request), ecco perché dovrebbe essere collocato nel Servicelivello. Inoltre voglio notare: non creare astrazioni inutili . Usa saggiamente tutti questi approcci. Perché se travolgerai la tua applicazione con astrazioni, ciò aumenterà la sua complessità accidentale e la complessità causerà più problemi nei sistemi software di ogni altra cosa

Ti descrivo un "vecchio" esempio di Objective-C, ma questo approccio può essere adattato molto facilmente al linguaggio Swift con molti più miglioramenti, perché ha caratteristiche più utili e funzioni funzionali. Consiglio vivamente di usare questa libreria: Moya . Ti consente di creare un livello più elegante APIClient(il nostro cavallo di battaglia come ricordi). Ora il nostro APIClientfornitore sarà un tipo di valore (enum) con estensioni conformi ai protocolli e sfruttando la corrispondenza del modello destrutturante. Swift enums + pattern matching ci consente di creare tipi di dati algebrici come nella classica programmazione funzionale. I nostri microservizi useranno questo APIClientprovider migliorato come nel solito approccio Objective-C. Per il layer modello invece di Mantlete puoi usare la libreria ObjectMappero mi piace usare la libreria Argo più elegante e funzionale .

Quindi, ho descritto il mio approccio architettonico generale, che può essere adattato per qualsiasi applicazione, credo. Ci possono essere molti più miglioramenti, ovviamente. Ti consiglio di imparare la programmazione funzionale, perché puoi trarne un grande vantaggio, ma non esagerare troppo. L'eliminazione di uno stato mutabile eccessivo, condiviso e globale, la creazione di un modello di dominio immutabile o la creazione di funzioni pure senza effetti collaterali esterni è generalmente una buona pratica e il nuovo Swiftlinguaggio lo incoraggia. Ma ricorda sempre che sovraccaricare il tuo codice con pesanti schemi funzionali puri, approcci teorici di categoria è una cattiva idea, perché altri sviluppatori leggeranno e supporteranno il tuo codice e possono essere frustrati o spaventosi delprismatic profunctorse questo genere di cose nel tuo modello immutabile. La stessa cosa con ReactiveCocoa: non RACifyusare troppo il tuo codice , perché può diventare illeggibile molto velocemente, soprattutto per i neofiti. Usalo quando può davvero semplificare i tuoi obiettivi e la tua logica.

Quindi read a lot, mix, experiment, and try to pick up the best from different architectural approaches. È il miglior consiglio che posso darti.


Anche un approccio interessante e solido. Grazie.
MainstreamDeveloper00

1
@darksider Come ho già scritto nella mia risposta: "` Io non uso mai single, classe Dio APIManagerWhatever o altre cose sbagliato, perché Singleton è un anti-modello, e nella maggior parte dei casi (ad eccezione di quelle rare) è una soluzione sbagliata. ". I don't like singletons. I have an opinion that if you decided to use singletons in your project you should have at least three criteria why you do this (I edited my answer). So I inject them (lazy of course and not each time, but una volta `) in ogni controller
Oleksandr Karaberov il

14
Ciao @alexander. Hai qualche esempio di progetto su GitHub? Descrivi un approccio molto interessante. Grazie. Ma sono un principiante nello sviluppo di Objective-C. E per me è difficile capire alcuni aspetti. Forse puoi caricare qualche progetto di test su GitHub e fornire un link?
Denis,

1
Ciao @AlexanderKaraberov, sono un po 'confuso riguardo alla spiegazione del negozio che hai dato. Supponiamo di avere 5 modelli, per ognuno ho 2 classi, una che mantiene la rete e l'altra cache degli oggetti. Ora dovrei avere una classe Store separata per ogni modello che chiama la funzione di rete e classe cache o una singola classe Store che ha tutte le funzioni per ciascun modello, quindi il controller accede sempre al singolo file per i dati.
meteore

1
@icodebuster questo progetto demo mi ha aiutato a comprendere molti dei concetti delineati qui: github.com/darthpelo/NetworkLayerExample

31

Secondo l'obiettivo di questa domanda, vorrei descrivere il nostro approccio all'architettura.

Approccio architettonico

L'architettura della nostra applicazione iOS generale si basa sui seguenti modelli: livelli di servizio , MVVM , associazione dati UI , iniezione di dipendenza ; e paradigma di programmazione reattiva funzionale .

È possibile suddividere un'applicazione tipica rivolta al consumatore nei seguenti livelli logici:

  • montaggio
  • Modello
  • Servizi
  • Conservazione
  • I gestori
  • coordinatori
  • UI
  • Infrastruttura

Il layer di assemblaggio è un punto di avvio della nostra applicazione. Contiene un contenitore di iniezione delle dipendenze e dichiarazioni degli oggetti dell'applicazione e delle relative dipendenze. Questo livello potrebbe contenere anche la configurazione dell'applicazione (URL, chiavi dei servizi di terze parti e così via). A tale scopo utilizziamo la libreria Typhoon .

Il livello del modello contiene classi di modelli di dominio, convalide, mappature. Usiamo la libreria Mantle per mappare i nostri modelli: supporta la serializzazione / deserializzazione in JSONformato e NSManagedObjectmodelli. Per la convalida e la rappresentazione dei moduli dei nostri modelli utilizziamo le librerie FXForms e FXModelValidation .

Il livello Servizi dichiara i servizi che utilizziamo per interagire con sistemi esterni al fine di inviare o ricevere dati rappresentati nel nostro modello di dominio. Quindi di solito abbiamo servizi per la comunicazione con API del server (per entità), servizi di messaggistica (come PubNub ), servizi di archiviazione (come Amazon S3), ecc. Fondamentalmente i servizi avvolgono gli oggetti forniti dagli SDK (ad esempio PubNub SDK) o implementano la propria comunicazione logica. Per le reti generali utilizziamo la libreria AFNetworking .

Lo scopo del livello di archiviazione è organizzare la memorizzazione dei dati locali sul dispositivo. Per questo utilizziamo Core Data o Realm (entrambi hanno pro e contro, la decisione su cosa usare si basa su specifiche concrete). Per la configurazione di Core Data utilizziamo la libreria MDMCoreData e un sacco di classi - archivi - (simili ai servizi) che forniscono accesso all'archiviazione locale per ogni entità. Per Realm utilizziamo solo archivi simili per avere accesso all'archiviazione locale.

Il livello Manager è un luogo in cui vivono le nostre astrazioni / involucri.

In un ruolo di manager potrebbe essere:

  • Credentials Manager con le sue diverse implementazioni (keychain, NSDefaults, ...)
  • Current Session Manager che sa come mantenere e fornire la sessione dell'utente corrente
  • Cattura pipeline che fornisce l'accesso ai dispositivi multimediali (registrazione video, audio, scattare foto)
  • BLE Manager che fornisce l'accesso ai servizi e alle periferiche bluetooth
  • Geo Location Manager
  • ...

Quindi, nel ruolo di manager potrebbe essere qualsiasi oggetto che implementa la logica di un particolare aspetto o preoccupazione necessaria per il funzionamento dell'applicazione.

Cerchiamo di evitare i Singleton, ma questo livello è un posto dove vivono se sono necessari.

Il livello Coordinatori fornisce oggetti che dipendono da oggetti di altri livelli (Servizio, Archiviazione, Modello) al fine di combinare la loro logica in una sequenza di lavoro necessaria per determinati moduli (funzionalità, schermo, user story o esperienza utente). Di solito concatena operazioni asincrone e sa come reagire ai casi di successo e fallimento. Ad esempio, puoi immaginare una funzione di messaggistica e l' MessagingCoordinatoroggetto corrispondente . La gestione delle operazioni di invio dei messaggi potrebbe essere simile alla seguente:

  1. Convalida messaggio (livello modello)
  2. Salva messaggio localmente (archiviazione messaggi)
  3. Carica allegato messaggio (servizio amazon s3)
  4. Aggiorna lo stato dei messaggi e gli URL degli allegati e salva i messaggi localmente (archiviazione dei messaggi)
  5. Serializza il messaggio nel formato JSON (livello del modello)
  6. Pubblica messaggio su PubNub (servizio PubNub)
  7. Aggiorna lo stato e gli attributi del messaggio e salvalo localmente (archiviazione dei messaggi)

Su ciascuno dei passaggi precedenti viene gestito un errore corrispondente.

Il livello dell'interfaccia utente è costituito dai seguenti sublayer:

  1. ViewModels
  2. ViewControllers
  3. Visualizzazioni

Per evitare Massive View Controller utilizziamo il modello MVVM e implementiamo la logica necessaria per la presentazione dell'interfaccia utente in ViewModels. Un ViewModel di solito ha coordinatori e manager come dipendenze. ViewModels utilizzati da ViewController e alcuni tipi di viste (ad es. Celle con vista tabella). La colla tra ViewControllers e ViewModels è il modello Data Binding e Command. Per rendere possibile avere quella colla usiamo la libreria ReactiveCocoa .

Usiamo anche ReactiveCocoa e il suo RACSignalconcetto come interfaccia e tipo di valore di ritorno di tutti i coordinatori, servizi, metodi di memorizzazione. Questo ci consente di eseguire operazioni a catena, eseguirle in parallelo o in serie e molte altre cose utili fornite da ReactiveCocoa.

Cerchiamo di implementare il nostro comportamento dell'interfaccia utente in modo dichiarativo. Rilegatura dati e layout automatico aiutano molto a raggiungere questo obiettivo.

Il livello dell'infrastruttura contiene tutti gli helper, le estensioni e le utilità necessarie per il lavoro dell'applicazione.


Questo approccio funziona bene per noi e per quei tipi di app che di solito costruiamo. Ma dovresti capire che questo è solo un approccio soggettivo che dovrebbe essere adattato / cambiato allo scopo di una squadra concreta.

Spero che questo ti possa aiutare!

Inoltre puoi trovare maggiori informazioni sul processo di sviluppo di iOS in questo post sul blog Sviluppo iOS come servizio


Ho iniziato ad apprezzare questa architettura qualche mese fa, grazie Alex per averla condivisa! Vorrei provarlo con RxSwift nel prossimo futuro!
Ingaham,

18

Poiché tutte le app iOS sono diverse, penso che qui ci siano diversi approcci da considerare, ma di solito vado in questo modo:
Crea una classe di gestore centrale (singleton) per gestire tutte le richieste API (di solito chiamato APICommunicator) e ogni metodo di istanza è una chiamata API . E c'è un metodo centrale (non pubblico):

-(RACSignal *)sendGetToServerToSubPath:(NSString *)path withParameters:(NSDictionary *)params;

Per la cronaca, utilizzo 2 librerie / framework principali, ReactiveCocoa e AFNetworking. ReactiveCocoa gestisce perfettamente le risposte di rete asincrone, puoi farlo (sendNext :, sendError :, ecc.).
Questo metodo chiama l'API, ottiene i risultati e li invia tramite RAC in formato 'raw' (come NSArray che restituisce AFNetworking).
Quindi un metodo come quello getStuffList:chiamato il metodo sopra si abbona al suo segnale, analizza i dati grezzi in oggetti (con qualcosa come Motis) e invia gli oggetti uno a uno al chiamante ( getStuffList:e metodi simili restituiscono anche un segnale a cui il controller può abbonarsi ).
Il controller sottoscritto riceve gli oggetti dal subscribeNext:blocco e li gestisce.

Ho provato molti modi in diverse app, ma questa ha funzionato al meglio, quindi l'ho usata di recente in alcune app, si adatta sia a progetti piccoli che grandi ed è facile da estendere e mantenere se qualcosa deve essere modificato.
Spero che questo aiuti, mi piacerebbe sentire le opinioni degli altri sul mio approccio e forse su come gli altri potrebbero essere migliorati.


2
Grazie per la risposta +1. Buon approccio. Lascio la domanda. Forse avremo altri approcci da altri sviluppatori.
MainstreamDeveloper00

1
Mi piace una variazione di questo approccio: utilizzo un gestore API centrale che si occupa dei meccanismi di comunicazione con l'API. Tuttavia, provo a rendere tutte le funzionalità esposte sui miei oggetti modello. I modelli forniranno metodi simili + (void)getAllUsersWithSuccess:(void(^)(NSArray*))success failure:(void(^)(NSError*))failure;e - (void)postWithSuccess:(void(^)(instancetype))success failure:(void(^)(NSError*))failure;che eseguono i preparativi necessari e quindi richiamano il gestore API.
jsadler,

1
Questo approccio è semplice, ma con l'aumentare del numero di API, diventa più difficile mantenere il gestore API singleton. E ogni nuova API aggiunta si riferirà al gestore, indipendentemente dal modulo a cui appartiene questa API. Prova a utilizzare github.com/kevin0571/STNetTaskQueue per gestire le richieste API.
Kevin,

A parte il motivo per cui stai pubblicizzando la tua biblioteca che è il più lontano possibile dalla mia soluzione e molto più complicata, ho provato questo approccio su innumerevoli progetti sia piccoli che grandi come menzionato e l'ho usato esattamente come lo stesso da quando ho scritto questa risposta. Con convenzioni di denominazione intelligenti non è affatto difficile da mantenere.
Rickye,

8

Nella mia situazione di solito sto usando la libreria ResKit per configurare il livello di rete. Fornisce analisi di facile utilizzo. Riduce i miei sforzi per impostare la mappatura per diverse risposte e cose.

Aggiungo solo un po 'di codice per impostare automaticamente la mappatura. Definisco la classe base per i miei modelli (non protocollo a causa del sacco di codice per verificare se qualche metodo è implementato o meno e meno codice nei modelli stessi):

MappableEntry.h

@interface MappableEntity : NSObject

+ (NSArray*)pathPatterns;
+ (NSArray*)keyPathes;
+ (NSArray*)fieldsArrayForMapping;
+ (NSDictionary*)fieldsDictionaryForMapping;
+ (NSArray*)relationships;

@end

MappableEntry.m

@implementation MappableEntity

+(NSArray*)pathPatterns {
    return @[];
}

+(NSArray*)keyPathes {
    return nil;
}

+(NSArray*)fieldsArrayForMapping {
    return @[];
}

+(NSDictionary*)fieldsDictionaryForMapping {
    return @{};
}

+(NSArray*)relationships {
    return @[];
}

@end

Le relazioni sono oggetti che rappresentano oggetti nidificati in risposta:

RelationshipObject.h

@interface RelationshipObject : NSObject

@property (nonatomic,copy) NSString* source;
@property (nonatomic,copy) NSString* destination;
@property (nonatomic) Class mappingClass;

+(RelationshipObject*)relationshipWithKey:(NSString*)key andMappingClass:(Class)mappingClass;
+(RelationshipObject*)relationshipWithSource:(NSString*)source destination:(NSString*)destination andMappingClass:(Class)mappingClass;

@end

RelationshipObject.m

@implementation RelationshipObject

+(RelationshipObject*)relationshipWithKey:(NSString*)key andMappingClass:(Class)mappingClass {
    RelationshipObject* object = [[RelationshipObject alloc] init];
    object.source = key;
    object.destination = key;
    object.mappingClass = mappingClass;
    return object;
}

+(RelationshipObject*)relationshipWithSource:(NSString*)source destination:(NSString*)destination andMappingClass:(Class)mappingClass {
    RelationshipObject* object = [[RelationshipObject alloc] init];
    object.source = source;
    object.destination = destination;
    object.mappingClass = mappingClass;
    return object;
}

@end

Quindi sto impostando la mappatura per RestKit in questo modo:

ObjectMappingInitializer.h

@interface ObjectMappingInitializer : NSObject

+(void)initializeRKObjectManagerMapping:(RKObjectManager*)objectManager;

@end

ObjectMappingInitializer.m

@interface ObjectMappingInitializer (Private)

+ (NSArray*)mappableClasses;

@end

@implementation ObjectMappingInitializer

+(void)initializeRKObjectManagerMapping:(RKObjectManager*)objectManager {

    NSMutableDictionary *mappingObjects = [NSMutableDictionary dictionary];

    // Creating mappings for classes
    for (Class mappableClass in [self mappableClasses]) {
        RKObjectMapping *newMapping = [RKObjectMapping mappingForClass:mappableClass];
        [newMapping addAttributeMappingsFromArray:[mappableClass fieldsArrayForMapping]];
        [newMapping addAttributeMappingsFromDictionary:[mappableClass fieldsDictionaryForMapping]];
        [mappingObjects setObject:newMapping forKey:[mappableClass description]];
    }

    // Creating relations for mappings
    for (Class mappableClass in [self mappableClasses]) {
        RKObjectMapping *mapping = [mappingObjects objectForKey:[mappableClass description]];
        for (RelationshipObject *relation in [mappableClass relationships]) {
            [mapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:relation.source toKeyPath:relation.destination withMapping:[mappingObjects objectForKey:[relation.mappingClass description]]]];
        }
    }

    // Creating response descriptors with mappings
    for (Class mappableClass in [self mappableClasses]) {
        for (NSString* pathPattern in [mappableClass pathPatterns]) {
            if ([mappableClass keyPathes]) {
                for (NSString* keyPath in [mappableClass keyPathes]) {
                    [objectManager addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:[mappingObjects objectForKey:[mappableClass description]] method:RKRequestMethodAny pathPattern:pathPattern keyPath:keyPath statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]];
                }
            } else {
                [objectManager addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:[mappingObjects objectForKey:[mappableClass description]] method:RKRequestMethodAny pathPattern:pathPattern keyPath:nil statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]];
            }
        }
    }

    // Error Mapping
    RKObjectMapping *errorMapping = [RKObjectMapping mappingForClass:[Error class]];
    [errorMapping addAttributeMappingsFromArray:[Error fieldsArrayForMapping]];
    for (NSString *pathPattern in Error.pathPatterns) {
        [[RKObjectManager sharedManager] addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:errorMapping method:RKRequestMethodAny pathPattern:pathPattern keyPath:nil statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassClientError)]];
    }
}

@end

@implementation ObjectMappingInitializer (Private)

+ (NSArray*)mappableClasses {
    return @[
        [FruiosPaginationResults class],
        [FruioItem class],
        [Pagination class],
        [ContactInfo class],
        [Credentials class],
        [User class]
    ];
}

@end

Alcuni esempi di implementazione di MappableEntry:

User.h

@interface User : MappableEntity

@property (nonatomic) long userId;
@property (nonatomic, copy) NSString *username;
@property (nonatomic, copy) NSString *email;
@property (nonatomic, copy) NSString *password;
@property (nonatomic, copy) NSString *token;

- (instancetype)initWithUsername:(NSString*)username email:(NSString*)email password:(NSString*)password;

- (NSDictionary*)registrationData;

@end

User.m

@implementation User

- (instancetype)initWithUsername:(NSString*)username email:(NSString*)email password:(NSString*)password {
    if (self = [super init]) {
        self.username = username;
        self.email = email;
        self.password = password;
    }
    return self;
}

- (NSDictionary*)registrationData {
    return @{
        @"username": self.username,
        @"email": self.email,
        @"password": self.password
    };
}

+ (NSArray*)pathPatterns {
    return @[
        [NSString stringWithFormat:@"/api/%@/users/register", APIVersionString],
        [NSString stringWithFormat:@"/api/%@/users/login", APIVersionString]
    ];
}

+ (NSArray*)fieldsArrayForMapping {
    return @[ @"username", @"email", @"password", @"token" ];
}

+ (NSDictionary*)fieldsDictionaryForMapping {
    return @{ @"id": @"userId" };
}

@end

Ora sul wrapping delle richieste:

Ho un file di intestazione con la definizione di blocchi, per ridurre la lunghezza della linea in tutte le classi APIRequest:

APICallbacks.h

typedef void(^SuccessCallback)();
typedef void(^SuccessCallbackWithObjects)(NSArray *objects);
typedef void(^ErrorCallback)(NSError *error);
typedef void(^ProgressBlock)(float progress);

Ed esempio della mia classe APIRequest che sto usando:

LoginAPI.h

@interface LoginAPI : NSObject

- (void)loginWithCredentials:(Credentials*)credentials onSuccess:(SuccessCallbackWithObjects)onSuccess onError:(ErrorCallback)onError;

@end

LoginAPI.m

@implementation LoginAPI

- (void)loginWithCredentials:(Credentials*)credentials onSuccess:(SuccessCallbackWithObjects)onSuccess onError:(ErrorCallback)onError {
    [[RKObjectManager sharedManager] postObject:nil path:[NSString stringWithFormat:@"/api/%@/users/login", APIVersionString] parameters:[credentials credentialsData] success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
        onSuccess(mappingResult.array);
    } failure:^(RKObjectRequestOperation *operation, NSError *error) {
        onError(error);
    }];
}

@end

E tutto ciò che devi fare nel codice, è sufficiente inizializzare l'oggetto API e chiamarlo quando ne hai bisogno:

SomeViewController.m

@implementation SomeViewController {
    LoginAPI *_loginAPI;
    // ...
}

- (void)viewDidLoad {
    [super viewDidLoad];

    _loginAPI = [[LoginAPI alloc] init];
    // ...
}

// ...

- (IBAction)signIn:(id)sender {
    [_loginAPI loginWithCredentials:_credentials onSuccess:^(NSArray *objects) {
        // Success Block
    } onError:^(NSError *error) {
        // Error Block
    }];
}

// ...

@end

Il mio codice non è perfetto, ma è facile da impostare una volta e utilizzare per diversi progetti. Se è interessante per chiunque, mb potrei passare un po 'di tempo e crearne una soluzione universale da qualche parte su GitHub e CocoaPods.


7

Secondo me tutta l'architettura del software è guidata dalle necessità. Se questo è per scopi personali o di apprendimento, quindi decidere l'obiettivo principale e guidare quell'architettura. Se si tratta di un'opera a noleggio, la necessità aziendale è fondamentale. Il trucco è non lasciare che le cose brillanti ti distraggano dai bisogni reali. Lo trovo difficile da fare. Ci sono sempre nuove cose luccicanti che appaiono in questo business e molte non sono utili, ma non puoi sempre dirlo in anticipo. Concentrati sulla necessità e sii disposto ad abbandonare le cattive scelte se puoi.

Ad esempio, recentemente ho realizzato un rapido prototipo di un'app di condivisione di foto per un'azienda locale. Dato che l'azienda aveva bisogno di fare qualcosa di rapido e sporco, l'architettura ha finito per essere un po 'di codice iOS per far apparire una telecamera e un codice di rete collegato a un pulsante Invia che ha caricato l'immagine in un negozio S3 e ha scritto su un dominio SimpleDB. Il codice era banale e il costo minimo e il client ha una raccolta di foto scalabile accessibile sul web con chiamate REST. Economica e stupida, l'app presentava molti difetti e bloccava l'interfaccia utente in alcune occasioni, ma sarebbe uno spreco fare di più per un prototipo e consentire loro di implementare il proprio personale e generare facilmente migliaia di immagini di prova senza prestazioni o scalabilità preoccupazioni. Architettura scadente, ma si adatta perfettamente alle esigenze e ai costi.

Un altro progetto prevedeva l'implementazione di un database sicuro locale che si sincronizza con il sistema aziendale in background quando la rete è disponibile. Ho creato un sincronizzatore in background che utilizza RestKit in quanto sembrava avere tutto ciò di cui avevo bisogno. Ma ho dovuto scrivere così tanto codice personalizzato per RestKit per gestire JSON idiosincratico che avrei potuto fare tutto più velocemente scrivendo le mie trasformazioni JSON su CoreData. Tuttavia, il cliente voleva portare questa app in casa e ho pensato che RestKit sarebbe stato simile ai framework che hanno usato su altre piattaforme. Sto aspettando di vedere se è stata una buona decisione.

Ancora una volta, il problema per me è concentrarsi sulla necessità e lasciare che questo determini l'architettura. Cerco da morire di evitare l'uso di pacchetti di terze parti in quanto comportano costi che compaiono solo dopo che l'app è stata sul campo per un po '. Cerco di evitare di creare gerarchie di classi poiché raramente pagano. Se riesco a scrivere qualcosa in un periodo di tempo ragionevole invece di adottare un pacchetto che non si adatta perfettamente, allora lo faccio. Il mio codice è ben strutturato per il debug e opportunamente commentato, ma raramente i pacchetti di terze parti lo sono. Detto questo, trovo AF Networking troppo utile per ignorare e ben strutturato, ben commentato e mantenuto e lo uso molto! RestKit copre molti casi comuni, ma mi sento come se fossi in una rissa quando lo uso, e la maggior parte delle fonti di dati che incontro sono piene di stranezze e problemi che sono meglio gestiti con il codice personalizzato. Nelle mie ultime app uso solo i convertitori JSON integrati e scrivo alcuni metodi di utilità.

Un modello che uso sempre è quello di ottenere le chiamate di rete dal thread principale. Le ultime 4-5 app che ho fatto hanno impostato un'attività in background usando dispatch_source_create che si sveglia ogni tanto e fa le attività di rete secondo necessità. È necessario eseguire alcune operazioni di sicurezza del thread e assicurarsi che il codice di modifica dell'interfaccia utente venga inviato al thread principale. Aiuta anche a eseguire l'onboarding / l'inizializzazione in modo tale che l'utente non si senta gravato o ritardato. Finora questo ha funzionato piuttosto bene. Suggerisco di esaminare queste cose.

Infine, penso che mentre lavoriamo di più e man mano che il sistema operativo si evolve, tendiamo a sviluppare soluzioni migliori. Mi ci sono voluti anni per superare la mia convinzione di dover seguire schemi e progetti che altre persone dichiarano obbligatori. Se sto lavorando in un contesto in cui fa parte della religione locale, ehm, intendo le migliori pratiche di ingegneria dipartimentale, quindi seguo i costumi alla lettera, è per questo che mi stanno pagando. Ma raramente trovo che seguire modelli e schemi più vecchi sia la soluzione ottimale. Cerco sempre di esaminare la soluzione attraverso il prisma delle esigenze aziendali e di costruire l'architettura per abbinarla e mantenere le cose il più semplici possibile. Quando sento che non c'è abbastanza, ma tutto funziona correttamente, allora sono sulla strada giusta.


4

Uso l'approccio che ho ottenuto da qui: https://github.com/Constantine-Fry/Foursquare-API-v2 . Ho riscritto quella libreria in Swift e puoi vedere l'approccio architettonico da queste parti del codice:

typealias OpertaionCallback = (success: Bool, result: AnyObject?) -> ()

class Foursquare{
    var authorizationCallback: OperationCallback?
    var operationQueue: NSOperationQueue
    var callbackQueue: dispatch_queue_t?

    init(){
        operationQueue = NSOperationQueue()
        operationQueue.maxConcurrentOperationCount = 7;
        callbackQueue = dispatch_get_main_queue();
    }

    func checkIn(venueID: String, shout: String, callback: OperationCallback) -> NSOperation {
        let parameters: Dictionary <String, String> = [
            "venueId":venueID,
            "shout":shout,
            "broadcast":"public"]
        return self.sendRequest("checkins/add", parameters: parameters, httpMethod: "POST", callback: callback)
    }

    func sendRequest(path: String, parameters: Dictionary <String, String>, httpMethod: String, callback:OperationCallback) -> NSOperation{
        let url = self.constructURL(path, parameters: parameters)
        var request = NSMutableURLRequest(URL: url)
        request.HTTPMethod = httpMethod
        let operation = Operation(request: request, callbackBlock: callback, callbackQueue: self.callbackQueue!)
        self.operationQueue.addOperation(operation)
        return operation
    }

    func constructURL(path: String, parameters: Dictionary <String, String>) -> NSURL {
        var parametersString = kFSBaseURL+path
        var firstItem = true
        for key in parameters.keys {
            let string = parameters[key]
            let mark = (firstItem ? "?" : "&")
            parametersString += "\(mark)\(key)=\(string)"
            firstItem = false
        }
    return NSURL(string: parametersString.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding))
    }
}

class Operation: NSOperation {
    var callbackBlock: OpertaionCallback
    var request: NSURLRequest
    var callbackQueue: dispatch_queue_t

    init(request: NSURLRequest, callbackBlock: OpertaionCallback, callbackQueue: dispatch_queue_t) {
        self.request = request
        self.callbackBlock = callbackBlock
        self.callbackQueue = callbackQueue
    }

    override func main() {
        var error: NSError?
        var result: AnyObject?
        var response: NSURLResponse?

        var recievedData: NSData? = NSURLConnection.sendSynchronousRequest(self.request, returningResponse: &response, error: &error)

        if self.cancelled {return}

        if recievedData{
            result = NSJSONSerialization.JSONObjectWithData(recievedData, options: nil, error: &error)
            if result != nil {
                if result!.isKindOfClass(NSClassFromString("NSError")){
                    error = result as? NSError
            }
        }

        if self.cancelled {return}

        dispatch_async(self.callbackQueue, {
            if (error) {
                self.callbackBlock(success: false, result: error!);
            } else {
                self.callbackBlock(success: true, result: result!);
            }
            })
    }

    override var concurrent:Bool {get {return true}}
}

Fondamentalmente, esiste una sottoclasse NSOperation che crea NSURLRequest, analizza la risposta JSON e aggiunge il blocco di callback con il risultato alla coda. La classe API principale crea NSURLRequest, inizializza la sottoclasse NSOperation e la aggiunge alla coda.


3

Utilizziamo alcuni approcci a seconda della situazione. Per la maggior parte delle cose AFNetworking è l'approccio più semplice e robusto in quanto è possibile impostare intestazioni, caricare dati multipart, utilizzare GET, POST, PUT & DELETE e ci sono un sacco di categorie aggiuntive per UIKit che consentono ad esempio di impostare un'immagine da un url. In un'app complessa con molte chiamate, a volte lo deduciamo a un nostro metodo di praticità che sarebbe qualcosa del tipo:

-(void)makeRequestToUrl:(NSURL *)url withParameters:(NSDictionary *)parameters success:(void (^)(id responseObject))success failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure;

Vi sono alcune situazioni in cui AFNetworking non è appropriato, tuttavia, ad esempio in cui si sta creando un framework o un altro componente di libreria in quanto AFNetworking potrebbe già trovarsi in un'altra base di codice. In questa situazione, useresti un NSMutableURLRequest in linea se stai effettuando una singola chiamata o estratto in una classe richiesta / risposta.


Per me questa è la risposta migliore e più chiara, evviva. "È così semplice". @martin, personalmente usiamo sempre NSMutableURLRequest; c'è qualche vero motivo per usare AFNetworking?
Fattie,

AFNetworking è davvero conveniente. Per me i blocchi di successo e fallimento fanno valere la pena, poiché facilita la gestione del codice. Concordo sul fatto che a volte è un eccesso totale.
Martin,

Un punto superbo sui blocchi, grazie per quello. Immagino che la natura specifica di questo cambierà con Swift.
Fattie

2

Evito i singoli punti durante la progettazione delle mie applicazioni. Sono una scelta tipica per molte persone, ma penso che tu possa trovare soluzioni più eleganti altrove. In genere quello che faccio è costruire le mie entità in CoreData e quindi inserire il mio codice REST in una categoria NSManagedObject. Se per esempio volessi creare e postare un nuovo utente, farei questo:

User* newUser = [User createInManagedObjectContext:managedObjectContext];
[newUser postOnSuccess:^(...) { ... } onFailure:^(...) { ... }];

Uso RESTKit per la mappatura degli oggetti e lo inizializzo all'avvio. Trovo che instradare tutte le tue chiamate attraverso un singleton sia una perdita di tempo e aggiunge un sacco di boilerplate che non sono necessari.

In NSManagedObject + Extensions.m:

+ (instancetype)createInContext:(NSManagedObjectContext*)context
{
    NSAssert(context.persistentStoreCoordinator.managedObjectModel.entitiesByName[[self entityName]] != nil, @"Entity with name %@ not found in model. Is your class name the same as your entity name?", [self entityName]);
    return [NSEntityDescription insertNewObjectForEntityForName:[self entityName] inManagedObjectContext:context];
}

In NSManagedObject + Networking.m:

- (void)getOnSuccess:(RESTSuccess)onSuccess onFailure:(RESTFailure)onFailure blockInput:(BOOL)blockInput
{
    [[RKObjectManager sharedManager] getObject:self path:nil parameters:nil success:onSuccess failure:onFailure];
    [self handleInputBlocking:blockInput];
}

Perché aggiungere ulteriori classi di supporto quando è possibile estendere la funzionalità di una classe base comune attraverso le categorie?

Se sei interessato a informazioni più dettagliate sulla mia soluzione, fammelo sapere. Sono felice di condividere.


3
Sarebbe sicuramente interessato a leggere questo approccio in modo più dettagliato in un post sul blog.
Danyal Aytekin,


0

Da una prospettiva di design puramente di classe, di solito avrai qualcosa del genere:

  • I controller di visualizzazione che controllano una o più visualizzazioni
  • Classe del modello di dati : dipende davvero da quante entità reali distinte hai a che fare e da come sono correlate.

    Ad esempio, se si dispone di una matrice di elementi da visualizzare in quattro diverse rappresentazioni (elenco, grafico, grafico, ecc.), Si avrà una classe di modello di dati per l'elenco di articoli, un'altra per un articolo. L' elenco della classe di elementi verrà condiviso da quattro controller di visualizzazione, tutti elementi secondari di un controller della barra delle schede o di un controller di navigazione.

    Le classi del modello di dati saranno utili non solo per visualizzare i dati, ma anche per serializzarli in cui ciascuno di essi può esporre il proprio formato di serializzazione tramite i metodi di esportazione JSON / XML / CSV (o qualsiasi altra cosa).

  • È importante comprendere che sono necessarie anche le classi del builder di richieste API mappate direttamente con gli endpoint API REST. Supponiamo che tu abbia un'API che accede l'utente, quindi la tua classe builder API di accesso creerà payload JSON POST per API di accesso. In un altro esempio, una classe del generatore di richieste API per un elenco di API di voci di catalogo creerà una stringa di query GET per l'API corrispondente e genererà la query GEST REST.

    Queste classi del builder di richieste API normalmente ricevono dati dai controller di visualizzazione e restituiscono gli stessi dati per visualizzare i controller per l'aggiornamento dell'interfaccia utente / altre operazioni. I controller di vista decideranno quindi come aggiornare gli oggetti Modello dati con tali dati.

  • Infine, il cuore del client REST - la classe di recupero dei dati API che è ignara di tutti i tipi di richieste API effettuate dall'app. Questa classe sarà più probabilmente un singleton, ma come altri hanno sottolineato, non deve essere un singleton.

    Si noti che il collegamento è solo un'implementazione tipica e non prende in considerazione scenari come sessione, cookie ecc., Ma è sufficiente per iniziare senza utilizzare framework di terze parti.


0

Questa domanda ha già molte risposte eccellenti ed estese, ma sento di doverle menzionare poiché nessun altro ha.

Alamofire per Swift. https://github.com/Alamofire/Alamofire

È stato creato dalle stesse persone di AFNetworking, ma è progettato più direttamente pensando a Swift.


0

Penso che per il progetto medio utilizzi l'architettura MVVM e il grande progetto usi l'architettura VIPER e cerco di raggiungere

  • Programmazione orientata al protocollo
  • Modelli di progettazione software
  • VENDUTO principio
  • Programmazione generica
  • Non ripeterti (SECCO)

E approcci architetturali per la creazione di applicazioni di rete iOS (client REST)

La preoccupazione di separazione per codice pulito e leggibile evita la duplicazione:

import Foundation
enum DataResponseError: Error {
    case network
    case decoding

    var reason: String {
        switch self {
        case .network:
            return "An error occurred while fetching data"
        case .decoding:
            return "An error occurred while decoding data"
        }
    }
}

extension HTTPURLResponse {
    var hasSuccessStatusCode: Bool {
        return 200...299 ~= statusCode
    }
}

enum Result<T, U: Error> {
    case success(T)
    case failure(U)
}

inversione di dipendenza

 protocol NHDataProvider {
        func fetchRemote<Model: Codable>(_ val: Model.Type, url: URL, completion: @escaping (Result<Codable, DataResponseError>) -> Void)
    }

Responsabile principale:

  final class NHClientHTTPNetworking : NHDataProvider {

        let session: URLSession

        init(session: URLSession = URLSession.shared) {
            self.session = session
        }

        func fetchRemote<Model: Codable>(_ val: Model.Type, url: URL,
                             completion: @escaping (Result<Codable, DataResponseError>) -> Void) {
            let urlRequest = URLRequest(url: url)
            session.dataTask(with: urlRequest, completionHandler: { data, response, error in
                guard
                    let httpResponse = response as? HTTPURLResponse,
                    httpResponse.hasSuccessStatusCode,
                    let data = data
                    else {
                        completion(Result.failure(DataResponseError.network))
                        return
                }
                guard let decodedResponse = try? JSONDecoder().decode(Model.self, from: data) else {
                    completion(Result.failure(DataResponseError.decoding))
                    return
                }
                completion(Result.success(decodedResponse))
            }).resume()
        }
    }

Qui troverai l' architettura GitHub MVVM con il resto dell'API Swift Project


0

Nell'ingegneria del software mobile, i più utilizzati sono i modelli Clean Architecture + MVVM e Redux.

Clean Architecture + MVVM sono composti da 3 livelli: Dominio, Presentazione, Livelli dati. Dove il livello di presentazione e il livello dei repository di dati dipendono dal livello di dominio:

Presentation Layer -> Domain Layer <- Data Repositories Layer

E il livello di presentazione è costituito da ViewModels e Views (MVVM):

Presentation Layer (MVVM) = ViewModels + Views
Domain Layer = Entities + Use Cases + Repositories Interfaces
Data Repositories Layer = Repositories Implementations + API (Network) + Persistence DB

In questo articolo, c'è una descrizione più dettagliata di Clean Architecture + MVVM https://tech.olx.com/clean-architecture-and-mvvm-on-ios-c9d167d9f5b3

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.