Protocol-Oriented Network in swift
Abdoelrahman Eaita
iOS Software Engineer | Expert in SwiftUI & Mobile Architecture | Building Scalable, User-Centric Apps
Just imagine hundreds of the above image lines written for every call your app has to make, so you’ll just repeat it, name it after your call: loginUser, getUserData, logoutUser, getUserFriends, etc.
All of this can be and will be put into a single function that’s exactly 4 lines! in this blog and coming ones I’ll be talking about how to get to this result, so let’s start:
UserRequest.login(name:nameTF.text!, pass: passTF.text!).sendRequest{ response in ... }
It has been a while since I started working with the network in my apps, basically, on a daily bases, you call an endpoint to send or receive a request.
I’ve been through many iterations in the network starting with the simple call for our needed service in every view controller with full code rewritten: server URL + path, params, headers, and more! all of it was written in a function repeated through every ViewController, that’s MVC, right? then you improve that using static functions in a single file, or maybe use a singleton, and so on.
So here, I’m trying to give that pile of code another look, using our new friend: Protocols.
Many of the network layer approaches I’ve seen also came from Objective-C and made it to swift and that’s what Crusty indeed told you, this’s the way we used to do it and so should you! Guess what? No, you don’t, swift gives you the power to do better! Why not do it better then?!
So, Let’s build that layer!
URLRequest - Foundation | Apple Developer Documentation
URLRequest:
Url request is the main element in the network layer, it’s the object you always assemble to send it through your layer and the web in order to get it data to and from your server, and that’s what you actually need to recreate in every server call place through your app.
So you don’t need to repeat the network code, do a 1000 line class that’s called”APIManager, ServerManager, APIShared, APIServer” and make it a singleton without having the need to. (Yes I’ve seen this for real)! You simply just need to reconstruct the request depending on your needs aka path, parameters, headers, body content, etc.
What you also get from this approach is not having to write your parameters keys in every place you wanna call network, which is very error prone in my opinion and it looks very ugly!
This approach is already made by a network layer, meet: Moya/Moya
You’re a smart developer. You probably use Alamofire to abstract away access to URLSessionand all those nasty details you don't really care about. But then, like lots of smart developers, you write ad hoc network abstraction layers. They are probably called "APIManager" or "NetworkModel", and they always end in tears.
What Moya doing is actually a pretty interesting separation, you basically build a request with it through its builders, which is using Enums and then sending the request through it to alamofire.
Using Enums, which I’m using it more recently to handle different tasks. So in case you wanna learn more about enums I always refer to this blog post about advanced and practical enum usages:
Separation of concerns in creating a request object gives us the chance to test the request: params, paths, or tokens in the header, Unit tests will just love you!
So how to create it? Meet the “Router” way:
Alamofire/Alamofire: Alamofire - Elegant HTTP Networking in Swiftgithub.com
Alamofire comes with the idea of creating the router, check this tutorial:
To create your request you now simply do it with an enum confirming to URLConvertible, which is a simple protocol that requires having a function that created a URL request
func asURLRequest() throws -> URLRequest { }
and your enum becomes:
enum TodoRouter: URLRequestConvertible {
static let baseURLString = "https://jsonplaceholder.typicode.com/"
case get(Int)
case create([String: Any])
case delete(Int)
func asURLRequest() throws -> URLRequest {
var method: HTTPMethod {
switch self {
case .get:
return .get
case .create:
return .post
case .delete:
return .delete
}
}
let params: ([String: Any]?) = {
switch self {
case .get, .delete:
return nil
case .create(let newTodo):
return (newTodo)
}
}()
let url: URL = {
// build up and return the URL for each endpoint
let relativePath: String?
switch self {
case .get(let number):
relativePath = "todos/\(number)"
case .create:
relativePath = "todos"
case .delete(let number):
relativePath = "todos/\(number)"
}
var url = URL(string: TodoRouter.baseURLString)!
if let relativePath = relativePath {
url = url.appendingPathComponent(relativePath)
}
return url
}()
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = method.rawValue
let encoding: ParameterEncoding = {
switch method {
case .get:
return URLEncoding.default
default:
return JSONEncoding.default
}
}()
return try encoding.encode(urlRequest, with: params)
}
}
This’s a way to do it, but if you’re creating many enums like above for your app different parts, something to match your postman collection or api docs for example:
So you’d have User, Product, Order, Payment, Information enums in your app matching the above collection, let’s see how to do this!
We will be using many configurations in our requests that will be matching in every enum, so let’s get them all in a single protocol that rule them all so every enum can write way less code to be created, just paths and params mostly!
URLRequestBuilder:
import Foundation
import Alamofire
protocol URLRequestBuilder: URLRequestConvertible {
var mainURL: URL { get }
var requestURL: URL { get }
// MARK: - Path
var path: ServerPaths { get }
var headers: HTTPHeaders { get }
// MARK: - Parameters
var parameters: Parameters? { get }
// MARK: - Methods
var method: HTTPMethod { get }
var encoding: ParameterEncoding { get }
var urlRequest: URLRequest { get }
var deviceId: String { get }
}
extension URLRequestBuilder {
var mainURL: URL {
switch AppEnvironement.currentState {
case .development:
return URL(string: "https://staging.mysite.com")!
default:
return URL(string: "https://www.mysite.com")!
}
}
var requestURL: URL {
return mainURL.appendingPathComponent(path.rawValue)
}
var headers: HTTPHeaders {
var header = HTTPHeaders()
if let token = KeyChain.userToken {
header["Authorization"] = "Bearer \(token)"
}
return header
}
var defaultParams: Parameters {
var param = Parameters()
param["app_lang"] = AppEnvironement.currentLang ?? "en"
param["mobile_id"] = deviceId
return param
}
var encoding: ParameterEncoding {
switch method {
case .get:
return URLEncoding.default
default:
return JSONEncoding.default
}
}
var urlRequest: URLRequest {
var request = URLRequest(url: requestURL)
request.httpMethod = method.rawValue
headers.forEach { request.addValue($1, forHTTPHeaderField: $0) }
return request
}
var deviceId: String {
return UIDevice.current.identifierForVendor?.uuidString ?? ""
}
func asURLRequest() throws -> URLRequest {
return try encoding.encode(urlRequest, with: parameters)
}
}
We simply add headers, main url, and the required elements to create our request, we also add URLRequestConvertibleto the enum conformance in order to do it here once instead of rewriting it in an every enum as well!
Notice: URLRequestConvertible is a simple protocol from Alamofire that you can replace it with your own, simply copy it, or just create a protocol that has one function:
func asURLRequest() throws -> URLRequest
After that you just create a new enum, and add the previous protocol to it and you suddenly have requests ready with every case you add to it!
let’s see UserRequests enum:
enum UserRequest: URLRequestBuilder {
case login(email: String, password: String)
case register(name: String, email: String, password: String, phone: String)
case userInfo
// MARK: - Path
internal var path: ServerPaths {
switch self {
case .login:
return .login
case .register:
return .register
case .userInfo:
return .userInfo
}
}
// MARK: - Parameters
internal var parameters: Parameters? {
var params = defaultParams
switch self {
case .login(let email, let password):
params["email"] = email
params["password"] = password
case let .register(fullname, email, password, phone):
params["email"] = email
params["password"] = password
params["password_confirmation"] = password
params["phone"] = phone
params["name"] = fullname
params["mobile_id"] = deviceId
default: break
}
return params
}
// MARK: - Methods
internal var method: HTTPMethod {
return .post
}
}
The request has the elements that actually changes, the params, the path, and that’s it for most of apis!
ServerPath is an enum with strings for the server paths, it looks like this:
enum ServerPaths: String {
case login
case register
case phoneActivation = "phone_activation"
case resendPhoneActivation = "resend_phone_activation_code"
case resendEmailLink = "send_reset_link_email"
case resetPassword = "reset_password"
case userInfo = "get_account_info"
case updateInfo = "update_account_info"
case userBalance = "get_user_internal_balance"
case search
}
Do you think that was hard? It’s very simple, testable, and easy to create! and you're not entitled to any network layer you decide to use!
To make for example a login request, you’ll just need to write a single line! and there it’s, now you’ve a request to be sent through the network layer!
How to send it?
let request = UserRequest.login(email: “sent email”, password: “password”)
Alamofire.request(request).response ...
So far we created the request, sent it, and all done!
But I’ve another thing, that’s depending on protocols too, that will make you not have to add “import Alamofire” in your network caller class/struct!
Another horror movie is indeed handling the response, which will be a good place to work on too in coming posts.
Next, I’ll explain how to do more with protocols in the network layer, so follow me for the coming posts! ?? but in case you wanna have a sneak peek, check my GitHub repo:
sharing and comments are appreciated, also I’m on twitter @yoloabdo if you wanna talk or discuss anything.
Thanks! and happy coding times!
Lead iOS Engineer | The No.1 Digital Business Builder. We turn your physical assets into digital miracles
6 年Rehan Ali