{"id":13799967,"url":"https://github.com/AndrejKolar/NetworkStack","last_synced_at":"2025-05-13T08:32:31.355Z","repository":{"id":139655664,"uuid":"72936882","full_name":"AndrejKolar/NetworkStack","owner":"AndrejKolar","description":"Clean \u0026 simple Swift networking stack playground","archived":false,"fork":false,"pushed_at":"2020-07-24T06:53:09.000Z","size":69,"stargazers_count":35,"open_issues_count":0,"forks_count":7,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-04-22T12:31:09.924Z","etag":null,"topics":["decodable","networking","no-dependencies","swift5","urlsession"],"latest_commit_sha":null,"homepage":null,"language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/AndrejKolar.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2016-11-05T16:00:43.000Z","updated_at":"2023-05-26T17:42:17.000Z","dependencies_parsed_at":"2023-07-23T08:45:08.816Z","dependency_job_id":null,"html_url":"https://github.com/AndrejKolar/NetworkStack","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AndrejKolar%2FNetworkStack","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AndrejKolar%2FNetworkStack/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AndrejKolar%2FNetworkStack/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AndrejKolar%2FNetworkStack/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/AndrejKolar","download_url":"https://codeload.github.com/AndrejKolar/NetworkStack/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253903928,"owners_count":21981772,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["decodable","networking","no-dependencies","swift5","urlsession"],"created_at":"2024-08-04T00:01:07.724Z","updated_at":"2025-05-13T08:32:31.094Z","avatar_url":"https://github.com/AndrejKolar.png","language":"Swift","funding_links":[],"categories":["Learning Swift: Advanced Topics"],"sub_categories":[],"readme":"# NetworkStack [![Language](https://img.shields.io/badge/swift-5.1-brightgreen.svg)](http://swift.org)\n\nClean \u0026amp; simple Swift networking stack\n\n## About\n\nFull network client is written in Swift without any external dependencies. The base code is around 200 LOC.\nThe idea was to create an extendable and maintainable client that can be used to quickly create a network layer with minimal boilerplate.\nIt was inspired by [Moya](https://github.com/Moya/Moya), it just uses `URLSession` where `Moya` depends on `Alamofire`\n\n## Features\n\n- `enum Result\u003cT, Error\u003e` response handling\n- dependancy injection\n- endpoint modeling with the `Endpoint` protocol\n- JSON parsing\n- observable class for the network activity\n- easy mocking and testing\n\n## Base code\n\nBase code for the `NetworkStack` implementation.\n\n### Types\n\nBase types used in the client. Typealias callback with the `Result` response and the custom errors thrown by the networking stack.\n\n```swift\n\ntypealias ResultCallback\u003cT\u003e = (Result\u003cT, NetworkStackError\u003e) -\u003e Void\n\nenum NetworkStackError: Error {\n    case invalidRequest\n    case dataMissing\n    case endpointNotMocked\n    case mockDataMissing\n    case responseError(error: Error)\n    case parserError(error: Error)\n}\n```\n\n### WebService\n\nThe `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.\n\n```swift\nprotocol WebServiceProtocol {\n    func request\u003cT: Decodable\u003e(_ endpoint: Endpoint, completition: @escaping ResultCallback\u003cT\u003e)\n}\n\nclass WebService: WebServiceProtocol {\n    private let urlSession: URLSession\n    private let parser: Parser\n    private let networkActivity: NetworkActivityProtocol\n\n    init(urlSession: URLSession = URLSession(configuration: URLSessionConfiguration.default),\n         parser: Parser = Parser(),\n         networkActivity: NetworkActivityProtocol = NetworkActivity()) {\n        self.urlSession = urlSession\n        self.parser = parser\n        self.networkActivity = networkActivity\n    }\n\n    func request\u003cT: Decodable\u003e(_ endpoint: Endpoint, completition: @escaping ResultCallback\u003cT\u003e) {\n\n        guard let request = endpoint.request else {\n            OperationQueue.main.addOperation({ completition(.failure(NetworkStackError.invalidRequest)) })\n            return\n        }\n\n        networkActivity.increment()\n\n        let task = urlSession.dataTask(with: request) { [unowned self] (data, response, error) in\n\n            self.networkActivity.decrement()\n\n            if let error = error {\n                OperationQueue.main.addOperation({ completition(.failure(.responseError(error: error))) })\n                return\n            }\n\n            guard let data = data else {\n                OperationQueue.main.addOperation({ completition(.failure(NetworkStackError.dataMissing)) })\n                return\n            }\n\n            self.parser.json(data: data, completition: completition)\n        }\n\n        task.resume()\n    }\n}\n```\n\n### MockWebService\n\nThe `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.\n\n```swift\nclass MockWebService: WebServiceProtocol {\n    private let parser: Parser\n\n    init(parser: Parser = Parser()) {\n        self.parser = parser\n    }\n\n    func request\u003cT: Decodable\u003e(_ endpoint: Endpoint, completition: @escaping ResultCallback\u003cT\u003e) {\n\n        guard let endpoint = endpoint as? MockEndpoint else {\n            OperationQueue.main.addOperation({ completition(.failure(NetworkStackError.endpointNotMocked)) })\n            return\n        }\n\n        guard let data = endpoint.mockData() else {\n            OperationQueue.main.addOperation({ completition(.failure(NetworkStackError.mockDataMissing)) })\n            return\n        }\n\n        parser.json(data: data, completition: completition)\n    }\n}\n```\n\n### Network Activity\n\nService 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.\n\n```swift\nenum NetworkActivityState {\n    case show\n    case hide\n}\n\nprotocol NetworkActivityProtocol {\n    func increment()\n    func decrement()\n    func observe(using closure: @escaping (NetworkActivityState) -\u003e Void)\n}\n\nclass NetworkActivity: NetworkActivityProtocol {\n    private var observations = [(NetworkActivityState) -\u003e Void]()\n\n    private var activityCount: Int = 0 {\n        didSet {\n\n            if (activityCount \u003c 0) {\n                activityCount = 0\n            }\n\n            if (oldValue \u003e 0 \u0026\u0026 activityCount \u003e 0) {\n                return\n            }\n\n            stateDidChange()\n        }\n    }\n\n    private func stateDidChange() {\n\n        let state = activityCount \u003e 0 ? NetworkActivityState.show : NetworkActivityState.hide\n        observations.forEach { closure in\n             OperationQueue.main.addOperation({ closure(state) })\n        }\n    }\n\n    func increment() {\n        self.activityCount += 1\n    }\n\n    func decrement() {\n        self.activityCount -= 1\n    }\n\n    func observe(using closure: @escaping (NetworkActivityState) -\u003e Void) {\n        observations.append(closure)\n    }\n}\n```\n\n### Parser\n\nCalled from the `Webservice`, parses the `Data` response and calls the result callback with initialized data structs.\n\n```swift\nprotocol ParserProtocol {\n    func json\u003cT: Decodable\u003e(data: Data, completition: @escaping ResultCallback\u003cT\u003e)\n}\n\nstruct Parser {\n    let jsonDecoder = JSONDecoder()\n\n    func json\u003cT: Decodable\u003e(data: Data, completition: @escaping ResultCallback\u003cT\u003e) {\n        do {\n            let result: T = try jsonDecoder.decode(T.self, from: data)\n            OperationQueue.main.addOperation { completition(.success(result)) }\n\n        } catch let error {\n            OperationQueue.main.addOperation { completition(.failure(.parserError(error: error))) }\n        }\n    }\n}\n```\n\n### Endpoint\n\nThe 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.\n\n```swift\nprotocol Endpoint {\n    var request: URLRequest? { get }\n    var httpMethod: String { get }\n    var httpHeaders: [String : String]? { get }\n    var queryItems: [URLQueryItem]? { get }\n    var scheme: String { get }\n    var host: String { get }\n}\n\n```\n\nThe protocol extension defines the request method that is used for creating an `URLRequest` from the `Endpoint` enum.\n\n```swift\nextension Endpoint {\n    func request(forEndpoint endpoint: String) -\u003e URLRequest? {\n\n        var urlComponents = URLComponents()\n        urlComponents.scheme = scheme\n        urlComponents.host = host\n        urlComponents.path = endpoint\n        urlComponents.queryItems = queryItems\n        guard let url = urlComponents.url else { return nil }\n        var request = URLRequest(url: url)\n        request.httpMethod = httpMethod\n\n        if let httpHeaders = httpHeaders {\n            for (key, value) in httpHeaders {\n                request.setValue(value, forHTTPHeaderField: key)\n            }\n        }\n\n        return request\n    }\n}\n```\n\nThe `MockEndpoint` protocol inherits the Endpoint protocol and defines the data required for returning mocked responses.\n\n```swift\nprotocol MockEndpoint: Endpoint {\n    var mockFilename: String? { get }\n    var mockExtension: String? { get }\n}\n```\n\nThe first extension defines the `mockData` method that will load the `.json` file for that endpoint and return it as a `Data` object.\n\n```swift\nextension MockEndpoint {\n    func mockData() -\u003e Data? {\n        guard let mockFileUrl = Bundle.main.url(forResource: mockFilename, withExtension: mockExtension),\n            let mockData = try? Data(contentsOf: mockFileUrl) else {\n                return nil\n        }\n        return mockData\n    }\n}\n\n```\n\nThe second extension has the default values for the `mockExtension`.\n\n```swift\nextension MockEndpoint {\n    var mockExtension: String? {\n        return \"json\"\n    }\n}\n```\n\n## Example\n\nAn example implementation of a single endpoint for fetching user data with two methods.\n\n### Shared values\n\nTo 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.\n\n```swift\nextension Endpoint {\n    var scheme: String {\n        return \"https\"\n    }\n\n    var host: String {\n        return \"jsonplaceholder.typicode.com\"\n    }\n}\n```\n\n### UserEndpoint\n\nCreate 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.\n\n```swift\nenum UserEndpoint {\n    case all\n    case get(userId: Int)\n}\n```\n\nThe 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`.\n\n```swift\nextension UserEndpoint: Endpoint {\n\n    var request: URLRequest? {\n        switch self {\n        case .all:\n            return request(forEndpoint: \"/users\")\n        case .get(let userId):\n            return request(forEndpoint: \"/users/\\(userId)\")\n        }\n    }\n\n    var httpMethod: String {\n        switch self {\n        case .all:\n            return \"GET\"\n        case .get( _):\n            return \"GET\"\n        }\n    }\n\n    var queryItems: [URLQueryItem]? {\n        switch self {\n        case .all:\n            return nil\n        case .get(let userId):\n            return [URLQueryItem(name: \"userId\", value: String(userId))]\n        }\n    }\n\n    var httpHeaders: [String: String]? {\n        let headers: [String: String] = [\"headerField\" : \"headerValue\"]\n        switch self {\n        case .all, .get( _):\n            return headers\n        }\n    }\n}\n```\n\n### User\n\nCreate a User struct that represents the model that will be created by the `Parser` service. It needs to conform to the Codable protocol.\n\n```swift\nstruct User: Codable {\n    let id: Int\n    let username: String\n    let email: String\n}\n```\n\n### Use\n\nCreate 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.\n\n```swift\nlet webService = WebService()\n\nwebService.request(UserEndpoint.all) { (result: Result\u003c[User], NetworkStackError\u003e) in\n    switch result {\n    case .failure(let error):\n        dump(error)\n    case .success(let users):\n        dump(users)\n    }\n}\n\nwebService.request(UserEndpoint.get(userId: 10)) { (result: Result\u003cUser, NetworkStackError\u003e) in\n    switch result {\n    case .failure(let error):\n        dump(error)\n    case .success(let users):\n        dump(users)\n    }\n}\n```\n\n### Network activity\n\nUse the `observe` method on the `NetworkActivity` service to subscribe to network activity changes and toggle the network activity indicator\n\n```swift\nlet networkActivity = NetworkActivity()\nlet webService = WebService(networkActivity: networkActivity)\n\nnetworkActivity.observe { state in\n    switch state {\n    case .show:\n        print(\"Network activity indicator: SHOW\")\n    case .hide:\n        print(\"Network activity indicator: HIDE\")\n    }\n}\n```\n\n## Mocking\n\n### Setup\n\nCreate 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.\n\n```swift\nextension UserEndpoint: MockEndpoint {\n    var mockFilename: String? {\n        switch self {\n        case .all:\n            return \"users\"\n        case .get( _):\n            return \"user\"\n        }\n    }\n}\n```\n\n### Use\n\nCreate a `MockWebService` instance and call the request method exactly the same way as for a normal `WebService`.\n\n```swift\nlet mockWebService = MockWebService()\n\nmockWebService.request(UserEndpoint.get(userId: 10)) { (result: Result\u003cUser, NetworkStackError\u003e) in\n    switch result {\n    case .failure(let error):\n        dump(error)\n    case .success(let users):\n        dump(users)\n    }\n}\n\nmockWebService.request(UserEndpoint.all) { (result: Result\u003c[User], NetworkStackError\u003e) in\n    switch result {\n    case .failure(let error):\n        dump(error)\n    case .success(let users):\n        dump(users)\n    }\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FAndrejKolar%2FNetworkStack","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FAndrejKolar%2FNetworkStack","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FAndrejKolar%2FNetworkStack/lists"}