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)