iOS Router pattern example
1. Why do we need Router pattern?
If we use Router pattern, we can make network layer abstract.
So, We can avoid boilerplater code to make network request.
2. Example code
1)Http Router
protocol HttpRouter: URLRequestConvertible {
var baseUrlString: String { get }
var path: String { get }
var method: HTTPMethod { get }
var headers: HTTPHeaders? { get }
var parameters: Parameters? { get }
func body() throws -> Data?
func request(usingHttpService service: HttpService) throws -> DataRequest
}
extension HttpRouter {
var parameter: Parameters? { return nil }
func body() throws -> Data? { return nil }
func asURLRequest() throws -> URLRequest {
var url = try baseUrlString.asURL()
url.appendPathComponent(path)
var request = try URLRequest(url: url, method: method, headers: headers)
request.httpBody = try body()
return request
}
func request(usingHttpService service: HttpService) throws -> DataRequest {
return try service.request(asURLRequest())
}
}
2) AuthHttpRouter
import Foundation
import Alamofire
enum AuthHttpRouter {
case login(AuthModel)
case signUp(AuthModel)
case validate(token: String)
case refreshToken(token: String)
case logout(token: String)
case validateEmail(email: String)
}
extension AuthHttpRouter: HttpRouter {
var baseUrlString: String {
return "https://letscodeeasy.com/groceryapi/public/api"
}
var path: String {
switch(self) {
case .login:
return "login"
case .signUp:
return "register"
case .validate:
return "user"
case .refreshToken:
return "token/refresh"
case .logout:
return "logout"
case .validateEmail:
return "validate/email"
}
}
var method: HTTPMethod {
switch (self) {
case .login, .signUp, .refreshToken, .logout, .validateEmail:
return .post
case .validate:
return .get
}
}
var headers: HTTPHeaders? {
switch(self) {
case .login, .signUp, .validateEmail:
return ["Content-Type": "application/json; charset=UTF-8"]
case .validate(let token), .refreshToken(let token), .logout(let token):
return [
"Authorization" : "Bearer \(token)",
"Content-Type" : "application/json; charset=UTF-8"
]
}
}
var parameters: Parameters? {
return nil
}
func body() throws -> Data? {
switch self {
case .signUp(let user):
return try JSONEncoder().encode(user)
case .validateEmail(let email):
return try JSONEncoder().encode(["email" : email])
default:
return nil
}
}
}
AuthHttpRouter class implement HttpRouter protocol.
So, We can pick up the path, method, body parameter when we make a network request.
3) HttpService
protocol HttpService {
var sessionManager: Session { get set }
func request(_ urlRequest: URLRequestConvertible) -> DataRequest
}
4) AuthHttpService
import Alamofire
final class AuthHttpService : HttpService {
var sessionManager: Session = Session.default
func request(_ urlRequest: URLRequestConvertible) -> DataRequest {
return sessionManager.request(urlRequest).validate(statusCode: 200..<400)
}
}
AuthHttpService implements HttpService protocol.
We make a network request using AuthHttpService class.
The detailed request content is exposed on lower level of network layer, AuthAPI.
5) AuthAPI
import Foundation
import Combine
protocol AuthAPI {
func checkEmail(email: String) -> Future<Bool, Never>
func signUp(username: String, email: String, password: String) -> Future<(statusCode: Int, data: Data), Error>
}
6)AuthService
import Foundation
import Combine
enum SignUpError : Error {
case emailExists
case invalidData
case invalidJSON
case error(error: String)
}
enum AuthResult<T> {
case success(value: T)
case failure(message: String)
}
class AuthService {
lazy var httpService = AuthHttpService()
static let shared: AuthService = AuthService()
private init() {}
}
extension AuthService: AuthAPI {
func signUp(username: String, email: String, password: String) -> Future<(statusCode: Int, data: Data), Error> {
return Future<(statusCode: Int, data: Data), Error> { [httpService] promise in
do {
try AuthHttpRouter
.signUp(AuthModel(name: username, email: email, password: password))
.request(usingHttpService: httpService)
.responseJSON { response in
guard
let statusCode = response.response?.statusCode,
let data = response.data,
statusCode == 200 else {
promise(.failure(SignUpError.invalidData))
return
}
promise(.success((statusCode: statusCode, data: data)))
}
} catch {
print("Something went wrong : \(error)")
promise(.failure(SignUpError.invalidData))
}
}
}
func checkEmail(email: String) -> Future<Bool, Never> {
return Future<Bool, Never> { [httpService] promise in
do {
try AuthHttpRouter
.validateEmail(email: email)
.request(usingHttpService: httpService)
.responseJSON { response in
guard response.response?.statusCode == 200 else {
print("checkEmail fail")
promise(.success(false))
return
}
promise(.success(true))
print("checkEmail success")
}
} catch {
promise(.success(false))
print("checkEmail error")
}
}
}
}
AuthService class implement AuthAPI protocol.
We start to make a network request on this layer.
If you use MVVM pattern, you make a network request using AuthService on ViewModel.