كتابة طبقة الشبكة في Swift: النهج الموجه للبروتوكول



الآن ما يقرب من 100 ٪ من التطبيقات تستخدم الشبكات ، لذلك الجميع يواجهون تنظيم واستخدام طبقة الشبكة. هناك طريقتان رئيسيتان لحل هذه المشكلة ، هما إما استخدام مكتبات الجهة الخارجية ، أو تطبيقك لطبقة الشبكة. سننظر في هذا المقال في الخيار الثاني ، ونحاول تطبيق طبقة شبكة باستخدام جميع أحدث ميزات اللغة ، باستخدام البروتوكولات والتعدادات. سيوفر هذا المشروع من التبعيات غير الضرورية في شكل مكتبات إضافية. أولئك الذين رأوا مويا من أي وقت مضى سوف يتعرفون على الفور على الكثير من التفاصيل المماثلة في التنفيذ والاستخدام ، كما هي ، هذه المرة فقط سنفعل ذلك بأنفسنا دون لمس مويا وألاموفير.


في هذا الدليل ، سننظر في كيفية تنفيذ طبقة الشبكة على Swift الخالص ، دون استخدام أي مكتبات تابعة لجهات أخرى. بمجرد مراجعة هذه المقالة ، سيصبح الرمز الخاص بك

  • الموجهة للبروتوكول
  • سهل الاستخدام
  • سهل الاستخدام
  • اكتب آمنة
  • لنقاط النهاية سيتم استخدام التعدادات


فيما يلي مثال عن كيفية استخدام طبقة الشبكة الخاصة بنا لرعاية تنفيذها:



ببساطة عن طريق كتابة router.request (. وباستخدام كل قوة التعدادات ، سنرى كل خيارات الاستعلام الممكنة ومعلماتها.

أولا ، قليلا عن هيكل المشروع

كلما ابتكرت شيئًا جديدًا ، ولكي تتمكن من فهم كل شيء بسهولة في المستقبل ، من المهم جدًا تنظيم وتنظيم كل شيء بشكل صحيح. أؤمن أن بنية المجلد المنظمة بشكل صحيح هي تفاصيل مهمة عند إنشاء بنية التطبيق. حتى يتسنى لنا ترتيب كل شيء بشكل صحيح في مجلدات ، دعنا ننشئها مسبقًا. سوف يبدو هذا مثل بنية المجلد العام في المشروع:



بروتوكول نقطة النهاية

بادئ ذي بدء ، نحتاج إلى تعريف بروتوكول EndPointType الخاص بنا. سيحتوي هذا البروتوكول على جميع المعلومات اللازمة لتكوين الطلب. ما هو الطلب (نقطة النهاية)؟ في جوهره ، هو URLRequest مع جميع المكونات ذات الصلة ، مثل الرؤوس ، معلمات الطلب ، نص الطلب. يعد بروتوكول EndPointType هو الجزء الأكثر أهمية في تطبيق طبقة الشبكة. دعونا إنشاء ملف وتسميته EndPointType . ضع هذا الملف في مجلد الخدمة (وليس في مجلد EndPoint ، لماذا - سيكون واضحًا لاحقًا)



بروتوكولات HTTP

يحتوي EndPointType الخاص بنا على العديد من البروتوكولات التي نحتاجها لإنشاء طلب. دعونا نرى ما هي هذه البروتوكولات.

HTTPMethod

قم بإنشاء ملف ، وقم بتسميته HTTPMethod ووضعه في مجلد الخدمة. سيتم استخدام هذه القائمة لضبط طريقة HTTP لطلبنا.



HTTPTask
قم بإنشاء ملف ، وقم بتسميته HTTPTask ووضعه في مجلد الخدمة. HTTPTask مسؤول عن تكوين معلمات طلب معين. يمكنك إضافة ما تريده من خيارات الاستعلام المختلفة ، لكنني سأقوم بدوره بإجراء استعلامات عادية واستعلامات ذات معلمات واستعلامات ذات معلمات ورؤوس ، ولذا فإنني سأقوم بهذه الأنواع الثلاثة من الاستعلامات فقط.



في القسم التالي ، سنناقش المعايير وكيف سنعمل معها

HTTPHeaders

HTTPHeaders هي فقط typealias لقاموس. يمكنك إنشائه في الجزء العلوي من ملف HTTPTask الخاص بك.

public typealias HTTPHeaders = [String:String] 


المعلمات والترميز

قم بإنشاء ملف ، وقم بتسمية ParameterEncoding وقم بوضعه في مجلد Encoding. إنشاء typealias للمعلمات ، سيكون مرة أخرى قاموس منتظم. نحن نفعل هذا لجعل الكود يبدو أكثر قابلية للفهم وقراءة.

 public typealias Parameters = [String:Any] 


بعد ذلك ، قم بتعريف بروتوكول ParameterEncoder مع وظيفة ترميز واحدة. تحتوي طريقة التشفير على معلمتين: inout URLRequest and Parameters . INOUT هي كلمة رئيسية Swift تعرّف معلمة دالة كمرجع. عادة ، يتم تمرير المعلمات إلى الدالة كقيم. عندما تكتب الإدخال قبل معلمة دالة في مكالمة ، فإنك تحدد هذه المعلمة كنوع مرجعي. لمعرفة المزيد حول وسيطات inout ، يمكنك اتباع هذا الرابط. باختصار ، يسمح لك inout بتغيير قيمة المتغير نفسه ، والذي تم تمريره إلى الوظيفة ، وليس فقط الحصول على قيمته في المعلمة والعمل معها داخل الوظيفة. سيتم تنفيذ بروتوكول ParameterEncoder في JSONParameterEncoder وفي URLPameterEncoder .

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


يحتوي ParameterEncoder على وظيفة واحدة تتمثل مهمتها في تشفير المعلمات. قد تؤدي هذه الطريقة إلى حدوث خطأ يجب معالجته ، لذلك نستخدم ميزة رمي.

قد يكون من المفيد أيضًا إنتاج أخطاء غير قياسية ، بل أخطاء مخصصة. من الصعب دائمًا فك تشفير ما يمنحك Xcode. عندما يكون لديك كل الأخطاء المخصصة والموصوفة ، فأنت تعرف دائمًا ما حدث بالضبط. للقيام بذلك ، دعونا نحدد التعداد الذي يرث من خطأ .



قم بإنشاء ملف ، وقم بتسمية URLParameterEncoder ووضعه في مجلد Encoding .



تأخذ هذه الشفرة قائمة من المعلمات وتحولها وتنسقها للاستخدام كمعلمات URL. كما تعلم ، لا يُسمح ببعض الأحرف في عنوان URL. يتم فصل المعلمات أيضًا بواسطة الرمز "&" ، لذلك يجب أن نحرص على ذلك. يجب علينا أيضًا تعيين القيمة الافتراضية للرؤوس إذا لم يتم تعيينها في الطلب.

هذا هو الجزء من الكود الذي من المفترض أن يتم تغطيته بواسطة اختبارات الوحدة. بناء طلب عنوان URL هو المفتاح ، وإلا يمكننا إثارة العديد من الأخطاء غير الضرورية. إذا كنت تستخدم واجهة برمجة التطبيقات المفتوحة ، فمن الواضح أنك لن ترغب في استخدام الحجم الكامل الكامل لطلبات الاختبارات الفاشلة. إذا كنت تريد معرفة المزيد عن اختبارات الوحدة ، فيمكنك البدء بهذه المقالة.

JSONParameterEncoder

قم بإنشاء ملف ، وقم بتسمية JSONParameterEncoder ووضعه في مجلد Encoding.



كل شيء هو نفسه كما في حالة URLParameter ، هنا سنقوم بتحويل المعلمات لـ JSON ونضيف مرة أخرى المعلمات التي تحدد "application / json" الترميز إلى الرأس.

Networkrouter

قم بإنشاء ملف وتسميته NetworkRouter ووضعه في مجلد الخدمة. لنبدأ بتحديد typealias للإغلاق.

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


بعد ذلك ، نحدد بروتوكول NetworkRouter .



لدى NetworkRouter نقطة نهاية تستخدمها للطلبات ، وبمجرد اكتمال الطلب ، يتم تمرير نتيجة هذا الطلب إلى إغلاق NetworkRouterCompletion . يحتوي البروتوكول أيضًا على وظيفة إلغاء ، والتي يمكن استخدامها لمقاطعة طلبات التحميل والتفريغ على المدى الطويل. لقد استخدمنا أيضًا النمط المرتبط هنا لأننا نريد أن يدعم جهاز التوجيه الخاص بنا أي نوع من أنواع EndPointType . دون استخدام النوع المقترن ، سيتعين على جهاز التوجيه أن يكون لديه نوع معين يطبق EndPointType . إذا كنت ترغب في معرفة المزيد عن نوع المرتبطة ، يمكنك قراءة هذه المقالة .

جهاز التوجيه

قم بإنشاء ملف ، وقم بتسميته بجهاز التوجيه ووضعه في مجلد الخدمة. نعلن متغير خاص من نوع URLSessionTask . كل العمل سيكون عليه. نحن نجعلها خاصة لأننا لا نريد لأي شخص في الخارج أن يكون قادرًا على تغييرها.



طلب

هنا نقوم بإنشاء URLSession باستخدام URLSession.shared ، وهذه هي أسهل طريقة لإنشاء. لكن تذكر أن هذه الطريقة ليست هي الطريقة الوحيدة. يمكنك استخدام تكوينات URLSession الأكثر تعقيدًا التي يمكنها تغيير سلوكها. المزيد عن هذا في هذه المقالة .

يتم إنشاء الطلب عن طريق استدعاء دالة buildRequest ، حيث يتم لف استدعاء المكالمة في do-try-catch ، لأن وظائف الترميز داخل buildRequest قد تلقي استثناءات. يتم تمرير الاستجابة والبيانات والخطأ إلى الاكتمال.



بناء الطلب

نقوم بإنشاء طلبنا باستخدام دالة buildRequest . هذه الوظيفة مسؤولة عن جميع الأعمال الحيوية في طبقة شبكتنا. يحول EndPointType بشكل أساسي إلى URLRequest . وبمجرد أن تتحول EndPoint إلى طلب ، يمكننا نقله إلى الجلسة . تحدث أشياء كثيرة هنا ، لذلك دعونا نلقي نظرة على الأساليب. أولاً ، لنفحص طريقة buildRequest :

1. نهيئ متغير طلب URLRequest . قمنا بتعيين عنوان URL الأساسي الخاص بنا فيه ونضيف مسار الطلب المحدد الذي سيتم استخدامه فيه.

2. قم بتعيين request.httpMethod طريقة http من EndPoint الخاص بنا.

3. نقوم بإنشاء كتلة "جرب الصيد" ، لأن برامج التشفير الخاصة بنا قد تتسبب في حدوث خطأ. من خلال إنشاء كتلة واحدة كبيرة للتجربة ، نلغي الحاجة إلى إنشاء كتلة منفصلة لكل محاولة.

4. في التبديل ، تحقق route.task .

5. اعتمادًا على نوع المهمة ، نسمي المشفر المقابل.



تكوين المعلمات

قم بإنشاء دالة configParameters في جهاز التوجيه.



هذه الوظيفة مسؤولة عن تحويل معلمات طلب البحث. نظرًا لأن API الخاصة بنا تفترض استخدام معلمات body في شكل JSON و URLParameters المحولة إلى تنسيق URL ، فنحن ببساطة ننقل المعلمات المناسبة إلى وظائف التحويل المقابلة ، التي وصفناها في بداية المقالة. إذا كنت تستخدم واجهة برمجة التطبيقات (API) التي تتضمن أنواعًا مختلفة من الترميز ، فإنني أوصي في هذه الحالة بإضافة HTTPTask مع تعداد إضافي بنوع الترميز. يجب أن تحتوي هذه القائمة على جميع أنواع الترميزات الممكنة. بعد ذلك ، في configParameters إضافة حجة واحدة أخرى مع هذا التعداد. اعتمادًا على قيمتها ، قم بالتبديل باستخدام المفتاح وقم بإجراء الترميز الذي تحتاجه.

إضافة رؤوس إضافية

قم بإنشاء وظيفة addAdditionalHeaders في جهاز التوجيه.



فقط أضف كل الرؤوس اللازمة للطلب.

الغاء

ستبدو وظيفة الإلغاء بسيطة جدًا:



مثال للاستخدام

الآن دعونا نحاول استخدام طبقة شبكتنا في مثال حقيقي. سنقوم بالاتصال بـ TheMovieDB لاستلام بيانات طلبنا.

MovieEndPoint

قم بإنشاء ملف MovieEndPoint ووضعه في مجلد EndPoint. MovieEndPoint هو نفسه
و TargetType في Moya. هنا ننفذ EndPointType الخاصة بنا بدلاً من ذلك. يمكن العثور على مقالة تصف كيفية استخدام Moya للحصول على مثال مشابه على هذا الرابط .

 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

لتحليل نموذج بيانات MovieModel و JSON في النموذج ، يتم استخدام بروتوكول فك التشفير. ضع هذا الملف في مجلد الطراز .

ملاحظة : للحصول على معرفة أكثر تفصيلاً مع بروتوكولات Codable و Decodable و Encodable ، يمكنك قراءة مقالتي الأخرى التي تصف بالتفصيل جميع ميزات العمل معهم.

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


مدير الشبكة

قم بإنشاء ملف NetworkManager في مجلد Manager. في الوقت الحالي ، يحتوي NetworkManager على خاصيتين ثابتتين: مفتاح API وتعداد يصف نوع الخادم للاتصال به. يحتوي NetworkManager أيضًا على جهاز توجيه من النوع MovieApi .



استجابة الشبكة

إنشاء تعداد NetworkResponse في NetworkManager.



نحن نستخدم هذا التعداد عند معالجة الردود على الطلبات وسنعرض الرسالة المقابلة.

نتيجة

إنشاء تعداد نتيجة في NetworkManager.



نستخدم النتيجة لتحديد ما إذا كان الطلب ناجحًا أم لا. إذا لم يكن كذلك ، فسنقوم بإرجاع رسالة خطأ مع السبب.

طلب معالجة الاستجابة

قم بإنشاء دالة handleNetworkResponse . تأخذ هذه الوظيفة وسيطة واحدة ، مثل HTTPResponse ، وتُرجع النتيجة.



في هذه الوظيفة ، بناءً على كود الحالة المستلم من HTTPResponse ، نرجع رسالة خطأ أو علامة على طلب ناجح. عادة ، رمز في نطاق 200..299 يعني النجاح.

تقديم طلب شبكة

لذلك ، لقد فعلنا كل شيء للبدء في استخدام طبقة شبكتنا ، دعونا نحاول تقديم طلب.

سنطلب قائمة الأفلام الجديدة. إنشاء وظيفة وتسميتها getNewMovies .



لنأخذها خطوة بخطوة:

1. نحدد طريقة getNewMovies من خلال وسيطين : رقم صفحة ترقيم الصفحات ومعالج الإكمال ، والتي تُرجع صفيفًا اختياريًا من طرازات الأفلام ، أو خطأً اختياريًا.

2. دعوة راوتر . نمرر رقم الصفحة ونكمل العملية في الإغلاق.

3. إرجاع URLSession خطأ إذا لم يكن هناك شبكة أو لم يكن من الممكن تقديم طلب لأي سبب من الأسباب. يرجى ملاحظة أن هذا ليس خطأ API ، تحدث مثل هذه الأخطاء على العميل وعادة ما تحدث بسبب رداءة جودة الاتصال بالإنترنت.

4. نحتاج إلى إرسال ردنا على HTTPURLResponse ، لأننا بحاجة إلى الوصول إلى خاصية statusCode .

5. إعلان النتيجة وتهيئتها باستخدام أسلوب handleNetworkResponse

6. النجاح يعني أن الطلب كان ناجحًا وتلقينا الرد المتوقع. ثم نتحقق مما إذا كانت البيانات تأتي مع الإجابة ، وإذا لم يكن الأمر كذلك ، فإننا ببساطة ننهي الطريقة عن طريق الإرجاع.

7. إذا كانت الإجابة تأتي مع البيانات ، فمن الضروري تحليل البيانات المستلمة في النموذج. بعد ذلك ، نقوم بتمرير مجموعة النماذج الناتجة إلى الاكتمال.

8. في حالة وجود خطأ ، ما عليك سوى تمرير الخطأ إلى الاكتمال .

هذه هي الطريقة التي تعمل بها طبقة الشبكة الخاصة بنا على Swift الخالص ، دون استخدام أي تبعيات في شكل حافظات ومكتبات تابعة لجهات خارجية. من أجل تقديم طلب اختبار api للحصول على قائمة الأفلام ، قم بإنشاء MainViewController باستخدام خاصية NetworkManager واتصل بطريقة getNewMovies من خلاله.

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


مكافأة صغيرة

واجهت مواقف في Xcode عندما لم تفهم ما هو نوع العنصر النائب المستخدم في مكان معين؟ على سبيل المثال ، انظر إلى الكود الذي كتبناه للتو من أجل Router .



لقد قررنا أن NetworkRouterCompletion بأنفسنا ، ولكن حتى في هذه الحالة ، من السهل أن ننسى نوعها وكيفية استخدامها. لكن Xcode المحبب الخاص بنا اعتنى بكل شيء ، ويكفي النقر نقرًا مزدوجًا على العنصر النائب وسيحل Xcode محل النوع المطلوب.



استنتاج

الآن لدينا تطبيق طبقة شبكة موجهة للبروتوكول ، والتي هي سهلة الاستخدام للغاية والتي يمكنك دائمًا تخصيصها حسب احتياجاتك. لقد فهمنا وظائفه وكيف تعمل جميع الآليات.

يمكنك العثور على الكود المصدري في هذا المستودع .

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


All Articles