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

مرحبا يا هبر!

بدءًا من Swift 4 ، لدينا إمكانية الوصول إلى بروتوكول Codable الجديد ، مما يجعل من السهل ترميز / فك تشفير النماذج. تحتوي مشاريعي على الكثير من التعليمات البرمجية لمكالمات API ، وخلال العام الماضي قمت بعمل كثير لتحسين هذه المجموعة الضخمة من التعليمات البرمجية إلى شيء خفيف للغاية وموجز وبسيط عن طريق قتل التعليمات البرمجية المتكررة واستخدام Codable حتى للطلبات متعددة الأجزاء ومعلمات استعلام url. اتضح أن هناك العديد من الفئات الممتازة في رأيي لإرسال الطلبات وتحليل الردود من الخادم. بالإضافة إلى بنية ملف ملائمة ، وهي وحدات التحكم لكل مجموعة من الطلبات ، والتي جذرتها عند استخدام Vapor 3 على الواجهة الخلفية. منذ أيام قليلة ، قمت بتخصيص جميع تطورياتي لمكتبة منفصلة وأطلق عليها اسم CodyFire. أود التحدث عنها في هذه المقالة.

تنويه


يعتمد CodyFire على Alamofire ، ولكنه إلى حد ما أكثر من مجرد غلاف على Alamofire ، إنه نهج نظام كامل للعمل مع REST API لنظام iOS. لهذا لا أشعر بالقلق من أن Alamofire تشهد الإصدار الخامس الذي سيكون فيه دعم Codable ، لأنه لن يقتل خليقي.

التهيئة


لنبدأ قليلاً من بعيد ، أي أنه غالبًا ما يكون لدينا ثلاثة خوادم:

ديف - من أجل التنمية ، ما نبدأ من Xcode
المرحلة - للاختبار قبل الإصدار ، عادةً في TestFlight أو InHouse
همز - إنتاج ، ل AppStore

بالطبع ، يدرك العديد من مطوري iOS وجود متغيرات البيئة ومخططات بدء التشغيل في Xcode ، ولكن على مدار عملي (8+ سنوات) ، 90 ٪ من المطورين يكتبون الخادم الصحيح يدويًا في بعض الحالات الثابتة أثناء الاختبار أو قبل التجميع ، وهذا ما أود إصلاحه من خلال إظهار مثال جيد لكيفية القيام بذلك بشكل صحيح.

افتراضيًا ، يحدد CodyFire تلقائيًا البيئة التي يعمل فيها التطبيق حاليًا ، مما يجعله بسيطًا جدًا:

#if DEBUG //DEV environment #else if Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" { //TESTFLIGHT environment } else { //APPSTORE environment } #endif 

هذا بالطبع تحت غطاء المحرك ، وفي المشروع في AppDelegate ما عليك سوى تسجيل ثلاثة عناوين URL

 import CodyFire @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { let dev = CodyFireEnvironment(baseURL: "http://localhost:8080") let testFlight = CodyFireEnvironment(baseURL: "https://stage.myapi.com") let appStore = CodyFireEnvironment(baseURL: "https://api.myapi.com") CodyFire.shared.configureEnvironments(dev: dev, testFlight: testFlight, appStore: appStore) return true } } 

ويمكن للمرء أن يكون سعيدًا بهذا ولا يفعل شيئًا آخر.

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

الصورة
نصيحة: في قسم إدارة المخططات ، لا تنس تحديد مربع الاختيار `مشترك 'لكل مخطط بحيث تكون متاحة لجميع المطورين في المشروع.

في كل مخطط ، تحتاج إلى كتابة متغير البيئة `env` الذي يمكن أن يأخذ ثلاث قيم: dev ، testFlight ، appStore.

الصورة

ولكي تعمل هذه المخططات مع CodyFire ، تحتاج إلى إضافة التعليمات البرمجية التالية إلى AppDelegate.didFinishLaunchingWithOptions بعد تهيئة CodyFire

 CodyFire.shared.setupEnvByProjectScheme() 

علاوة على ذلك ، غالبًا ما يطلب منك المدير أو المختبرون في مشروعك تبديل الخادم بسرعة في مكان ما على شاشة تسجيل الدخول . مع CodyFire ، يمكنك تنفيذ ذلك بسهولة عن طريق تبديل الخادم في سطر واحد ، وتغيير البيئة:

 CodyFire.shared.environmentMode = .appStore 

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

هيكل الملف ووحدات التحكم


الآن يمكنك التحدث عن رؤيتي لبنية الملف لجميع مكالمات API ، وهذا يمكن أن يسمى إيديولوجية CodyFire.

دعونا نرى كيف يبدو أخيراً في المشروع

الصورة

الآن دعونا نلقي نظرة على قوائم الملفات ، لنبدأ مع API.swift .

 class API { typealias auth = AuthController typealias post = PostController } 

يتم سرد ارتباطات كافة وحدات التحكم هنا بحيث يمكن استدعاؤها بسهولة عبر `API.controller.method`.

 class AuthController {} 

API + Login.swift

 extension AuthController { struct LoginResponse: Codable { var token: String } static func login(email: String, password: String) -> APIRequest<LoginResponse> { return APIRequest("login").method(.post) .basicAuth(email: email, password: password) .addCustomError(.notFound, "User not found") } } 

في هذا الديكور ، نعلن وظيفة لاستدعاء API:

- تحديد نقطة النهاية
- طريقة HTTP POST
- استخدام المجمع للمصادقة الأساسية
- الإعلان عن النص المطلوب لاستجابة محددة من الخادم (هذا مناسب)
- وبيان النموذج الذي سيتم به فك البيانات

ما الذي يبقى مخفيا؟

- لا حاجة لتحديد عنوان URL الكامل للخادم ، لأنه تم تعيينه بالفعل عالميًا
- لم يكن من الضروري الإشارة إلى أننا نتوقع الحصول على 200 موافق إذا كان كل شيء على ما يرام
200 OK هو رمز الحالة الافتراضي الذي تتوقعه CodyFire لجميع الطلبات ، وفي هذه الحالة يتم فك تشفير البيانات واستدعاء الاستدعاء ، بأن كل شيء على ما يرام ، إليك بياناتك.
علاوة على ذلك ، في مكان ما في الرمز الخاص بشاشة LoginScreen الخاصة بك ، يمكنك ببساطة الاتصال

 API.auth.login(email: "test@mail.com", password: "qwerty").onError { error in switch error.code { case .notFound: print(error.description) //: User not found default: print(error.description) } }.onSuccess { token in //TODO:  auth token    print("Received auth token: "+ token) } 

يعد onError و onSuccess جزءًا صغيرًا من عمليات رد الاتصال التي يمكن أن تعود إليها APIRequest ، وسنتحدث عنها لاحقًا.

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

API + Signup.swift

 extension AuthController { struct SignupRequest: JSONPayload { let email, password: String let firstName, lastName, mobileNumber: String init(email: String, password: String, firstName: String, lastName: String, mobileNumber: String) { self.email = email self.password = password self.firstName = firstName self.lastName = lastName self.mobileNumber = mobileNumber } } struct SignupResponse: Codable { let token: String } static func signup(_ request: SignupRequest) -> APIRequest<SignupResponse> { return APIRequest("signup", payload: request).method(.post) .addError(.conflict, "Account already exists") } } 

على عكس تسجيل الدخول ، نقوم أثناء التسجيل بإرسال كمية كبيرة من البيانات.

في هذا المثال ، لدينا نموذج SignupRequest الذي يتوافق مع بروتوكول JSONPayload (وبالتالي يفهم CodyFire نوع الحمولة) بحيث يكون نص طلبنا في شكل JSON. إذا كنت بحاجة إلى x-www-form-urlencoded ، فاستخدم FormURLEncodedPayload .

ونتيجة لذلك ، تحصل على وظيفة بسيطة تقبل نموذج الحمولة
 API.auth.signup(request) 

والتي ، إذا نجحت ، ستعيد لك نموذج استجابة محددًا.

أعتقد أنه رائع ، أليس كذلك؟

ولكن ماذا لو كانت متعددة الأجزاء؟


لنلق نظرة على مثال عندما يمكنك إنشاء مشاركة .

نشر + Create.swift

 extension PostController { struct CreateRequest: MultipartPayload { var text: String var tags: [String] var images: [Attachment] var video: Data init (text: String, tags: [String], images: [Attachment], video: Data) { self.text = text self.tags = tags self.images = images self.video = video } } struct Post: Codable { let text: String let tags: [String] let linksToImages: [String] let linkToVideo: String } static func create(_ request: CreateRequest) -> APIRequest<CreateRequest> { return APIRequest("post", payload: request).method(.post) } } 

سيتمكن هذا الرمز من إرسال نموذج متعدد الأجزاء مع مجموعة من ملفات الصور وفيديو واحد.
دعونا نرى كيفية استدعاء إرسال. هذه هي اللحظة الأكثر إثارة للاهتمام حول المرفقات .

 let videoData = FileManager.default.contents(atPath: "/path/to/video.mp4")! let imageAttachment = Attachment(data: UIImage(named: "cat")!.jpeg(.high)!, fileName: "cat.jpg", mimeType: .jpg) let payload = PostController.CreateRequest(text: "CodyFire is awesome", tags: ["codyfire", "awesome"], images: [imageAttachment], video: videoData) API.post.create(payload).onProgress { progress in print(" : \(progress)") }.onError { error in print(error.description) }.onSuccess { createdPost in print("  : \(createdPost)") } 

المرفق هو نموذج يتم فيه نقل اسم الملف ونوعه MimeType بالإضافة إلى البيانات .

إذا سبق لك إرسال نموذج متعدد الأجزاء من Swift باستخدام Alamofire أو عنوان URL عارض ، فأنا متأكد من أنك ستقدر بساطة CodyFire .

الآن أبسط ولكن ليس أقل من رائع أمثلة مكالمات GET.

نشر + Get.swift

 extension PostController { struct ListQuery: Codable { let offset, limit: Int init (offset: Int, limit: Int) { self.offset = offset self.limit = limit } } static func get(_ query: ListQuery? = nil) -> APIRequest<[Post]> { return APIRequest("post").query(query) } static func get(id: UUID) -> APIRequest<Post> { return APIRequest("post/" + id.uuidString) } } 

أبسط مثال على ذلك

 API.post.get(id:) 

والتي في onSuccess ستعيد لك نموذج المشاركة .

هنا مثال أكثر إثارة للاهتمام.

 API.post.get(PostController.ListQuery(offset: 0, limit: 100)) 

الذي يأخذ نموذج ListQuery كمدخل ،
الذي يتحول APIRequest في النهاية إلى مسار URL للنموذج

 post?limit=0&offset=100 

وسيعيد مصفوفة [Post] إلى onSuccess .

بالطبع ، يمكنك كتابة مسار URL بالطريقة القديمة ، لكنك تعلم الآن أنه يمكنك برمجة تمامًا.

سيكون طلب المثال الأخير هو DELETE

نشر + حذف

 extension PostController { static func delete(id: UUID) -> APIRequest<Nothing> { return APIRequest("post/" + id.uuidString) .method(.delete) .desiredStatusCode(.noContent) } } 

هناك نقطتان مثيرتان للاهتمام.

- نوع الإرجاع هو APIRequest ، وهو يحدد النوع العام Nothing ، وهو نموذج قابل للتشفير فارغ.
- أشرنا صراحةً إلى أننا نتوقع تلقي 204 لا محتوى ، وستتصل CodyFire فقط بالنجاح في هذه الحالة.

أنت تعرف بالفعل كيفية استدعاء نقطة النهاية هذه من ViewController.

ولكن هناك خياران ، الأول مع onSuccess ، والثاني بدون. سنلقي نظرة عليه

 API.post.delete(id:).execute() 

بمعنى ، إذا لم يكن يهمك ما إذا كان الطلب يعمل أم لا ، يمكنك ببساطة استدعاء .execute () عليه وهذا كل شيء ، وإلا فسيبدأ بعد إعلان onSuccess للمعالج.

الوظائف المتاحة


إذن لكل طلب


لتوقيع كل طلب API مع أي رؤوس http ، يتم استخدام معالج عام ، والذي يمكنك تعيينه في مكان ما في AppDelegate . علاوة على ذلك ، يمكنك استخدام النموذج الكلاسيكي [String: String] أو Codable للاختيار من بينها.

مثال لحامل التفويض.

1. كود (التوصية)
 CodyFire.shared.fillCodableHeaders = { struct Headers: Codable { //NOTE:  nil,     headers var Authorization: String? var anythingElse: String } return Headers(Authorization: nil, anythingElse: "hello") } 

2. كلاسيكي [سلسلة: سلسلة]
 CodyFire.shared.fillHeaders = { guard let apiToken = LocalAuthStorage.savedToken else { return [:] } return ["Authorization": "Bearer \(apiToken)"] } 

قم بإضافة بعض عناوين http بشكل انتقائي إلى الطلب


يمكن القيام بذلك عند إنشاء APIRequest ، على سبيل المثال:

 APIRequest("some/endpoint").headers(["someKey": "someValue"]) 

معالجة الطلبات غير المصرح بها


يمكنك معالجتها بشكل عام ، على سبيل المثال في AppDelegate

 CodyFire.shared.unauthorizedHandler = { //   WelcomeScreen } 

أو محليًا في كل طلب

 API.post.create(request).onNotAuthorized { //   } 

إذا كانت الشبكة غير متوفرة


 API.post.create(request). onNetworkUnavailable { //   ,  ,     } 
خلاف ذلك في onError تحصل على خطأ. notnotContectedToInternet

بدء شيء قبل بدء الطلب


يمكنك تعيين .onRequestStarted وبدء عرض ، على سبيل المثال ، محمل فيه.
هذا مكان مناسب ، لأنه لا يتم استدعاؤه في حالة نقص الإنترنت ، وليس عليك أن تذهب دون جدوى لعرض محمل ، على سبيل المثال.

كيفية تعطيل / تمكين إخراج السجل عالميا


 CodyFire.shared.logLevel = .debug CodyFire.shared.logLevel = .error CodyFire.shared.logLevel = .info CodyFire.shared.logLevel = .off 

كيفية تعطيل إخراج السجل لطلب واحد


 .avoidLogError() 

سجلات العملية بطريقتك الخاصة


 CodyFire.shared.logHandler = { level, text in print("  CodyFire: " + text) } 

كيفية تعيين كود استجابة http المتوقع للخادم


كما قلت أعلاه ، بشكل افتراضي ، تتوقع CodyFire تلقي 200 موافق ، وإذا حدث ذلك ، فإنها تبدأ في تحليل البيانات والمكالمات على النجاح .

ولكن يمكن تعيين الكود المتوقع في شكل تعداد مناسب ، على سبيل المثال ، لـ 201 CREATED

 .desiredStatusCode(.created) 

أو يمكنك حتى تعيين رمز متوقع مخصص

 .desiredStatusCode(.custom(777)) 

إلغاء الطلب


 .cancel() 

ويمكنك معرفة أن الطلب قد تم إلغاؤه عن طريق الإعلان عن معالج onCancellation

 .onCancellation { //   } 

وإلا سيتم رفع onError

تحديد مهلة لطلب


 .responseTimeout(30) //   30  

يمكن أيضاً تعليق معالج على مهلة

 . onTimeout { //    } 

وإلا سيتم رفع onError

إعداد مهلة إضافية تفاعلية


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

تكمن الفكرة في أنه يريد أن يستمر فحص البريد الإلكتروني / كلمة المرور لمدة ثانيتين أو أكثر. وإذا استمرت 0.5 ثانية فقط ، فأنت بحاجة إلى رمي 1.5 مرة أخرى ثم استدعاء onSuccess . وإذا استغرق الأمر ثانيتين أو 2.5 ثانية بالضبط ، فاتصل على النجاح على الفور.

 .additionalTimeout(2) // 2     

ترميز / فك تشفير التاريخ المخصص


لدى CodyFire تعداد DateCodingStrategy الخاص بها ، حيث توجد ثلاث قيم

- ثواني منذ 1970
- ميلي ثانية منذ 1970
- منسق (_ customDateFormatter: DateFormatter)

يمكن تعيين DateCodingStrategy بثلاث طرق وبشكل منفصل لفك التشفير والتشفير
- عالميًا في AppDelegate

 CodyFire.shared.dateEncodingStrategy = .secondsSince1970 let customDateFormatter = DateFormatter() CodyFire.shared.dateDecodingStrategy = .formatted(customDateFormatter) 

- لطلب واحد

 APIRequest("some/endpoint") .dateDecodingStrategy(.millisecondsSince1970) .dateEncodingStrategy(.secondsSince1970) 

- أو حتى بشكل منفصل لكل نموذج ، تحتاج فقط إلى النموذج لمطابقة CustomDateEncodingStrategy و / أو CustomDateDecodingStrategy .

 struct SomePayload: JSONPayload, CustomDateEncodingStrategy, CustomDateDecodingStrategy { var dateEncodingStrategy: DateCodingStrategy var dateDecodingStrategy: DateCodingStrategy } 

كيفية الإضافة إلى المشروع


المكتبة متاحة على GitHub بموجب ترخيص MIT.

التثبيت متاح حاليًا فقط من خلال CocoaPods
 pod 'CodyFire' 


آمل حقًا أن يكون CodyFire مفيدًا لمطوري iOS الآخرين ، وسوف يبسط التطوير بالنسبة لهم ، وبشكل عام سيجعل العالم أفضل قليلاً والناس أكثر لطفًا.

هذا كل شيء ، شكرا على وقتك.

UPD: تمت إضافة دعم ReactiveCocoa و RxSwift
 pod 'ReactiveCodyFire' # ReactiveCocoa pod 'RxCodyFire' # RxSwift #      'CodyFire',     

APIRequest لـ ReactiveCoca سيكون لديه .signalProducer ، و RxSwift.

UPD2: يمكنك الآن تشغيل طلبات متعددة
إذا كان من المهم أن تحصل على نتيجة كل استعلام ، فاستخدم .and ()
الحد الأقصى في هذا الوضع ، يمكنك تشغيل ما يصل إلى 10 طلبات ، وسيتم تنفيذها بدقة واحدة تلو الأخرى.
 API.employee.all() .and(API.office.all()) .and(API.car.all()) .and(API.event.all()) .and(API.post.all()) .onError { error in print(error.description) }.onSuccess { employees, offices, cars, events, posts in //    !!! } 

onRequestStarted ، onNetworkUnavailable ، onCancellation ، onNotAuthorized ، onTimeout متاحة أيضًا.
onProgress - لا يزال قيد التطوير

إذا كنت لا تهتم بنتائج طلبات البحث ، فيمكنك استخدام .flatten ()
 [API.employee.all(), API.office.all(), API.car.all()].flatten().onError { print(error.description) }.onSuccess { print("flatten finished!") } 
لتشغيلها في نفس الوقت ما عليك سوى إضافة .currurrent (بواسطة: 3) وهذا سيسمح بتنفيذ ثلاثة طلبات في وقت واحد ، ويمكن تحديد أي رقم.
لتخطي أخطاء الاستعلام الفاشلة ، أضف .avoidCancelOnError ()
للحصول على تقدم ، أضف .onProgress

UPD3: يمكنك الآن تعيين خادم منفصل لكل طلب
من الضروري إنشاء عناوين الخادم الضرورية في مكان ما ، على سبيل المثال مثل هذا
 let server1 = ServerURL(base: "https://server1.com", path: "v1") let server2 = ServerURL(base: "https://server2.com", path: "v1") let server3 = ServerURL(base: "https://server3.com") 
والآن يمكنك استخدامها مباشرة في تهيئة الطلب قبل تحديد نقطة النهاية
 APIRequest(server1, "endpoint", payload: payloadObject) APIRequest(server2, "endpoint", payload: payloadObject) APIRequest(server3, "endpoint", payload: payloadObject) 
أو يمكنك تحديد الخادم بعد تهيئة الطلب
 APIRequest("endpoint", payload: payloadObject).serverURL(server1) 

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


All Articles