Schreiben Sie Ihre Netzwerkschicht in Swift: Protokollorientierter Ansatz



Mittlerweile verwenden fast 100% der Anwendungen Netzwerke, sodass jeder mit der Organisation und Verwendung der Netzwerkschicht konfrontiert ist. Es gibt zwei Hauptansätze zur Lösung dieses Problems: Sie verwenden entweder Bibliotheken von Drittanbietern oder Ihre eigene Implementierung der Netzwerkschicht. In diesem Artikel werden wir die zweite Option betrachten und versuchen, eine Netzwerkschicht unter Verwendung der neuesten Funktionen der Sprache unter Verwendung von Protokollen und Aufzählungen zu implementieren. Dadurch wird das Projekt vor unnötigen Abhängigkeiten in Form zusätzlicher Bibliotheken geschützt. Diejenigen, die Moya jemals gesehen haben, werden sofort viele ähnliche Details bei der Implementierung und Verwendung erkennen, so wie es ist. Nur dieses Mal werden wir es selbst tun, ohne Moya und Alamofire zu berühren.


In diesem Handbuch erfahren Sie, wie Sie eine Netzwerkschicht auf reinem Swift implementieren, ohne Bibliotheken von Drittanbietern zu verwenden. Sobald Sie diesen Artikel gelesen haben, wird Ihr Code

  • protokollorientiert
  • einfach zu bedienen
  • einfach zu bedienen
  • Typ sicher
  • Für Endpunkte werden Aufzählungen verwendet


Nachfolgend finden Sie ein Beispiel dafür, wie die Verwendung unserer Netzwerkschicht nach ihrer Implementierung aussehen wird:



Durch einfaches Schreiben von router.request (. Und unter Verwendung aller Möglichkeiten von Aufzählungen werden alle möglichen Abfrageoptionen und ihre Parameter angezeigt .

Zunächst ein wenig zur Struktur des Projekts

Wann immer Sie etwas Neues schaffen und um in Zukunft alles leicht verstehen zu können, ist es sehr wichtig, alles richtig zu organisieren und zu strukturieren. Ich bin der Überzeugung, dass eine ordnungsgemäß organisierte Ordnerstruktur ein wichtiges Detail beim Erstellen der Anwendungsarchitektur ist. Damit wir alles richtig in Ordnern anordnen können, erstellen wir sie im Voraus. Dies sieht wie die allgemeine Ordnerstruktur im Projekt aus:



Endpointtype-Protokoll

Zunächst müssen wir unser EndPointType- Protokoll definieren. Dieses Protokoll enthält alle erforderlichen Informationen zum Konfigurieren der Anforderung. Was ist eine Anfrage (Endpunkt)? Im Wesentlichen handelt es sich um eine URLRequest mit allen zugehörigen Komponenten wie Headern, Anforderungsparametern und Anforderungshauptteil. Das EndPointType- Protokoll ist der wichtigste Teil unserer Implementierung auf Netzwerkebene. Erstellen wir eine Datei und nennen sie EndPointType . Legen Sie diese Datei in den Dienstordner (nicht in den EndPoint-Ordner, warum - es wird etwas später klar sein).



HTTP-Protokolle

Unser EndPointType enthält mehrere Protokolle, die wir zum Erstellen einer Anforderung benötigen. Mal sehen, was diese Protokolle sind.

HTTPMethod

Erstellen Sie eine Datei, nennen Sie sie HTTPMethod und legen Sie sie im Ordner Service ab. Diese Auflistung wird verwendet, um die HTTP-Methode unserer Anfrage festzulegen.



HTTPTask
Erstellen Sie eine Datei, nennen Sie sie HTTPTask und legen Sie sie im Dienstordner ab . HTTPTask ist für die Konfiguration der Parameter einer bestimmten Anforderung verantwortlich. Sie können so viele verschiedene Abfrageoptionen hinzufügen, wie Sie benötigen, aber ich werde im Gegenzug regelmäßige Abfragen, Abfragen mit Parametern, Abfragen mit Parametern und Überschriften durchführen, sodass ich nur diese drei Arten von Abfragen ausführen werde.



Im nächsten Abschnitt werden wir die Parameter diskutieren und wie wir mit ihnen arbeiten werden

HTTPHeaders

HTTPHeaders sind nur Typealias für ein Wörterbuch. Sie können es oben in Ihrer HTTPTask- Datei erstellen.

public typealias HTTPHeaders = [String:String] 


Parameter & Kodierung

Erstellen Sie eine Datei, nennen Sie sie ParameterEncoding und legen Sie sie im Ordner Encoding ab. Erstellen Sie Typealien für Parameter , es wird wieder ein reguläres Wörterbuch sein. Wir tun dies, um den Code verständlicher und lesbarer zu machen.

 public typealias Parameters = [String:Any] 


Definieren Sie als Nächstes ein ParameterEncoder- Protokoll mit einer einzelnen Codierungsfunktion. Die Codierungsmethode verfügt über zwei Parameter: inout URLRequest und Parameters . INOUT ist ein Swift-Schlüsselwort, das einen Funktionsparameter als Referenz definiert. In der Regel werden Parameter als Werte an die Funktion übergeben. Wenn Sie in einem Aufruf vor einem Funktionsparameter inout schreiben, definieren Sie diesen Parameter als Referenztyp. Um mehr über inout Argumente zu erfahren, können Sie diesem Link folgen. Kurz gesagt, mit inout können Sie den Wert der Variablen selbst ändern, die an die Funktion übergeben wurde, und nicht nur ihren Wert im Parameter abrufen und innerhalb der Funktion damit arbeiten. Das ParameterEncoder- Protokoll wird in JSONParameterEncoder und in URLPameterEncoder implementiert .

 public protocol ParameterEncoder { static func encode(urlRequest: inout URLRequest, with parameters: Parameters) throws } 


ParameterEncoder enthält eine einzelne Funktion, deren Aufgabe es ist, Parameter zu codieren. Diese Methode kann einen Fehler auslösen, der behandelt werden muss, daher verwenden wir throw.

Es kann auch nützlich sein, keine Standardfehler, sondern benutzerdefinierte Fehler zu erzeugen. Es ist immer ziemlich schwierig zu entschlüsseln, was Xcode Ihnen bietet. Wenn Sie alle Fehler angepasst und beschrieben haben, wissen Sie immer genau, was passiert ist. Definieren wir dazu eine Aufzählung, die von Error erbt.



Erstellen Sie eine Datei, nennen Sie sie URLParameterEncoder und legen Sie sie im Ordner Encoding ab .



Dieser Code nimmt eine Liste von Parametern auf, konvertiert und formatiert sie zur Verwendung als URL-Parameter. Wie Sie wissen, sind einige Zeichen in der URL nicht zulässig. Die Parameter werden auch durch das Symbol "&" getrennt, daher müssen wir uns darum kümmern. Wir müssen auch den Standardwert für die Header festlegen, wenn sie nicht in der Anforderung festgelegt sind.

Dies ist der Teil des Codes, der durch Unit-Tests abgedeckt werden soll. Das Erstellen einer URL-Anfrage ist der Schlüssel, da wir sonst viele unnötige Fehler provozieren können. Wenn Sie die offene API verwenden, möchten Sie offensichtlich nicht das gesamte mögliche Volumen an Anforderungen für fehlgeschlagene Tests verwenden. Wenn Sie mehr über Unit-Tests erfahren möchten, können Sie mit diesem Artikel beginnen.

JSONParameterEncoder

Erstellen Sie eine Datei, nennen Sie sie JSONParameterEncoder und legen Sie sie im Ordner Encoding ab.



Alles ist das gleiche wie im Fall von URLParameter , nur hier werden wir die Parameter für JSON konvertieren und die Parameter, die die Codierung "application / json" definieren, erneut zum Header hinzufügen.

Netzwerkrouter

Erstellen Sie eine Datei, nennen Sie sie NetworkRouter und legen Sie sie im Dienstordner ab. Beginnen wir mit der Definition von Typealien für den Abschluss.

 public typealias NetworkRouterCompletion = (_ data: Data?,_ response: URLResponse?,_ error: Error?)->() 


Als nächstes definieren wir das NetworkRouter- Protokoll.



NetworkRouter verfügt über einen EndPoint , den es für Anforderungen verwendet. Sobald die Anforderung abgeschlossen ist, wird das Ergebnis dieser Anforderung an den NetworkRouterCompletion- Abschluss übergeben. Das Protokoll verfügt außerdem über eine Abbruchfunktion , mit der langfristige Lade- und Entladeanforderungen unterbrochen werden können. Wir haben hier auch den zugehörigen Typ verwendet, da unser Router alle Arten von EndPointType unterstützen soll . Ohne den zugehörigen Typ müsste der Router einen bestimmten Typ haben, der EndPointType implementiert. Wenn Sie mehr über den zugehörigen Typ erfahren möchten, können Sie diesen Artikel lesen.

Router

Erstellen Sie eine Datei, nennen Sie sie Router und legen Sie sie im Ordner Service ab. Wir deklarieren eine private Variable vom Typ URLSessionTask . Alle Arbeiten werden daran sein. Wir machen es privat, weil wir nicht möchten, dass jemand außerhalb es ändern kann.



Anfrage

Hier erstellen wir URLSession mit URLSession.shared . Dies ist der einfachste Weg zum Erstellen. Denken Sie jedoch daran, dass diese Methode nicht die einzige ist. Sie können komplexere URLSession- Konfigurationen verwenden, die das Verhalten ändern können. Mehr dazu in diesem Artikel .

Die Anforderung wird durch Aufrufen der Funktion buildRequest erstellt. Der Funktionsaufruf wird in do-try-catch eingeschlossen, da die Codierungsfunktionen in buildRequest möglicherweise Ausnahmen auslösen . Antwort , Daten und Fehler werden vollständig übergeben.



Build-Anfrage

Wir erstellen unsere Anfrage mit der Funktion buildRequest . Diese Funktion ist für alle wichtigen Arbeiten in unserer Netzwerkschicht verantwortlich. Konvertiert EndPointType im Wesentlichen in URLRequest . Sobald sich EndPoint in eine Anfrage verwandelt, können wir diese an die Sitzung weitergeben . Hier passiert viel, also schauen wir uns die Methoden an. Lassen Sie uns zunächst die buildRequest- Methode untersuchen:

1. Wir initialisieren die URLRequest- Anforderungsvariable. Wir legen unsere Basis-URL darin fest und fügen den Pfad der spezifischen Anfrage hinzu, die dazu verwendet wird.

2. Weisen Sie request.httpMethod die http-Methode aus unserem EndPoint zu .

3. Wir erstellen einen Do-Try-Catch-Block, da unsere Encoder möglicherweise einen Fehler auslösen. Durch das Erstellen eines großen Do-Try-Catch-Blocks entfällt die Notwendigkeit, für jeden Versuch einen separaten Block zu erstellen.

4. Überprüfen Sie in switch route.task .

5. Je nach Art der Aufgabe rufen wir den entsprechenden Encoder auf.



Parameter konfigurieren

Erstellen Sie die Funktion configureParameters im Router.



Diese Funktion ist für die Konvertierung unserer Abfrageparameter verantwortlich. Da unsere API die Verwendung von bodyParameters in Form von JSON und URLParameters voraussetzt, die in das URL-Format konvertiert wurden, übergeben wir einfach die entsprechenden Parameter an die entsprechenden Konvertierungsfunktionen, die wir am Anfang des Artikels beschrieben haben. Wenn Sie eine API verwenden, die verschiedene Arten von Codierungen enthält, würde ich in diesem Fall empfehlen, HTTPTask mit einer zusätzlichen Aufzählung mit dem Codierungstyp hinzuzufügen. Diese Auflistung sollte alle möglichen Arten von Codierungen enthalten. Fügen Sie danach in configureParameters ein weiteres Argument mit dieser Aufzählung hinzu. Wechseln Sie je nach Wert mit switch und stellen Sie die gewünschte Codierung ein.

Zusätzliche Überschriften hinzufügen

Erstellen Sie die Funktion addAdditionalHeaders im Router.



Fügen Sie der Anfrage einfach alle erforderlichen Header hinzu.

Abbrechen

Die Abbruchfunktion sieht ziemlich einfach aus:



Anwendungsbeispiel

Versuchen wir nun, unsere Netzwerkschicht anhand eines realen Beispiels zu verwenden. Wir werden uns mit TheMovieDB verbinden , um Daten für unsere Anwendung zu erhalten.

MovieEndPoint

Erstellen Sie eine MovieEndPoint- Datei und legen Sie sie im EndPoint-Ordner ab. MovieEndPoint ist dasselbe wie
und TargetType in Moya. Hier implementieren wir stattdessen unseren eigenen EndPointType. Ein Artikel, der beschreibt, wie Moya für ein ähnliches Beispiel verwendet wird, finden Sie unter diesem Link .

 import Foundation enum NetworkEnvironment { case qa case production case staging } public enum MovieApi { case recommended(id:Int) case popular(page:Int) case newMovies(page:Int) case video(id:Int) } extension MovieApi: EndPointType { var environmentBaseURL : String { switch NetworkManager.environment { case .production: return "https://api.themoviedb.org/3/movie/" case .qa: return "https://qa.themoviedb.org/3/movie/" case .staging: return "https://staging.themoviedb.org/3/movie/" } } var baseURL: URL { guard let url = URL(string: environmentBaseURL) else { fatalError("baseURL could not be configured.")} return url } var path: String { switch self { case .recommended(let id): return "\(id)/recommendations" case .popular: return "popular" case .newMovies: return "now_playing" case .video(let id): return "\(id)/videos" } } var httpMethod: HTTPMethod { return .get } var task: HTTPTask { switch self { case .newMovies(let page): return .requestParameters(bodyParameters: nil, urlParameters: ["page":page, "api_key":NetworkManager.MovieAPIKey]) default: return .request } } var headers: HTTPHeaders? { return nil } } 


Moviemodel

Um das MovieModel- und JSON-Datenmodell in das Modell zu analysieren, wird das Decodable-Protokoll verwendet. Legen Sie diese Datei im Ordner Modell ab .

Hinweis : Für eine detailliertere Kenntnis der Protokolle Codable, Decodable und Encodable können Sie meinen anderen Artikel lesen, in dem alle Funktionen der Arbeit mit ihnen ausführlich beschrieben werden.

 import Foundation struct MovieApiResponse { let page: Int let numberOfResults: Int let numberOfPages: Int let movies: [Movie] } extension MovieApiResponse: Decodable { private enum MovieApiResponseCodingKeys: String, CodingKey { case page case numberOfResults = "total_results" case numberOfPages = "total_pages" case movies = "results" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: MovieApiResponseCodingKeys.self) page = try container.decode(Int.self, forKey: .page) numberOfResults = try container.decode(Int.self, forKey: .numberOfResults) numberOfPages = try container.decode(Int.self, forKey: .numberOfPages) movies = try container.decode([Movie].self, forKey: .movies) } } struct Movie { let id: Int let posterPath: String let backdrop: String let title: String let releaseDate: String let rating: Double let overview: String } extension Movie: Decodable { enum MovieCodingKeys: String, CodingKey { case id case posterPath = "poster_path" case backdrop = "backdrop_path" case title case releaseDate = "release_date" case rating = "vote_average" case overview } init(from decoder: Decoder) throws { let movieContainer = try decoder.container(keyedBy: MovieCodingKeys.self) id = try movieContainer.decode(Int.self, forKey: .id) posterPath = try movieContainer.decode(String.self, forKey: .posterPath) backdrop = try movieContainer.decode(String.self, forKey: .backdrop) title = try movieContainer.decode(String.self, forKey: .title) releaseDate = try movieContainer.decode(String.self, forKey: .releaseDate) rating = try movieContainer.decode(Double.self, forKey: .rating) overview = try movieContainer.decode(String.self, forKey: .overview) } } 


Netzwerkmanager

Erstellen Sie eine NetworkManager- Datei im Manager-Ordner. Derzeit enthält NetworkManager nur zwei statische Eigenschaften: einen API-Schlüssel und eine Aufzählung, die den Servertyp beschreibt, zu dem eine Verbindung hergestellt werden soll. NetworkManager enthält auch einen Router vom Typ MovieApi .



Netzwerkantwort

Erstellen Sie die NetworkResponse- Enumeration in NetworkManager.



Wir verwenden diese Aufzählung bei der Verarbeitung von Antworten auf Anfragen und zeigen die entsprechende Nachricht an.

Ergebnis

Erstellen Sie eine Ergebnisaufzählung in NetworkManager.



Wir verwenden das Ergebnis, um festzustellen, ob die Anfrage erfolgreich war oder nicht. Wenn nicht, geben wir eine Fehlermeldung mit dem Grund zurück.

Antwortverarbeitung anfordern

Erstellen Sie die Funktion handleNetworkResponse . Diese Funktion verwendet ein Argument, z. B. eine HTTP-Antwort, und gibt das Ergebnis zurück.



In dieser Funktion geben wir abhängig vom empfangenen statusCode von HTTPResponse eine Fehlermeldung oder ein Zeichen für eine erfolgreiche Anforderung zurück. In der Regel bedeutet ein Code im Bereich von 200 bis 299 Erfolg.

Netzwerkanfrage stellen

Wir haben also alles getan, um unsere Netzwerkschicht zu nutzen. Versuchen wir, eine Anfrage zu stellen.

Wir werden eine Liste neuer Filme anfordern. Erstellen Sie eine Funktion und nennen Sie sie getNewMovies .



Gehen wir Schritt für Schritt vor:

1. Wir definieren die Methode getNewMovies mit zwei Argumenten: der Paginierungsseitenzahl und dem Vervollständigungshandler, der ein optionales Array von Filmmodellen zurückgibt, oder einen optionalen Fehler.

2. Rufen Sie den Router an . Wir übergeben die Seitenzahl und den Prozessabschluss im Abschluss.

3. URLSession gibt einen Fehler zurück, wenn kein Netzwerk vorhanden ist oder aus irgendeinem Grund keine Anfrage gestellt werden konnte. Bitte beachten Sie, dass dies kein API-Fehler ist. Solche Fehler treten auf dem Client auf und treten normalerweise aufgrund der schlechten Qualität der Internetverbindung auf.

4. Wir müssen unsere Antwort in HTTPURLResponse umwandeln , da wir auf die statusCode- Eigenschaft zugreifen müssen .

5. Deklarieren Sie das Ergebnis und initialisieren Sie es mit der handleNetworkResponse- Methode

6. Erfolg bedeutet, dass die Anfrage erfolgreich war und wir die erwartete Antwort erhalten haben. Dann prüfen wir, ob die Daten mit der Antwort geliefert wurden, und wenn nicht, beenden wir die Methode einfach über return.

7. Wenn die Antwort Daten enthält, müssen die empfangenen Daten in das Modell analysiert werden. Danach übergeben wir das resultierende Array von Modellen vollständig.

8. Übergeben Sie den Fehler im Fehlerfall vollständig .

So funktioniert unsere eigene Netzwerkschicht auf reinem Swift, ohne Abhängigkeiten in Form von Pods und Bibliotheken von Drittanbietern zu verwenden. Um eine Test-API-Anforderung zum Abrufen einer Liste von Filmen zu erstellen, erstellen Sie einen MainViewController mit der NetworkManager- Eigenschaft und rufen Sie die getNewMovies- Methode auf.

  class MainViewController: UIViewController { var networkManager: NetworkManager! init(networkManager: NetworkManager) { super.init(nibName: nil, bundle: nil) self.networkManager = networkManager } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .green networkManager.getNewMovies(page: 1) { movies, error in if let error = error { print(error) } if let movies = movies { print(movies) } } } } 


Kleiner Bonus

Sie hatten Situationen in Xcode, in denen Sie nicht verstanden haben, welche Art von Platzhalter an einem bestimmten Ort verwendet wird? Schauen Sie sich zum Beispiel den Code an, den wir gerade für Router geschrieben haben .



Wir haben die NetworkRouterCompletion selbst festgelegt, aber selbst in diesem Fall kann man leicht vergessen, um welchen Typ es sich handelt und wie man ihn verwendet. Aber unser geliebter Xcode hat sich um alles gekümmert, und es reicht aus, nur auf den Platzhalter zu doppelklicken, und Xcode ersetzt den gewünschten Typ.



Fazit

Jetzt haben wir eine Implementierung einer protokollorientierten Netzwerkschicht, die sehr einfach zu verwenden ist und die Sie jederzeit an Ihre Bedürfnisse anpassen können. Wir haben seine Funktionalität verstanden und wie alle Mechanismen funktionieren.

Sie finden den Quellcode in diesem Repository .

Source: https://habr.com/ru/post/de443514/


All Articles