vendredi 30 juin 2017

Building Networking layer in iOS for multiple API's

I'm creating a social network app. I will be using various web services like Facebook for login, Instagram to get a users photos, and maybe others like Spotify.

I need to create a flexible Networking layer that can consume multiple REST API's, while minimizing duplicate code, coupling, and respects Single Responsibility Principle.

What I've managed to do is Abstract the process of sending a request into multiple parts.

  • First: its necessary to make enums that conform to the Request protocol. The enum will provide a group of related requests that can be executed. For instance, I'll have a UserRequest enum that contains a list of requests that involve the User Object Model. Same pattern for Messages, Posts, etc... By providing the associated values with a given enum case, the components of a URLRequest are assigned in an extension.

  • Second: create a Dispatcher Protocol, to create "client" objects that execute the actual requests. I only plan on needing one object that conforms, named NetworkDispatcher. This is where all the code that handles URL's, URLRequests, URLSessions, and handling response of that request.

  • Third: Create a service layer, that will interact directly with the view controllers. Service layer pretty much exposes an interface of all the requests that can be made that pertain to the type of service. For each method in the service layer, there is a corresponding enum case for the Request. Example: UserRequest has the method addUser('arguments'), which constructs a urlRequest. so UserService has the method addUser('arguments'), which calls on its instance of NetworkDispatcher to execute a request.

Is this a good flexible & maintainable approach? Im trying to pickup design patterns and follow good coding practices.

One thing Im also unsure about is if its okay for Services to be dependent on one another. For instance, what if I my ChatService class needs to make a request that the UserService exposes. Can I make the services singletons?

Any tip or guidance on how this can be improved upon or where I may have taken some wrong turns is greatly appreciated!

 /// - Request: Protocol that defines the components of a URLRequest.


protocol Request {

    var path: String { get }
    var method: Method { get }
    var body: [String: AnyObject]? { get }
    var queryItems: [URLQueryItem]? { get }
    var headers: [String: String]? { get }
}



enum UserRequest {
    case getUser(id: String, authToken: String)
    case addUser(data: [String: AnyObject], authToken: String)
}


extension UserRequest: Request {

    var path: String {
        switch self {
        case .getUser(let id, _): return "users/\(id)/" + ".json"
        case .addUser: return "users/" + ".json"
        }

    }

    var method: Method {
        switch self {
        case .getUser: return .get
        case .addUser: return .post
        }
    }


    var body: [String: AnyObject]? {
        switch self {
        case .addUser(let data, _): return data
        default: return nil
        }

    }


    var queryItems: [URLQueryItem]? {
        switch self {

        case .getUser(_, let authToken):
            let item = URLQueryItem(name: "auth", value: authToken)
            return [item]

        case .addUser(_, let authToken):
            let item = URLQueryItem(name: "auth", value: authToken)
            return [item]
        }
    }


    var headers: [String: String]? {
        switch self {
        default: return nil
        }
    }



}




 /// - Dispatcher: Responsible for making actual API requests.


protocol Dispatcher {

    init(host: String)

    func fireRequest(request: Request, _ completion: @escaping (_ success: [String: AnyObject]?, _ failure: NetworkError?) -> Void ) -> Void
}



 ///- Network Dispatcher: Conforms to Dispatcher, processes & fires off requests


class NetworkDispatcher: Dispatcher {

    private var host: String

    private var session: URLSession


    required init(host: String) {

        self.host = host

        self.session = URLSession(configuration: .default)
    }



    /// - Fires URLRequest via URLSession, leverages private helper methods to construct URLRequest and handle response.

    func fireRequest(request: Request, _ completion: @escaping ([String : AnyObject]?, NetworkError?) -> Void) {

        do {

            let urlRequest = try processRequest(request: request)

            session.dataTask(with: urlRequest, completionHandler: { (data, response, error) in
                let response = Response(data: data, response: response, error: error)
                switch response {
                case .json(let json): completion(json, nil)
                case .error(let error): completion(nil, error)
                }
            }).resume()

        } catch {
            completion(nil, error as? NetworkError)
            return
        }


    }




    /// MARK: - Helper Methods
    ///
    /// - Processes components from Request
    /// - Either returns URLRequest or throws an error


    private func processRequest(request: Request) throws  -> URLRequest {

        guard let url = processUrl(request: request) else {
            throw NetworkError.invalidUrl
        }

        var urlRequest = URLRequest(url: url)
        urlRequest.allHTTPHeaderFields = request.headers
        urlRequest.httpMethod = request.method.rawValue


        if let body = request.body {
            do {
                let _ = try JSONSerialization.data(withJSONObject: body, options: .init(rawValue: 0))
            } catch {
                throw NetworkError.invalidHTTPBody
            }
        }

        return urlRequest
    }



    private func processUrl(request: Request) -> URL? {

        var urlComponents: URLComponents {
            var components = URLComponents()
            components.path = host + request.path
            components.queryItems = request.queryItems
            return components
        }

        return urlComponents.url
    }


}





  /// - Response: handles response as json or error


typealias JSON = [String: AnyObject]

enum Response {

    case json(_: JSON)
    case error(_: NetworkError?)


    init(data: Data?, response: URLResponse?, error: Error?) {

        guard let httpResponse = response as? HTTPURLResponse else {
            self = .error(NetworkError.requestFailed)
            return
        }

        guard httpResponse.statusCode == 200 else {
            self = .error(NetworkError.responseUnsuccessful)
            return
        }

        guard let data = data else {
            self = .error(NetworkError.invalidData)
            return
        }

        do {

            let jsonData = try JSONSerialization.jsonObject(with: data, options: [])

            guard let json = jsonData as? [String: AnyObject] else {
                self = .error(NetworkError.jsonConversionFailure)
                return
            }

            self = .json(json)

        } catch {
            self = .error(NetworkError.jsonSerializationFailure)
        }
    }

}




/// SERVICE LAYER: Service objects that handle business logic for each Model.
///
/// - I'd have more services like ChatService, FeedService, and so forth.
/// - Interacts with View Controllers

class UserService {

    var dispatcher = NetworkDispatcher(host: "https://firebase.com/")


    func add(user: User, authToken: String, completion: @escaping (_ success: User?,_ failure: NetworkError?) -> Void ) {

        let request = UserRequest.addUser(data: user.toAnyObject(), authToken: authToken)

        dispatcher.fireRequest(request: request) { (data, error) in

            // TODO: Handle callback.

        }


    }

Aucun commentaire:

Enregistrer un commentaire