Clean & simple Swift networking stack
Full network client is written in Swift without any external dependencies. The base code is around 200 LOC.
The idea was to create an extendable and maintainable client that can be used to quickly create a network layer with minimal boilerplate.
It was inspired by Moya, it just uses URLSession where Moya depends on Alamofire
enum Result<T, Error>response handling- dependancy injection
- endpoint modeling with the
Endpointprotocol - JSON parsing
- observable class for the network activity
- easy mocking and testing
Base code for the NetworkStack implementation.
Base types used in the client. Typealias callback with the Result response and the custom errors thrown by the networking stack.
typealiasResultCallback<T>=(Result<T,NetworkStackError>)->VoidenumNetworkStackError:Error{case invalidRequest
case dataMissing
case endpointNotMocked
case mockDataMissing
case responseError(error:Error)case parserError(error:Error)}The WebService class is used for making web requests. It implements the WebServiceProtocol which allows easy dependency injection and testing. The request method takes an Endpoint enum and a ResultCallback. It automatically toggles the network activity indicator using the NetworkActivty service and parses the data response using the Parser service.
protocolWebServiceProtocol{func request<T:Decodable>(_ endpoint:Endpoint, completition:@escapingResultCallback<T>)}classWebService:WebServiceProtocol{privateleturlSession:URLSessionprivateletparser:ParserprivateletnetworkActivity:NetworkActivityProtocolinit(urlSession:URLSession=URLSession(configuration:URLSessionConfiguration.default),
parser:Parser=Parser(),
networkActivity:NetworkActivityProtocol=NetworkActivity()){self.urlSession = urlSession
self.parser = parser
self.networkActivity = networkActivity
}func request<T:Decodable>(_ endpoint:Endpoint, completition:@escapingResultCallback<T>){guardlet request = endpoint.request else{OperationQueue.main.addOperation({completition(.failure(NetworkStackError.invalidRequest))})return}
networkActivity.increment()lettask= urlSession.dataTask(with: request){[unowned self](data, response, error)inself.networkActivity.decrement()iflet error = error {OperationQueue.main.addOperation({completition(.failure(.responseError(error: error)))})return}guardlet data = data else{OperationQueue.main.addOperation({completition(.failure(NetworkStackError.dataMissing))})return}self.parser.json(data: data, completition: completition)}
task.resume()}}The MockWebService implements the same WebServiceProtocol. It skips making the actual web request and returns JSON data directly from a .json file included with the project. It is useful for running tests or returning mocked responses until the backend endpoint is ready.
classMockWebService:WebServiceProtocol{privateletparser:Parserinit(parser:Parser=Parser()){self.parser = parser
}func request<T:Decodable>(_ endpoint:Endpoint, completition:@escapingResultCallback<T>){guardlet endpoint = endpoint as?MockEndpointelse{OperationQueue.main.addOperation({completition(.failure(NetworkStackError.endpointNotMocked))})return}guardlet data = endpoint.mockData()else{OperationQueue.main.addOperation({completition(.failure(NetworkStackError.mockDataMissing))})return}
parser.json(data: data, completition: completition)}}Service that handles the network activity indicator. It implements the observer pattern using closures. An observing class can subscribe to state updates using the observe method and can toggle the network activity indicator.
enumNetworkActivityState{case show
case hide
}protocolNetworkActivityProtocol{func increment()func decrement()func observe(using closure:@escaping(NetworkActivityState)->Void)}classNetworkActivity:NetworkActivityProtocol{privatevarobservations=[(NetworkActivityState)->Void]()privatevaractivityCount:Int=0{
didSet {if(activityCount <0){
activityCount =0}if(oldValue >0 && activityCount >0){return}stateDidChange()}}privatefunc stateDidChange(){letstate= activityCount >0?NetworkActivityState.show :NetworkActivityState.hide
observations.forEach{ closure inOperationQueue.main.addOperation({closure(state)})}}func increment(){self.activityCount +=1}func decrement(){self.activityCount -=1}func observe(using closure:@escaping(NetworkActivityState)->Void){
observations.append(closure)}}Called from the Webservice, parses the Data response and calls the result callback with initialized data structs.
protocolParserProtocol{func json<T:Decodable>(data:Data, completition:@escapingResultCallback<T>)}structParser{letjsonDecoder=JSONDecoder()func json<T:Decodable>(data:Data, completition:@escapingResultCallback<T>){do{letresult:T=try jsonDecoder.decode(T.self, from: data)OperationQueue.main.addOperation{completition(.success(result))}}catchlet error{OperationQueue.main.addOperation{completition(.failure(.parserError(error: error)))}}}}The base protocol that defines the data for a specific endpoint. An enum that implements the Endpoint protocol is passed to the WebService when creating a request.
protocolEndpoint{varrequest:URLRequest?{get}varhttpMethod:String{get}varhttpHeaders:[String:String]?{get}varqueryItems:[URLQueryItem]?{get}varscheme:String{get}varhost:String{get}}The protocol extension defines the request method that is used for creating an URLRequest from the Endpoint enum.
extensionEndpoint{func request(forEndpoint endpoint:String)->URLRequest?{varurlComponents=URLComponents()
urlComponents.scheme = scheme
urlComponents.host = host
urlComponents.path = endpoint
urlComponents.queryItems = queryItems
guardlet url = urlComponents.url else{returnnil}varrequest=URLRequest(url: url)
request.httpMethod = httpMethod
iflet httpHeaders = httpHeaders {for(key, value)in httpHeaders {
request.setValue(value, forHTTPHeaderField: key)}}return request
}}The MockEndpoint protocol inherits the Endpoint protocol and defines the data required for returning mocked responses.
protocolMockEndpoint:Endpoint{varmockFilename:String?{get}varmockExtension:String?{get}}The first extension defines the mockData method that will load the .json file for that endpoint and return it as a Data object.
extensionMockEndpoint{func mockData()->Data?{guardlet mockFileUrl =Bundle.main.url(forResource: mockFilename, withExtension: mockExtension),let mockData =try?Data(contentsOf: mockFileUrl)else{returnnil}return mockData
}}The second extension has the default values for the mockExtension.
extensionMockEndpoint{varmockExtension:String?{return"json"}}An example implementation of a single endpoint for fetching user data with two methods.
To set shared values between all the endpoints extend the base Endpoint enum. In this example, we are setting the scheme and host for all endpoints.
extensionEndpoint{varscheme:String{return"https"}varhost:String{return"jsonplaceholder.typicode.com"}}Create the UserEndpoint for describing the users' endpoint. The enum has one case for each endpoint method. .all fetches all users and get(userId: Int) is used to fetch a user with a specific id.
enumUserEndpoint{case all
case get(userId:Int)}The extension of the UserEndpoint defines the values that will be used when converting the UserEndpoint enum case into a URLRequest. The request property defines the URL, we also define the httpMethod, queryItems and httpHeaders.
extensionUserEndpoint:Endpoint{varrequest:URLRequest?{switchself{case.all:returnrequest(forEndpoint:"/users")case.get(let userId):returnrequest(forEndpoint:"/users/\(userId)")}}varhttpMethod:String{switchself{case.all:return"GET"case.get( _):return"GET"}}varqueryItems:[URLQueryItem]?{switchself{case.all:returnnilcase.get(let userId):return[URLQueryItem(name:"userId", value:String(userId))]}}varhttpHeaders:[String:String]?{letheaders:[String:String]=["headerField":"headerValue"]switchself{case.all,.get( _):return headers
}}}Create a User struct that represents the model that will be created by the Parser service. It needs to conform to the Codable protocol.
structUser:Codable{letid:Intletusername:Stringletemail:String}Create a WebService object, call its request method and pass it an Endpoint enum. Its also needed to specify the type of the result callback so that the Parser service knows how to create the model structs.
letwebService=WebService()
webService.request(UserEndpoint.all){(result:Result<[User],NetworkStackError>)inswitch result {case.failure(let error):dump(error)case.success(let users):dump(users)}}
webService.request(UserEndpoint.get(userId:10)){(result:Result<User,NetworkStackError>)inswitch result {case.failure(let error):dump(error)case.success(let users):dump(users)}}Use the observe method on the NetworkActivity service to subscribe to network activity changes and toggle the network activity indicator
letnetworkActivity=NetworkActivity()letwebService=WebService(networkActivity: networkActivity)
networkActivity.observe{ state inswitch state {case.show:print("Network activity indicator: SHOW")case.hide:print("Network activity indicator: HIDE")}}Create two .json files with the responses we want to return and add them to the project. Also, extend the UserEndpoint with the MockEndpoint protocol and set the filenames for the JSON response files.
extensionUserEndpoint:MockEndpoint{varmockFilename:String?{switchself{case.all:return"users"case.get( _):return"user"}}}Create a MockWebService instance and call the request method exactly the same way as for a normal WebService.
letmockWebService=MockWebService()
mockWebService.request(UserEndpoint.get(userId:10)){(result:Result<User,NetworkStackError>)inswitch result {case.failure(let error):dump(error)case.success(let users):dump(users)}}
mockWebService.request(UserEndpoint.all){(result:Result<[User],NetworkStackError>)inswitch result {case.failure(let error):dump(error)case.success(let users):dump(users)}}