├── Example ├── CleanNetworkExample │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Info.plist │ ├── Constants │ │ └── Closures.swift │ ├── Entity │ │ └── Models │ │ │ ├── APIError.swift │ │ │ ├── Post.swift │ │ │ └── CreatePostRequestBody.swift │ ├── ViewControllers │ │ ├── MainViewController.swift │ │ ├── CreatePostViewController.swift │ │ └── PostsViewController.swift │ ├── Networking │ │ └── Requests │ │ │ ├── PostsRequest.swift │ │ │ └── CreatePostRequest.swift │ ├── Extensions │ │ └── UITableView+Loading.swift │ ├── Cells │ │ └── PostCell │ │ │ ├── PostTableViewCell.swift │ │ │ └── PostTableViewCell.xib │ ├── AppDelegate.swift │ ├── Protocols │ │ └── AlertingController.swift │ └── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard └── CleanNetworkExample.xcodeproj │ ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved │ └── project.pbxproj ├── .gitignore ├── Tests └── CleanNetworkTests │ ├── Mock │ ├── UserMockModel.swift │ ├── UsersMockRequest.swift │ ├── MockRequest.swift │ ├── MockBodyRequest.swift │ └── MockURLSessionProtocol.swift │ ├── CLNetworkRequestTests.swift │ ├── CLNetworkServiceTests.swift │ └── Supporting Files │ └── UsersMock.json ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Sources └── CleanNetwork │ ├── Core │ ├── CLHTTPMethod.swift │ ├── CLError.swift │ ├── CLNetworkConfig.swift │ ├── CLEndpoint.swift │ ├── CLNetworkService.swift │ └── CLNetworkLogger.swift │ └── Protocols │ ├── CLNetworkDecodableRequest.swift │ ├── NetworkService.swift │ ├── CLNetworkBodyRequest.swift │ └── CLNetworkRequest.swift ├── .github └── workflows │ └── swift.yml ├── Package.swift ├── LICENSE └── README.md /Example/CleanNetworkExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /Example/CleanNetworkExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Example/CleanNetworkExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/CleanNetworkExample/Constants/Closures.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Closures.swift 3 | // CleanNetworkExample 4 | // 5 | // Created by Alperen Ünal on 11.06.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | typealias VoidClosure = () -> () 11 | -------------------------------------------------------------------------------- /Example/CleanNetworkExample/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Example/CleanNetworkExample/Entity/Models/APIError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIError.swift 3 | // CleanNetworkExample 4 | // 5 | // Created by Alperen Ünal on 26.06.2022. 6 | // 7 | 8 | struct APIError: Decodable, Error { 9 | let errorMessage: String 10 | } 11 | -------------------------------------------------------------------------------- /Example/CleanNetworkExample/Entity/Models/Post.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Post.swift 3 | // CleanNetworkExample 4 | // 5 | // Created by Alperen Ünal on 11.06.2022. 6 | // 7 | 8 | struct Post: Decodable, Identifiable { 9 | let id: Int 10 | let title: String 11 | let body: String 12 | } 13 | -------------------------------------------------------------------------------- /Tests/CleanNetworkTests/Mock/UserMockModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserMockModel.swift 3 | // 4 | // 5 | // Created by Alperen Ünal on 2.10.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | struct UserMockModel: Codable, Equatable { 11 | let id: Int 12 | let name: String 13 | let username: String 14 | } 15 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/CleanNetworkExample/Entity/Models/CreatePostRequestBody.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreatePostRequestBody.swift 3 | // CleanNetworkExample 4 | // 5 | // Created by Alperen Ünal on 3.07.2022. 6 | // 7 | 8 | struct CreatePostRequestBody: Encodable { 9 | let userId: Int 10 | let title: String 11 | let body: String 12 | } 13 | -------------------------------------------------------------------------------- /Example/CleanNetworkExample.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/CleanNetworkExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/CleanNetworkExample/ViewControllers/MainViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainViewController.swift 3 | // CleanNetworkExample 4 | // 5 | // Created by Alperen Ünal on 15.05.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | final class MainViewController: UIViewController { 11 | override func viewDidLoad() { 12 | super.viewDidLoad() 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /Sources/CleanNetwork/Core/CLHTTPMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CLHTTPMethod.swift 3 | // 4 | // 5 | // Created by Alperen Ünal on 28.05.2022. 6 | // 7 | 8 | public enum CLHTTPMethod { 9 | case get 10 | case post 11 | case put 12 | case delete 13 | case patch 14 | 15 | public var rawValue: String { 16 | String(describing: self).uppercased() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Example/CleanNetworkExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "cleannetwork", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/alperen23230/CleanNetwork", 7 | "state" : { 8 | "branch" : "main", 9 | "revision" : "e449c4a80af1d354555d57e7ee678a9e5874ef73" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /Example/CleanNetworkExample/Networking/Requests/PostsRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostsRequest.swift 3 | // CleanNetworkExample 4 | // 5 | // Created by Alperen Ünal on 11.06.2022. 6 | // 7 | 8 | import CleanNetwork 9 | 10 | struct PostsRequest: CLNetworkDecodableRequest { 11 | typealias ResponseType = [Post] 12 | 13 | let endpoint: CLEndpoint = CLEndpoint(path: "posts") 14 | let method: CLHTTPMethod = .get 15 | 16 | init() {} 17 | } 18 | -------------------------------------------------------------------------------- /Tests/CleanNetworkTests/Mock/UsersMockRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UsersMockRequest.swift 3 | // 4 | // 5 | // Created by Alperen Ünal on 2.10.2022. 6 | // 7 | 8 | import CleanNetwork 9 | 10 | struct UsersMockRequest: CLNetworkDecodableRequest { 11 | typealias ResponseType = [UserMockModel] 12 | 13 | let endpoint: CLEndpoint 14 | let method: CLHTTPMethod = .get 15 | 16 | init(endpoint: CLEndpoint) { 17 | self.endpoint = endpoint 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | pull_request: 8 | branches: [ "main", "development" ] 9 | 10 | jobs: 11 | build: 12 | runs-on: macos-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: swift-actions/setup-swift@v2 16 | with: 17 | swift-version: "5.10.0" 18 | - name: Build 19 | run: swift build -v 20 | - name: Run tests 21 | run: swift test -v 22 | -------------------------------------------------------------------------------- /Sources/CleanNetwork/Core/CLError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CLError.swift 3 | // 4 | // 5 | // Created by Alperen Ünal on 29.05.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum CLError: Error { 11 | case errorMessage(CLErrorMessage) 12 | /// APIError response data, HTTP status code 13 | case apiError(Data, Int?) 14 | } 15 | 16 | public enum CLErrorMessage: String { 17 | case dataIsNil = "Error: Data is nil" 18 | case statusCodeIsNotValid = "Error: Status code is not valid" 19 | } 20 | -------------------------------------------------------------------------------- /Sources/CleanNetwork/Protocols/CLNetworkDecodableRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CLNetworkDecodableRequest.swift 3 | // 4 | // 5 | // Created by Alperen Ünal on 28.05.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol CLNetworkDecodableRequest: CLNetworkRequest { 11 | associatedtype ResponseType: Decodable 12 | 13 | var endpoint: CLEndpoint { get } 14 | var method: CLHTTPMethod { get } 15 | var headers: [String: String] { get } 16 | 17 | func build(with sharedHeaders: [String: String]) -> URLRequest 18 | } 19 | -------------------------------------------------------------------------------- /Example/CleanNetworkExample/Networking/Requests/CreatePostRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreatePostRequest.swift 3 | // CleanNetworkExample 4 | // 5 | // Created by Alperen Ünal on 3.07.2022. 6 | // 7 | 8 | import CleanNetwork 9 | 10 | struct CreatePostRequest: CLNetworkBodyRequest { 11 | typealias ResponseType = Post 12 | 13 | let endpoint: CLEndpoint = CLEndpoint(path: "posts") 14 | let requestBody: CreatePostRequestBody 15 | 16 | init(requestBody: CreatePostRequestBody) { 17 | self.requestBody = requestBody 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/CleanNetworkTests/Mock/MockRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockRequest.swift 3 | // 4 | // 5 | // Created by Alperen Ünal on 29.05.2022. 6 | // 7 | 8 | import CleanNetwork 9 | 10 | struct MockAPIResponse: Decodable {} 11 | 12 | struct MockRequest: CLNetworkDecodableRequest { 13 | typealias ResponseType = MockAPIResponse 14 | 15 | let endpoint: CLEndpoint 16 | let method: CLHTTPMethod 17 | 18 | init(endpoint: CLEndpoint, method: CLHTTPMethod) { 19 | self.endpoint = endpoint 20 | self.method = method 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/CleanNetwork/Protocols/NetworkService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkService.swift 3 | // 4 | // 5 | // Created by Alperen Ünal on 28.05.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol NetworkService { 11 | var config: NetworkConfig { get set } 12 | 13 | func fetch(_ request: T) async throws -> T.ResponseType 14 | func fetch(_ request: T) async throws -> T.ResponseType 15 | func fetch(_ request: T) async throws -> Data 16 | func fetch(urlRequest: URLRequest) async throws -> T 17 | } 18 | -------------------------------------------------------------------------------- /Example/CleanNetworkExample/Extensions/UITableView+Loading.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITableView+Loading.swift 3 | // CleanNetworkExample 4 | // 5 | // Created by Alperen Ünal on 11.06.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UITableView { 11 | func startLoading() { 12 | let activityIndicatorView = UIActivityIndicatorView(style: .medium) 13 | backgroundView = activityIndicatorView 14 | activityIndicatorView.startAnimating() 15 | separatorStyle = .none 16 | } 17 | 18 | func stopLoading() { 19 | backgroundView = nil 20 | separatorStyle = .singleLine 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Example/CleanNetworkExample/Cells/PostCell/PostTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostTableViewCell.swift 3 | // CleanNetworkExample 4 | // 5 | // Created by Alperen Ünal on 11.06.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | class PostTableViewCell: UITableViewCell { 11 | 12 | static let reuseIdentifier = "PostTableViewCell" 13 | 14 | @IBOutlet private weak var titleLabel: UILabel! 15 | 16 | override func prepareForReuse() { 17 | super.prepareForReuse() 18 | titleLabel.text = nil 19 | } 20 | 21 | func setCell(with post: Post) { 22 | titleLabel.text = post.title 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Tests/CleanNetworkTests/Mock/MockBodyRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockBodyRequest.swift 3 | // 4 | // 5 | // Created by Alperen Ünal on 3.07.2022. 6 | // 7 | 8 | import CleanNetwork 9 | 10 | struct MockRequestBodyModel: Encodable, Equatable {} 11 | struct MockBodyRequestAPIResponse: Decodable {} 12 | 13 | struct MockBodyRequest: CLNetworkBodyRequest { 14 | typealias ResponseType = MockBodyRequestAPIResponse 15 | 16 | let endpoint: CLEndpoint 17 | let method: CLHTTPMethod 18 | var requestBody: MockRequestBodyModel 19 | 20 | init(endpoint: CLEndpoint, method: CLHTTPMethod, requestBody: MockRequestBodyModel) { 21 | self.endpoint = endpoint 22 | self.method = method 23 | self.requestBody = requestBody 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Example/CleanNetworkExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CleanNetworkExample 4 | // 5 | // Created by Alperen Ünal on 15.05.2022. 6 | // 7 | 8 | import UIKit 9 | import CleanNetwork 10 | 11 | @main 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Set a base URL for CLNetworkEndpoint 18 | CLURLComponent.baseURL = "jsonplaceholder.typicode.com" 19 | CLNetworkService.shared.config.sharedHeaders = [ 20 | "Content-Type": "application/json; charset=utf-8" 21 | ] 22 | return true 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /Sources/CleanNetwork/Core/CLNetworkConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CLNetworkConfig.swift 3 | // 4 | // 5 | // Created by Alperen Ünal on 29.05.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol NetworkConfig { 11 | var decoder: JSONDecoder { get set } 12 | var encoder: JSONEncoder { get set } 13 | var urlSession: URLSession { get set } 14 | var loggerEnabled: Bool { get set } 15 | var sharedHeaders: [String: String] { get set } 16 | } 17 | 18 | public class CLNetworkConfig: NetworkConfig { 19 | public var decoder = JSONDecoder() 20 | public var encoder = JSONEncoder() 21 | public var urlSession = URLSession.shared 22 | public var loggerEnabled = true { 23 | didSet { 24 | CLNetworkLogger.loggerEnabled = loggerEnabled 25 | } 26 | } 27 | public var sharedHeaders: [String: String] = [:] 28 | 29 | public init() {} 30 | } 31 | -------------------------------------------------------------------------------- /Example/CleanNetworkExample/Protocols/AlertingController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlertingController.swift 3 | // CleanNetworkExample 4 | // 5 | // Created by Alperen Ünal on 11.06.2022. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | protocol AlertingController { 12 | func showSimpleAlert(title: String?, message: String?, actionTitle: String?, actionHandler: VoidClosure?) 13 | } 14 | 15 | extension AlertingController where Self: UIViewController { 16 | func showSimpleAlert(title: String?, message: String?, actionTitle: String? = "OK", actionHandler: VoidClosure? = nil) { 17 | let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) 18 | let action = UIAlertAction(title: actionTitle, style: .default) { _ in 19 | actionHandler?() 20 | } 21 | alert.addAction(action) 22 | present(alert, animated: true) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.10 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "CleanNetwork", 8 | platforms: [ 9 | .iOS(.v15), 10 | .macOS(.v12), 11 | .tvOS(.v15), 12 | .watchOS(.v8) 13 | ], 14 | products: [ 15 | .library( 16 | name: "CleanNetwork", 17 | targets: ["CleanNetwork"]), 18 | ], 19 | targets: [ 20 | .target( 21 | name: "CleanNetwork", 22 | dependencies: [], 23 | path: "Sources" 24 | ), 25 | .testTarget( 26 | name: "CleanNetworkTests", 27 | dependencies: ["CleanNetwork"], 28 | resources: [ 29 | .process("Supporting Files") 30 | ] 31 | ) 32 | ] 33 | ) 34 | -------------------------------------------------------------------------------- /Sources/CleanNetwork/Protocols/CLNetworkBodyRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CLNetworkBodyRequest.swift 3 | // 4 | // 5 | // Created by Alperen Ünal on 3.07.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol CLNetworkBodyRequest: CLNetworkDecodableRequest { 11 | associatedtype RequestBodyType: Encodable 12 | 13 | var requestBody: RequestBodyType { get } 14 | 15 | func build(encoder: JSONEncoder, with sharedHeaders: [String: String]) throws -> URLRequest 16 | } 17 | 18 | public extension CLNetworkBodyRequest { 19 | var method: CLHTTPMethod { .post } 20 | } 21 | 22 | public extension CLNetworkBodyRequest { 23 | func build(encoder: JSONEncoder, with sharedHeaders: [String: String]) throws -> URLRequest { 24 | var urlRequest = build(with: sharedHeaders) 25 | urlRequest.httpBody = try encoder.encode(requestBody) 26 | 27 | return urlRequest 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/CleanNetwork/Protocols/CLNetworkRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CLNetworkRequest.swift 3 | // 4 | // 5 | // Created by Alperen Ünal on 7.08.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol CLNetworkRequest { 11 | var endpoint: CLEndpoint { get } 12 | var method: CLHTTPMethod { get } 13 | var headers: [String: String] { get } 14 | 15 | func build(with sharedHeaders: [String: String]) -> URLRequest 16 | } 17 | 18 | public extension CLNetworkRequest { 19 | var method: CLHTTPMethod { .get } 20 | var headers: [String: String] { .init() } 21 | } 22 | 23 | public extension CLNetworkDecodableRequest { 24 | func build(with sharedHeaders: [String: String]) -> URLRequest { 25 | var urlRequest = URLRequest(url: endpoint.url) 26 | let allHeaders = sharedHeaders.merging(headers) { (_, new) in new } 27 | urlRequest.allHTTPHeaderFields = allHeaders 28 | urlRequest.httpMethod = method.rawValue 29 | 30 | return urlRequest 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Alperen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Tests/CleanNetworkTests/Mock/MockURLSessionProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockURLSessionProtocol.swift 3 | // 4 | // 5 | // Created by Alperen Ünal on 2.10.2022. 6 | // 7 | 8 | import Foundation 9 | import XCTest 10 | 11 | class MockURLSessionProtocol: URLProtocol { 12 | static var loadingHandler: (() throws -> (HTTPURLResponse, Data?))? 13 | 14 | override class func canInit(with request: URLRequest) -> Bool { 15 | return true 16 | } 17 | 18 | override class func canonicalRequest(for request: URLRequest) -> URLRequest { 19 | return request 20 | } 21 | 22 | override func startLoading() { 23 | guard let handler = MockURLSessionProtocol.loadingHandler 24 | else { 25 | XCTFail("Loading handler should be set.") 26 | return 27 | } 28 | 29 | do { 30 | let (response, data) = try handler() 31 | client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) 32 | if let data { 33 | client?.urlProtocol(self, didLoad: data) 34 | } 35 | } catch { 36 | client?.urlProtocol(self, didFailWithError: error) 37 | } 38 | 39 | client?.urlProtocolDidFinishLoading(self) 40 | } 41 | 42 | override func stopLoading() {} 43 | } 44 | -------------------------------------------------------------------------------- /Sources/CleanNetwork/Core/CLEndpoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CLEndpoint.swift 3 | // 4 | // 5 | // Created by Alperen Ünal on 28.05.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum CLURLComponent { 11 | public static var baseURL = "" 12 | public static var urlScheme = "https" 13 | } 14 | 15 | public struct CLEndpoint { 16 | public var baseURL: String 17 | public var path: String 18 | public var queryItems: [URLQueryItem] 19 | 20 | public init(baseURL: String = CLURLComponent.baseURL, 21 | path: String, 22 | queryItems: [URLQueryItem] = []) { 23 | self.baseURL = baseURL 24 | self.path = path 25 | self.queryItems = queryItems 26 | } 27 | } 28 | 29 | // MARK: - URL 30 | public extension CLEndpoint { 31 | var url: URL { 32 | var components = URLComponents() 33 | components.scheme = CLURLComponent.urlScheme 34 | components.host = baseURL 35 | components.path = "/" + path 36 | components.queryItems = queryItems 37 | 38 | guard let url = components.url else { 39 | preconditionFailure("Invalid URL components: \(components)") 40 | } 41 | 42 | return url 43 | } 44 | } 45 | 46 | // MARK: - Equatable 47 | extension CLEndpoint: Equatable { 48 | public static func == (lhs: Self, rhs: Self) -> Bool { 49 | return lhs.url == rhs.url 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Example/CleanNetworkExample/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Tests/CleanNetworkTests/CLNetworkRequestTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CLNetworkRequestTests.swift 3 | // 4 | // 5 | // Created by Alperen Ünal on 29.05.2022. 6 | // 7 | 8 | import XCTest 9 | import CleanNetwork 10 | 11 | class CLNetworkRequestTests: XCTestCase { 12 | 13 | override func setUp() async throws { 14 | try await super.setUp() 15 | } 16 | 17 | override func tearDown() async throws { 18 | CLURLComponent.baseURL = "" 19 | try await super.tearDown() 20 | } 21 | 22 | func test_post_request() throws { 23 | let endpoint = CLEndpoint(path: "/mockPost") 24 | let method: CLHTTPMethod = .post 25 | let requestBody = MockRequestBodyModel() 26 | let mockRequest = MockBodyRequest(endpoint: endpoint, method: method, requestBody: requestBody) 27 | 28 | XCTAssertEqual(mockRequest.endpoint, endpoint) 29 | XCTAssertEqual(mockRequest.method, method) 30 | XCTAssertEqual(mockRequest.requestBody, requestBody) 31 | } 32 | 33 | func test_request() throws { 34 | let endpoint = CLEndpoint(path: "/mock") 35 | let method: CLHTTPMethod = .get 36 | let mockRequest = MockRequest(endpoint: endpoint, method: method) 37 | 38 | XCTAssertEqual(mockRequest.endpoint, endpoint) 39 | XCTAssertEqual(mockRequest.method, method) 40 | } 41 | 42 | func test_request_with_different_baseURL() throws { 43 | CLURLComponent.baseURL = "mockAPI.com" 44 | let differentBaseURL = "mockAPI2.com" 45 | let endpoint = CLEndpoint(baseURL: differentBaseURL, path: "/mock") 46 | let method: CLHTTPMethod = .get 47 | let mockRequest = MockRequest(endpoint: endpoint, method: method) 48 | 49 | XCTAssertEqual(mockRequest.endpoint, endpoint) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Example/CleanNetworkExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "2x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "83.5x83.5" 82 | }, 83 | { 84 | "idiom" : "ios-marketing", 85 | "scale" : "1x", 86 | "size" : "1024x1024" 87 | } 88 | ], 89 | "info" : { 90 | "author" : "xcode", 91 | "version" : 1 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Example/CleanNetworkExample/ViewControllers/CreatePostViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreatePostViewController.swift 3 | // CleanNetworkExample 4 | // 5 | // Created by Alperen Ünal on 3.07.2022. 6 | // 7 | 8 | import UIKit 9 | import CleanNetwork 10 | 11 | final class CreatePostViewController: UIViewController, AlertingController { 12 | @IBOutlet private weak var titleTextField: UITextField! 13 | @IBOutlet private weak var bodyTextField: UITextField! 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | } 18 | } 19 | 20 | // MARK: - Actions 21 | extension CreatePostViewController { 22 | @IBAction func createPostButtonTapped(_ sender: Any) { 23 | guard let title = titleTextField.text, 24 | let body = bodyTextField.text else { return } 25 | Task { [weak self] in 26 | await self?.createPost(title: title, body: body) 27 | } 28 | } 29 | } 30 | 31 | // MARK: - Networking 32 | extension CreatePostViewController { 33 | private func createPost(title: String, body: String) async { 34 | let requestBody = CreatePostRequestBody(userId: 1, title: title, body: body) 35 | let request = CreatePostRequest(requestBody: requestBody) 36 | do { 37 | let response = try await CLNetworkService.shared.fetch(request) 38 | showSimpleAlert(title: "Successfull", message: "Post added with id: \(response.id)") 39 | } catch { 40 | var errorMessage = "" 41 | if let error = error as? CLError { 42 | switch error { 43 | case .errorMessage(let message): 44 | errorMessage = message.rawValue 45 | case .apiError(let data, let statusCode): 46 | if let statusCode = statusCode { 47 | print(statusCode) 48 | } 49 | // If you have a API error type handle here with 'data' 50 | if let apiError = try? JSONDecoder().decode(APIError.self, from: data) { 51 | errorMessage = apiError.errorMessage 52 | } 53 | } 54 | } else { 55 | errorMessage = error.localizedDescription 56 | } 57 | 58 | showSimpleAlert(title: "Error", message: errorMessage, actionTitle: "Retry") { 59 | Task { [weak self] in 60 | await self?.createPost(title: title, body: body) 61 | } 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/CleanNetwork/Core/CLNetworkService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CLNetworkService.swift 3 | // 4 | // 5 | // Created by Alperen Ünal on 28.05.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct CLNetworkService: NetworkService { 11 | public static var shared = CLNetworkService() 12 | 13 | public var config: NetworkConfig = CLNetworkConfig() 14 | private let successRange = Set(200...299) 15 | 16 | private init() {} 17 | 18 | public func fetch(_ request: T) async throws -> T.ResponseType { 19 | let urlRequest = try request.build(encoder: config.encoder, with: config.sharedHeaders) 20 | return try await fetch(urlRequest: urlRequest) 21 | } 22 | 23 | public func fetch(_ request: T) async throws -> T.ResponseType { 24 | return try await fetch(urlRequest: request.build(with: config.sharedHeaders)) 25 | } 26 | 27 | public func fetch(_ request: T) async throws -> Data { 28 | return try await fetch(urlRequest: request.build(with: config.sharedHeaders)) 29 | } 30 | 31 | func fetch(urlRequest: URLRequest) async throws -> T { 32 | let data = try await fetch(urlRequest: urlRequest) 33 | 34 | do { 35 | return try config.decoder.decode(T.self, from: data) 36 | } catch { 37 | if config.loggerEnabled, let error = error as? DecodingError { 38 | CLNetworkLogger.logDecodingError(with: error) 39 | } 40 | 41 | throw error 42 | } 43 | } 44 | 45 | func fetch(urlRequest: URLRequest) async throws -> Data { 46 | CLNetworkLogger.log(request: urlRequest) 47 | 48 | do { 49 | let (data, response) = try await config.urlSession.data(for: urlRequest) 50 | 51 | if let urlResponse = response as? HTTPURLResponse { 52 | CLNetworkLogger.log(data: data, response: urlResponse) 53 | } 54 | 55 | guard let urlResponse = response as? HTTPURLResponse, 56 | successRange.contains(urlResponse.statusCode) else { 57 | if let statusCode = (response as? HTTPURLResponse)?.statusCode { 58 | throw CLError.apiError(data, statusCode) 59 | } else { 60 | throw CLError.errorMessage(.statusCodeIsNotValid) 61 | } 62 | } 63 | 64 | return data 65 | } catch { 66 | CLNetworkLogger.logError(with: error) 67 | throw error 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Tests/CleanNetworkTests/CLNetworkServiceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CLNetworkServiceTests.swift 3 | // 4 | // 5 | // Created by Alperen Ünal on 2.10.2022. 6 | // 7 | 8 | import XCTest 9 | import CleanNetwork 10 | 11 | final class CLNetworkServiceTests: XCTestCase { 12 | private var networkService: CLNetworkService! 13 | private var request: UsersMockRequest! 14 | 15 | override func setUpWithError() throws { 16 | try super.setUpWithError() 17 | CLURLComponent.baseURL = "mockAPI.com" 18 | let endpoint = CLEndpoint(path: "/mock") 19 | request = UsersMockRequest(endpoint: endpoint) 20 | 21 | let sessionConfiguration = URLSessionConfiguration.ephemeral 22 | sessionConfiguration.protocolClasses = [MockURLSessionProtocol.self] 23 | 24 | networkService = .shared 25 | networkService.config.loggerEnabled = false 26 | networkService.config.urlSession = URLSession(configuration: sessionConfiguration) 27 | } 28 | 29 | override func tearDownWithError() throws { 30 | CLURLComponent.baseURL = "" 31 | request = nil 32 | networkService = nil 33 | try super.tearDownWithError() 34 | } 35 | 36 | func test_when_came_valid_response_data() async throws { 37 | guard let path = Bundle.module.path(forResource: "UsersMock", ofType: "json"), 38 | let data = FileManager.default.contents(atPath: path) 39 | else { 40 | XCTFail("Failed to get mock json data.") 41 | return 42 | } 43 | 44 | let expectedData = try JSONDecoder().decode([UserMockModel].self, from: data) 45 | 46 | MockURLSessionProtocol.loadingHandler = { 47 | let response = HTTPURLResponse(url: self.request.endpoint.url, statusCode: 200, 48 | httpVersion: nil, headerFields: nil) 49 | return (response!, data) 50 | } 51 | 52 | let usersResponseData = try await networkService.fetch(request) 53 | 54 | XCTAssertEqual(usersResponseData, expectedData) 55 | } 56 | 57 | func test_when_came_error_from_api() async { 58 | let expectedError = NSError(domain: "url not found", code: -1) 59 | 60 | MockURLSessionProtocol.loadingHandler = { 61 | throw expectedError 62 | } 63 | 64 | do { 65 | _ = try await networkService.fetch(request) 66 | XCTFail("This call should throw an error.") 67 | } catch let error as NSError { 68 | XCTAssertEqual(error.domain, expectedError.domain) 69 | XCTAssertEqual(error.code, expectedError.code) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Example/CleanNetworkExample/ViewControllers/PostsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostsViewController.swift 3 | // CleanNetworkExample 4 | // 5 | // Created by Alperen Ünal on 11.06.2022. 6 | // 7 | 8 | import UIKit 9 | import CleanNetwork 10 | 11 | final class PostsViewController: UIViewController, AlertingController { 12 | @IBOutlet private weak var tableView: UITableView! 13 | 14 | private var posts: [Post] = [] 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | configureContents() 19 | Task { 20 | await fetchPosts() 21 | } 22 | } 23 | 24 | private func configureContents() { 25 | title = "Posts" 26 | tableView.dataSource = self 27 | tableView.rowHeight = UITableView.automaticDimension 28 | tableView.estimatedRowHeight = 44 29 | tableView.register(UINib(nibName: PostTableViewCell.reuseIdentifier, bundle: nil), 30 | forCellReuseIdentifier: PostTableViewCell.reuseIdentifier) 31 | } 32 | } 33 | 34 | // MARK: - UITableViewDataSource 35 | extension PostsViewController: UITableViewDataSource { 36 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 37 | posts.count 38 | } 39 | 40 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 41 | let cell = tableView.dequeueReusableCell(withIdentifier: PostTableViewCell.reuseIdentifier, for: indexPath) as! PostTableViewCell 42 | let post = posts[indexPath.row] 43 | cell.setCell(with: post) 44 | return cell 45 | } 46 | } 47 | 48 | // MARK: - Networking 49 | extension PostsViewController { 50 | private func fetchPosts() async { 51 | let request = PostsRequest() 52 | tableView.startLoading() 53 | do { 54 | posts = try await CLNetworkService.shared.fetch(request) 55 | await MainActor.run { 56 | tableView.stopLoading() 57 | tableView.reloadData() 58 | } 59 | } catch { 60 | var errorMessage = "" 61 | if let error = error as? CLError { 62 | switch error { 63 | case .errorMessage(let message): 64 | errorMessage = message.rawValue 65 | case .apiError(let data, let statusCode): 66 | if let statusCode = statusCode { 67 | print(statusCode) 68 | } 69 | // If you have a API error type handle here with 'data' 70 | if let apiError = try? JSONDecoder().decode(APIError.self, from: data) { 71 | errorMessage = apiError.errorMessage 72 | } 73 | } 74 | } else { 75 | errorMessage = error.localizedDescription 76 | } 77 | 78 | showSimpleAlert(title: "Error", message: errorMessage, actionTitle: "Retry") { 79 | Task { [weak self] in 80 | await self?.fetchPosts() 81 | } 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Example/CleanNetworkExample/Cells/PostCell/PostTableViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Sources/CleanNetwork/Core/CLNetworkLogger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CLNetworkLogger.swift 3 | // 4 | // 5 | // Created by Alperen Ünal on 8.07.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | struct CLNetworkLogger { 11 | static var loggerEnabled = true 12 | 13 | static func log(request: URLRequest) { 14 | guard loggerEnabled else { return } 15 | 16 | print("\n - - - - - - - - - - OUTGOING - - - - - - - - - - \n") 17 | defer { print("\n - - - - - - - - - - END - - - - - - - - - - \n") } 18 | 19 | let urlAsString = request.url?.absoluteString ?? "" 20 | let urlComponents = URLComponents(string: urlAsString) 21 | let method = request.httpMethod ?? "" 22 | let path = urlComponents?.path ?? "" 23 | let query = urlComponents?.query ?? "" 24 | let host = urlComponents?.host ?? "" 25 | 26 | var output = """ 27 | URL: \(urlAsString) \n\n 28 | METHOD: \(method) PATH: \(path)?\(query) HTTP/1.1 \n 29 | HOST: \(host)\n 30 | """ 31 | if let httpHeaders = request.allHTTPHeaderFields, !httpHeaders.isEmpty { 32 | output += "\nHTTP Headers: \n" 33 | for (key,value) in httpHeaders { 34 | output += "\(key): \(value) \n" 35 | } 36 | output += "HTTP Headers End \n" 37 | } 38 | 39 | if let body = request.httpBody { 40 | output += "\n \(String(data: body, encoding: .utf8) ?? "")" 41 | } 42 | print(output) 43 | } 44 | 45 | static func log(data: Data?, response: HTTPURLResponse?) { 46 | guard loggerEnabled else { return } 47 | 48 | print("\n - - - - - - - - - - INCOMMING - - - - - - - - - - \n") 49 | defer { print("\n - - - - - - - - - - END - - - - - - - - - - \n") } 50 | 51 | let urlString = response?.url?.absoluteString 52 | let components = NSURLComponents(string: urlString ?? "") 53 | let path = components?.path ?? "" 54 | let query = components?.query ?? "" 55 | 56 | var output = "" 57 | if let urlString = urlString { 58 | output += "URL: \(urlString)" 59 | output += "\n\n" 60 | } 61 | if let statusCode = response?.statusCode { 62 | output += "HTTP \(statusCode) PATH: \(path)?\(query)\n" 63 | } 64 | if let host = components?.host { 65 | output += "Host: \(host)\n" 66 | } 67 | if let httpHeaders = response?.allHeaderFields, !httpHeaders.isEmpty { 68 | output += "\nHTTP Headers: \n" 69 | for (key,value) in httpHeaders { 70 | output += "\(key): \(value) \n" 71 | } 72 | output += "HTTP Headers End \n" 73 | } 74 | if let body = data { 75 | output += "\n\(String(data: body, encoding: .utf8) ?? "")\n" 76 | } 77 | print(output) 78 | } 79 | 80 | static func logError(with error: Error) { 81 | guard loggerEnabled else { return } 82 | print("\nError: \(error.localizedDescription)\n") 83 | } 84 | 85 | static func logDecodingError(with error: DecodingError) { 86 | guard loggerEnabled else { return } 87 | 88 | print("\n\nDecoding Error\n") 89 | switch error { 90 | case .typeMismatch(let type, let context): 91 | print("Type '\(type)' mismatch:", context.debugDescription) 92 | print("codingPath:", context.codingPath) 93 | case .valueNotFound(let value, let context): 94 | print("Value '\(value)' not found:", context.debugDescription) 95 | print("codingPath:", context.codingPath) 96 | case .keyNotFound(let key, let context): 97 | print("Key '\(key)' not found:", context.debugDescription) 98 | print("codingPath:", context.codingPath) 99 | case .dataCorrupted(let context): 100 | print(context) 101 | @unknown default: 102 | print("Unknown decoding error") 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CleanNetwork 2 | 3 | ![swift package manager](https://img.shields.io/badge/swift%20package%20manager-compatible-brightgreen) 4 | ![swift](https://img.shields.io/badge/swift-5.5-orange?logo=swift) 5 | ![platforms](https://img.shields.io/badge/platforms-macOS--12_iOS--15_tvOS--15_watchOS--8-yellowgreen) 6 | ![license](https://img.shields.io/badge/license-MIT-green) 7 | [![Twitter:@alperenunal68](https://img.shields.io/badge/twitter-@alperenunal68-blue.svg?style=flat)](https://twitter.com/alperenunal68) 8 | 9 | `CleanNetwork` is a lightweight URLSession wrapper for using async/await in networking. You can use CleanNetwork for creating a modular network layer in projects. CleanNetwork is best way to combine asnc/await with networking. Feel free to contribute :) 10 | 11 | # Table of contents 12 | 13 | - [CleanNetwork](#cleannetwork) 14 | - [Table of contents](#table-of-contents) 15 | - [Swift Style Guide](#swift-style-guide) 16 | - [Installation](#installation) 17 | - [Swift Package Manager](#swift-package-manager) 18 | - [Simple Usage](#simple-usage) 19 | - [Creating CLEndpoint](#creating-clendpoint) 20 | - [Creating Request Object](#creating-request-object) 21 | - [Sending Request](#sending-request) 22 | - [Advance Usage](#advance-usage) 23 | - [Requests](#requests) 24 | - [Customize CLNetworkService](#customize-clnetworkservice) 25 | - [CLNetworkConfig](#clnetworkconfig) 26 | - [Error Handling](#error-handling) 27 | 28 | ## Swift Style Guide 29 | This project uses Swift API and Raywenderlich guideline. Please check it out before contributing. 30 | 31 | * [The Swift API Design Guidelines](https://swift.org/documentation/api-design-guidelines) 32 | * [The Raywenderlich Swift Style Guide](https://github.com/raywenderlich/swift-style-guide) 33 | 34 | ## Installation 35 | ### Swift Package Manager 36 | 37 | Once you have your Swift package set up, adding CleanNetwork as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`. 38 | 39 | ```swift 40 | dependencies: [ 41 | .package(url: "https://github.com/alperen23230/CleanNetwork", .upToNextMajor(from: "1.1.0")) 42 | ] 43 | ``` 44 | 45 | ## Simple Usage 46 | Firstly you have to create a request object for sending request. You can use `CLNetworkDecodableRequest` type for this. 47 | 48 | ```swift 49 | struct ExampleRequest: CLNetworkDecodableRequest { 50 | typealias ResponseType = ExampleDecodableModel 51 | 52 | let endpoint: CLEndpoint 53 | let method: CLHTTPMethod 54 | 55 | init(endpoint: CLEndpoint, method: CLHTTPMethod) { 56 | self.endpoint = endpoint 57 | self.method = method 58 | } 59 | } 60 | ``` 61 | ### Creating CLEndpoint 62 | CLEndpoint is a struct which represents an API endpoint. It has a baseURL, path and queryItems variables. For baseURL, there is a static variable inside the `CLURLComponent` enum. It's called `CLURLComponent.baseURL`. (For example you can set `CLURLComponent.baseURL` in AppDelegate) For url scheme, it uses `https` by default but you can change using `urlScheme` static variable inside the `CLURLComponent`. 63 | 64 | ```swift 65 | public struct CLEndpoint { 66 | public var baseURL: String 67 | public var path: String 68 | public var queryItems: [URLQueryItem] 69 | 70 | public init(baseURL: String = CLURLComponent.baseURL, 71 | path: String, 72 | queryItems: [URLQueryItem] = []) { 73 | self.baseURL = baseURL 74 | self.path = path 75 | self.queryItems = queryItems 76 | } 77 | } 78 | ``` 79 | 80 | ### Creating Request Object 81 | ```swift 82 | let endpoint = CLEndpoint(path: "/example") 83 | let method: CLHTTPMethod = .get 84 | let exampleRequest = ExampleRequest(endpoint: endpoint, method: method) 85 | ``` 86 | 87 | ### Sending Request 88 | You have to use `CLNetworkService` for sending request. You can use both shared object or creating object. Use `fetch` method of network service object. 89 | 90 | ```swift 91 | do { 92 | let response = try await CLNetworkService.shared.fetch(exampleRequest) 93 | await MainActor.run { 94 | // Switch to main thread 95 | } 96 | } catch { 97 | // Handle error here 98 | } 99 | ``` 100 | 101 | ## Advance Usage 102 | ### Requests 103 | There are 3 request protocol. 104 | 105 | `CLNetworkRequest` is the basic protocol. It's for fetching raw `Data`. By default `method` is GET and `headers` are empty. 106 | 107 | ```swift 108 | public protocol CLNetworkRequest { 109 | var endpoint: CLEndpoint { get } 110 | var method: CLHTTPMethod { get } 111 | var headers: [String: String] { get } 112 | } 113 | ``` 114 | 115 | `CLNetworkDecodableRequest` is for fetching decodable response. You have to specify response type in your request. It has to be `Decodable`. 116 | 117 | ```swift 118 | public protocol CLNetworkDecodableRequest: CLNetworkRequest { 119 | associatedtype ResponseType: Decodable 120 | 121 | var endpoint: CLEndpoint { get } 122 | var method: CLHTTPMethod { get } 123 | var headers: [String: String] { get } 124 | } 125 | ``` 126 | 127 | `CLNetworkBodyRequest` is for sending body request. You have to specify body type in your request. It has to be `Encodable`. By default `method` is POST. 128 | 129 | ```swift 130 | public protocol CLNetworkBodyRequest: CLNetworkDecodableRequest { 131 | associatedtype RequestBodyType: Encodable 132 | 133 | var requestBody: RequestBodyType { get } 134 | } 135 | ``` 136 | ### Customize CLNetworkService 137 | 138 | When you want to customize the `CLNetworkService` you have to use the `NetworkConfig` instance inside the `CLNetworkService`. By default it uses instance of `CLNetworkConfig`. 139 | 140 | ```swift 141 | public var config: NetworkConfig = CLNetworkConfig() 142 | ``` 143 | 144 | #### CLNetworkConfig 145 | You can change the configuration using `CLNetworkConfig` instance. 146 | 147 | ```swift 148 | public class CLNetworkConfig: NetworkConfig { 149 | public var decoder = JSONDecoder() 150 | public var encoder = JSONEncoder() 151 | public var urlSession = URLSession.shared 152 | public var loggerEnabled = true 153 | public var sharedHeaders: [String: String] = [:] 154 | 155 | public init() {} 156 | } 157 | ``` 158 | 159 | ### Error Handling 160 | There is an error enum for unique errors. It's called `CLError`. 161 | 162 | ```swift 163 | public enum CLError: Error { 164 | case errorMessage(CLErrorMessage) 165 | /// APIError response data, HTTP status code 166 | case apiError(Data, Int?) 167 | } 168 | ``` 169 | The enum has a 2 case. The first case, `errorMessage` case is for handling known errors. (For example data returning nil.) 170 | 171 | The second case, `apiError` case is for handling API error json models. This case is an associated value case. It's returning the data which will decode. Also returns the HTTP status code to handle some situations. 172 | -------------------------------------------------------------------------------- /Tests/CleanNetworkTests/Supporting Files/UsersMock.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "name": "Leanne Graham", 5 | "username": "Bret", 6 | "email": "Sincere@april.biz", 7 | "address": { 8 | "street": "Kulas Light", 9 | "suite": "Apt. 556", 10 | "city": "Gwenborough", 11 | "zipcode": "92998-3874", 12 | "geo": { 13 | "lat": "-37.3159", 14 | "lng": "81.1496" 15 | } 16 | }, 17 | "phone": "1-770-736-8031 x56442", 18 | "website": "hildegard.org", 19 | "company": { 20 | "name": "Romaguera-Crona", 21 | "catchPhrase": "Multi-layered client-server neural-net", 22 | "bs": "harness real-time e-markets" 23 | } 24 | }, 25 | { 26 | "id": 2, 27 | "name": "Ervin Howell", 28 | "username": "Antonette", 29 | "email": "Shanna@melissa.tv", 30 | "address": { 31 | "street": "Victor Plains", 32 | "suite": "Suite 879", 33 | "city": "Wisokyburgh", 34 | "zipcode": "90566-7771", 35 | "geo": { 36 | "lat": "-43.9509", 37 | "lng": "-34.4618" 38 | } 39 | }, 40 | "phone": "010-692-6593 x09125", 41 | "website": "anastasia.net", 42 | "company": { 43 | "name": "Deckow-Crist", 44 | "catchPhrase": "Proactive didactic contingency", 45 | "bs": "synergize scalable supply-chains" 46 | } 47 | }, 48 | { 49 | "id": 3, 50 | "name": "Clementine Bauch", 51 | "username": "Samantha", 52 | "email": "Nathan@yesenia.net", 53 | "address": { 54 | "street": "Douglas Extension", 55 | "suite": "Suite 847", 56 | "city": "McKenziehaven", 57 | "zipcode": "59590-4157", 58 | "geo": { 59 | "lat": "-68.6102", 60 | "lng": "-47.0653" 61 | } 62 | }, 63 | "phone": "1-463-123-4447", 64 | "website": "ramiro.info", 65 | "company": { 66 | "name": "Romaguera-Jacobson", 67 | "catchPhrase": "Face to face bifurcated interface", 68 | "bs": "e-enable strategic applications" 69 | } 70 | }, 71 | { 72 | "id": 4, 73 | "name": "Patricia Lebsack", 74 | "username": "Karianne", 75 | "email": "Julianne.OConner@kory.org", 76 | "address": { 77 | "street": "Hoeger Mall", 78 | "suite": "Apt. 692", 79 | "city": "South Elvis", 80 | "zipcode": "53919-4257", 81 | "geo": { 82 | "lat": "29.4572", 83 | "lng": "-164.2990" 84 | } 85 | }, 86 | "phone": "493-170-9623 x156", 87 | "website": "kale.biz", 88 | "company": { 89 | "name": "Robel-Corkery", 90 | "catchPhrase": "Multi-tiered zero tolerance productivity", 91 | "bs": "transition cutting-edge web services" 92 | } 93 | }, 94 | { 95 | "id": 5, 96 | "name": "Chelsey Dietrich", 97 | "username": "Kamren", 98 | "email": "Lucio_Hettinger@annie.ca", 99 | "address": { 100 | "street": "Skiles Walks", 101 | "suite": "Suite 351", 102 | "city": "Roscoeview", 103 | "zipcode": "33263", 104 | "geo": { 105 | "lat": "-31.8129", 106 | "lng": "62.5342" 107 | } 108 | }, 109 | "phone": "(254)954-1289", 110 | "website": "demarco.info", 111 | "company": { 112 | "name": "Keebler LLC", 113 | "catchPhrase": "User-centric fault-tolerant solution", 114 | "bs": "revolutionize end-to-end systems" 115 | } 116 | }, 117 | { 118 | "id": 6, 119 | "name": "Mrs. Dennis Schulist", 120 | "username": "Leopoldo_Corkery", 121 | "email": "Karley_Dach@jasper.info", 122 | "address": { 123 | "street": "Norberto Crossing", 124 | "suite": "Apt. 950", 125 | "city": "South Christy", 126 | "zipcode": "23505-1337", 127 | "geo": { 128 | "lat": "-71.4197", 129 | "lng": "71.7478" 130 | } 131 | }, 132 | "phone": "1-477-935-8478 x6430", 133 | "website": "ola.org", 134 | "company": { 135 | "name": "Considine-Lockman", 136 | "catchPhrase": "Synchronised bottom-line interface", 137 | "bs": "e-enable innovative applications" 138 | } 139 | }, 140 | { 141 | "id": 7, 142 | "name": "Kurtis Weissnat", 143 | "username": "Elwyn.Skiles", 144 | "email": "Telly.Hoeger@billy.biz", 145 | "address": { 146 | "street": "Rex Trail", 147 | "suite": "Suite 280", 148 | "city": "Howemouth", 149 | "zipcode": "58804-1099", 150 | "geo": { 151 | "lat": "24.8918", 152 | "lng": "21.8984" 153 | } 154 | }, 155 | "phone": "210.067.6132", 156 | "website": "elvis.io", 157 | "company": { 158 | "name": "Johns Group", 159 | "catchPhrase": "Configurable multimedia task-force", 160 | "bs": "generate enterprise e-tailers" 161 | } 162 | }, 163 | { 164 | "id": 8, 165 | "name": "Nicholas Runolfsdottir V", 166 | "username": "Maxime_Nienow", 167 | "email": "Sherwood@rosamond.me", 168 | "address": { 169 | "street": "Ellsworth Summit", 170 | "suite": "Suite 729", 171 | "city": "Aliyaview", 172 | "zipcode": "45169", 173 | "geo": { 174 | "lat": "-14.3990", 175 | "lng": "-120.7677" 176 | } 177 | }, 178 | "phone": "586.493.6943 x140", 179 | "website": "jacynthe.com", 180 | "company": { 181 | "name": "Abernathy Group", 182 | "catchPhrase": "Implemented secondary concept", 183 | "bs": "e-enable extensible e-tailers" 184 | } 185 | }, 186 | { 187 | "id": 9, 188 | "name": "Glenna Reichert", 189 | "username": "Delphine", 190 | "email": "Chaim_McDermott@dana.io", 191 | "address": { 192 | "street": "Dayna Park", 193 | "suite": "Suite 449", 194 | "city": "Bartholomebury", 195 | "zipcode": "76495-3109", 196 | "geo": { 197 | "lat": "24.6463", 198 | "lng": "-168.8889" 199 | } 200 | }, 201 | "phone": "(775)976-6794 x41206", 202 | "website": "conrad.com", 203 | "company": { 204 | "name": "Yost and Sons", 205 | "catchPhrase": "Switchable contextually-based project", 206 | "bs": "aggregate real-time technologies" 207 | } 208 | }, 209 | { 210 | "id": 10, 211 | "name": "Clementina DuBuque", 212 | "username": "Moriah.Stanton", 213 | "email": "Rey.Padberg@karina.biz", 214 | "address": { 215 | "street": "Kattie Turnpike", 216 | "suite": "Suite 198", 217 | "city": "Lebsackbury", 218 | "zipcode": "31428-2261", 219 | "geo": { 220 | "lat": "-38.2386", 221 | "lng": "57.2232" 222 | } 223 | }, 224 | "phone": "024-648-3804", 225 | "website": "ambrose.net", 226 | "company": { 227 | "name": "Hoeger LLC", 228 | "catchPhrase": "Centralized empowering task-force", 229 | "bs": "target end-to-end models" 230 | } 231 | } 232 | ] -------------------------------------------------------------------------------- /Example/CleanNetworkExample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 31 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | -------------------------------------------------------------------------------- /Example/CleanNetworkExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 8431B53D2854C560002056FC /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8431B53C2854C560002056FC /* Post.swift */; }; 11 | 84462D9728539F7100A22ECD /* CleanNetwork in Frameworks */ = {isa = PBXBuildFile; productRef = 84462D9628539F7100A22ECD /* CleanNetwork */; }; 12 | 8450B590285482070018778F /* PostsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8450B58F285482070018778F /* PostsViewController.swift */; }; 13 | 8450B593285483340018778F /* UITableView+Loading.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8450B592285483340018778F /* UITableView+Loading.swift */; }; 14 | 8450B596285484B60018778F /* AlertingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8450B595285484B60018778F /* AlertingController.swift */; }; 15 | 8450B5992854853E0018778F /* Closures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8450B5982854853E0018778F /* Closures.swift */; }; 16 | 845ABA702871A5ED00230200 /* CreatePostViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845ABA6F2871A5ED00230200 /* CreatePostViewController.swift */; }; 17 | 845ABA722871A81C00230200 /* CreatePostRequestBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845ABA712871A81C00230200 /* CreatePostRequestBody.swift */; }; 18 | 845ABA742871A86C00230200 /* CreatePostRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845ABA732871A86C00230200 /* CreatePostRequest.swift */; }; 19 | 8460D0342854BFDC00E4C939 /* PostsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8460D0332854BFDC00E4C939 /* PostsRequest.swift */; }; 20 | 8460D0392854C22300E4C939 /* PostTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8460D0372854C22300E4C939 /* PostTableViewCell.swift */; }; 21 | 8460D03A2854C22300E4C939 /* PostTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 8460D0382854C22300E4C939 /* PostTableViewCell.xib */; }; 22 | 84A900382868D94500E3D6F1 /* APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A900372868D94500E3D6F1 /* APIError.swift */; }; 23 | 84FA649028310BCA00F10993 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FA648F28310BCA00F10993 /* AppDelegate.swift */; }; 24 | 84FA649428310BCA00F10993 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FA649328310BCA00F10993 /* MainViewController.swift */; }; 25 | 84FA649728310BCA00F10993 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 84FA649528310BCA00F10993 /* Main.storyboard */; }; 26 | 84FA649928310BCA00F10993 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 84FA649828310BCA00F10993 /* Assets.xcassets */; }; 27 | 84FA649C28310BCA00F10993 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 84FA649A28310BCA00F10993 /* LaunchScreen.storyboard */; }; 28 | /* End PBXBuildFile section */ 29 | 30 | /* Begin PBXFileReference section */ 31 | 8431B53C2854C560002056FC /* Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = ""; }; 32 | 8450B58F285482070018778F /* PostsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostsViewController.swift; sourceTree = ""; }; 33 | 8450B592285483340018778F /* UITableView+Loading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+Loading.swift"; sourceTree = ""; }; 34 | 8450B595285484B60018778F /* AlertingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertingController.swift; sourceTree = ""; }; 35 | 8450B5982854853E0018778F /* Closures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Closures.swift; sourceTree = ""; }; 36 | 845ABA6F2871A5ED00230200 /* CreatePostViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePostViewController.swift; sourceTree = ""; }; 37 | 845ABA712871A81C00230200 /* CreatePostRequestBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePostRequestBody.swift; sourceTree = ""; }; 38 | 845ABA732871A86C00230200 /* CreatePostRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePostRequest.swift; sourceTree = ""; }; 39 | 8460D0332854BFDC00E4C939 /* PostsRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostsRequest.swift; sourceTree = ""; }; 40 | 8460D0372854C22300E4C939 /* PostTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostTableViewCell.swift; sourceTree = ""; }; 41 | 8460D0382854C22300E4C939 /* PostTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PostTableViewCell.xib; sourceTree = ""; }; 42 | 84A900372868D94500E3D6F1 /* APIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIError.swift; sourceTree = ""; }; 43 | 84FA648C28310BCA00F10993 /* CleanNetworkExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CleanNetworkExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 44 | 84FA648F28310BCA00F10993 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 45 | 84FA649328310BCA00F10993 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; 46 | 84FA649628310BCA00F10993 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 47 | 84FA649828310BCA00F10993 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 48 | 84FA649B28310BCA00F10993 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 49 | 84FA649D28310BCA00F10993 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50 | /* End PBXFileReference section */ 51 | 52 | /* Begin PBXFrameworksBuildPhase section */ 53 | 84FA648928310BCA00F10993 /* Frameworks */ = { 54 | isa = PBXFrameworksBuildPhase; 55 | buildActionMask = 2147483647; 56 | files = ( 57 | 84462D9728539F7100A22ECD /* CleanNetwork in Frameworks */, 58 | ); 59 | runOnlyForDeploymentPostprocessing = 0; 60 | }; 61 | /* End PBXFrameworksBuildPhase section */ 62 | 63 | /* Begin PBXGroup section */ 64 | 8431B53B2854C553002056FC /* Entity */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | 8431B53E2854C564002056FC /* Models */, 68 | ); 69 | path = Entity; 70 | sourceTree = ""; 71 | }; 72 | 8431B53E2854C564002056FC /* Models */ = { 73 | isa = PBXGroup; 74 | children = ( 75 | 8431B53C2854C560002056FC /* Post.swift */, 76 | 84A900372868D94500E3D6F1 /* APIError.swift */, 77 | 845ABA712871A81C00230200 /* CreatePostRequestBody.swift */, 78 | ); 79 | path = Models; 80 | sourceTree = ""; 81 | }; 82 | 8450B58E285481CF0018778F /* ViewControllers */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | 84FA649328310BCA00F10993 /* MainViewController.swift */, 86 | 8450B58F285482070018778F /* PostsViewController.swift */, 87 | 845ABA6F2871A5ED00230200 /* CreatePostViewController.swift */, 88 | ); 89 | path = ViewControllers; 90 | sourceTree = ""; 91 | }; 92 | 8450B591285483060018778F /* Extensions */ = { 93 | isa = PBXGroup; 94 | children = ( 95 | 8450B592285483340018778F /* UITableView+Loading.swift */, 96 | ); 97 | path = Extensions; 98 | sourceTree = ""; 99 | }; 100 | 8450B594285484490018778F /* Protocols */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | 8450B595285484B60018778F /* AlertingController.swift */, 104 | ); 105 | path = Protocols; 106 | sourceTree = ""; 107 | }; 108 | 8450B597285485300018778F /* Constants */ = { 109 | isa = PBXGroup; 110 | children = ( 111 | 8450B5982854853E0018778F /* Closures.swift */, 112 | ); 113 | path = Constants; 114 | sourceTree = ""; 115 | }; 116 | 8460D0312854BF8500E4C939 /* Networking */ = { 117 | isa = PBXGroup; 118 | children = ( 119 | 8460D0322854BF8C00E4C939 /* Requests */, 120 | ); 121 | path = Networking; 122 | sourceTree = ""; 123 | }; 124 | 8460D0322854BF8C00E4C939 /* Requests */ = { 125 | isa = PBXGroup; 126 | children = ( 127 | 8460D0332854BFDC00E4C939 /* PostsRequest.swift */, 128 | 845ABA732871A86C00230200 /* CreatePostRequest.swift */, 129 | ); 130 | path = Requests; 131 | sourceTree = ""; 132 | }; 133 | 8460D0362854C1FF00E4C939 /* Cells */ = { 134 | isa = PBXGroup; 135 | children = ( 136 | 8460D03B2854C22800E4C939 /* PostCell */, 137 | ); 138 | path = Cells; 139 | sourceTree = ""; 140 | }; 141 | 8460D03B2854C22800E4C939 /* PostCell */ = { 142 | isa = PBXGroup; 143 | children = ( 144 | 8460D0372854C22300E4C939 /* PostTableViewCell.swift */, 145 | 8460D0382854C22300E4C939 /* PostTableViewCell.xib */, 146 | ); 147 | path = PostCell; 148 | sourceTree = ""; 149 | }; 150 | 84FA648328310BCA00F10993 = { 151 | isa = PBXGroup; 152 | children = ( 153 | 84FA648E28310BCA00F10993 /* CleanNetworkExample */, 154 | 84FA648D28310BCA00F10993 /* Products */, 155 | ); 156 | sourceTree = ""; 157 | }; 158 | 84FA648D28310BCA00F10993 /* Products */ = { 159 | isa = PBXGroup; 160 | children = ( 161 | 84FA648C28310BCA00F10993 /* CleanNetworkExample.app */, 162 | ); 163 | name = Products; 164 | sourceTree = ""; 165 | }; 166 | 84FA648E28310BCA00F10993 /* CleanNetworkExample */ = { 167 | isa = PBXGroup; 168 | children = ( 169 | 8431B53B2854C553002056FC /* Entity */, 170 | 8460D0362854C1FF00E4C939 /* Cells */, 171 | 8460D0312854BF8500E4C939 /* Networking */, 172 | 8450B597285485300018778F /* Constants */, 173 | 8450B594285484490018778F /* Protocols */, 174 | 8450B591285483060018778F /* Extensions */, 175 | 8450B58E285481CF0018778F /* ViewControllers */, 176 | 84FA648F28310BCA00F10993 /* AppDelegate.swift */, 177 | 84FA649528310BCA00F10993 /* Main.storyboard */, 178 | 84FA649828310BCA00F10993 /* Assets.xcassets */, 179 | 84FA649A28310BCA00F10993 /* LaunchScreen.storyboard */, 180 | 84FA649D28310BCA00F10993 /* Info.plist */, 181 | ); 182 | path = CleanNetworkExample; 183 | sourceTree = ""; 184 | }; 185 | /* End PBXGroup section */ 186 | 187 | /* Begin PBXNativeTarget section */ 188 | 84FA648B28310BCA00F10993 /* CleanNetworkExample */ = { 189 | isa = PBXNativeTarget; 190 | buildConfigurationList = 84FA64A028310BCA00F10993 /* Build configuration list for PBXNativeTarget "CleanNetworkExample" */; 191 | buildPhases = ( 192 | 84FA648828310BCA00F10993 /* Sources */, 193 | 84FA648928310BCA00F10993 /* Frameworks */, 194 | 84FA648A28310BCA00F10993 /* Resources */, 195 | ); 196 | buildRules = ( 197 | ); 198 | dependencies = ( 199 | ); 200 | name = CleanNetworkExample; 201 | packageProductDependencies = ( 202 | 84462D9628539F7100A22ECD /* CleanNetwork */, 203 | ); 204 | productName = CleanNetworkExample; 205 | productReference = 84FA648C28310BCA00F10993 /* CleanNetworkExample.app */; 206 | productType = "com.apple.product-type.application"; 207 | }; 208 | /* End PBXNativeTarget section */ 209 | 210 | /* Begin PBXProject section */ 211 | 84FA648428310BCA00F10993 /* Project object */ = { 212 | isa = PBXProject; 213 | attributes = { 214 | BuildIndependentTargetsInParallel = 1; 215 | LastSwiftUpdateCheck = 1330; 216 | LastUpgradeCheck = 1330; 217 | TargetAttributes = { 218 | 84FA648B28310BCA00F10993 = { 219 | CreatedOnToolsVersion = 13.3.1; 220 | }; 221 | }; 222 | }; 223 | buildConfigurationList = 84FA648728310BCA00F10993 /* Build configuration list for PBXProject "CleanNetworkExample" */; 224 | compatibilityVersion = "Xcode 13.0"; 225 | developmentRegion = en; 226 | hasScannedForEncodings = 0; 227 | knownRegions = ( 228 | en, 229 | Base, 230 | ); 231 | mainGroup = 84FA648328310BCA00F10993; 232 | packageReferences = ( 233 | 84462D9528539F7100A22ECD /* XCRemoteSwiftPackageReference "CleanNetwork" */, 234 | ); 235 | productRefGroup = 84FA648D28310BCA00F10993 /* Products */; 236 | projectDirPath = ""; 237 | projectRoot = ""; 238 | targets = ( 239 | 84FA648B28310BCA00F10993 /* CleanNetworkExample */, 240 | ); 241 | }; 242 | /* End PBXProject section */ 243 | 244 | /* Begin PBXResourcesBuildPhase section */ 245 | 84FA648A28310BCA00F10993 /* Resources */ = { 246 | isa = PBXResourcesBuildPhase; 247 | buildActionMask = 2147483647; 248 | files = ( 249 | 84FA649C28310BCA00F10993 /* LaunchScreen.storyboard in Resources */, 250 | 84FA649928310BCA00F10993 /* Assets.xcassets in Resources */, 251 | 84FA649728310BCA00F10993 /* Main.storyboard in Resources */, 252 | 8460D03A2854C22300E4C939 /* PostTableViewCell.xib in Resources */, 253 | ); 254 | runOnlyForDeploymentPostprocessing = 0; 255 | }; 256 | /* End PBXResourcesBuildPhase section */ 257 | 258 | /* Begin PBXSourcesBuildPhase section */ 259 | 84FA648828310BCA00F10993 /* Sources */ = { 260 | isa = PBXSourcesBuildPhase; 261 | buildActionMask = 2147483647; 262 | files = ( 263 | 84FA649428310BCA00F10993 /* MainViewController.swift in Sources */, 264 | 8460D0342854BFDC00E4C939 /* PostsRequest.swift in Sources */, 265 | 8450B5992854853E0018778F /* Closures.swift in Sources */, 266 | 84A900382868D94500E3D6F1 /* APIError.swift in Sources */, 267 | 8460D0392854C22300E4C939 /* PostTableViewCell.swift in Sources */, 268 | 845ABA742871A86C00230200 /* CreatePostRequest.swift in Sources */, 269 | 8450B590285482070018778F /* PostsViewController.swift in Sources */, 270 | 84FA649028310BCA00F10993 /* AppDelegate.swift in Sources */, 271 | 845ABA722871A81C00230200 /* CreatePostRequestBody.swift in Sources */, 272 | 845ABA702871A5ED00230200 /* CreatePostViewController.swift in Sources */, 273 | 8431B53D2854C560002056FC /* Post.swift in Sources */, 274 | 8450B593285483340018778F /* UITableView+Loading.swift in Sources */, 275 | 8450B596285484B60018778F /* AlertingController.swift in Sources */, 276 | ); 277 | runOnlyForDeploymentPostprocessing = 0; 278 | }; 279 | /* End PBXSourcesBuildPhase section */ 280 | 281 | /* Begin PBXVariantGroup section */ 282 | 84FA649528310BCA00F10993 /* Main.storyboard */ = { 283 | isa = PBXVariantGroup; 284 | children = ( 285 | 84FA649628310BCA00F10993 /* Base */, 286 | ); 287 | name = Main.storyboard; 288 | sourceTree = ""; 289 | }; 290 | 84FA649A28310BCA00F10993 /* LaunchScreen.storyboard */ = { 291 | isa = PBXVariantGroup; 292 | children = ( 293 | 84FA649B28310BCA00F10993 /* Base */, 294 | ); 295 | name = LaunchScreen.storyboard; 296 | sourceTree = ""; 297 | }; 298 | /* End PBXVariantGroup section */ 299 | 300 | /* Begin XCBuildConfiguration section */ 301 | 84FA649E28310BCA00F10993 /* Debug */ = { 302 | isa = XCBuildConfiguration; 303 | buildSettings = { 304 | ALWAYS_SEARCH_USER_PATHS = NO; 305 | CLANG_ANALYZER_NONNULL = YES; 306 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 307 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 308 | CLANG_ENABLE_MODULES = YES; 309 | CLANG_ENABLE_OBJC_ARC = YES; 310 | CLANG_ENABLE_OBJC_WEAK = YES; 311 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 312 | CLANG_WARN_BOOL_CONVERSION = YES; 313 | CLANG_WARN_COMMA = YES; 314 | CLANG_WARN_CONSTANT_CONVERSION = YES; 315 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 316 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 317 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 318 | CLANG_WARN_EMPTY_BODY = YES; 319 | CLANG_WARN_ENUM_CONVERSION = YES; 320 | CLANG_WARN_INFINITE_RECURSION = YES; 321 | CLANG_WARN_INT_CONVERSION = YES; 322 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 323 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 324 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 325 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 326 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 327 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 328 | CLANG_WARN_STRICT_PROTOTYPES = YES; 329 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 330 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 331 | CLANG_WARN_UNREACHABLE_CODE = YES; 332 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 333 | COPY_PHASE_STRIP = NO; 334 | DEBUG_INFORMATION_FORMAT = dwarf; 335 | ENABLE_STRICT_OBJC_MSGSEND = YES; 336 | ENABLE_TESTABILITY = YES; 337 | GCC_C_LANGUAGE_STANDARD = gnu11; 338 | GCC_DYNAMIC_NO_PIC = NO; 339 | GCC_NO_COMMON_BLOCKS = YES; 340 | GCC_OPTIMIZATION_LEVEL = 0; 341 | GCC_PREPROCESSOR_DEFINITIONS = ( 342 | "DEBUG=1", 343 | "$(inherited)", 344 | ); 345 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 346 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 347 | GCC_WARN_UNDECLARED_SELECTOR = YES; 348 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 349 | GCC_WARN_UNUSED_FUNCTION = YES; 350 | GCC_WARN_UNUSED_VARIABLE = YES; 351 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 352 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 353 | MTL_FAST_MATH = YES; 354 | ONLY_ACTIVE_ARCH = YES; 355 | SDKROOT = iphoneos; 356 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 357 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 358 | }; 359 | name = Debug; 360 | }; 361 | 84FA649F28310BCA00F10993 /* Release */ = { 362 | isa = XCBuildConfiguration; 363 | buildSettings = { 364 | ALWAYS_SEARCH_USER_PATHS = NO; 365 | CLANG_ANALYZER_NONNULL = YES; 366 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 367 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 368 | CLANG_ENABLE_MODULES = YES; 369 | CLANG_ENABLE_OBJC_ARC = YES; 370 | CLANG_ENABLE_OBJC_WEAK = YES; 371 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 372 | CLANG_WARN_BOOL_CONVERSION = YES; 373 | CLANG_WARN_COMMA = YES; 374 | CLANG_WARN_CONSTANT_CONVERSION = YES; 375 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 376 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 377 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 378 | CLANG_WARN_EMPTY_BODY = YES; 379 | CLANG_WARN_ENUM_CONVERSION = YES; 380 | CLANG_WARN_INFINITE_RECURSION = YES; 381 | CLANG_WARN_INT_CONVERSION = YES; 382 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 383 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 384 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 385 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 386 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 387 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 388 | CLANG_WARN_STRICT_PROTOTYPES = YES; 389 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 390 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 391 | CLANG_WARN_UNREACHABLE_CODE = YES; 392 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 393 | COPY_PHASE_STRIP = NO; 394 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 395 | ENABLE_NS_ASSERTIONS = NO; 396 | ENABLE_STRICT_OBJC_MSGSEND = YES; 397 | GCC_C_LANGUAGE_STANDARD = gnu11; 398 | GCC_NO_COMMON_BLOCKS = YES; 399 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 400 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 401 | GCC_WARN_UNDECLARED_SELECTOR = YES; 402 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 403 | GCC_WARN_UNUSED_FUNCTION = YES; 404 | GCC_WARN_UNUSED_VARIABLE = YES; 405 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 406 | MTL_ENABLE_DEBUG_INFO = NO; 407 | MTL_FAST_MATH = YES; 408 | SDKROOT = iphoneos; 409 | SWIFT_COMPILATION_MODE = wholemodule; 410 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 411 | VALIDATE_PRODUCT = YES; 412 | }; 413 | name = Release; 414 | }; 415 | 84FA64A128310BCA00F10993 /* Debug */ = { 416 | isa = XCBuildConfiguration; 417 | buildSettings = { 418 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 419 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 420 | CODE_SIGN_STYLE = Automatic; 421 | CURRENT_PROJECT_VERSION = 1; 422 | DEVELOPMENT_TEAM = MTMVBPSRA5; 423 | GENERATE_INFOPLIST_FILE = YES; 424 | INFOPLIST_FILE = CleanNetworkExample/Info.plist; 425 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 426 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 427 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 428 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 429 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 430 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 431 | LD_RUNPATH_SEARCH_PATHS = ( 432 | "$(inherited)", 433 | "@executable_path/Frameworks", 434 | ); 435 | MARKETING_VERSION = 1.0; 436 | PRODUCT_BUNDLE_IDENTIFIER = com.alperenunal.CleanNetworkExample; 437 | PRODUCT_NAME = "$(TARGET_NAME)"; 438 | SWIFT_EMIT_LOC_STRINGS = YES; 439 | SWIFT_VERSION = 5.0; 440 | TARGETED_DEVICE_FAMILY = "1,2"; 441 | }; 442 | name = Debug; 443 | }; 444 | 84FA64A228310BCA00F10993 /* Release */ = { 445 | isa = XCBuildConfiguration; 446 | buildSettings = { 447 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 448 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 449 | CODE_SIGN_STYLE = Automatic; 450 | CURRENT_PROJECT_VERSION = 1; 451 | DEVELOPMENT_TEAM = MTMVBPSRA5; 452 | GENERATE_INFOPLIST_FILE = YES; 453 | INFOPLIST_FILE = CleanNetworkExample/Info.plist; 454 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 455 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 456 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 457 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 458 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 459 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 460 | LD_RUNPATH_SEARCH_PATHS = ( 461 | "$(inherited)", 462 | "@executable_path/Frameworks", 463 | ); 464 | MARKETING_VERSION = 1.0; 465 | PRODUCT_BUNDLE_IDENTIFIER = com.alperenunal.CleanNetworkExample; 466 | PRODUCT_NAME = "$(TARGET_NAME)"; 467 | SWIFT_EMIT_LOC_STRINGS = YES; 468 | SWIFT_VERSION = 5.0; 469 | TARGETED_DEVICE_FAMILY = "1,2"; 470 | }; 471 | name = Release; 472 | }; 473 | /* End XCBuildConfiguration section */ 474 | 475 | /* Begin XCConfigurationList section */ 476 | 84FA648728310BCA00F10993 /* Build configuration list for PBXProject "CleanNetworkExample" */ = { 477 | isa = XCConfigurationList; 478 | buildConfigurations = ( 479 | 84FA649E28310BCA00F10993 /* Debug */, 480 | 84FA649F28310BCA00F10993 /* Release */, 481 | ); 482 | defaultConfigurationIsVisible = 0; 483 | defaultConfigurationName = Release; 484 | }; 485 | 84FA64A028310BCA00F10993 /* Build configuration list for PBXNativeTarget "CleanNetworkExample" */ = { 486 | isa = XCConfigurationList; 487 | buildConfigurations = ( 488 | 84FA64A128310BCA00F10993 /* Debug */, 489 | 84FA64A228310BCA00F10993 /* Release */, 490 | ); 491 | defaultConfigurationIsVisible = 0; 492 | defaultConfigurationName = Release; 493 | }; 494 | /* End XCConfigurationList section */ 495 | 496 | /* Begin XCRemoteSwiftPackageReference section */ 497 | 84462D9528539F7100A22ECD /* XCRemoteSwiftPackageReference "CleanNetwork" */ = { 498 | isa = XCRemoteSwiftPackageReference; 499 | repositoryURL = "https://github.com/alperen23230/CleanNetwork"; 500 | requirement = { 501 | branch = main; 502 | kind = branch; 503 | }; 504 | }; 505 | /* End XCRemoteSwiftPackageReference section */ 506 | 507 | /* Begin XCSwiftPackageProductDependency section */ 508 | 84462D9628539F7100A22ECD /* CleanNetwork */ = { 509 | isa = XCSwiftPackageProductDependency; 510 | package = 84462D9528539F7100A22ECD /* XCRemoteSwiftPackageReference "CleanNetwork" */; 511 | productName = CleanNetwork; 512 | }; 513 | /* End XCSwiftPackageProductDependency section */ 514 | }; 515 | rootObject = 84FA648428310BCA00F10993 /* Project object */; 516 | } 517 | --------------------------------------------------------------------------------