This lib is about network requests with blackjack, roulette and craps!
Using it you will be able to convert your massive API layer code into an awesome convenient controllers with easy and maintainable network requests!
Use Codable models for everything related to API requests:
- json payload
- url-encoded payload
- multipart payload
- plain and json response
- url query
- headers
Wondered? That's only little part of what you will get from this lib! π»
APIRequest<ResponseModel>("endpoint").onSuccess { model in
//here's your decoded model!
//no need to check http.statusCode, I already did it for you! By default it's 200 OK
//of course you can choose which statusCode is equal to success (look at the `POST` and `DELETE` examples below)
}
APIRequest<ResponseModel>("endpoint", payload: payloadModel)
.method(.post)
.desiredStatusCode(.created) //201 CREATED
.onSuccess { model in
//here's your decoded model!
//success was determined by comparing desiredStatusCode with http.statusCode
}
APIRequest<Nothing>("endpoint")
.method(.delete)
.desiredStatusCode(.noContent) //204 NO CONTENT
.onSuccess { _ in
//here's empty successful response!
//success was determined by comparing desiredStatusCode with http.statusCode
}
You're able to run up to 10 requests one-by-one!
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
// do what you want with received results!!! π»
}
Or you can run unlimited amount of requests one-by-one or at the same time, if you need just a completion handler.
[API.employee.all(), API.office.all(), API.car.all()].flatten().onError {
print(error.description)
}.onSuccess {
print("flatten finished!")
}
to run them concurrently just add .concurrent(by: 3)
to run by 3 at the same time
Of course you'll be able to send PUT and PATCH requests, send multipart codable structs with upload progress callback, catch errors, even redefine error descriptions for every endpoint. Wondered? π Let's read the whole readme below! π»
CodyFire is available through CocoaPods and SPM.
To install it, simply add the following line in your Podfile:
pod 'CodyFire', '~> 1.15.4'
Or you're looking for reactive code support? I have it! πΊ
pod 'RxCodyFire', '~> 1.1.0'
# no need to install `CodyFire` cause it's in dependencies
using this pod you should always import just RxCodyFire
and every APIRequest will have .observable
- for ReactiveCocoa https://github.com/MihaelIsaev/ReactiveCodyFire
pod 'ReactiveCodyFire', '~> 1.1.0'
# no need to install `CodyFire` cause it's in dependencies
using this pod you should always import just ReactiveCodyFire
and every APIRequest will have .signalProducer
CodyFire automatically detects which environment you're on, so I suggest you to definitely use this awesome feature π
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)
//Also if you want to be able to switch environments manually just uncomment the line below (read more about that)
//CodyFire.shared.setupEnvByProjectScheme()
return true
}
}
Isn't it cool? π
Or you can set different Server URL for every APIRequest
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")
And then initialize your APIRequest
s like this π₯
APIRequest(server1, "endpoint", payload: payloadObject)
APIRequest(server2, "endpoint", payload: payloadObject)
APIRequest(server3, "endpoint", payload: payloadObject)
Or in some cases you even can do it like this π
APIRequest("endpoint", payload: payloadObject).serverURL(server1)
I promise that this is API code architecture from your dreams which are come true!
class API {
typealias auth = AuthController
typealias task = TaskController
}
API/Controllers/Auth/Auth.swift
class AuthController {}
API/Controllers/Task/Task.swift
class TaskController {}
API/Controllers/Auth/Auth+Login.swift
import CodyFire
extension AuthController {
struct LoginRequest: JSONPayload {
let email, password: String
init (email: String, password: String) {
self.email = email
self.password = password
}
}
struct LoginResponse: Codable {
var token: String
}
static func login(_ request: LoginRequest) -> APIRequest<LoginResponse> {
return APIRequest("login", payload: request).method(.post).addCustomError(.notFound, "User not found")
}
}
API/Controllers/Auth/Auth+Login.swift
import CodyFire
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/Controllers/Task/Task+Get.swift
import CodyFire
extension TaskController {
struct Task: Codable {
var id: UUID
var name: String
}
struct ListQuery: Codable {
var offset, limit: Int
init (offset: Int, limit: Int) {
self.offset = offset
self.limit = limit
}
}
static func get(_ query: ListQuery? = nil) -> APIRequest<[Task]> {
return APIRequest("task").query(query)
}
static func get(id: UUID) -> APIRequest<Task> {
return APIRequest("task/" + id.uuidString)
}
}
API/Controllers/Task/Task+Create.swift
import CodyFire
extension TaskController {
struct CreateRequest: JSONPayload {
var name: String
init (name: String) {
self.name = name
}
}
static func create(_ request: CreateRequest) -> APIRequest<Task> {
return APIRequest("post", payload: request).method(.post).desiredStatusCode(.created)
}
}
API/Controllers/Task/Task+Edit.swift
import CodyFire
extension TaskController {
struct EditRequest: JSONPayload {
var name: String
init (name: String) {
self.name = name
}
}
static func create(id: UUID, request: EditRequest) -> APIRequest<Task> {
return APIRequest("post/" + id.uuidString, payload: request).method(.patch)
}
}
API/Controllers/Task/Task+Delete.swift
import CodyFire
extension TaskController {
static func delete(id: UUID) -> APIRequest<Nothing> {
return APIRequest("post/" + id.uuidString).method(.delete).desiredStatusCode(.noContent)
}
}
API.auth.login(email: "[email protected]", password: "qwerty").onError { error in
switch error.code {
case .notFound: print("User not found")
default: print(error.description)
}
}.onSuccess { token in
print("Received auth token: "+ token)
}
API.task.get().onError { error in
print(error.description)
}.onSuccess { tasks in
print("received \(tasks.count) tasks")
}
API.task.create(TaskController.CreateRequest(name: "Install CodyFire")).onError { error in
print(error.description)
}.onSuccess { task in
print("just created new task: \(task)")
}
let taskId = UUID()
API.task.delete(id: taskId).onError { error in
print(error.description)
}.onSuccess { _ in
print("just removed task with id: \(taskId)")
}
//declare a PostController
class PostController()
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 PostResponse: Codable {
let id: UUID
let text: String
let tags: [String]
let linksToImages: [String]
let linkToVideo: String
}
static func create(_ request: CreateRequest) -> APIRequest<PostResponse> {
return APIRequest("post", payload: request).method(.post)
}
}
//then somewhere send creation request!
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("tracking post uploading progress: \(progress)")
}.onError { error in
print(error.description)
}.onSuccess { createdPost in
print("just created post: \(createdPost)")
}
Easy right? π
For that we have a global headers wrapper, which is called for every request.
You need to declare it e.g. somewhere in AppDelegate.
There are two options
- Use Codable model for headers (recommended)
CodyFire.shared.fillCodableHeaders = {
struct Headers: Codable {
var Authorization: String? //NOTE: nil values will be excluded
var anythingElse: String
}
return Headers(Authorization: nil, anythingElse: "hello")
}
- Use [String: String] dictionary
CodyFire.shared.fillHeaders = {
guard let apiToken = LocalAuthStorage.savedToken else { return [:] }
return ["Authorization": "Bearer \(apiToken)"]
}
Again, somewhere in AppDelegate declare it like this CodyFire.shared.unauthorizedHandler = { //kick out user }
.onNetworkUnavailable {
print("unfortunately there're no internet connection!")
}
.onRequestStarted {
print("request started normally")
}
.avoidLogError()
Usually servers response with 200 OK
and CodyFire expect to receive 200 OK
to call onSuccess
handler by default.
You may need to specify another code, e.g. 201 CREATED
for some POST requests.
.desiredStatusCode(.created)
or you even can set your custom code
.desiredStatusCode(.custom(777))
.headers(["myHeader":"myValue"])
//or for basic auth
.basicAuth(email: "", password: "")
You may use: GET, POST, PUT, PATCH, DELETE, HEAD, TRACE, CONNECT, OPTIONS
It's really useful feature and I suggest to use it in every iOS project!
Create three schemes named: Development, TestFlight, AppStore like on the screenshot below
TIP: Make sure that they're marked as Shared
to have them in git
Then in every scheme in Arguments
tab add Environment variable
named env
with one of those values: dev, testFlight, appStore.
Then somewhere in AppDelegate.didFinishLaunchingWithOptions add
CodyFire.shared.setupEnvByProjectScheme()
All done, now you're able to easily switch environments!
Sometimes useful for DELETE or PATCH requests
APIRequest<Nothing>("endpoint").method(.delete).execute()
let request = APIRequest("").execute()
request.cancel()
and you're able to handle cancellation
.onCancellation {
print("request was cancelled :(")
}
You may define your own custom errors, globally or for each request.
onError
block contains NetworkError
object with StatusCode
enum, an error description, and a raw response Data
. Error description you could change to whatever you want for any error code.
By default there are already defined some good descriptions for common errors.
Let's take a look how we can use powerful onError
block
.onError { error in
switch error.code {
case .notFound: print("It's not found :(")
case .internalServerError: print("Oooops... Something really went wrong...")
case .custom(let code): print("My non-standard-custom error happened: " + error.description)
case .unknown(let code): print("Totally unknown error happened: " + error.description)
default:
print("Another error happened: " + error.description)
if let raw = error.raw, let rawResponse = String(data: raw, encoding: .utf8) {
print("Raw response: " + rawResponse)
}
}
}
More than that!!! In your controller while declaring APIRequest you're able to add your own custom errors!!! π
APIRequest("login")
.method(.post)
.basicAuth(email: "[email protected]", password: "qwerty")
.addError(.notFound, "User not found")
I believe that's really awesome and useful! Finally a lot of things may be declared in one place! π
.responseTimeout(30) //request timeout set for 30 seconds
and of course you're able to catch timeout
.onTimeout {
//timeout happened :(
}
If you want to make sure that your request will take 2 or more seconds (to not be too fast π ) you can do that
.additionalTimeout(2)
e.g. in case if your request will be executed in 0.5 seconds, onSuccess
handler will be fired only in 1.5s after that
but in case if your request will take more than 2s then onSuccess
handler will be fired immediatelly
Your struct/class just should conform to MultipartPayload
protocol
struct SomePayload: MultipartPayload {
let name: String
let names: [String]
let date: Date
let dates: [Dates]
let number: Double
let numbers: [Int]
let attachment: Data
let attachments: [Data]
let fileAttachment: Attachment
let fileAttachments: [Attachment]
}
You may conform your struct/class to: FormURLEncodedPayload
, MultipartPayload
, and JSONPayload
Your struct/class just should conform to JSONPayload
protocol
struct SomePayload: JSONPayload {
let name: String
let names: [String]
let date: Date
let dates: [Dates]
let number: Double
let numbers: [Int]
}
Your struct/class just should conform to FormURLEncodedPayload
protocol
struct SomePayload: FormURLEncodedPayload {
let name: String
let names: [String]
let date: Date
let dates: [Dates]
let number: Double
let numbers: [Int]
}
Your struct/class just should conform to Codable
protocol
struct SomePayload: Codable {
let name: String
let names: [String]
let date: Date
let dates: [Dates]
let number: Double
let numbers: [Int]
}
Our DateCodingStrategy
support
- secondsSince1970
- millisecondsSince1970
- formatted(customDateFormatter: DateFormatter)
By default all the dates are in
yyyy-MM-dd'T'HH:mm:ss'Z'
format
You have interesting options here:
- you can set global date decoder/encoder
CodyFire.shared.dateEncodingStrategy = .secondsSince1970
let customDateFormatter = DateFormatter()
CodyFire.shared.dateDecodingStrategy = .formatted(customDateFormatter)
- you can set date decoder/encoder for request in your controller
APIRequest().dateDecodingStrategy(.millisecondsSince1970).dateEncodingStrategy(.secondsSince1970)
- or you even can use different date encoder/decoder for each payload type (highest priority)
struct SomePayload: JSONPayload, CustomDateEncodingStrategy, CustomDateDecodingStrategy {
var dateEncodingStrategy: DateCodingStrategy
var dateDecodingStrategy: DateCodingStrategy
}
e.g. in AppDelegate you may set logging mode
CodyFire.shared.logLevel = .debug
CodyFire.shared.logLevel = .error
CodyFire.shared.logLevel = .info
CodyFire.shared.logLevel = .off
and also you can set log handler
CodyFire.shared.logHandler = { level, text in
print("manually printing codyfire error: " + text)
}
by default for the AppStore the log level if .off
It's easy
#if DEBUG
//DEV environment
#else
if Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" {
//TESTFLIGHT environment
} else {
//APPSTORE environment
}
#endif
Run up to 10 requests one-by-one!
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
// do what you want with received results!!! π»
}
onRequestStarted, onNetworkUnavailable, onCancellation, onNotAuthorized, onTimeout also available!
//TBD: onProgress
I believe it is awesome! Especially for whom who not familiar or don't like reactive programming π
If you want to run several requests one-by-one or at the same time but with just completion handler you can do that with .flatten()
[API.employee.all(), API.office.all(), API.car.all()].flatten().onError {
print(error.description)
}.onSuccess {
print("flatten finished!")
}
to run them concurrently just add .concurrent(by: 3)
to run by 3 at the same time
to skip errors also add .avoidCancelOnError()
to get progress add .onProgress
Please feel free to send pull requests and ask your questions in issues
Hope this lib will be really useful in your projects! Tell you friends about it! Please press STAR βοΈ button!!!
Mike Isaev, [email protected]