Halo, Habr!
Dimulai dengan Swift 4, kami memiliki akses ke protokol Codable baru, yang membuatnya mudah untuk menyandikan / mendekodekan model. Proyek saya memiliki banyak kode untuk panggilan API, dan selama setahun terakhir saya telah melakukan banyak pekerjaan untuk mengoptimalkan array kode yang sangat besar ini menjadi sesuatu yang sangat ringan, ringkas dan sederhana dengan membunuh kode berulang dan menggunakan Codable bahkan untuk permintaan multi bagian dan parameter kueri url. Ternyata beberapa kelas yang sangat baik menurut pendapat saya untuk mengirim permintaan dan mem-parsing tanggapan dari server. Serta struktur file yang nyaman, yang merupakan pengendali untuk setiap kelompok permintaan, yang saya ambil root saat menggunakan Vapor 3 di backend. Beberapa hari yang lalu, saya mengalokasikan semua perkembangan saya ke perpustakaan terpisah dan menamainya CodyFire. Saya ingin berbicara tentang dia di artikel ini.
Penafian
CodyFire didasarkan pada Alamofire, tetapi lebih dari sekadar pembungkus di Alamofire, ini adalah pendekatan seluruh sistem untuk bekerja dengan REST API untuk iOS. Itu sebabnya saya tidak khawatir Alamofire menggergaji versi kelima di mana akan ada dukungan Codable, karena itu tidak akan membunuh kreasi saya.
Inisialisasi
Mari kita mulai dari jauh, yaitu bahwa kita sering memiliki tiga server:
dev - untuk pengembangan, apa yang kita mulai dari Xcode
tahap - untuk pengujian sebelum rilis, biasanya di TestFlight atau InHouse
prod - produksi, untuk AppStore
Dan banyak pengembang iOS, tentu saja, menyadari keberadaan 
Variabel Lingkungan dan skema startup di Xcode, tetapi selama praktik saya (8+ tahun), 90% pengembang secara manual menulis server yang tepat dalam beberapa konstanta saat pengujian, atau sebelum perakitan, dan ini adalah apa yang ingin saya perbaiki dengan menunjukkan contoh yang baik tentang bagaimana melakukannya dengan benar.
Secara default, CodyFire secara otomatis menentukan di lingkungan mana aplikasi sedang berjalan, itu membuatnya sangat sederhana:
#if DEBUG //DEV environment #else if Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" { //TESTFLIGHT environment } else { //APPSTORE environment } #endif 
Ini tentu saja di bawah tenda, dan dalam proyek di AppDelegate Anda hanya perlu mendaftarkan tiga 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 } } 
Dan orang bisa senang dengan ini dan tidak melakukan apa pun.
Tetapi dalam kehidupan nyata, kita sering perlu menguji server dev, stage dan prod dalam Xcode, dan untuk ini saya mendorong Anda untuk menggunakan skema startup.

Kiat: di bagian Kelola skema , jangan lupa untuk memeriksa kotak centang `shared` untuk setiap skema sehingga tersedia untuk semua pengembang di proyek.
Dalam setiap skema, Anda perlu menulis variabel lingkungan `env` yang dapat mengambil tiga nilai: dev, testFlight, appStore.

Dan agar skema ini berfungsi dengan CodyFire, Anda perlu menambahkan kode berikut ke 
AppDelegate.didFinishLaunchingWithOptions setelah menginisialisasi CodyFire
 CodyFire.shared.setupEnvByProjectScheme() 
Selain itu, seringkali bos atau penguji proyek Anda mungkin meminta Anda untuk mengganti server dengan cepat di suatu tempat di 
LoginScreen . Dengan CodyFire, Anda dapat dengan mudah menerapkan ini dengan mengganti server dalam satu baris, mengubah lingkungan:
 CodyFire.shared.environmentMode = .appStore 
Ini akan berfungsi sampai aplikasi di-restart, dan jika Anda ingin disimpan setelah peluncuran, simpan nilai di UserDefaults , lakukan pemeriksaan ketika aplikasi dimulai di AppDelegate dan alihkan lingkungan ke yang diperlukan.
Saya mengatakan poin penting ini, saya berharap akan ada lebih banyak proyek di mana peralihan lingkungan akan dilakukan dengan indah. Dan pada saat yang sama, kami telah menginisialisasi perpustakaan.
Struktur dan Pengontrol File
Sekarang Anda dapat berbicara tentang visi saya tentang struktur file untuk semua panggilan API, ini bisa disebut ideologi CodyFire.
Mari kita lihat bagaimana akhirnya terlihat dalam proyek

Sekarang mari kita lihat daftar file, mari mulai dengan 
API.swift .
 class API { typealias auth = AuthController typealias post = PostController } 
Tautan ke semua pengontrol terdaftar di sini sehingga mereka dapat dengan mudah dipanggil melalui `API.controller.method`.
 class AuthController {} 
API + Login. Cepat 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") } } 
Di dekorator ini, kami mendeklarasikan fungsi untuk memanggil API kami:
- tentukan titik akhir
- Metode HTTP POST
- Gunakan pembungkus untuk otentikasi dasar
- mendeklarasikan teks yang diinginkan untuk respons spesifik dari server (ini nyaman)
- dan menunjukkan model dimana data akan diterjemahkan
Apa yang masih tersembunyi?
- tidak perlu menentukan URL server lengkap, karena sudah diatur secara global
- tidak harus menunjukkan bahwa kami berharap menerima 
200 OK jika semuanya baik-baik saja
200 OK adalah kode status default yang diharapkan oleh CodyFire untuk semua permintaan, di mana data kasus diterjemahkan dan panggilan balik dipanggil, bahwa semuanya baik-baik saja, di sini adalah data Anda.
Lebih lanjut, di suatu tempat dalam kode untuk 
Layar Masuk Anda 
, Anda dapat memanggil
 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 dan 
onSuccess hanyalah sebagian kecil dari panggilan balik yang dapat dikembalikan oleh APIR, kita akan membicarakannya nanti.
Dalam contoh input, kami hanya mempertimbangkan opsi ketika data yang dikembalikan diterjemahkan secara otomatis, tetapi Anda dapat mengatakan bahwa Anda sendiri dapat mengimplementasikannya, dan Anda akan benar. Karena itu, mari pertimbangkan kemungkinan pengiriman data pada model menggunakan formulir pendaftaran sebagai contoh.
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") } } 
Tidak seperti login, saat pendaftaran kami mengirimkan sejumlah besar data.
Dalam contoh ini, kami memiliki model 
SignupRequest yang sesuai dengan protokol 
JSONPayload (sehingga CodyFire memahami tipe payload) sehingga isi permintaan kami adalah dalam bentuk JSON. Jika Anda perlu x-www-form-urlencoded, maka gunakan 
FormURLEncodedPayload .
Hasilnya, Anda mendapatkan fungsi sederhana yang menerima model payload
 API.auth.signup(request) 
dan yang, jika berhasil, akan mengembalikan model respons spesifik kepada Anda.
Saya pikir itu keren, bukan?
Tetapi bagaimana jika multipart?
Mari kita lihat contoh ketika Anda dapat membuat 
Posting .
Posting + Buat.segera 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) } } 
Kode ini akan dapat mengirim formulir multi bagian dengan larik file gambar dan dengan satu video.
Mari kita lihat cara menelepon pengiriman. Inilah momen paling menarik tentang 
Lampiran .
 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)") } 
Lampiran adalah model di mana, selain 
Data, nama file dan MimeType-nya juga ditransmisikan.
Jika Anda pernah mengirimkan formulir multi bagian dari Swift menggunakan Alamofire atau 
URLRequest telanjang 
, saya yakin Anda akan menghargai kesederhanaan 
CodyFire .
Sekarang contoh sederhana dari GET panggilan.
Posting + 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) } } 
Contoh paling sederhana adalah
 API.post.get(id:) 
yang di 
onSuccess akan mengembalikan model 
Post kepada Anda.
Berikut ini contoh yang lebih menarik.
 API.post.get(PostController.ListQuery(offset: 0, limit: 100)) 
yang mengambil model 
ListQuery sebagai input,
yang akhirnya diajukan APIR ke jalur URL formulir
 post?limit=0&offset=100 
dan akan mengembalikan array 
[Posting] ke 
onSuccess .
Tentu saja, Anda dapat menulis jalur URL dengan cara lama, tetapi sekarang Anda tahu bahwa Anda dapat sepenuhnya Codable.
Permintaan contoh terakhir akan HAPUS
Posting + Delete.swift extension PostController { static func delete(id: UUID) -> APIRequest<Nothing> { return APIRequest("post/" + id.uuidString) .method(.delete) .desiredStatusCode(.noContent) } } 
Ada dua hal menarik.
- tipe kembalinya adalah APIRequest, ini menentukan tipe generik 
Nothing , yang merupakan model 
Codable kosong.
- kami secara eksplisit menunjukkan bahwa kami berharap menerima 204 TANPA KONTEN, dan CodyFire hanya akan memanggil 
onSuccess dalam kasus ini.
Anda sudah tahu cara memanggil titik akhir ini dari ViewController Anda.
Tetapi ada dua opsi, yang pertama dengan 
onSuccess , dan yang kedua tanpa. Kami akan melihatnya
 API.post.delete(id:).execute() 
Artinya, jika itu tidak masalah bagi Anda apakah permintaannya 
berhasil , Anda dapat memanggil 
.execute () di atasnya dan hanya itu, jika tidak maka akan dimulai setelah deklarasi handler 
onSuccess dari handler.
Fungsi yang tersedia
Otorisasi setiap permintaan
Untuk menandatangani setiap API permintaan dengan header http apa pun, penangan global digunakan, yang dapat Anda atur di 
AppDelegate . Selain itu, Anda dapat menggunakan model [String: String] atau Codable klasik untuk dipilih.
Contoh untuk Pembawa Otorisasi.
1. Codable (merekomendasikan)
 CodyFire.shared.fillCodableHeaders = { struct Headers: Codable { //NOTE:  nil,     headers var Authorization: String? var anythingElse: String } return Headers(Authorization: nil, anythingElse: "hello") } 
2. Klasik [String: String]
 CodyFire.shared.fillHeaders = { guard let apiToken = LocalAuthStorage.savedToken else { return [:] } return ["Authorization": "Bearer \(apiToken)"] } 
Tambahkan beberapa http-header secara selektif ke permintaan
Ini dapat dilakukan saat membuat Permintaan APIR, misalnya:
 APIRequest("some/endpoint").headers(["someKey": "someValue"]) 
Menangani Permintaan Tidak Resmi
Anda dapat memprosesnya secara global, misalnya di 
AppDelegate CodyFire.shared.unauthorizedHandler = {  
atau secara lokal di setiap permintaan
 API.post.create(request).onNotAuthorized {  
Jika jaringan tidak tersedia
 API.post.create(request). onNetworkUnavailable {  
jika tidak di 
onError Anda mendapatkan kesalahan 
._notConnectedToInternetMulai sesuatu sebelum permintaan dimulai
Anda dapat mengatur 
.onRequestStarted dan mulai menunjukkan, misalnya, sebuah loader di dalamnya.
Ini adalah tempat yang nyaman, karena tidak dipanggil jika Internet tidak tersedia, dan Anda tidak perlu menunjukkan loader secara gratis, misalnya.
Cara menonaktifkan / mengaktifkan log output secara global
 CodyFire.shared.logLevel = .debug CodyFire.shared.logLevel = .error CodyFire.shared.logLevel = .info CodyFire.shared.logLevel = .off 
Cara menonaktifkan log output untuk satu permintaan
 .avoidLogError() 
Memproses log dengan cara Anda sendiri
 CodyFire.shared.logHandler = { level, text in print("  CodyFire: " + text) } 
Cara mengatur kode respons http yang diharapkan dari server
Seperti yang saya katakan di atas, secara default, CodyFire mengharapkan untuk menerima 
200 OK, dan jika ya, ia mulai mengurai data dan memanggil 
onSuccess .
Tetapi kode yang diharapkan dapat diatur dalam bentuk enum yang nyaman, misalnya, untuk 
201 CREATED .desiredStatusCode(.created) 
atau Anda bahkan dapat mengatur kode yang diharapkan khusus
 .desiredStatusCode(.custom(777)) 
Batalkan permintaan
 .cancel() 
dan Anda dapat mengetahui bahwa permintaan dibatalkan dengan menyatakan penangan 
.onCancellation .onCancellation {  
jika tidak 
onError akan 
dimunculkanMengatur batas waktu untuk permintaan
 .responseTimeout(30)  
seorang pawang juga dapat digantung pada acara timeout
 . onTimeout {  
jika tidak 
onError akan 
dimunculkanMengatur Batas Waktu Ekstra Interaktif
Ini adalah fitur favorit saya. Salah satu pelanggan dari AS pernah bertanya kepada saya tentang dia, karena dia tidak suka bahwa formulir masuknya bekerja terlalu cepat, menurutnya itu tidak terlihat alami, seolah-olah itu palsu, bukan otorisasi.
Idenya adalah bahwa ia ingin pemeriksaan email / kata sandi berlangsung 2 detik atau lebih. Dan jika itu hanya berlangsung 0,5 detik, maka Anda perlu melempar 1,5 dan hanya kemudian memanggil 
Proses berhasil . Dan jika dibutuhkan tepat 2 atau 2,5 detik, maka panggil 
onSuccess segera.
 .additionalTimeout(2)  
Encoder / dekoder Tanggal Kustom
CodyFire memiliki enum 
DateCodingStrategy sendiri, di mana ada tiga nilai
- detikSejak tahun1970
- milidetik Sejak 1970
- diformat (_ customDateFormatter: DateFormatter)
DateCodingStrategy dapat diatur dalam tiga cara dan secara terpisah untuk decoding dan encoding
- secara global di 
AppDelegate CodyFire.shared.dateEncodingStrategy = .secondsSince1970 let customDateFormatter = DateFormatter() CodyFire.shared.dateDecodingStrategy = .formatted(customDateFormatter) 
- untuk satu permintaan
 APIRequest("some/endpoint") .dateDecodingStrategy(.millisecondsSince1970) .dateEncodingStrategy(.secondsSince1970) 
- atau bahkan secara terpisah untuk setiap model, Anda hanya perlu model yang cocok dengan 
CustomDateEncodingStrategy dan / atau 
CustomDateDecodingStrategy .
 struct SomePayload: JSONPayload, CustomDateEncodingStrategy, CustomDateDecodingStrategy { var dateEncodingStrategy: DateCodingStrategy var dateDecodingStrategy: DateCodingStrategy } 
Cara menambahkan ke proyek
Perpustakaan tersedia di GitHub di bawah lisensi MIT.Instalasi saat ini hanya tersedia melalui CocoaPods
 pod 'CodyFire' 
Saya sangat berharap bahwa CodyFire akan bermanfaat bagi pengembang iOS lainnya, ini akan menyederhanakan pengembangan untuk mereka, dan secara umum akan membuat dunia sedikit lebih baik dan orang-orang ramah.
Itu saja, terima kasih atas waktu Anda.
UPD: Dukungan ReactiveCocoa dan RxSwift ditambahkan pod 'ReactiveCodyFire'  
APIRequest untuk ReactiveCoca akan memiliki 
.signalProducer , dan untuk RxSwift 
.observableUPD2: Sekarang Anda dapat menjalankan beberapa permintaanJika penting bagi Anda untuk mendapatkan hasil dari setiap permintaan, gunakan 
.and ()Maksimum dalam mode ini, Anda dapat menjalankan hingga 10 permintaan, mereka akan dieksekusi secara ketat satu demi satu.
 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 juga tersedia.
onProgress - masih dalam pengembangan
Jika Anda tidak peduli dengan hasil kueri Anda, Anda dapat menggunakan 
.flatten () [API.employee.all(), API.office.all(), API.car.all()].flatten().onError { print(error.description) }.onSuccess { print("flatten finished!") } 
Untuk menjalankannya pada saat yang sama cukup tambahkan 
.concurrent (dengan: 3) ini akan memungkinkan tiga permintaan untuk dieksekusi secara bersamaan, nomor apa pun dapat ditentukan.
Untuk melewati kesalahan kueri yang gagal, tambahkan 
.avoidCancelOnError ()Untuk mendapatkan kemajuan, tambahkan 
.onProgressUPD3: sekarang Anda dapat mengatur server terpisah untuk setiap permintaanPenting untuk membuat alamat server yang diperlukan di suatu tempat, misalnya seperti ini
 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") 
Dan sekarang Anda dapat menggunakannya tepat di inisialisasi permintaan sebelum menentukan titik akhir
 APIRequest(server1, "endpoint", payload: payloadObject) APIRequest(server2, "endpoint", payload: payloadObject) APIRequest(server3, "endpoint", payload: payloadObject) 
atau Anda dapat menentukan server setelah menginisialisasi permintaan
 APIRequest("endpoint", payload: payloadObject).serverURL(server1)