Beste architektonische Ansätze zum Erstellen von iOS-Netzwerkanwendungen (REST-Clients)


323

Ich bin ein iOS-Entwickler mit etwas Erfahrung und diese Frage ist wirklich interessant für mich. Ich habe viele verschiedene Ressourcen und Materialien zu diesem Thema gesehen, bin aber trotzdem verwirrt. Was ist die beste Architektur für eine iOS-Netzwerkanwendung? Ich meine grundlegende abstrakte Frameworks, Muster, die zu jeder Netzwerkanwendung passen, egal ob es sich um eine kleine App mit nur wenigen Serveranforderungen oder einen komplexen REST-Client handelt. Apple empfiehlt, MVCals grundlegenden Architekturansatz für alle iOS-Anwendungen zu verwenden, jedoch weder für MVCdie modernerenMVVM Muster erklären, wo Netzwerklogikcode abgelegt und wie er allgemein organisiert werden soll.

Muss ich so etwas wie MVCS( Sfür Service) entwickeln und in diese ServiceEbene alle APIAnforderungen und andere Netzwerklogiken einfügen, die in der Perspektive sehr komplex sein können? Nach einigen Recherchen habe ich zwei grundlegende Ansätze dafür gefunden. Hier wurde empfohlen, für jede Netzwerkanforderung an den Webdienst API(wie LoginRequestKlasse oder PostCommentRequestKlasse usw.) eine separate Klasse zu erstellen, die alle von der abstrakten Klassenoptimierung der Basisanforderung erbt , wenn komplexe Objektzuordnungen und Persistenz vorliegen, oder sogar eine eigene Netzwerkkommunikationsimplementierung mit Standard-API). Aber dieser Ansatz scheint mir ein Overhead zu sein. Ein anderer Ansatz besteht darin, eine Singleton- Dispatcher- oder Manager-Klasse wie im ersten Ansatz zu haben.AbstractBaseRequest und zusätzlich einen globalen Netzwerkmanager zu erstellen, der gemeinsamen Netzwerkcode und kapselt andere Einstellungen (es kann AFNetworkingAnpassung sein oderRestKitAPI jedoch nicht für jede Anforderung Klassen zu erstellen und stattdessen jede Anforderung als öffentliche Instanzmethode dieser Manager-Klasse wie : fetchContacts, loginUserMethoden usw. zu kapseln . Also, was ist der beste und richtige Weg? Gibt es andere interessante Ansätze, die ich noch nicht kenne?

Und sollte ich eine weitere Ebene für all diese Netzwerk-Inhalte wie Serviceoder eine NetworkProviderEbene oder was auch immer über meiner MVCArchitektur erstellen, oder sollte diese Ebene in vorhandene MVCEbenen integriert (injiziert) werden , z Model.

Ich weiß, dass es schöne Ansätze gibt, oder wie gehen solche mobilen Monster wie der Facebook-Client oder der LinkedIn-Client mit der exponentiell wachsenden Komplexität der Netzwerklogik um?

Ich weiß, dass es keine genaue und formale Antwort auf das Problem gibt. Ziel dieser Frage ist es, die interessantesten Ansätze von erfahrenen iOS-Entwicklern zu sammeln . Der am besten vorgeschlagene Ansatz wird als akzeptiert markiert und mit einer Reputationsprämie ausgezeichnet, andere werden positiv bewertet. Es ist meist eine theoretische und Forschungsfrage. Ich möchte den grundlegenden, abstrakten und korrekten Architekturansatz für Netzwerkanwendungen in iOS verstehen. Ich hoffe auf eine ausführliche Erklärung von erfahrenen Entwicklern.


14
Ist das nicht eine "Einkaufsliste" Frage? Ich hatte gerade eine Frage zur Hölle abgewählt und geschlossen, weil gesagt wurde, dass "Was ist das Beste?" Was macht diese Einkaufslistenfrage zu einer guten Frage, die es wert ist, positiv bewertet zu werden, während andere geschlossen werden?
Alvin Thompson

1
In der Regel wird die Netzwerklogik in die Steuerung integriert, wodurch ein Modellobjekt geändert und Delegierte oder Beobachter benachrichtigt werden.
quellish

1
Sehr interessante Fragen und Antworten. Nach 4 Jahren iOS-Codierung und dem Versuch, den schönsten Weg zu finden, der App eine Netzwerkebene hinzuzufügen. Welche Klasse sollte für die Verwaltung einer Netzwerkanforderung verantwortlich sein? Die Antworten unten sind wirklich relevant. Vielen Dank
Darksider

@ JoeBlow das ist nicht wahr. Die Branche für mobile Apps ist nach wie vor stark auf die Kommunikation zwischen Server und Client angewiesen.
Scord

Antworten:


327

I want to understand basic, abstract and correct architectural approach for networking applications in iOS: Es gibt keinen "besten" oder "richtigsten" Ansatz zum Erstellen einer Anwendungsarchitektur. Es ist ein sehr kreativer Job. Sie sollten immer die einfachste und erweiterbarste Architektur wählen, die für jeden Entwickler, der mit der Arbeit an Ihrem Projekt beginnt, oder für andere Entwickler in Ihrem Team klar ist, aber ich stimme zu, dass es eine "gute" und eine "schlechte" geben kann " die Architektur.

Sie sagten: „ collect the most interesting approaches from experienced iOS developersIch denke nicht, dass mein Ansatz am interessantesten oder korrektesten ist, aber ich habe ihn in mehreren Projekten verwendet und bin damit zufrieden. Es ist ein hybrider Ansatz der oben genannten und auch Verbesserungen meiner eigenen Forschungsanstrengungen. Ich interessiere mich für die Probleme beim Aufbau von Ansätzen, die mehrere bekannte Muster und Redewendungen kombinieren. Ich denke, viele von Fowlers Unternehmensmustern können erfolgreich auf mobile Anwendungen angewendet werden. Hier ist eine Liste der interessantesten, die wir zum Erstellen einer iOS-Anwendungsarchitektur anwenden können ( meiner Meinung nach ): Serviceschicht , Arbeitseinheit , Remote-Fassade , Datenübertragungsobjekt ,Gateway erstellen LevelDB basiert, Layer Supertype , Sonderfall , Domänenmodell . Sie sollten eine Modellebene immer korrekt entwerfen und die Persistenz nicht vergessen (dies kann die Leistung Ihrer App erheblich steigern). Sie können Core Datadies verwenden. Sie sollten jedoch nicht vergessen, dass dies Core Datakein ORM oder eine Datenbank ist, sondern ein Objektdiagramm-Manager mit Persistenz als gute Option. Daher kann es sehr oft Core Datazu schwer für Ihre Anforderungen sein und Sie können sich neue Lösungen wie Realm und Couchbase Lite ansehen oder eine eigene kompakte Objektzuordnungs- / Persistenzschicht erstellen, die auf SQLite oder SQLite basiert . Ich rate Ihnen auch, sich mit dem vertraut zu machenDomain Driven Design und CQRS .

Zuerst denke ich, wir sollten eine weitere Ebene für das Networking schaffen, weil wir keine Fat Controller oder schweren, überforderten Modelle wollen. Ich glaube nicht an diese fat model, skinny controllerDinge. Aber ich glaube an skinny everythingAnsatz, weil keine Klasse jemals fett sein sollte. Alle Netzwerke können im Allgemeinen als Geschäftslogik abstrahiert werden, daher sollten wir eine andere Ebene haben, auf der wir sie platzieren können. Service Layer ist das, was wir brauchen:

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

In unserem MVCBereich Service Layerist so etwas wie ein Vermittler zwischen Domänenmodell und Controllern. Es gibt eine ziemlich ähnliche Variante dieses Ansatzes namens MVCS, bei der a Storetatsächlich unsere ServiceSchicht ist. Storeverkauft Modellinstanzen und kümmert sich um das Netzwerk, das Caching usw. Ich möchte erwähnen, dass Sie nicht Ihre gesamte Netzwerk- und Geschäftslogik in Ihre Serviceschicht schreiben sollten . Dies kann auch als schlechtes Design angesehen werden. Weitere Informationen finden Sie in den Domain-Modellen Anemic und Rich . Einige Servicemethoden und Geschäftslogiken können im Modell behandelt werden, sodass es sich um ein "reichhaltiges" Modell (mit Verhalten) handelt.

Ich benutze immer ausgiebig zwei Bibliotheken: AFNetworking 2.0 und ReactiveCocoa . Ich denke, es ist ein Muss für jede moderne Anwendung, die mit dem Netzwerk und den Webdiensten interagiert oder komplexe UI-Logik enthält.

DIE ARCHITEKTUR

Zuerst erstelle ich eine allgemeine APIClientKlasse, die eine Unterklasse von AFHTTPSessionManager ist . Dies ist ein Arbeitspferd aller Netzwerke in der Anwendung: Alle Serviceklassen delegieren tatsächliche REST-Anforderungen an diese. Es enthält alle Anpassungen des HTTP-Clients, die ich in der jeweiligen Anwendung benötige: SSL-Pinning, Fehlerverarbeitung und Erstellen einfacher NSErrorObjekte mit detaillierten Fehlergründen und Beschreibungen aller APIund Verbindungsfehler (in diesem Fall kann der Controller die richtigen Meldungen für anzeigen Benutzer), Festlegen von Anforderungs- und Antwortserialisierern, http-Headern und anderen netzwerkbezogenen Dingen. Dann logisch teile ich die alle API - Anfragen in Subservices oder, richtiger gesagt , Microservices : UserSerivces, CommonServices, SecurityServices,FriendsServicesund so weiter, entsprechend der von ihnen implementierten Geschäftslogik. Jeder dieser Mikrodienste ist eine separate Klasse. Sie bilden zusammen eine Service Layer. Diese Klassen enthalten Methoden für jede API-Anforderung, verarbeiten Domänenmodelle und geben immer eine RACSignalmit dem analysierten Antwortmodell oder NSErroran den Aufrufer zurück.

Ich möchte erwähnen, dass Sie, wenn Sie eine komplexe Modell-Serialisierungslogik haben, eine weitere Ebene dafür erstellen: etwas wie Data Mapper, aber allgemeiner, z. B. JSON / XML -> Model Mapper. Wenn Sie über einen Cache verfügen, erstellen Sie ihn auch als separaten Layer / Service (Sie sollten Geschäftslogik nicht mit Caching mischen). Warum? Weil die richtige Caching-Ebene mit ihren eigenen Fallstricken sehr komplex sein kann. Menschen implementieren komplexe Logik, um gültiges, vorhersehbares Caching zu erhalten, wie z. B. monoidales Caching mit Projektionen, die auf Profunktoren basieren. Sie können über diese schöne Bibliothek namens Carlos lesen , um mehr zu verstehen. Und vergessen Sie nicht, dass Core Data Ihnen bei allen Caching-Problemen wirklich helfen kann und es Ihnen ermöglicht, weniger Logik zu schreiben. Wenn Sie eine Logik zwischen NSManagedObjectContextund Serveranforderungsmodellen haben, können Sie diese auch verwenden Repository habenMuster, das die Logik, die die Daten abruft und sie dem Entitätsmodell zuordnet, von der Geschäftslogik trennt, die auf das Modell einwirkt. Daher empfehle ich, das Repository-Muster auch dann zu verwenden, wenn Sie über eine auf Core Data basierende Architektur verfügen. Repository kann abstrakte Dinge, wie NSFetchRequest, NSEntityDescription, NSPredicateund so weiter in einfachen Methoden wie getoderput .

Nach all diesen Aktionen in der Service-Schicht kann der Aufrufer (View Controller) einige komplexe asynchrone Aufgaben mit der Antwort ausführen: Signalmanipulationen, Verkettung, Zuordnung usw. mithilfe von ReactiveCocoaGrundelementen oder sie einfach abonnieren und Ergebnisse in der Ansicht anzeigen . Ich spritze mit der Dependency Injection in allen diesen Serviceklassen meinen APIClient, die einen bestimmten Service - Aufruf in entsprechenden übersetzen werden GET, POST, PUT, DELETEetc. Anfrage an den REST - Endpunkt. In diesem Fall APIClientwird implizit an alle Controller übergeben, Sie können dies mit einer über APIClientServiceklassen parametrisierten explizit machen . Dies kann sinnvoll sein, wenn Sie verschiedene Anpassungen des verwenden möchtenAPIClientfür bestimmte Serviceklassen, aber wenn Sie aus bestimmten Gründen keine zusätzlichen Kopien wünschen oder sicher sind, dass Sie immer eine bestimmte Instanz (ohne Anpassungen) von APIClient- verwenden, machen Sie es zu einem Singleton, aber NICHT, bitte NICHT Machen Sie keine Serviceklassen als Singletons.

Dann injiziert jeder View Controller erneut mit dem DI die benötigte Serviceklasse, ruft geeignete Servicemethoden auf und erstellt deren Ergebnisse mit der UI-Logik. Für die Abhängigkeitsinjektion verwende ich gerne BloodMagic oder ein leistungsfähigeres Framework Typhoon . Ich benutze niemals Singletons, Gottesunterricht APIManagerWhateveroder andere falsche Sachen. Denn wenn Sie Ihre Klasse anrufen WhateverManager, bedeutet dies, dass Sie den Zweck nicht kennen und es sich um eine schlechte Designentscheidung handelt . Singletons sind auch ein Anti-Muster und in den meisten Fällen (außer in seltenen Fällen) eine falsche Lösung. Singleton sollte nur berücksichtigt werden, wenn alle drei der folgenden Kriterien erfüllt sind:

  1. Das Eigentum an der einzelnen Instanz kann nicht angemessen zugewiesen werden.
  2. Eine verzögerte Initialisierung ist wünschenswert.
  3. Ein globaler Zugriff ist nicht anders vorgesehen.

In unserem Fall ist der Besitz der einzelnen Instanz kein Problem, und wir benötigen auch keinen globalen Zugriff, nachdem wir unseren God-Manager in Dienste unterteilt haben, da jetzt nur ein oder mehrere dedizierte Controller einen bestimmten Dienst benötigen (z. B. UserProfileController-Anforderungen UserServicesusw.). .

Wir sollten Sbei SOLID immer das Prinzip respektieren und die Trennung von Bedenken verwenden. Ordnen Sie daher nicht alle Ihre Servicemethoden und Netzwerkaufrufe einer Klasse zu, da dies verrückt ist, insbesondere wenn Sie eine große Unternehmensanwendung entwickeln. Aus diesem Grund sollten wir den Ansatz der Abhängigkeitsinjektion und der Dienste in Betracht ziehen. Ich betrachte diesen Ansatz als modern und post-OO . In diesem Fall teilen wir unsere Anwendung in zwei Teile: Steuerlogik (Steuerungen und Ereignisse) und Parameter.

Eine Art von Parametern wären gewöhnliche "Daten" -Parameter. Das ist es, was wir Funktionen weitergeben, manipulieren, modifizieren, beibehalten usw. Dies sind Entitäten, Aggregate, Sammlungen, Fallklassen. Die andere Art wären "Service" -Parameter. Dies sind Klassen, die Geschäftslogik kapseln, die Kommunikation mit externen Systemen ermöglichen und den Datenzugriff ermöglichen.

Hier ist ein allgemeiner Workflow meiner Architektur anhand eines Beispiels. Nehmen wir an, wir haben eine FriendsViewController, die eine Liste der Freunde des Benutzers anzeigt, und wir haben eine Option zum Entfernen von Freunden. Ich erstelle in meiner FriendsServicesKlasse eine Methode namens:

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

Wo Friendist ein Modell- / Domänenobjekt (oder es kann nur ein UserObjekt sein, wenn sie ähnliche Attribute haben). Unter der Motorhaube dieser Methode parst , Friendum NSDictionaryvon JSON Parametern friend_id, name, surname, friend_request_idund so weiter. Ich verwende die Mantle- Bibliothek immer für diese Art von Boilerplate und für meine Modellebene (Parsen vor und zurück, Verwalten verschachtelter Objekthierarchien in JSON usw.). Nach dem Parsen ruft APIClient DELETEMethode eine tatsächliche REST Anfrage und kehrt zu machen Responsein RACSignalden Anrufer ( FriendsViewControllerin unserem Fall) für den Benutzer oder was auch immer entsprechende Meldung angezeigt werden soll .

Wenn unsere Anwendung sehr groß ist, müssen wir unsere Logik noch klarer trennen. Zum Beispiel ist es nicht immer gut, RepositoryLogik mit Serviceeiner zu mischen oder zu modellieren . Als ich meinen Ansatz beschrieb, hatte ich gesagt, dass die removeFriendMethode in der ServiceEbene sein sollte, aber wenn wir pedantischer sind, können wir feststellen, dass sie besser dazu gehört Repository. Erinnern wir uns, was Repository ist. Eric Evans gab es eine genaue Beschreibung in seinem Buch [DDD]:

Ein Repository repräsentiert alle Objekte eines bestimmten Typs als konzeptionelle Menge. Es verhält sich wie eine Sammlung, außer mit einer detaillierteren Abfragefunktion.

A Repositoryist also im Wesentlichen eine Fassade, die die Semantik im Sammlungsstil (Hinzufügen, Aktualisieren, Entfernen) verwendet, um den Zugriff auf Daten / Objekte zu ermöglichen. Das ist der Grund, warum Sie, wenn Sie so etwas wie : getFriendsList, haben getUserGroups, removeFriendes in das platzieren können Repository, da die sammlungsähnliche Semantik hier ziemlich klar ist. Und Code wie:

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

ist definitiv eine Geschäftslogik, da sie über grundlegende CRUDOperationen hinausgeht und zwei Domänenobjekte ( Friendund Request) verbindet. Deshalb sollte sie in der ServiceEbene platziert werden. Auch ich möchte beachten: Erstellen Sie keine unnötigen Abstraktionen . Verwenden Sie alle diese Ansätze mit Bedacht aus. Denn wenn Sie Ihre Anwendung mit Abstraktionen überfordern, erhöht dies ihre zufällige Komplexität, und Komplexität verursacht mehr Probleme in Softwaresystemen als alles andere

Ich beschreibe Ihnen ein "altes" Objective-C-Beispiel, aber dieser Ansatz kann sehr einfach für die Swift-Sprache angepasst werden, mit viel mehr Verbesserungen, da er nützlichere Funktionen und funktionalen Zucker enthält. Ich empfehle dringend, diese Bibliothek zu verwenden: Moya . Sie können damit eine elegantere APIClientEbene erstellen (unser Arbeitstier, wie Sie sich erinnern). Jetzt wird unser APIClientAnbieter ein Wertetyp (enum) mit protokollkonformen Erweiterungen sein, der den Destrukturierungsmusterabgleich nutzt. Schnelle Enums + Pattern Matching ermöglichen es uns, algebraische Datentypen wie bei der klassischen funktionalen Programmierung zu erstellen . Unsere Microservices werden diesen verbesserten APIClientAnbieter wie beim üblichen Objective-C-Ansatz verwenden. Für die Modellebene Mantlekönnen Sie stattdessen die ObjectMapper-Bibliothek verwendenoder ich verwende gerne eine elegantere und funktionalere Argo- Bibliothek.

Also habe ich meinen allgemeinen architektonischen Ansatz beschrieben, der meiner Meinung nach für jede Anwendung angepasst werden kann. Natürlich kann es noch viel mehr Verbesserungen geben. Ich rate Ihnen, funktionale Programmierung zu lernen, weil Sie viel davon profitieren können, aber nicht zu weit damit gehen. Das Eliminieren eines übermäßigen, gemeinsamen, global veränderlichen Zustands, das Erstellen eines unveränderlichen Domänenmodells oder das Erstellen reiner Funktionen ohne externe Nebenwirkungen ist im Allgemeinen eine gute Praxis, und eine neue SwiftSprache fördert dies. Denken Sie jedoch immer daran, dass das Überladen Ihres Codes mit starken reinen Funktionsmustern und kategorietheoretischen Ansätzen eine schlechte Idee ist, da andere Entwickler Ihren Code lesen und unterstützen und sie frustriert oder beängstigend sein könnenprismatic profunctorsund solche Sachen in Ihrem unveränderlichen Modell. Das Gleiche gilt für ReactiveCocoa: Nicht zu viel , da es besonders für Neulinge sehr schnell unlesbar werden kann. Verwenden Sie es, wenn es Ihre Ziele und Ihre Logik wirklich vereinfachen kann. RACify Ihren Code nicht

Also , read a lot, mix, experiment, and try to pick up the best from different architectural approaches. Es ist der beste Rat, den ich Ihnen geben kann.


Auch ein interessanter und solider Ansatz. Danke.
MainstreamDeveloper00

1
@darksider Wie ich schon schrieb bereits in meiner Antwort: „` ich nie Singletons verwenden, Gott APIManagerWhatever Klasse oder andere falsche Sachen, weil Singletons ein Anti-Muster ist, und in den meisten Fällen (außer seltenen) ist eine falsche Lösung. ". 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 einmal `) in jedem Controller.
Oleksandr Karaberov

14
Hallo @alexander. Haben Sie Beispielprojekte auf GitHub? Sie beschreiben einen sehr interessanten Ansatz. Vielen Dank. Aber ich bin ein Anfänger in der Objective-C-Entwicklung. Und für mich ist es schwierig, einige Aspekte zu verstehen. Vielleicht kannst du ein Testprojekt auf GitHub hochladen und einen Link geben?
Denis

1
Hallo @AlexanderKaraberov, ich bin etwas verwirrt über die Store-Erklärung, die Sie gegeben haben. Angenommen, ich habe 5 Modelle, für jede habe ich 2 Klassen, von denen eine die Vernetzung und das andere Zwischenspeichern von Objekten aufrechterhält. Jetzt sollte ich für jedes Modell eine separate Store-Klasse haben, die die Funktion der Netzwerk- und Cache-Klasse aufruft, oder eine einzelne Store-Klasse, die alle Funktionen für jedes Modell hat, damit der Controller immer auf eine einzelne Datei für Daten zugreift.
Meteore

1
@icodebuster Dieses Demo-Projekt hat mir geholfen, viele der hier beschriebenen Konzepte zu verstehen: github.com/darthpelo/NetworkLayerExample

31

Entsprechend dem Ziel dieser Frage möchte ich unseren Architekturansatz beschreiben.

Architekturansatz

Die Architektur unserer allgemeinen iOS-Anwendung basiert auf folgenden Mustern: Service-Layer , MVVM , UI-Datenbindung , Abhängigkeitsinjektion ; und funktionales reaktives Programmierparadigma .

Wir können eine typische Consumer-Anwendung in folgende logische Ebenen unterteilen:

  • Versammlung
  • Modell
  • Dienstleistungen
  • Lager
  • Manager
  • Koordinatoren
  • Benutzeroberfläche
  • Infrastruktur

Die Assembly-Schicht ist ein Bootstrap-Punkt unserer Anwendung. Es enthält einen Container für Abhängigkeitsinjektionen und Deklarationen der Anwendungsobjekte und ihrer Abhängigkeiten. Diese Schicht kann auch die Konfiguration der Anwendung enthalten (URLs, Dienstschlüssel von Drittanbietern usw.). Zu diesem Zweck verwenden wir die Typhoon- Bibliothek.

Die Modellebene enthält Klassen, Validierungen und Zuordnungen für Domänenmodelle. Wir verwenden die Mantle- Bibliothek für die Zuordnung unserer Modelle: Sie unterstützt die Serialisierung / Deserialisierung in JSONFormat und NSManagedObjectModelle. Zur Validierung und Formulardarstellung unserer Modelle verwenden wir die Bibliotheken FXForms und FXModelValidation .

Die Serviceschicht deklariert Services, die wir für die Interaktion mit externen Systemen verwenden, um Daten zu senden oder zu empfangen, die in unserem Domänenmodell dargestellt sind. Normalerweise verfügen wir über Dienste für die Kommunikation mit Server-APIs (pro Entität), Messaging-Diensten (wie PubNub ), Speicherdiensten (wie Amazon S3) usw. Grundsätzlich verpacken Dienste Objekte, die von SDKs bereitgestellt werden (z. B. PubNub SDK), oder implementieren ihre eigene Kommunikation Logik. Für die allgemeine Vernetzung verwenden wir die AFNetworking- Bibliothek.

Speicherschicht ‚s Zweck ist die lokale Datenspeicherung auf dem Gerät zu organisieren. Wir verwenden hierfür Core Data oder Realm (beide haben Vor- und Nachteile, die Entscheidung über die Verwendung basiert auf konkreten Spezifikationen). Für die Einrichtung der Kerndaten verwenden wir die MDMCoreData- Bibliothek und eine Reihe von Klassen - Speicher - (ähnlich wie Dienste), die für jede Entität Zugriff auf den lokalen Speicher bieten. Für Realm verwenden wir nur ähnliche Speicher, um Zugriff auf den lokalen Speicher zu erhalten.

Die Manager-Ebene ist ein Ort, an dem unsere Abstraktionen / Wrapper leben.

In einer Manager-Rolle könnte sein:

  • Credentials Manager mit seinen verschiedenen Implementierungen (Schlüsselbund, NSDefaults, ...)
  • Aktueller Sitzungsmanager, der weiß, wie die aktuelle Benutzersitzung beibehalten und bereitgestellt wird
  • Capture-Pipeline für den Zugriff auf Mediengeräte (Videoaufzeichnung, Audio, Aufnahme von Bildern)
  • BLE Manager, der Zugriff auf Bluetooth-Dienste und Peripheriegeräte bietet
  • Geo Location Manager
  • ...

Die Rolle des Managers kann also jedes Objekt sein, das die Logik eines bestimmten Aspekts oder Anliegens implementiert, das für die Anwendungsarbeit erforderlich ist.

Wir versuchen, Singletons zu vermeiden, aber diese Schicht ist ein Ort, an dem sie leben, wenn sie gebraucht werden.

Die Ebene "Koordinatoren" stellt Objekte bereit, die von Objekten aus anderen Ebenen (Service, Speicher, Modell) abhängen, um ihre Logik in einer Arbeitssequenz zu kombinieren, die für ein bestimmtes Modul (Feature, Bildschirm, User Story oder Benutzererfahrung) erforderlich ist. Es verkettet normalerweise asynchrone Vorgänge und weiß, wie auf Erfolgs- und Fehlerfälle zu reagieren ist. Als Beispiel können Sie sich eine Nachrichtenfunktion und ein entsprechendes MessagingCoordinatorObjekt vorstellen . Die Behandlung des Sendevorgangs kann folgendermaßen aussehen:

  1. Nachricht validieren (Modellebene)
  2. Nachricht lokal speichern (Nachrichtenspeicher)
  3. Nachrichtenanhang hochladen (Amazon S3-Dienst)
  4. Aktualisieren Sie den Nachrichtenstatus und die URLs der Anhänge und speichern Sie die Nachricht lokal (Nachrichtenspeicher).
  5. Nachricht im JSON-Format serialisieren (Modellebene)
  6. Nachricht an PubNub veröffentlichen (PubNub-Dienst)
  7. Aktualisieren Sie den Nachrichtenstatus und die Attribute und speichern Sie sie lokal (Nachrichtenspeicher).

Bei jedem der obigen Schritte wird ein Fehler entsprechend behandelt.

Die UI-Ebene besteht aus folgenden Unterschichten:

  1. ViewModels
  2. ViewController
  3. Ansichten

Um Massive View Controller zu vermeiden, verwenden wir MVVM-Muster und implementieren die für die UI-Präsentation in ViewModels erforderliche Logik. Ein ViewModel hat normalerweise Koordinatoren und Manager als Abhängigkeiten. ViewModels, die von ViewControllern und einigen Arten von Ansichten verwendet werden (z. B. Tabellenansichtszellen). Der Klebstoff zwischen ViewControllern und ViewModels ist Datenbindung und Befehlsmuster. Um diesen Kleber zu erhalten, verwenden wir die ReactiveCocoa- Bibliothek.

Wir verwenden ReactiveCocoa und sein RACSignalKonzept auch als Schnittstelle und Rückgabewert aller Koordinatoren, Dienste und Speichermethoden. Auf diese Weise können wir Vorgänge verketten, parallel oder seriell ausführen und viele andere nützliche Dinge, die von ReactiveCocoa bereitgestellt werden.

Wir versuchen, unser UI-Verhalten deklarativ zu implementieren. Datenbindung und automatisches Layout tragen viel dazu bei, dieses Ziel zu erreichen.

Die Infrastrukturschicht enthält alle Helfer, Erweiterungen und Dienstprogramme, die für die Anwendungsarbeit erforderlich sind.


Dieser Ansatz funktioniert gut für uns und die Arten von Apps, die wir normalerweise erstellen. Aber Sie sollten verstehen, dass dies nur ein subjektiver Ansatz ist, der sollte für Beton - Team Zweck angepasst / verändert werden.

Hoffe das wird dir helfen!

Weitere Informationen zum iOS-Entwicklungsprozess finden Sie in diesem Blogbeitrag iOS Development as a Service


Ich habe vor ein paar Monaten angefangen, diese Architektur zu mögen, danke Alex, dass du sie geteilt hast! Ich würde es gerne in naher Zukunft mit RxSwift versuchen!
Ingaham

18

Da alle iOS-Apps unterschiedlich sind, sollten hier unterschiedliche Ansätze berücksichtigt werden. In der Regel gehe ich jedoch folgendermaßen vor:
Erstellen Sie eine zentrale Manager-Klasse (Singleton), um alle API-Anforderungen (normalerweise APICommunicator) zu verarbeiten, und jede Instanzmethode ist ein API-Aufruf . Und es gibt eine zentrale (nicht öffentliche) Methode:

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

Für die Aufzeichnung verwende ich 2 Hauptbibliotheken / Frameworks, ReactiveCocoa und AFNetworking. ReactiveCocoa verarbeitet asynchrone Netzwerkantworten perfekt (sendNext:, sendError: usw.).
Diese Methode ruft die API auf, ruft die Ergebnisse ab und sendet sie im Rohformat über RAC (wie NSArray, was AFNetworking zurückgibt).
Dann getStuffList:abonniert eine Methode wie die oben genannte Methode das Signal, analysiert die Rohdaten in Objekte (mit etwas wie Motis) und sendet die Objekte einzeln an den Aufrufer ( getStuffList:und ähnliche Methoden geben auch ein Signal zurück, das der Controller abonnieren kann ).
Der abonnierte Controller empfängt die Objekte per subscribeNext:Block und verarbeitet sie.

Ich habe viele Möglichkeiten in verschiedenen Apps ausprobiert, aber diese hat am besten funktioniert, daher habe ich sie kürzlich in einigen Apps verwendet. Sie eignet sich sowohl für kleine als auch für große Projekte und ist einfach zu erweitern und zu warten, wenn etwas geändert werden muss.
Hoffe das hilft, ich würde gerne die Meinungen anderer über meinen Ansatz hören und vielleicht, wie andere denken, dass dies vielleicht verbessert werden könnte.


2
Danke für die Antwort +1. Guter Ansatz. Ich lasse die Frage. Möglicherweise haben wir andere Ansätze von anderen Entwicklern.
MainstreamDeveloper00

1
Ich mag eine Variation dieses Ansatzes - ich verwende einen zentralen API-Manager, der sich um die Mechanismen der Kommunikation mit der API kümmert. Ich versuche jedoch, alle Funktionen meiner Modellobjekte verfügbar zu machen. Modelle stellen Methoden wie + (void)getAllUsersWithSuccess:(void(^)(NSArray*))success failure:(void(^)(NSError*))failure;und bereit, - (void)postWithSuccess:(void(^)(instancetype))success failure:(void(^)(NSError*))failure;die die erforderlichen Vorbereitungen treffen und dann den API-Manager aufrufen.
Jsadler

1
Dieser Ansatz ist unkompliziert, aber mit zunehmender Anzahl von APIs wird es schwieriger, den Singleton-API-Manager zu warten. Und jede neu hinzugefügte API bezieht sich auf den Manager, unabhängig davon, zu welchem ​​Modul diese API gehört. Versuchen Sie , die API-Anforderungen mit github.com/kevin0571/STNetTaskQueue zu verwalten.
Kevin

Abgesehen davon, warum Sie für Ihre Bibliothek werben, die so weit wie möglich von meiner Lösung entfernt und viel komplizierter ist, habe ich diesen Ansatz bei unzähligen kleinen und großen Projekten ausprobiert und genau das verwendet das gleiche, seit ich diese Antwort geschrieben habe. Mit cleveren Namenskonventionen ist es überhaupt nicht schwer zu pflegen.
Rickye

8

In meiner Situation verwende ich normalerweise die ResKit- Bibliothek, um die Netzwerkschicht einzurichten. Es bietet eine benutzerfreundliche Analyse. Dies reduziert meinen Aufwand beim Einrichten des Mappings für verschiedene Antworten und Dinge.

Ich füge nur Code hinzu, um das Mapping automatisch einzurichten. Ich definiere die Basisklasse für meine Modelle (kein Protokoll, da viel Code vorhanden ist, um zu überprüfen, ob eine Methode implementiert ist oder nicht, und weniger Code in den Modellen selbst):

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

Beziehungen sind Objekte, die als Antwort verschachtelte Objekte darstellen:

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

Dann richte ich das Mapping für RestKit folgendermaßen ein:

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

Ein Beispiel für die Implementierung von 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

Nun zum Wrapping der Anfragen:

Ich habe eine Header-Datei mit Blockdefinition, um die Zeilenlänge in allen APIRequest-Klassen zu reduzieren:

APICallbacks.h

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

Und Beispiel meiner APIRequest-Klasse, die ich verwende:

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

Und alles, was Sie im Code tun müssen, initialisieren Sie einfach das API-Objekt und rufen Sie es auf, wann immer Sie es benötigen:

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

Mein Code ist nicht perfekt, aber es ist einfach, ihn einmal festzulegen und für verschiedene Projekte zu verwenden. Wenn es für jemanden interessant ist, könnte ich einige Zeit damit verbringen, irgendwo auf GitHub und CocoaPods eine universelle Lösung dafür zu finden.


7

Meiner Meinung nach wird die gesamte Softwarearchitektur von den Anforderungen bestimmt. Wenn dies zu Lern- oder persönlichen Zwecken dient, entscheiden Sie das primäre Ziel und lassen Sie die Architektur steuern. Wenn es sich um eine Mietarbeit handelt, ist der geschäftliche Bedarf von größter Bedeutung. Der Trick besteht darin, sich nicht von glänzenden Dingen von den tatsächlichen Bedürfnissen ablenken zu lassen. Ich finde das schwer zu tun. Es gibt immer neue glänzende Dinge in diesem Geschäft und viele davon sind nicht nützlich, aber das kann man nicht immer im Voraus sagen. Konzentrieren Sie sich auf die Notwendigkeit und seien Sie bereit, schlechte Entscheidungen aufzugeben, wenn Sie können.

Zum Beispiel habe ich kürzlich einen kurzen Prototyp einer Foto-Sharing-App für ein lokales Unternehmen erstellt. Da das Geschäftsbedürfnis darin bestand, schnell und schmutzig zu arbeiten, bestand die Architektur aus iOS-Code zum Aufrufen einer Kamera und Netzwerkcode, der an eine Senden-Schaltfläche angehängt war, mit der das Bild in einen S3-Speicher hochgeladen und in eine SimpleDB-Domäne geschrieben wurde. Der Code war trivial und die Kosten minimal, und der Client verfügt über eine skalierbare Fotosammlung, auf die über das Web mit REST-Aufrufen zugegriffen werden kann. Günstig und dumm, die App hatte viele Fehler und sperrte gelegentlich die Benutzeroberfläche, aber es wäre eine Verschwendung, mehr für einen Prototyp zu tun, und es ermöglicht ihnen, sie für ihre Mitarbeiter bereitzustellen und Tausende von Testbildern einfach ohne Leistung oder Skalierbarkeit zu generieren Sorgen. Beschissene Architektur, aber sie passte perfekt zu den Bedürfnissen und Kosten.

Ein weiteres Projekt umfasste die Implementierung einer lokalen sicheren Datenbank, die im Hintergrund mit dem Unternehmenssystem synchronisiert wird, wenn das Netzwerk verfügbar ist. Ich habe einen Hintergrundsynchronisierer erstellt, der RestKit verwendet, da es anscheinend alles enthält, was ich brauche. Aber ich musste so viel benutzerdefinierten Code für RestKit schreiben, um mit eigenwilligem JSON umgehen zu können, dass ich alles schneller hätte tun können, indem ich meinen eigenen JSON in CoreData-Transformationen geschrieben hätte. Der Kunde wollte diese App jedoch ins Haus bringen, und ich war der Meinung, dass RestKit den Frameworks ähneln würde, die sie auf anderen Plattformen verwendeten. Ich warte darauf, ob das eine gute Entscheidung war.

Auch hier geht es mir darum, mich auf die Notwendigkeit zu konzentrieren und die Architektur bestimmen zu lassen. Ich versuche höllisch, die Verwendung von Paketen von Drittanbietern zu vermeiden, da diese Kosten verursachen, die erst entstehen, nachdem die App eine Weile im Feld war. Ich versuche zu vermeiden, Klassenhierarchien zu erstellen, da sie sich selten auszahlen. Wenn ich in angemessener Zeit etwas schreiben kann, anstatt ein Paket zu übernehmen, das nicht perfekt passt, dann mache ich es. Mein Code ist für das Debuggen gut strukturiert und angemessen kommentiert, Pakete von Drittanbietern jedoch selten. Trotzdem finde ich AF Networking zu nützlich, um es zu ignorieren und gut zu strukturieren, gut zu kommentieren und zu pflegen, und ich benutze es oft! RestKit deckt viele häufige Fälle ab, aber ich habe das Gefühl, dass ich mich in einem Kampf befunden habe, wenn ich es benutze. und die meisten Datenquellen, auf die ich stoße, sind voller Macken und Probleme, die am besten mit benutzerdefiniertem Code behandelt werden können. In meinen letzten Apps verwende ich nur die integrierten JSON-Konverter und schreibe einige Dienstprogrammmethoden.

Ein Muster, das ich immer verwende, besteht darin, die Netzwerkanrufe vom Hauptthread zu entfernen. Die letzten 4-5 Apps, die ich ausgeführt habe, haben mit dispatch_source_create eine Hintergrund-Timer-Aufgabe eingerichtet, die von Zeit zu Zeit aufwacht und Netzwerkaufgaben nach Bedarf ausführt. Sie müssen einige Thread-Sicherheitsarbeiten durchführen und sicherstellen, dass der Code zum Ändern der Benutzeroberfläche an den Haupt-Thread gesendet wird. Es ist auch hilfreich, das Onboarding / die Initialisierung so durchzuführen, dass sich der Benutzer nicht belastet oder verzögert fühlt. Bisher hat das ziemlich gut funktioniert. Ich schlage vor, diese Dinge zu untersuchen.

Schließlich denke ich, dass wir mit zunehmender Arbeit und der Weiterentwicklung des Betriebssystems tendenziell bessere Lösungen entwickeln. Ich habe Jahre gebraucht, um meine Überzeugung zu überwinden, dass ich Mustern und Designs folgen muss, von denen andere behaupten, dass sie obligatorisch sind. Wenn ich in einem Kontext arbeite, in dem das Teil der lokalen Religion ist, ähm, ich meine die besten Ingenieurspraktiken der Abteilung, dann folge ich den Gepflogenheiten genau, dafür bezahlen sie mich. Aber ich finde selten, dass es die optimale Lösung ist, älteren Designs und Mustern zu folgen. Ich versuche immer, die Lösung durch das Prisma der Geschäftsanforderungen zu betrachten und die Architektur so zu gestalten, dass sie dazu passt und die Dinge so einfach wie möglich halten. Wenn ich das Gefühl habe, dass dort nicht genug ist, aber alles richtig funktioniert, bin ich auf dem richtigen Weg.


4

Ich verwende den Ansatz, den ich von hier erhalten habe: https://github.com/Constantine-Fry/Foursquare-API-v2 . Ich habe diese Bibliothek in Swift umgeschrieben und Sie können den architektonischen Ansatz anhand dieser Teile des Codes sehen:

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}}
}

Grundsätzlich gibt es eine NSOperation-Unterklasse, die die NSURLRequest erstellt, die JSON-Antwort analysiert und den Rückrufblock mit dem Ergebnis zur Warteschlange hinzufügt. Die Haupt-API-Klasse erstellt NSURLRequest, initialisiert diese NSOperation-Unterklasse und fügt sie der Warteschlange hinzu.


3

Wir verwenden je nach Situation einige Ansätze. Für die meisten Dinge ist AFNetworking der einfachste und robusteste Ansatz, da Sie Header festlegen, mehrteilige Daten hochladen, GET, POST, PUT & DELETE verwenden können und es eine Reihe zusätzlicher Kategorien für UIKit gibt, mit denen Sie beispielsweise ein Bild festlegen können eine URL. In einer komplexen App mit vielen Anrufen abstrahieren wir dies manchmal auf eine eigene Komfortmethode, die ungefähr so ​​aussieht:

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

Es gibt einige Situationen, in denen AFNetworking nicht geeignet ist, z. B. wenn Sie ein Framework oder eine andere Bibliothekskomponente erstellen, da sich AFNetworking möglicherweise bereits in einer anderen Codebasis befindet. In dieser Situation würden Sie eine NSMutableURLRequest entweder inline verwenden, wenn Sie einen einzelnen Aufruf tätigen, oder in eine Anforderungs- / Antwortklasse abstrahieren.


Für mich ist dies die beste und klarste Antwort, Prost. "So einfach ist das". @martin, persönlich verwenden wir ständig NSMutableURLRequest; Gibt es einen wirklichen Grund, AFNetworking zu verwenden?
Fattie

AFNetworking ist wirklich praktisch. Für mich lohnen sich Erfolgs- und Fehlerblöcke, da sich der Code dadurch einfacher verwalten lässt. Ich bin damit einverstanden, dass es manchmal total übertrieben ist.
Martin

Ein großartiger Punkt auf den Blöcken, danke dafür. Ich denke, die Besonderheiten werden sich mit Swift ändern.
Fattie

2

Ich vermeide Singletons beim Entwerfen meiner Anwendungen. Sie sind eine typische Anlaufstelle für viele Menschen, aber ich denke, Sie können anderswo elegantere Lösungen finden. Normalerweise erstelle ich meine Entitäten in CoreData und füge meinen REST-Code in eine NSManagedObject-Kategorie ein. Wenn ich zum Beispiel einen neuen Benutzer erstellen und veröffentlichen möchte, würde ich Folgendes tun:

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

Ich benutze RESTKit für die Objektzuordnung und initialisiere es beim Start. Ich empfinde das Weiterleiten all Ihrer Anrufe über einen Singleton als Zeitverschwendung und füge eine Menge Boilerplate hinzu, die nicht benötigt wird.

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];
}

Warum zusätzliche Hilfsklassen hinzufügen, wenn Sie die Funktionalität einer gemeinsamen Basisklasse durch Kategorien erweitern können?

Wenn Sie an detaillierteren Informationen zu meiner Lösung interessiert sind, lassen Sie es mich wissen. Ich teile gerne.


3
Würde auf jeden Fall daran interessiert sein, diesen Ansatz in einem Blog-Beitrag genauer zu lesen.
Danyal Aytekin


0

Aus einer rein klassenorientierten Designperspektive haben Sie normalerweise so etwas:

  • Ihre Ansichtssteuerungen steuern eine oder mehrere Ansichten
  • Datenmodellklasse - Es hängt wirklich davon ab, mit wie vielen realen unterschiedlichen Entitäten Sie es zu tun haben und wie sie zusammenhängen.

    Wenn Sie beispielsweise über ein Array von Elementen verfügen, die in vier verschiedenen Darstellungen (Liste, Diagramm, Grafik usw.) angezeigt werden sollen, verfügen Sie über eine Datenmodellklasse für die Liste der Elemente und eine weitere für ein Element. Die Liste der Elementklassen wird von vier Ansichts-Controllern gemeinsam genutzt - allen untergeordneten Elementen eines Registerkarten-Controllers oder eines Navigations-Controllers.

    Datenmodellklassen sind praktisch, um Daten nicht nur anzuzeigen, sondern auch zu serialisieren, wobei jede von ihnen ihr eigenes Serialisierungsformat über JSON / XML / CSV-Exportmethoden (oder andere Exportmethoden) verfügbar machen kann.

  • Es ist wichtig zu verstehen, dass Sie auch API-Anforderungsgeneratorklassen benötigen , die direkt Ihren REST-API-Endpunkten zugeordnet sind. Angenommen, Sie haben eine API, die den Benutzer anmeldet. Ihre Builder-Klasse für die Anmelde-API erstellt also POST-JSON-Nutzdaten für die Anmelde-API. In einem anderen Beispiel erstellt eine API-Anforderungsgeneratorklasse für eine Liste von Katalogelement-APIs eine GET-Abfragezeichenfolge für die entsprechende API und löst die REST-GET-Abfrage aus.

    Diese API-Anforderungsgeneratorklassen empfangen normalerweise Daten von Ansichtscontrollern und geben dieselben Daten auch an Ansichtscontroller für UI-Aktualisierungen / andere Vorgänge zurück. View Controller entscheiden dann, wie Datenmodellobjekte mit diesen Daten aktualisiert werden.

  • Schließlich das Herz der REST - Client - API - Daten Abholer Klasse , die für alle Arten von API nicht bewusst ist fordert Ihre Anwendung macht. Diese Klasse wird eher ein Singleton sein, aber wie andere betonten, muss es kein Singleton sein.

    Beachten Sie, dass der Link nur eine typische Implementierung ist und keine Szenarien wie Sitzungen, Cookies usw. berücksichtigt. Es reicht jedoch aus, um Sie ohne Verwendung von Frameworks von Drittanbietern zum Laufen zu bringen.


0

Diese Frage hat bereits viele ausgezeichnete und ausführliche Antworten, aber ich denke, ich muss sie erwähnen, da es sonst niemand hat.

Alamofire für Swift. https://github.com/Alamofire/Alamofire

Es wurde von denselben Personen wie AFNetworking erstellt, ist jedoch direkter für Swift konzipiert.


0

Ich denke, für den Moment verwenden mittlere Projekte MVVM-Architektur und große Projekte VIPER-Architektur und versuchen zu erreichen

  • Protokollorientierte Programmierung
  • Software-Design-Muster
  • VERKAUFT Prinzip
  • Generische Programmierung
  • Wiederhole dich nicht (TROCKEN)

Und architektonische Ansätze zum Erstellen von iOS-Netzwerkanwendungen (REST-Clients)

Trennungsbedenken für sauberen und lesbaren Code vermeiden Doppelarbeit:

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)
}

Abhängigkeitsinversion

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

Hauptverantwortlicher:

  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()
        }
    }

Hier finden Sie die GitHub MVVM-Architektur mit Rest API Swift Project


0

In der mobilen Softwareentwicklung werden am häufigsten Clean Architecture + MVVM- und Redux-Muster verwendet.

Clean Architecture + MVVM besteht aus 3 Ebenen: Domäne, Präsentation, Datenebenen. Wo die Präsentationsschicht und die Datenrepository-Schicht von der Domänenschicht abhängen:

Presentation Layer -> Domain Layer <- Data Repositories Layer

Die Präsentationsschicht besteht aus ViewModels und Views (MVVM):

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

In diesem Artikel finden Sie eine detailliertere Beschreibung von Clean Architecture + MVVM unter https://tech.olx.com/clean-architecture-and-mvvm-on-ios-c9d167d9f5b3

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.