Swiftpack.co - Package - martin-lalev/Flooid

Flooid is a Swift framework, which helps you abstract your networking layer via clear, self-contained endpoint definitions, which can be executed either on a URLSession, a 3rd party networking framework, or be mocked for testing.

Table of contents

  1. Installation
  2. Quick start
  3. Network Layer
  4. Integrations
  5. Roadmap

Installation

CocoaPods

Flooid is still in development and is not yet available in the CocoaPods repository. However, you can install it by referencing the latest version here:

pod 'Flooid', :git => 'https://github.com/martin-lalev/Flooid', :tag => '0.0.13'

Run pod install and then import Flooid wherever you need it.

Coming soon: manual installation guide and installation guide via Carthage

Quick start

FlooidTaskClient(host: "https://www.example.com/")
    .dataTask(for: "api/v2/posts".get(query: ["type":"recent"]), serializer: FlooidDecodableSerializer<[APIPost]>().asAny())
    .execute({ (result) in
        print("Posts result list:")
        for post in result { print(post) }
    }, { (error) in
        print(error)
    })

Here's a breakdown of what's going on:

// Create an URLSession client for a given host. There are two other alternaties - FlooidAlamofireClient(host: String) and FlooidMockClient()
let client: FlooidTaskClient = FlooidTaskClient(host: "https://www.example.com/")

// Create a request, which wraps the method name, path, query or body parameters, and optional headers map.
let request: FlooidRequest = "api/v2/posts".get(query: ["type":"recent"])

// Create a serializer which will parse the response from the request to a given value (or throw an Error).
let serializer: AnyFlooidSerializer<[APIPost]> = FlooidDecodableSerializer<[APIPost]>().asAny()

// Create an executable object for the request (may be executed multiple times)
let executable: AnyFlooidExecutable<[APIPost]> = client.dataTask(for: request, serializer: serializer)

// Create and start an execution task (usually an URLSessionDataTask, except if using a FlooidMockClient) and handle the success and fail callbacks.
let cancellable: FlooidCancellable = executable.execute({ (result) in
    print("Posts result list:")
    for post in result { print(post) }
}, { (error) in
    print(error)
})

Network Layer

Most mobile applications require executing requests on a remote API with multiple endpoints. Although this can be done using the method described in the above section, it's best to abstract away the access to the API. Also, it's often the case that an endpoint which the app requires is not yet implemented or we need to mock it for testing.

By using Flooid's FlooidService protocol, FlooidTaskClient and FlooidMockClient classes, and some Protocol Oriented Programming techniques, the network layer abstraction can be easily built, maintained, tested and just easy to read and use.

Define services for each group of endpoints

The first thing to do is define a protocol, which inherits from the FlooidService protocol. FlooidService only requires a dataTask<T>(...) -> AnyFlooidExecutable<T> function, however, beacuse we are just defining a protocol, we don't need to worry about it's implementation.

Usually though, most APIs have a single point of entry (e.g. /api/v2, /v3.2, etc.) and many of its endpoint may be grouped together (e.g. CRUD endpoints for a single resource). That's why, it's recommended to start with a RootService protocol, which inherits FlooidService, and extend with a default implementation for the root path. Then define multiple protocols for each group of endpoints, all of which should inherit from your RootService (thus receiving the default root path implementation too). Finally, for your convenience, define a typealias which combines all of the protocols into a single type.

APIService.swift
import Foundation
import Flooid

public protocol APIRootService: FlooidService {}

extension APIRootService {
    public var apiV2: FlooidPath { return "api/v2" }
}

public typealias APIService = APIRootService
    & APIUsersService
    & APIPostsService
    & APICommentsService
    & APILinksService

Implement the endpoints for each service

Next, provide extensions for each of the service protocols you have defined. The main purpose of these extension is to provide methods which return AnyFlooidExecutable<T> instances for each endpoint which belongs to this group. This is done by first creating a FlooidRequest (e.g. self.apiV2["users"]["verified"].get()) and then calling its convenience method dataTask(..), which requires a FlooidService and a AnyFlooidSerializer<T> instances.

Because FlooidSerializer is a protocol with an associatedtype, it cannot be used as a type. Therefore Flooid provides type erasure via AnyFlooidSerializer and FlooidSerializer's asAny() method.

APIUsersService.swift
import Foundation
import Flooid

public protocol APIUsersService: APIRootService {
    
    func getVerifiedUsers() -> AnyFlooidExecutable<[APIUser]>
    
    func getUserDetails(_ userID:String) -> AnyFlooidExecutable<APIUser>
    
    func getUserPosts(_ userID:String, offset: Int?, limit: Int?) -> AnyFlooidExecutable<[APIPost]>

    func blockUser(_ userID:String, offset: Int?, limit: Int?) -> AnyFlooidExecutable<[APIPost]>
    
}

extension APIUsersService {
    
    public func getVerifiedUsers() -> AnyFlooidExecutable<[APIUser]> {
        return self.apiV2["users"]["verified"].get().dataTask(for: self, serializer: FlooidDecodableSerializer().asAny())
    }
    
    public func getUserDetails(_ userID:String) -> AnyFlooidExecutable<APIUser> {
        return self.apiV2["users"][userID].get().dataTask(for: self, serializer: FlooidDecodableSerializer().asAny())
    }
    
    public func getUserPosts(_ userID:String, offset: Int?, limit: Int?) -> AnyFlooidExecutable<[APIPost]> {
        return self.apiV2["users"][userID]["venues"].get(query: [:]
            <++ ("offset", offset)
            <++ ("limit", limit)
        ).dataTask(for: self, serializer: FlooidDecodableSerializer().asAny())
    }

    public func blockUser(_ userID:String, offset: Int?, limit: Int?) -> AnyFlooidExecutable<[APIPost]> {
        return self.apiV2["users"][userID].put(body: FlooidBodyURLEncoded([:]
            <++ ("blocked", true)
        )).dataTask(for: self, serializer: FlooidDecodableSerializer().asAny())
    }
    
}

Implement the service protocols via FlooidClient

The final step is to provide an actual implementation for our service protocols. The easiest way to do this is by subclassing the FlooidTaskClient class, because it already conforms to the FlooidService protocol.

Note that all of the protocols defined up until now are empty and extended with default method implementations, and ultimately inherit from FlooidService. Because FlooidTaskClient already conforms to it, our subclass doesn't need to implement anything in order to conform to the service protocols.

FlooidTaskClient is also a subclass of FlooidClient - a class which holds some configuration for the API, most importantly the host url, but also timeout interval, default headers, etc. It is also the place where the components of a FlooidRequest, combined with FlooidClient's properties, are transformed into an URLRequest instance. Therefore, the subclass of FlooidClient is the place to override URLRequest generation in order to provide authorization or any other modification, which should be applied to each request.

APITaskClient.swift
import Foundation
import Flooid

public class APITaskClient : FlooidTaskClient, APIService {
    
    let apiSecret:String
    
    init(secret:String) {
        self.apiSecret = secret
        super.init(host: "www.example.com", encoder:FlooidURLEncoder(arrayFormatter:.commas))
    }
    
    override public func generateRequest(for request: FlooidRequest) -> URLRequest {
        
        // create a new `FlooidHeaders` instance based on the headers currently provided
        let customHeaders = (request.headers ?? [:])
            <++ ("api_secret",self.apiSecret) // add the api_secret
        
        // create a new `modifiedRequest` using
        let modifiedRequest = FlooidRequest(request.methodName, path: request.path, query: request.query, body: request.body, headers: customHeaders)

        // call super.generateRequest with the new `customHeaders` instance
        return super.generateRequest(for: modifiedRequest)
    }
    
}

In this case we only need an api secret, which is then added to the headers of each request being executed via this client.

Using the networking layer

At this point, calling our endpoints will be as simple as:

let apiClient = APITaskClient(secret: "SECRET_KEY")

apiClient.getVerifiedUsers().execute({ (users) in
    print(users)
}, { (error) in
    print(error)
}).resume()

apiClient.getUserDetails(userID).execute({ (user) in
    print(user)
}, { (error) in
    print(error)
})

In a more real-world scenario, let's say there is a UserViewModel, which requires access to our API. Because it will only use the endpoints defined in APIUsersService, it doesn't need an APITaskClient or APIService instance specifically.

UserViewModel.swift
import Foundation

class UserViewModel {
    
    let apiService: APIUsersService
    var users: [APIUser]
    
    init(with apiService: APIUsersService) {
        self.apiService = apiService
        self.users = []
    }
    
    func loadVerifiedUsers(_ finished: (Error?) -> Void) {
        self.apiService.getVerifiedUsers().execute({ (users) in
            self.users = users
            finished(nil);
        }, { (error) in
            finished(error)
        })
    }
    
    ...
}

Now, creating an instance of UserViewModel may look like this:

let apiClient = APITaskClient(secret: "SECRET_KEY")
let userViewModel = UserViewModel(with: apiClient)

And if you want to test this view model, here's one way to do it:


class APIMockUserClient: FlooidMockClient, APIUsersService {
    
    func getVerifiedUsers() -> AnyFlooidExecutable<[APIUser]> {
        return self.mock(response: {
            return []
        })
    }
    
}

let apiClient = APIMockUserClient()
let userViewModel = UserViewModel(with: apiClient)

You are not required to mock each endpoint from the APIUsersService, however calling an unmocked one will result in fatalError coming from the FlooidMockClient's default implementation of dataTask(...)

Integrations

Alamofire

Flooid may be easily fitted for use with Alamofire. You will find that both of the libraries complement each other. To integrate Flooid with Alamofire you just need these simple extension.

RxSwift

If you are using RxSwift with URLSession, you should check out this extension, which integrates Flooid with RxSwift. If, instead, you are using Alamofire with RxSwift and RxAlamofire, then this extension might come in handy.

Roadmap

  • [ ] Check if Flooid is capable of handling common authentication scenarios
  • [ ] Add extensions for executing Download and Upload tasks
  • [ ] Consider removing the Flooid- prefix from all classes
  • [ ] Check if there's a more appropriate way to use URLEncoding
  • [ ] Check if there's a more appropriate way to implement FlooidBody-s
  • [ ] Add ReactiveSwift extensions
  • [ ] Add documentation for each class

Github

link
Stars: 0
Help us keep the lights on

Dependencies

Used By

Total: 0