├── 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 | 
4 | 
5 | 
6 | 
7 | [](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 |
--------------------------------------------------------------------------------