├── .DS_Store ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── URLSessionAsyncNetworking.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── URLSessionAsyncNetworking ├── .DS_Store ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Controller │ ├── ViewController+UITableViewDataSource.swift │ └── ViewController.swift ├── Core │ ├── AppDelegate.swift │ └── SceneDelegate.swift ├── Info.plist ├── Model │ └── Post.swift └── Networking │ ├── .DS_Store │ ├── Global Models │ └── Global Models.swift │ ├── Multipart │ ├── Data+String.swift │ ├── DataField.swift │ ├── MimeType.swift │ └── NSMutableData+String.swift │ ├── Parameters │ └── Parameters.swift │ ├── Route │ ├── Route.swift │ └── TestRouter.swift │ ├── Service │ ├── Main │ │ └── Service.swift │ ├── ResponseCompletion.swift │ ├── ResponseHandler │ │ ├── Errors │ │ │ └── ResponseHandlerError.swift │ │ └── Main │ │ │ └── ResponseHandler.swift │ └── URLResponseHandler │ │ ├── Errors │ │ ├── ClientHTTPResponseError.swift │ │ └── ServerHTTPResponseError.swift │ │ └── Main │ │ └── URLResponseHandler.swift │ └── URLRequest │ ├── URLRequest+AcceptType.swift │ ├── URLRequest+Authorization.swift │ ├── URLRequest+Body.swift │ ├── URLRequest+ContentType.swift │ ├── URLRequest+HTTPMethod.swift │ └── URLRequest+Query.swift ├── URLSessionAsyncNetworkingTests └── URLSessionAsyncNetworkingTests.swift └── URLSessionAsyncNetworkingUITests ├── URLSessionAsyncNetworkingUITests.swift └── URLSessionAsyncNetworkingUITestsLaunchTests.swift /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmedfathy-m/URLSession-Modern-Concurrency/f0904363149d85ff4c8444a404bf9b8e7a16962f/.DS_Store -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ahmed Fathy 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # URLSession Modern Concurrency 2 | A networking layer based on URLSession with modern concurrency and multipart request capabilities and support for iOS 13.0+. 3 | 4 | ## Why: Modern Concurrency 5 | URLSession does support Modern Concurrency using the [data(for:delegate:)](https://developer.apple.com/documentation/foundation/urlsession/3767352-data) method but it's supported only from iOS 15.0 and upwards. So, if you're looking to support older systems. You're out of luck. 6 | 7 | ## How: Modern Concurrency 8 | In Swift 5.5, when async functions were introduced. It also introduced a way to wrap our older asynchronus functions in an async function and call them using [withCheckedContinuation(function:_:)](https://developer.apple.com/documentation/swift/withcheckedcontinuation(function:_:)) and its multiple variations. 9 | ``` 10 | func loadService(route: Route, model: Model.Type) async throws -> Model { 11 | try await withCheckedThrowingContinuation { continuation in 12 | performRequest(route: route, model: model.self) { model, error in 13 | if let error { 14 | continuation.resume(throwing: error) 15 | } else { 16 | continuation.resume(returning: model!) 17 | } 18 | } 19 | } 20 | } 21 | ``` 22 | 23 | The earlier code snippet uses [withCheckedThrowingContinuation(function:_:)](https://developer.apple.com/documentation/swift/withcheckedthrowingcontinuation(function:_:)) which allows me to throw an error if the call failed for whatever reason using the `resume(throwing:)` function. 24 | 25 | 26 | ## Why: Multipart Requests 27 | There's no direct way to make API requests with multipart content like uploading images, audio or any kind of files using URLSession and that's why people mostly use Alamofire to provide this capability. 28 | 29 | ## How: Multipart Requests 30 | [Orestis Papadopoulos](https://orjpap.github.io/) provided a detailed guide on [how to make multipart requests using URLSession](https://orjpap.github.io/swift/http/ios/urlsession/2021/04/26/Multipart-Form-Requests.html) which I encorporated in how my Router enums work 31 | ``` 32 | fileprivate func toMultiPart() -> Data? { 33 | if case .multipart(let boundary) = self.contentType { 34 | var data = Data() 35 | data.append(body.toMultiPart(with: boundary)) 36 | data.append(dataFields.toMultiPart(with: boundary)) 37 | data.append("--\(boundary)--") 38 | return data 39 | } else { 40 | return nil 41 | } 42 | } 43 | ``` 44 | 45 | This code generates multipart body segments form textFields and dataFields from the route object and joins them together to use them in the `httpBody` of the `URLRequest`. 46 | 47 | 48 | # Usage 49 | ## How to create a router? 50 | The router is mainly, an enum that conforms to the `Route` protocol. 51 | 52 | In the repository, I provided an example router object 53 | ``` 54 | enum Router { 55 | case placeholder 56 | // Add as many case as you need 57 | } 58 | ``` 59 | Create an extension that conforms to the `Route` protocol 60 | ``` 61 | extension Router: Route { 62 | var baseURL: String { 63 | "https://jsonplaceholder.typicode.com" 64 | } 65 | 66 | var routePath: String { 67 | switch self { 68 | case .placeholder: 69 | return "posts" 70 | } 71 | } 72 | 73 | var method: URLRequest.HTTPMethod { 74 | switch self { 75 | case .placeholder: return .get 76 | } 77 | } 78 | 79 | var body: HTTPBodyTextFields { 80 | // This is basically a dictionary [String: Any] 81 | .empty 82 | } 83 | 84 | var query: QueryParameters { 85 | // This is basically a dictionary [String: String] 86 | .empty 87 | } 88 | 89 | var contentType: URLRequest.ContentType { 90 | .formData 91 | } 92 | 93 | var acceptType: URLRequest.AcceptType { 94 | .json 95 | } 96 | 97 | var dataFields: HTTPBodyDataFields { 98 | switch self { 99 | case .placeholder: 100 | return .empty 101 | } 102 | } 103 | } 104 | 105 | ``` 106 | 107 | # Make an API call 108 | All you have to do, now, is to make an API call by calling the `loadService(route:model:)` function 109 | ``` 110 | Task { 111 | self.posts = try await Webservice.main.loadService(route: Router.placeholder, model: [Post].self) 112 | tableView.reloadData() 113 | } 114 | ``` 115 | 116 | In the above code snippet I made an API call to retrieve an array of post objects and view the in a `UITableView` and called `tableView.reloadData()` to update the tableView. 117 | 118 | The API call runs on the background thread and switches to the main thread when it retrieves the data. That enables me to reload the table view without worrying about crashing my application. 119 | 120 | Also, because the function call is marked with await inside a Task closure (Not that the compiler will allow you to do otherwise, anyway) the app will "wait" for the call to finish before executing `tableView.reloadData()` 121 | 122 | # Suggestions and Feedback 123 | If you have any feedback or suggestions I'd greatly appreciate it. 124 | 125 | You can contact me on my [LinkedIn](https://www.linkedin.com/in/ahmedfathy-mha/) or via [email](mailto:ahmedfathy.mha@gmail.com) 126 | 127 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 74495F9329BB7DE60082C6F9 /* URLResponseHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74495F9229BB7DE60082C6F9 /* URLResponseHandler.swift */; }; 11 | 74495F9529BB86860082C6F9 /* ClientHTTPResponseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74495F9429BB86860082C6F9 /* ClientHTTPResponseError.swift */; }; 12 | 74495F9729BB869A0082C6F9 /* ServerHTTPResponseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74495F9629BB869A0082C6F9 /* ServerHTTPResponseError.swift */; }; 13 | 74A4910229BCC386000504D5 /* ViewController+UITableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74A4910129BCC386000504D5 /* ViewController+UITableViewDataSource.swift */; }; 14 | 74E8322329BB515100E9FFAE /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74E8322229BB515100E9FFAE /* Post.swift */; }; 15 | 8C7117B829B88F67000485D0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7117B729B88F67000485D0 /* AppDelegate.swift */; }; 16 | 8C7117BA29B88F67000485D0 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7117B929B88F67000485D0 /* SceneDelegate.swift */; }; 17 | 8C7117BC29B88F67000485D0 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7117BB29B88F67000485D0 /* ViewController.swift */; }; 18 | 8C7117BF29B88F67000485D0 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8C7117BD29B88F67000485D0 /* Main.storyboard */; }; 19 | 8C7117C129B88F70000485D0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8C7117C029B88F70000485D0 /* Assets.xcassets */; }; 20 | 8C7117C429B88F70000485D0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8C7117C229B88F70000485D0 /* LaunchScreen.storyboard */; }; 21 | 8C7117CF29B88F71000485D0 /* URLSessionAsyncNetworkingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7117CE29B88F71000485D0 /* URLSessionAsyncNetworkingTests.swift */; }; 22 | 8C7117D929B88F71000485D0 /* URLSessionAsyncNetworkingUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7117D829B88F71000485D0 /* URLSessionAsyncNetworkingUITests.swift */; }; 23 | 8C7117DB29B88F71000485D0 /* URLSessionAsyncNetworkingUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7117DA29B88F71000485D0 /* URLSessionAsyncNetworkingUITestsLaunchTests.swift */; }; 24 | 8C7117EA29B890A7000485D0 /* Route.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7117E929B890A7000485D0 /* Route.swift */; }; 25 | 8C7117ED29B890FC000485D0 /* URLRequest+AcceptType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7117EC29B890FC000485D0 /* URLRequest+AcceptType.swift */; }; 26 | 8C7117EF29B89119000485D0 /* URLRequest+Authorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7117EE29B89119000485D0 /* URLRequest+Authorization.swift */; }; 27 | 8C7117F129B89136000485D0 /* URLRequest+Body.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7117F029B89136000485D0 /* URLRequest+Body.swift */; }; 28 | 8C7117F329B89157000485D0 /* URLRequest+ContentType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7117F229B89157000485D0 /* URLRequest+ContentType.swift */; }; 29 | 8C7117F529B89184000485D0 /* URLRequest+HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7117F429B89184000485D0 /* URLRequest+HTTPMethod.swift */; }; 30 | 8C7117F729B891A3000485D0 /* URLRequest+Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7117F629B891A3000485D0 /* URLRequest+Query.swift */; }; 31 | 8C7117FA29B891E9000485D0 /* Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7117F929B891E9000485D0 /* Service.swift */; }; 32 | 8C71180029B8CC6C000485D0 /* DataField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7117FF29B8CC6C000485D0 /* DataField.swift */; }; 33 | 8C71180229B8CDCB000485D0 /* NSMutableData+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C71180129B8CDCB000485D0 /* NSMutableData+String.swift */; }; 34 | 8C71180429B8CE31000485D0 /* TestRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C71180329B8CE31000485D0 /* TestRouter.swift */; }; 35 | 8C71180629B8CFC8000485D0 /* MimeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C71180529B8CFC8000485D0 /* MimeType.swift */; }; 36 | 8C71180A29B8D2E2000485D0 /* Parameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C71180929B8D2E2000485D0 /* Parameters.swift */; }; 37 | 8C71180C29B9C741000485D0 /* Data+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C71180B29B9C741000485D0 /* Data+String.swift */; }; 38 | 8C71180F29B9C852000485D0 /* ResponseHandlerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C71180E29B9C852000485D0 /* ResponseHandlerError.swift */; }; 39 | 8C71181529B9C8A7000485D0 /* ResponseHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C71181429B9C8A7000485D0 /* ResponseHandler.swift */; }; 40 | 8C71181729B9D125000485D0 /* ResponseCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C71181629B9D125000485D0 /* ResponseCompletion.swift */; }; 41 | /* End PBXBuildFile section */ 42 | 43 | /* Begin PBXContainerItemProxy section */ 44 | 8C7117CB29B88F71000485D0 /* PBXContainerItemProxy */ = { 45 | isa = PBXContainerItemProxy; 46 | containerPortal = 8C7117AC29B88F67000485D0 /* Project object */; 47 | proxyType = 1; 48 | remoteGlobalIDString = 8C7117B329B88F67000485D0; 49 | remoteInfo = URLSessionAsyncNetworking; 50 | }; 51 | 8C7117D529B88F71000485D0 /* PBXContainerItemProxy */ = { 52 | isa = PBXContainerItemProxy; 53 | containerPortal = 8C7117AC29B88F67000485D0 /* Project object */; 54 | proxyType = 1; 55 | remoteGlobalIDString = 8C7117B329B88F67000485D0; 56 | remoteInfo = URLSessionAsyncNetworking; 57 | }; 58 | /* End PBXContainerItemProxy section */ 59 | 60 | /* Begin PBXFileReference section */ 61 | 74495F9229BB7DE60082C6F9 /* URLResponseHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLResponseHandler.swift; sourceTree = ""; }; 62 | 74495F9429BB86860082C6F9 /* ClientHTTPResponseError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientHTTPResponseError.swift; sourceTree = ""; }; 63 | 74495F9629BB869A0082C6F9 /* ServerHTTPResponseError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerHTTPResponseError.swift; sourceTree = ""; }; 64 | 74A4910129BCC386000504D5 /* ViewController+UITableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewController+UITableViewDataSource.swift"; sourceTree = ""; }; 65 | 74E8322229BB515100E9FFAE /* Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = ""; }; 66 | 8C7117B429B88F67000485D0 /* URLSessionAsyncNetworking.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = URLSessionAsyncNetworking.app; sourceTree = BUILT_PRODUCTS_DIR; }; 67 | 8C7117B729B88F67000485D0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 68 | 8C7117B929B88F67000485D0 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 69 | 8C7117BB29B88F67000485D0 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 70 | 8C7117BE29B88F67000485D0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 71 | 8C7117C029B88F70000485D0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 72 | 8C7117C329B88F70000485D0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 73 | 8C7117C529B88F70000485D0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 74 | 8C7117CA29B88F71000485D0 /* URLSessionAsyncNetworkingTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = URLSessionAsyncNetworkingTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 75 | 8C7117CE29B88F71000485D0 /* URLSessionAsyncNetworkingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionAsyncNetworkingTests.swift; sourceTree = ""; }; 76 | 8C7117D429B88F71000485D0 /* URLSessionAsyncNetworkingUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = URLSessionAsyncNetworkingUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 77 | 8C7117D829B88F71000485D0 /* URLSessionAsyncNetworkingUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionAsyncNetworkingUITests.swift; sourceTree = ""; }; 78 | 8C7117DA29B88F71000485D0 /* URLSessionAsyncNetworkingUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionAsyncNetworkingUITestsLaunchTests.swift; sourceTree = ""; }; 79 | 8C7117E929B890A7000485D0 /* Route.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Route.swift; sourceTree = ""; }; 80 | 8C7117EC29B890FC000485D0 /* URLRequest+AcceptType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLRequest+AcceptType.swift"; sourceTree = ""; }; 81 | 8C7117EE29B89119000485D0 /* URLRequest+Authorization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLRequest+Authorization.swift"; sourceTree = ""; }; 82 | 8C7117F029B89136000485D0 /* URLRequest+Body.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLRequest+Body.swift"; sourceTree = ""; }; 83 | 8C7117F229B89157000485D0 /* URLRequest+ContentType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLRequest+ContentType.swift"; sourceTree = ""; }; 84 | 8C7117F429B89184000485D0 /* URLRequest+HTTPMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLRequest+HTTPMethod.swift"; sourceTree = ""; }; 85 | 8C7117F629B891A3000485D0 /* URLRequest+Query.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLRequest+Query.swift"; sourceTree = ""; }; 86 | 8C7117F929B891E9000485D0 /* Service.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Service.swift; sourceTree = ""; }; 87 | 8C7117FF29B8CC6C000485D0 /* DataField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataField.swift; sourceTree = ""; }; 88 | 8C71180129B8CDCB000485D0 /* NSMutableData+String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSMutableData+String.swift"; sourceTree = ""; }; 89 | 8C71180329B8CE31000485D0 /* TestRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestRouter.swift; sourceTree = ""; }; 90 | 8C71180529B8CFC8000485D0 /* MimeType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MimeType.swift; sourceTree = ""; }; 91 | 8C71180929B8D2E2000485D0 /* Parameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Parameters.swift; sourceTree = ""; }; 92 | 8C71180B29B9C741000485D0 /* Data+String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+String.swift"; sourceTree = ""; }; 93 | 8C71180E29B9C852000485D0 /* ResponseHandlerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseHandlerError.swift; sourceTree = ""; }; 94 | 8C71181429B9C8A7000485D0 /* ResponseHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseHandler.swift; sourceTree = ""; }; 95 | 8C71181629B9D125000485D0 /* ResponseCompletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseCompletion.swift; sourceTree = ""; }; 96 | /* End PBXFileReference section */ 97 | 98 | /* Begin PBXFrameworksBuildPhase section */ 99 | 8C7117B129B88F67000485D0 /* Frameworks */ = { 100 | isa = PBXFrameworksBuildPhase; 101 | buildActionMask = 2147483647; 102 | files = ( 103 | ); 104 | runOnlyForDeploymentPostprocessing = 0; 105 | }; 106 | 8C7117C729B88F71000485D0 /* Frameworks */ = { 107 | isa = PBXFrameworksBuildPhase; 108 | buildActionMask = 2147483647; 109 | files = ( 110 | ); 111 | runOnlyForDeploymentPostprocessing = 0; 112 | }; 113 | 8C7117D129B88F71000485D0 /* Frameworks */ = { 114 | isa = PBXFrameworksBuildPhase; 115 | buildActionMask = 2147483647; 116 | files = ( 117 | ); 118 | runOnlyForDeploymentPostprocessing = 0; 119 | }; 120 | /* End PBXFrameworksBuildPhase section */ 121 | 122 | /* Begin PBXGroup section */ 123 | 74A4910029BCC375000504D5 /* Controller */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | 8C7117BB29B88F67000485D0 /* ViewController.swift */, 127 | 74A4910129BCC386000504D5 /* ViewController+UITableViewDataSource.swift */, 128 | ); 129 | path = Controller; 130 | sourceTree = ""; 131 | }; 132 | 74A4910329BCC3FB000504D5 /* Core */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 8C7117B729B88F67000485D0 /* AppDelegate.swift */, 136 | 8C7117B929B88F67000485D0 /* SceneDelegate.swift */, 137 | ); 138 | path = Core; 139 | sourceTree = ""; 140 | }; 141 | 74A4910429BCC46B000504D5 /* URLResponseHandler */ = { 142 | isa = PBXGroup; 143 | children = ( 144 | 74A4910529BCC475000504D5 /* Main */, 145 | 74A4910629BCC47A000504D5 /* Errors */, 146 | ); 147 | path = URLResponseHandler; 148 | sourceTree = ""; 149 | }; 150 | 74A4910529BCC475000504D5 /* Main */ = { 151 | isa = PBXGroup; 152 | children = ( 153 | 74495F9229BB7DE60082C6F9 /* URLResponseHandler.swift */, 154 | ); 155 | path = Main; 156 | sourceTree = ""; 157 | }; 158 | 74A4910629BCC47A000504D5 /* Errors */ = { 159 | isa = PBXGroup; 160 | children = ( 161 | 74495F9429BB86860082C6F9 /* ClientHTTPResponseError.swift */, 162 | 74495F9629BB869A0082C6F9 /* ServerHTTPResponseError.swift */, 163 | ); 164 | path = Errors; 165 | sourceTree = ""; 166 | }; 167 | 74A4910729BCC4AB000504D5 /* Main */ = { 168 | isa = PBXGroup; 169 | children = ( 170 | 8C71181429B9C8A7000485D0 /* ResponseHandler.swift */, 171 | ); 172 | path = Main; 173 | sourceTree = ""; 174 | }; 175 | 74A4910829BCC4B3000504D5 /* ResponseHandler */ = { 176 | isa = PBXGroup; 177 | children = ( 178 | 74A4910729BCC4AB000504D5 /* Main */, 179 | 8C71180D29B9C83F000485D0 /* Errors */, 180 | ); 181 | path = ResponseHandler; 182 | sourceTree = ""; 183 | }; 184 | 74A4910929BCC4D4000504D5 /* Main */ = { 185 | isa = PBXGroup; 186 | children = ( 187 | 8C7117F929B891E9000485D0 /* Service.swift */, 188 | ); 189 | path = Main; 190 | sourceTree = ""; 191 | }; 192 | 74E8322129BB514800E9FFAE /* Model */ = { 193 | isa = PBXGroup; 194 | children = ( 195 | 74E8322229BB515100E9FFAE /* Post.swift */, 196 | ); 197 | path = Model; 198 | sourceTree = ""; 199 | }; 200 | 8C7117AB29B88F67000485D0 = { 201 | isa = PBXGroup; 202 | children = ( 203 | 8C7117B629B88F67000485D0 /* URLSessionAsyncNetworking */, 204 | 8C7117CD29B88F71000485D0 /* URLSessionAsyncNetworkingTests */, 205 | 8C7117D729B88F71000485D0 /* URLSessionAsyncNetworkingUITests */, 206 | 8C7117B529B88F67000485D0 /* Products */, 207 | ); 208 | sourceTree = ""; 209 | }; 210 | 8C7117B529B88F67000485D0 /* Products */ = { 211 | isa = PBXGroup; 212 | children = ( 213 | 8C7117B429B88F67000485D0 /* URLSessionAsyncNetworking.app */, 214 | 8C7117CA29B88F71000485D0 /* URLSessionAsyncNetworkingTests.xctest */, 215 | 8C7117D429B88F71000485D0 /* URLSessionAsyncNetworkingUITests.xctest */, 216 | ); 217 | name = Products; 218 | sourceTree = ""; 219 | }; 220 | 8C7117B629B88F67000485D0 /* URLSessionAsyncNetworking */ = { 221 | isa = PBXGroup; 222 | children = ( 223 | 74E8322129BB514800E9FFAE /* Model */, 224 | 8C7117E729B88F82000485D0 /* Networking */, 225 | 74A4910329BCC3FB000504D5 /* Core */, 226 | 74A4910029BCC375000504D5 /* Controller */, 227 | 8C7117BD29B88F67000485D0 /* Main.storyboard */, 228 | 8C7117C029B88F70000485D0 /* Assets.xcassets */, 229 | 8C7117C229B88F70000485D0 /* LaunchScreen.storyboard */, 230 | 8C7117C529B88F70000485D0 /* Info.plist */, 231 | ); 232 | path = URLSessionAsyncNetworking; 233 | sourceTree = ""; 234 | }; 235 | 8C7117CD29B88F71000485D0 /* URLSessionAsyncNetworkingTests */ = { 236 | isa = PBXGroup; 237 | children = ( 238 | 8C7117CE29B88F71000485D0 /* URLSessionAsyncNetworkingTests.swift */, 239 | ); 240 | path = URLSessionAsyncNetworkingTests; 241 | sourceTree = ""; 242 | }; 243 | 8C7117D729B88F71000485D0 /* URLSessionAsyncNetworkingUITests */ = { 244 | isa = PBXGroup; 245 | children = ( 246 | 8C7117D829B88F71000485D0 /* URLSessionAsyncNetworkingUITests.swift */, 247 | 8C7117DA29B88F71000485D0 /* URLSessionAsyncNetworkingUITestsLaunchTests.swift */, 248 | ); 249 | path = URLSessionAsyncNetworkingUITests; 250 | sourceTree = ""; 251 | }; 252 | 8C7117E729B88F82000485D0 /* Networking */ = { 253 | isa = PBXGroup; 254 | children = ( 255 | 8C71180829B8D2C7000485D0 /* Parameters */, 256 | 8C7117FE29B8CC4C000485D0 /* Multipart */, 257 | 8C7117F829B891B3000485D0 /* Service */, 258 | 8C7117EB29B890E7000485D0 /* URLRequest */, 259 | 8C7117E829B88F8E000485D0 /* Route */, 260 | ); 261 | path = Networking; 262 | sourceTree = ""; 263 | }; 264 | 8C7117E829B88F8E000485D0 /* Route */ = { 265 | isa = PBXGroup; 266 | children = ( 267 | 8C7117E929B890A7000485D0 /* Route.swift */, 268 | 8C71180329B8CE31000485D0 /* TestRouter.swift */, 269 | ); 270 | path = Route; 271 | sourceTree = ""; 272 | }; 273 | 8C7117EB29B890E7000485D0 /* URLRequest */ = { 274 | isa = PBXGroup; 275 | children = ( 276 | 8C7117EC29B890FC000485D0 /* URLRequest+AcceptType.swift */, 277 | 8C7117EE29B89119000485D0 /* URLRequest+Authorization.swift */, 278 | 8C7117F029B89136000485D0 /* URLRequest+Body.swift */, 279 | 8C7117F229B89157000485D0 /* URLRequest+ContentType.swift */, 280 | 8C7117F429B89184000485D0 /* URLRequest+HTTPMethod.swift */, 281 | 8C7117F629B891A3000485D0 /* URLRequest+Query.swift */, 282 | ); 283 | path = URLRequest; 284 | sourceTree = ""; 285 | }; 286 | 8C7117F829B891B3000485D0 /* Service */ = { 287 | isa = PBXGroup; 288 | children = ( 289 | 74A4910929BCC4D4000504D5 /* Main */, 290 | 74A4910829BCC4B3000504D5 /* ResponseHandler */, 291 | 74A4910429BCC46B000504D5 /* URLResponseHandler */, 292 | 8C71181629B9D125000485D0 /* ResponseCompletion.swift */, 293 | ); 294 | path = Service; 295 | sourceTree = ""; 296 | }; 297 | 8C7117FE29B8CC4C000485D0 /* Multipart */ = { 298 | isa = PBXGroup; 299 | children = ( 300 | 8C7117FF29B8CC6C000485D0 /* DataField.swift */, 301 | 8C71180129B8CDCB000485D0 /* NSMutableData+String.swift */, 302 | 8C71180529B8CFC8000485D0 /* MimeType.swift */, 303 | 8C71180B29B9C741000485D0 /* Data+String.swift */, 304 | ); 305 | path = Multipart; 306 | sourceTree = ""; 307 | }; 308 | 8C71180829B8D2C7000485D0 /* Parameters */ = { 309 | isa = PBXGroup; 310 | children = ( 311 | 8C71180929B8D2E2000485D0 /* Parameters.swift */, 312 | ); 313 | path = Parameters; 314 | sourceTree = ""; 315 | }; 316 | 8C71180D29B9C83F000485D0 /* Errors */ = { 317 | isa = PBXGroup; 318 | children = ( 319 | 8C71180E29B9C852000485D0 /* ResponseHandlerError.swift */, 320 | ); 321 | path = Errors; 322 | sourceTree = ""; 323 | }; 324 | /* End PBXGroup section */ 325 | 326 | /* Begin PBXNativeTarget section */ 327 | 8C7117B329B88F67000485D0 /* URLSessionAsyncNetworking */ = { 328 | isa = PBXNativeTarget; 329 | buildConfigurationList = 8C7117DE29B88F71000485D0 /* Build configuration list for PBXNativeTarget "URLSessionAsyncNetworking" */; 330 | buildPhases = ( 331 | 8C7117B029B88F67000485D0 /* Sources */, 332 | 8C7117B129B88F67000485D0 /* Frameworks */, 333 | 8C7117B229B88F67000485D0 /* Resources */, 334 | ); 335 | buildRules = ( 336 | ); 337 | dependencies = ( 338 | ); 339 | name = URLSessionAsyncNetworking; 340 | packageProductDependencies = ( 341 | ); 342 | productName = URLSessionAsyncNetworking; 343 | productReference = 8C7117B429B88F67000485D0 /* URLSessionAsyncNetworking.app */; 344 | productType = "com.apple.product-type.application"; 345 | }; 346 | 8C7117C929B88F71000485D0 /* URLSessionAsyncNetworkingTests */ = { 347 | isa = PBXNativeTarget; 348 | buildConfigurationList = 8C7117E129B88F71000485D0 /* Build configuration list for PBXNativeTarget "URLSessionAsyncNetworkingTests" */; 349 | buildPhases = ( 350 | 8C7117C629B88F71000485D0 /* Sources */, 351 | 8C7117C729B88F71000485D0 /* Frameworks */, 352 | 8C7117C829B88F71000485D0 /* Resources */, 353 | ); 354 | buildRules = ( 355 | ); 356 | dependencies = ( 357 | 8C7117CC29B88F71000485D0 /* PBXTargetDependency */, 358 | ); 359 | name = URLSessionAsyncNetworkingTests; 360 | productName = URLSessionAsyncNetworkingTests; 361 | productReference = 8C7117CA29B88F71000485D0 /* URLSessionAsyncNetworkingTests.xctest */; 362 | productType = "com.apple.product-type.bundle.unit-test"; 363 | }; 364 | 8C7117D329B88F71000485D0 /* URLSessionAsyncNetworkingUITests */ = { 365 | isa = PBXNativeTarget; 366 | buildConfigurationList = 8C7117E429B88F71000485D0 /* Build configuration list for PBXNativeTarget "URLSessionAsyncNetworkingUITests" */; 367 | buildPhases = ( 368 | 8C7117D029B88F71000485D0 /* Sources */, 369 | 8C7117D129B88F71000485D0 /* Frameworks */, 370 | 8C7117D229B88F71000485D0 /* Resources */, 371 | ); 372 | buildRules = ( 373 | ); 374 | dependencies = ( 375 | 8C7117D629B88F71000485D0 /* PBXTargetDependency */, 376 | ); 377 | name = URLSessionAsyncNetworkingUITests; 378 | productName = URLSessionAsyncNetworkingUITests; 379 | productReference = 8C7117D429B88F71000485D0 /* URLSessionAsyncNetworkingUITests.xctest */; 380 | productType = "com.apple.product-type.bundle.ui-testing"; 381 | }; 382 | /* End PBXNativeTarget section */ 383 | 384 | /* Begin PBXProject section */ 385 | 8C7117AC29B88F67000485D0 /* Project object */ = { 386 | isa = PBXProject; 387 | attributes = { 388 | BuildIndependentTargetsInParallel = 1; 389 | LastSwiftUpdateCheck = 1420; 390 | LastUpgradeCheck = 1420; 391 | TargetAttributes = { 392 | 8C7117B329B88F67000485D0 = { 393 | CreatedOnToolsVersion = 14.2; 394 | }; 395 | 8C7117C929B88F71000485D0 = { 396 | CreatedOnToolsVersion = 14.2; 397 | TestTargetID = 8C7117B329B88F67000485D0; 398 | }; 399 | 8C7117D329B88F71000485D0 = { 400 | CreatedOnToolsVersion = 14.2; 401 | TestTargetID = 8C7117B329B88F67000485D0; 402 | }; 403 | }; 404 | }; 405 | buildConfigurationList = 8C7117AF29B88F67000485D0 /* Build configuration list for PBXProject "URLSessionAsyncNetworking" */; 406 | compatibilityVersion = "Xcode 14.0"; 407 | developmentRegion = en; 408 | hasScannedForEncodings = 0; 409 | knownRegions = ( 410 | en, 411 | Base, 412 | ); 413 | mainGroup = 8C7117AB29B88F67000485D0; 414 | packageReferences = ( 415 | ); 416 | productRefGroup = 8C7117B529B88F67000485D0 /* Products */; 417 | projectDirPath = ""; 418 | projectRoot = ""; 419 | targets = ( 420 | 8C7117B329B88F67000485D0 /* URLSessionAsyncNetworking */, 421 | 8C7117C929B88F71000485D0 /* URLSessionAsyncNetworkingTests */, 422 | 8C7117D329B88F71000485D0 /* URLSessionAsyncNetworkingUITests */, 423 | ); 424 | }; 425 | /* End PBXProject section */ 426 | 427 | /* Begin PBXResourcesBuildPhase section */ 428 | 8C7117B229B88F67000485D0 /* Resources */ = { 429 | isa = PBXResourcesBuildPhase; 430 | buildActionMask = 2147483647; 431 | files = ( 432 | 8C7117C429B88F70000485D0 /* LaunchScreen.storyboard in Resources */, 433 | 8C7117C129B88F70000485D0 /* Assets.xcassets in Resources */, 434 | 8C7117BF29B88F67000485D0 /* Main.storyboard in Resources */, 435 | ); 436 | runOnlyForDeploymentPostprocessing = 0; 437 | }; 438 | 8C7117C829B88F71000485D0 /* Resources */ = { 439 | isa = PBXResourcesBuildPhase; 440 | buildActionMask = 2147483647; 441 | files = ( 442 | ); 443 | runOnlyForDeploymentPostprocessing = 0; 444 | }; 445 | 8C7117D229B88F71000485D0 /* Resources */ = { 446 | isa = PBXResourcesBuildPhase; 447 | buildActionMask = 2147483647; 448 | files = ( 449 | ); 450 | runOnlyForDeploymentPostprocessing = 0; 451 | }; 452 | /* End PBXResourcesBuildPhase section */ 453 | 454 | /* Begin PBXSourcesBuildPhase section */ 455 | 8C7117B029B88F67000485D0 /* Sources */ = { 456 | isa = PBXSourcesBuildPhase; 457 | buildActionMask = 2147483647; 458 | files = ( 459 | 8C7117ED29B890FC000485D0 /* URLRequest+AcceptType.swift in Sources */, 460 | 8C71180029B8CC6C000485D0 /* DataField.swift in Sources */, 461 | 74E8322329BB515100E9FFAE /* Post.swift in Sources */, 462 | 8C71181529B9C8A7000485D0 /* ResponseHandler.swift in Sources */, 463 | 8C71180629B8CFC8000485D0 /* MimeType.swift in Sources */, 464 | 74495F9729BB869A0082C6F9 /* ServerHTTPResponseError.swift in Sources */, 465 | 8C7117EF29B89119000485D0 /* URLRequest+Authorization.swift in Sources */, 466 | 8C7117BC29B88F67000485D0 /* ViewController.swift in Sources */, 467 | 8C7117F529B89184000485D0 /* URLRequest+HTTPMethod.swift in Sources */, 468 | 8C7117F329B89157000485D0 /* URLRequest+ContentType.swift in Sources */, 469 | 8C71180A29B8D2E2000485D0 /* Parameters.swift in Sources */, 470 | 8C7117EA29B890A7000485D0 /* Route.swift in Sources */, 471 | 8C7117B829B88F67000485D0 /* AppDelegate.swift in Sources */, 472 | 74A4910229BCC386000504D5 /* ViewController+UITableViewDataSource.swift in Sources */, 473 | 8C7117BA29B88F67000485D0 /* SceneDelegate.swift in Sources */, 474 | 8C7117FA29B891E9000485D0 /* Service.swift in Sources */, 475 | 8C71180429B8CE31000485D0 /* TestRouter.swift in Sources */, 476 | 8C7117F729B891A3000485D0 /* URLRequest+Query.swift in Sources */, 477 | 8C71180229B8CDCB000485D0 /* NSMutableData+String.swift in Sources */, 478 | 8C71181729B9D125000485D0 /* ResponseCompletion.swift in Sources */, 479 | 8C71180C29B9C741000485D0 /* Data+String.swift in Sources */, 480 | 74495F9329BB7DE60082C6F9 /* URLResponseHandler.swift in Sources */, 481 | 8C7117F129B89136000485D0 /* URLRequest+Body.swift in Sources */, 482 | 74495F9529BB86860082C6F9 /* ClientHTTPResponseError.swift in Sources */, 483 | 8C71180F29B9C852000485D0 /* ResponseHandlerError.swift in Sources */, 484 | ); 485 | runOnlyForDeploymentPostprocessing = 0; 486 | }; 487 | 8C7117C629B88F71000485D0 /* Sources */ = { 488 | isa = PBXSourcesBuildPhase; 489 | buildActionMask = 2147483647; 490 | files = ( 491 | 8C7117CF29B88F71000485D0 /* URLSessionAsyncNetworkingTests.swift in Sources */, 492 | ); 493 | runOnlyForDeploymentPostprocessing = 0; 494 | }; 495 | 8C7117D029B88F71000485D0 /* Sources */ = { 496 | isa = PBXSourcesBuildPhase; 497 | buildActionMask = 2147483647; 498 | files = ( 499 | 8C7117DB29B88F71000485D0 /* URLSessionAsyncNetworkingUITestsLaunchTests.swift in Sources */, 500 | 8C7117D929B88F71000485D0 /* URLSessionAsyncNetworkingUITests.swift in Sources */, 501 | ); 502 | runOnlyForDeploymentPostprocessing = 0; 503 | }; 504 | /* End PBXSourcesBuildPhase section */ 505 | 506 | /* Begin PBXTargetDependency section */ 507 | 8C7117CC29B88F71000485D0 /* PBXTargetDependency */ = { 508 | isa = PBXTargetDependency; 509 | target = 8C7117B329B88F67000485D0 /* URLSessionAsyncNetworking */; 510 | targetProxy = 8C7117CB29B88F71000485D0 /* PBXContainerItemProxy */; 511 | }; 512 | 8C7117D629B88F71000485D0 /* PBXTargetDependency */ = { 513 | isa = PBXTargetDependency; 514 | target = 8C7117B329B88F67000485D0 /* URLSessionAsyncNetworking */; 515 | targetProxy = 8C7117D529B88F71000485D0 /* PBXContainerItemProxy */; 516 | }; 517 | /* End PBXTargetDependency section */ 518 | 519 | /* Begin PBXVariantGroup section */ 520 | 8C7117BD29B88F67000485D0 /* Main.storyboard */ = { 521 | isa = PBXVariantGroup; 522 | children = ( 523 | 8C7117BE29B88F67000485D0 /* Base */, 524 | ); 525 | name = Main.storyboard; 526 | sourceTree = ""; 527 | }; 528 | 8C7117C229B88F70000485D0 /* LaunchScreen.storyboard */ = { 529 | isa = PBXVariantGroup; 530 | children = ( 531 | 8C7117C329B88F70000485D0 /* Base */, 532 | ); 533 | name = LaunchScreen.storyboard; 534 | sourceTree = ""; 535 | }; 536 | /* End PBXVariantGroup section */ 537 | 538 | /* Begin XCBuildConfiguration section */ 539 | 8C7117DC29B88F71000485D0 /* Debug */ = { 540 | isa = XCBuildConfiguration; 541 | buildSettings = { 542 | ALWAYS_SEARCH_USER_PATHS = NO; 543 | CLANG_ANALYZER_NONNULL = YES; 544 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 545 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 546 | CLANG_ENABLE_MODULES = YES; 547 | CLANG_ENABLE_OBJC_ARC = YES; 548 | CLANG_ENABLE_OBJC_WEAK = YES; 549 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 550 | CLANG_WARN_BOOL_CONVERSION = YES; 551 | CLANG_WARN_COMMA = YES; 552 | CLANG_WARN_CONSTANT_CONVERSION = YES; 553 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 554 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 555 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 556 | CLANG_WARN_EMPTY_BODY = YES; 557 | CLANG_WARN_ENUM_CONVERSION = YES; 558 | CLANG_WARN_INFINITE_RECURSION = YES; 559 | CLANG_WARN_INT_CONVERSION = YES; 560 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 561 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 562 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 563 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 564 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 565 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 566 | CLANG_WARN_STRICT_PROTOTYPES = YES; 567 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 568 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 569 | CLANG_WARN_UNREACHABLE_CODE = YES; 570 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 571 | COPY_PHASE_STRIP = NO; 572 | DEBUG_INFORMATION_FORMAT = dwarf; 573 | ENABLE_STRICT_OBJC_MSGSEND = YES; 574 | ENABLE_TESTABILITY = YES; 575 | GCC_C_LANGUAGE_STANDARD = gnu11; 576 | GCC_DYNAMIC_NO_PIC = NO; 577 | GCC_NO_COMMON_BLOCKS = YES; 578 | GCC_OPTIMIZATION_LEVEL = 0; 579 | GCC_PREPROCESSOR_DEFINITIONS = ( 580 | "DEBUG=1", 581 | "$(inherited)", 582 | ); 583 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 584 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 585 | GCC_WARN_UNDECLARED_SELECTOR = YES; 586 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 587 | GCC_WARN_UNUSED_FUNCTION = YES; 588 | GCC_WARN_UNUSED_VARIABLE = YES; 589 | INFOPLIST_FILE = URLSessionAsyncNetworking/Info.plist; 590 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 591 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 592 | MTL_FAST_MATH = YES; 593 | ONLY_ACTIVE_ARCH = YES; 594 | SDKROOT = iphoneos; 595 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 596 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 597 | }; 598 | name = Debug; 599 | }; 600 | 8C7117DD29B88F71000485D0 /* Release */ = { 601 | isa = XCBuildConfiguration; 602 | buildSettings = { 603 | ALWAYS_SEARCH_USER_PATHS = NO; 604 | CLANG_ANALYZER_NONNULL = YES; 605 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 606 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 607 | CLANG_ENABLE_MODULES = YES; 608 | CLANG_ENABLE_OBJC_ARC = YES; 609 | CLANG_ENABLE_OBJC_WEAK = YES; 610 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 611 | CLANG_WARN_BOOL_CONVERSION = YES; 612 | CLANG_WARN_COMMA = YES; 613 | CLANG_WARN_CONSTANT_CONVERSION = YES; 614 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 615 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 616 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 617 | CLANG_WARN_EMPTY_BODY = YES; 618 | CLANG_WARN_ENUM_CONVERSION = YES; 619 | CLANG_WARN_INFINITE_RECURSION = YES; 620 | CLANG_WARN_INT_CONVERSION = YES; 621 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 622 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 623 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 624 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 625 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 626 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 627 | CLANG_WARN_STRICT_PROTOTYPES = YES; 628 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 629 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 630 | CLANG_WARN_UNREACHABLE_CODE = YES; 631 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 632 | COPY_PHASE_STRIP = NO; 633 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 634 | ENABLE_NS_ASSERTIONS = NO; 635 | ENABLE_STRICT_OBJC_MSGSEND = YES; 636 | GCC_C_LANGUAGE_STANDARD = gnu11; 637 | GCC_NO_COMMON_BLOCKS = YES; 638 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 639 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 640 | GCC_WARN_UNDECLARED_SELECTOR = YES; 641 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 642 | GCC_WARN_UNUSED_FUNCTION = YES; 643 | GCC_WARN_UNUSED_VARIABLE = YES; 644 | INFOPLIST_FILE = URLSessionAsyncNetworking/Info.plist; 645 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 646 | MTL_ENABLE_DEBUG_INFO = NO; 647 | MTL_FAST_MATH = YES; 648 | SDKROOT = iphoneos; 649 | SWIFT_COMPILATION_MODE = wholemodule; 650 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 651 | VALIDATE_PRODUCT = YES; 652 | }; 653 | name = Release; 654 | }; 655 | 8C7117DF29B88F71000485D0 /* Debug */ = { 656 | isa = XCBuildConfiguration; 657 | buildSettings = { 658 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 659 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 660 | CODE_SIGN_STYLE = Automatic; 661 | CURRENT_PROJECT_VERSION = 1; 662 | DEVELOPMENT_TEAM = WZ92WPK5PN; 663 | GENERATE_INFOPLIST_FILE = YES; 664 | INFOPLIST_FILE = URLSessionAsyncNetworking/Info.plist; 665 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 666 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 667 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 668 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 669 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 670 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 671 | LD_RUNPATH_SEARCH_PATHS = ( 672 | "$(inherited)", 673 | "@executable_path/Frameworks", 674 | ); 675 | MARKETING_VERSION = 1.0; 676 | PRODUCT_BUNDLE_IDENTIFIER = com.fathy.URLSessionAsyncNetworking; 677 | PRODUCT_NAME = "$(TARGET_NAME)"; 678 | SWIFT_EMIT_LOC_STRINGS = YES; 679 | SWIFT_VERSION = 5.0; 680 | TARGETED_DEVICE_FAMILY = "1,2"; 681 | }; 682 | name = Debug; 683 | }; 684 | 8C7117E029B88F71000485D0 /* Release */ = { 685 | isa = XCBuildConfiguration; 686 | buildSettings = { 687 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 688 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 689 | CODE_SIGN_STYLE = Automatic; 690 | CURRENT_PROJECT_VERSION = 1; 691 | DEVELOPMENT_TEAM = WZ92WPK5PN; 692 | GENERATE_INFOPLIST_FILE = YES; 693 | INFOPLIST_FILE = URLSessionAsyncNetworking/Info.plist; 694 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 695 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 696 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 697 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 698 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 699 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 700 | LD_RUNPATH_SEARCH_PATHS = ( 701 | "$(inherited)", 702 | "@executable_path/Frameworks", 703 | ); 704 | MARKETING_VERSION = 1.0; 705 | PRODUCT_BUNDLE_IDENTIFIER = com.fathy.URLSessionAsyncNetworking; 706 | PRODUCT_NAME = "$(TARGET_NAME)"; 707 | SWIFT_EMIT_LOC_STRINGS = YES; 708 | SWIFT_VERSION = 5.0; 709 | TARGETED_DEVICE_FAMILY = "1,2"; 710 | }; 711 | name = Release; 712 | }; 713 | 8C7117E229B88F71000485D0 /* Debug */ = { 714 | isa = XCBuildConfiguration; 715 | buildSettings = { 716 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 717 | BUNDLE_LOADER = "$(TEST_HOST)"; 718 | CODE_SIGN_STYLE = Automatic; 719 | CURRENT_PROJECT_VERSION = 1; 720 | DEVELOPMENT_TEAM = WZ92WPK5PN; 721 | GENERATE_INFOPLIST_FILE = YES; 722 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 723 | MARKETING_VERSION = 1.0; 724 | PRODUCT_BUNDLE_IDENTIFIER = com.fathy.URLSessionAsyncNetworkingTests; 725 | PRODUCT_NAME = "$(TARGET_NAME)"; 726 | SWIFT_EMIT_LOC_STRINGS = NO; 727 | SWIFT_VERSION = 5.0; 728 | TARGETED_DEVICE_FAMILY = "1,2"; 729 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/URLSessionAsyncNetworking.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/URLSessionAsyncNetworking"; 730 | }; 731 | name = Debug; 732 | }; 733 | 8C7117E329B88F71000485D0 /* Release */ = { 734 | isa = XCBuildConfiguration; 735 | buildSettings = { 736 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 737 | BUNDLE_LOADER = "$(TEST_HOST)"; 738 | CODE_SIGN_STYLE = Automatic; 739 | CURRENT_PROJECT_VERSION = 1; 740 | DEVELOPMENT_TEAM = WZ92WPK5PN; 741 | GENERATE_INFOPLIST_FILE = YES; 742 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 743 | MARKETING_VERSION = 1.0; 744 | PRODUCT_BUNDLE_IDENTIFIER = com.fathy.URLSessionAsyncNetworkingTests; 745 | PRODUCT_NAME = "$(TARGET_NAME)"; 746 | SWIFT_EMIT_LOC_STRINGS = NO; 747 | SWIFT_VERSION = 5.0; 748 | TARGETED_DEVICE_FAMILY = "1,2"; 749 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/URLSessionAsyncNetworking.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/URLSessionAsyncNetworking"; 750 | }; 751 | name = Release; 752 | }; 753 | 8C7117E529B88F71000485D0 /* Debug */ = { 754 | isa = XCBuildConfiguration; 755 | buildSettings = { 756 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 757 | CODE_SIGN_STYLE = Automatic; 758 | CURRENT_PROJECT_VERSION = 1; 759 | DEVELOPMENT_TEAM = WZ92WPK5PN; 760 | GENERATE_INFOPLIST_FILE = YES; 761 | MARKETING_VERSION = 1.0; 762 | PRODUCT_BUNDLE_IDENTIFIER = com.fathy.URLSessionAsyncNetworkingUITests; 763 | PRODUCT_NAME = "$(TARGET_NAME)"; 764 | SWIFT_EMIT_LOC_STRINGS = NO; 765 | SWIFT_VERSION = 5.0; 766 | TARGETED_DEVICE_FAMILY = "1,2"; 767 | TEST_TARGET_NAME = URLSessionAsyncNetworking; 768 | }; 769 | name = Debug; 770 | }; 771 | 8C7117E629B88F71000485D0 /* Release */ = { 772 | isa = XCBuildConfiguration; 773 | buildSettings = { 774 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 775 | CODE_SIGN_STYLE = Automatic; 776 | CURRENT_PROJECT_VERSION = 1; 777 | DEVELOPMENT_TEAM = WZ92WPK5PN; 778 | GENERATE_INFOPLIST_FILE = YES; 779 | MARKETING_VERSION = 1.0; 780 | PRODUCT_BUNDLE_IDENTIFIER = com.fathy.URLSessionAsyncNetworkingUITests; 781 | PRODUCT_NAME = "$(TARGET_NAME)"; 782 | SWIFT_EMIT_LOC_STRINGS = NO; 783 | SWIFT_VERSION = 5.0; 784 | TARGETED_DEVICE_FAMILY = "1,2"; 785 | TEST_TARGET_NAME = URLSessionAsyncNetworking; 786 | }; 787 | name = Release; 788 | }; 789 | /* End XCBuildConfiguration section */ 790 | 791 | /* Begin XCConfigurationList section */ 792 | 8C7117AF29B88F67000485D0 /* Build configuration list for PBXProject "URLSessionAsyncNetworking" */ = { 793 | isa = XCConfigurationList; 794 | buildConfigurations = ( 795 | 8C7117DC29B88F71000485D0 /* Debug */, 796 | 8C7117DD29B88F71000485D0 /* Release */, 797 | ); 798 | defaultConfigurationIsVisible = 0; 799 | defaultConfigurationName = Release; 800 | }; 801 | 8C7117DE29B88F71000485D0 /* Build configuration list for PBXNativeTarget "URLSessionAsyncNetworking" */ = { 802 | isa = XCConfigurationList; 803 | buildConfigurations = ( 804 | 8C7117DF29B88F71000485D0 /* Debug */, 805 | 8C7117E029B88F71000485D0 /* Release */, 806 | ); 807 | defaultConfigurationIsVisible = 0; 808 | defaultConfigurationName = Release; 809 | }; 810 | 8C7117E129B88F71000485D0 /* Build configuration list for PBXNativeTarget "URLSessionAsyncNetworkingTests" */ = { 811 | isa = XCConfigurationList; 812 | buildConfigurations = ( 813 | 8C7117E229B88F71000485D0 /* Debug */, 814 | 8C7117E329B88F71000485D0 /* Release */, 815 | ); 816 | defaultConfigurationIsVisible = 0; 817 | defaultConfigurationName = Release; 818 | }; 819 | 8C7117E429B88F71000485D0 /* Build configuration list for PBXNativeTarget "URLSessionAsyncNetworkingUITests" */ = { 820 | isa = XCConfigurationList; 821 | buildConfigurations = ( 822 | 8C7117E529B88F71000485D0 /* Debug */, 823 | 8C7117E629B88F71000485D0 /* Release */, 824 | ); 825 | defaultConfigurationIsVisible = 0; 826 | defaultConfigurationName = Release; 827 | }; 828 | /* End XCConfigurationList section */ 829 | }; 830 | rootObject = 8C7117AC29B88F67000485D0 /* Project object */; 831 | } 832 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmedfathy-m/URLSession-Modern-Concurrency/f0904363149d85ff4c8444a404bf9b8e7a16962f/URLSessionAsyncNetworking/.DS_Store -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/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 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/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 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/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 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/Controller/ViewController+UITableViewDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController+UITableViewDataSource.swift 3 | // URLSessionAsyncNetworking 4 | // 5 | // Created by Ahmed Fathy on 11/03/2023. 6 | // 7 | 8 | import UIKit 9 | 10 | extension ViewController: UITableViewDataSource { 11 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 12 | let cell = tableView.dequeueReusableCell(withIdentifier: "PostCell", for: indexPath) 13 | cell.textLabel?.text = self.posts[indexPath.row].title 14 | cell.detailTextLabel?.text = self.posts[indexPath.row].body 15 | return cell 16 | } 17 | 18 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 19 | return posts.count 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/Controller/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // URLSessionAsyncNetworking 4 | // 5 | // Created by Ahmed Fathy on 08/03/2023. 6 | // 7 | 8 | import UIKit 9 | 10 | class ViewController: UIViewController { 11 | @IBOutlet weak var tableView: UITableView! 12 | private(set) var posts = [Post]() 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | tableView.dataSource = self 17 | makeANetworkCall() 18 | } 19 | 20 | fileprivate func makeANetworkCall() { 21 | Task { 22 | self.posts = try await Webservice.main.loadService(route: Router.placeholder, model: [Post].self) 23 | tableView.reloadData() 24 | } 25 | } 26 | } 27 | 28 | 29 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/Core/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // URLSessionAsyncNetworking 4 | // 5 | // Created by Ahmed Fathy on 08/03/2023. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | // Called when a new scene session is being created. 24 | // Use this method to select a configuration to create the new scene with. 25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 26 | } 27 | 28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 29 | // Called when the user discards a scene session. 30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 32 | } 33 | 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/Core/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // URLSessionAsyncNetworking 4 | // 5 | // Created by Ahmed Fathy on 08/03/2023. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | guard let _ = (scene as? UIWindowScene) else { return } 20 | } 21 | 22 | func sceneDidDisconnect(_ scene: UIScene) { 23 | // Called as the scene is being released by the system. 24 | // This occurs shortly after the scene enters the background, or when its session is discarded. 25 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 27 | } 28 | 29 | func sceneDidBecomeActive(_ scene: UIScene) { 30 | // Called when the scene has moved from an inactive state to an active state. 31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 32 | } 33 | 34 | func sceneWillResignActive(_ scene: UIScene) { 35 | // Called when the scene will move from an active state to an inactive state. 36 | // This may occur due to temporary interruptions (ex. an incoming phone call). 37 | } 38 | 39 | func sceneWillEnterForeground(_ scene: UIScene) { 40 | // Called as the scene transitions from the background to the foreground. 41 | // Use this method to undo the changes made on entering the background. 42 | } 43 | 44 | func sceneDidEnterBackground(_ scene: UIScene) { 45 | // Called as the scene transitions from the foreground to the background. 46 | // Use this method to save data, release shared resources, and store enough scene-specific state information 47 | // to restore the scene back to its current state. 48 | } 49 | 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | UISceneStoryboardFile 19 | Main 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/Model/Post.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Post.swift 3 | // URLSessionAsyncNetworking 4 | // 5 | // Created by Ahmed Fathy on 10/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Post: Codable, Hashable { 11 | let userID, id: Int 12 | let title, body: String 13 | 14 | enum CodingKeys: String, CodingKey { 15 | case userID = "userId" 16 | case id, title, body 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/Networking/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmedfathy-m/URLSession-Modern-Concurrency/f0904363149d85ff4c8444a404bf9b8e7a16962f/URLSessionAsyncNetworking/Networking/.DS_Store -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/Networking/Global Models/Global Models.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // URLSessionAsyncNetworking 4 | // 5 | // Created by Ahmed Fathy on 08/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - GlobalModel 11 | struct GlobalModel: Codable { 12 | let userStatus, msg: String 13 | let code: Int 14 | let data: DataModel? 15 | let key: ResponseStatus 16 | 17 | enum CodingKeys: String, CodingKey { 18 | case userStatus = "user_status" 19 | case msg, key, code, data 20 | } 21 | } 22 | 23 | struct GenericModel: Codable { 24 | let userStatus, msg: String 25 | let code: Int 26 | let key: ResponseStatus 27 | 28 | enum CodingKeys: String, CodingKey { 29 | case userStatus = "user_status" 30 | case msg, key, code 31 | } 32 | } 33 | 34 | enum ResponseStatus: String, Codable { 35 | case success 36 | case fail 37 | } 38 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/Networking/Multipart/Data+String.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data+String.swift 3 | // URLSessionAsyncNetworking 4 | // 5 | // Created by Ahmed Fathy on 09/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Data { 11 | mutating func append(_ string: String) { 12 | if let data = string.data(using: .utf8) { 13 | self.append(data) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/Networking/Multipart/DataField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataField.swift 3 | // URLSessionAsyncNetworking 4 | // 5 | // Created by Ahmed Fathy on 08/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | struct DataField { 11 | let key: String 12 | let mimeType: MimeType 13 | let data: Data 14 | } 15 | 16 | typealias HTTPBodyDataFields = Array 17 | 18 | extension HTTPBodyDataFields { 19 | /// Describes an empty array of HTTPBodyDataField objects 20 | static var empty: HTTPBodyDataFields { 21 | return [] 22 | } 23 | 24 | /// This function converts the body data field to a multipart segment using the boundary string 25 | /// - Parameter boundary: The boundary string used in a multipart request 26 | /// - Returns: A multipart segment represeting the data field as raw data 27 | func toMultiPart(with boundary: String) -> Data { 28 | let field = NSMutableData() 29 | self.forEach { dataField in 30 | field.append("--\(boundary)\r\n") 31 | field.append("Content-Disposition: form-data; name=\"\(dataField.key)\"\r\n") 32 | field.append("Content-Type: \(dataField.mimeType.rawValue)\r\n") 33 | field.append("\r\n") 34 | field.append(dataField.data) 35 | field.append("\r\n") 36 | } 37 | return field as Data 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/Networking/Multipart/MimeType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MimeType.swift 3 | // URLSessionAsyncNetworking 4 | // 5 | // Created by Ahmed Fathy on 08/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | enum MimeType: String { 11 | case jpeg 12 | case png 13 | } 14 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/Networking/Multipart/NSMutableData+String.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSMutableData+String.swift 3 | // URLSessionAsyncNetworking 4 | // 5 | // Created by Ahmed Fathy on 08/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | extension NSMutableData { 11 | func append(_ string: String) { 12 | if let data = string.data(using: .utf8) { 13 | self.append(data) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/Networking/Parameters/Parameters.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Parameters.swift 3 | // URLSessionAsyncNetworking 4 | // 5 | // Created by Ahmed Fathy on 08/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | typealias QueryParameters = [String: String] 11 | 12 | extension QueryParameters { 13 | /// Describes an empty dictionary of QueryParameters objects 14 | static var empty: QueryParameters { 15 | return [:] 16 | } 17 | } 18 | 19 | typealias HTTPBodyTextFields = [String: Any] 20 | 21 | extension HTTPBodyTextFields { 22 | /// Describes an empty dictionary of HTTPBodyTexField objects 23 | static var empty: HTTPBodyTextFields { 24 | return [:] 25 | } 26 | 27 | /// This function converts the text fields of an HTTP body to a multipart segment using the boundary string 28 | /// - Parameter boundary: The boundary string used in a multipart request 29 | /// - Returns: A multipart segment represeting the text field as raw data 30 | func toMultiPart(with boundary: String) -> Data { 31 | var fieldString = String() 32 | self.forEach { (key, value) in 33 | fieldString += "--\(boundary)\r\n" 34 | fieldString += "Content-Disposition: form-data; name=\"\(key)\"\r\n" 35 | fieldString += "Content-Type: text/plain; charset=ISO-8859-1\r\n" 36 | fieldString += "Content-Transfer-Encoding: 8bit\r\n" 37 | fieldString += "\r\n" 38 | fieldString += "\(value)\r\n" 39 | } 40 | return fieldString.data(using: .utf8)! 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/Networking/Route/Route.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Route.swift 3 | // URLSessionAsyncNetworking 4 | // 5 | // Created by Ahmed Fathy on 08/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | /// This protocols describes all details of a network call. The route protocols allows to create multiple routers using enums as long as you provide the required fields. 11 | protocol Route { 12 | var baseURL: String { get } 13 | var routePath: String { get } 14 | var method: URLRequest.HTTPMethod { get } 15 | var body: HTTPBodyTextFields { get } 16 | var query: QueryParameters { get } 17 | var contentType: URLRequest.ContentType { get } 18 | var acceptType: URLRequest.AcceptType { get } 19 | var dataFields: HTTPBodyDataFields { get } 20 | } 21 | 22 | extension Route { 23 | /// This function converts the network route object to a URLRequest. You don't need to call this function, manually. The Webservice object will call it for you and run the request, automatically. 24 | /// - Returns: A URLRequest-type object that describes the network call in detail. 25 | func asURLRequest() -> URLRequest { 26 | guard let url = URL(string: "\(baseURL)/\(routePath)") else { 27 | fatalError("Request URL is invalid URL") 28 | } 29 | var request = URLRequest(url: url) 30 | request.method = method 31 | request.queryItems = query 32 | request.contentType = contentType 33 | request.acceptType = acceptType 34 | 35 | if case .multipart(_) = self.contentType, !dataFields.isEmpty { 36 | request.httpBody = toMultiPart() 37 | } else { 38 | request.bodyParameters = body 39 | } 40 | 41 | request.cachePolicy = .reloadRevalidatingCacheData 42 | 43 | return request 44 | } 45 | 46 | fileprivate func toMultiPart() -> Data? { 47 | if case .multipart(let boundary) = self.contentType { 48 | var data = Data() 49 | data.append(body.toMultiPart(with: boundary)) 50 | data.append(dataFields.toMultiPart(with: boundary)) 51 | data.append("--\(boundary)--") 52 | return data 53 | } else { 54 | return nil 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/Networking/Route/TestRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestRouter.swift 3 | // URLSessionAsyncNetworking 4 | // 5 | // Created by Ahmed Fathy on 08/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | enum Router { 11 | case placeholder 12 | } 13 | 14 | extension Router: Route { 15 | var baseURL: String { 16 | "https://jsonplaceholder.typicode.com" 17 | } 18 | 19 | var routePath: String { 20 | switch self { 21 | case .placeholder: 22 | return "posts" 23 | } 24 | } 25 | 26 | var method: URLRequest.HTTPMethod { 27 | switch self { 28 | case .placeholder: return .get 29 | } 30 | } 31 | 32 | var body: HTTPBodyTextFields { 33 | .empty 34 | } 35 | 36 | var query: QueryParameters { 37 | .empty 38 | } 39 | 40 | var contentType: URLRequest.ContentType { 41 | .formData 42 | } 43 | 44 | var acceptType: URLRequest.AcceptType { 45 | .json 46 | } 47 | 48 | var dataFields: HTTPBodyDataFields { 49 | switch self { 50 | case .placeholder: 51 | return .empty 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/Networking/Service/Main/Service.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Service.swift 3 | // URLSessionAsyncNetworking 4 | // 5 | // Created by Ahmed Fathy on 08/03/2023. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | /// A networking layer based on URLSession with support for Modern Concurrency from iOS 13 and upwards. 12 | final class Webservice { 13 | /// The main service you need to call to use this networking layer. 14 | static let main = Webservice() 15 | private init() {} 16 | private let mainQueue = DispatchQueue.main 17 | private let responseHandler = ResponseHandler() 18 | private var cancellables = Set() 19 | 20 | /// Make an asynchronus call to a network route 21 | /// - Parameters: 22 | /// - route: The route defines all aspects of a network call. It represents the target URL and all data required. 23 | /// - model: The type of data model you need to retrieve 24 | /// - Returns: The response of the network call in the form of your required data type 25 | func loadService(route: Route, model: Model.Type) async throws -> Model { 26 | try await withCheckedThrowingContinuation { continuation in 27 | performRequest(route: route, model: model.self) { model, error in 28 | if let error { 29 | continuation.resume(throwing: error) 30 | } else { 31 | continuation.resume(returning: model!) 32 | } 33 | } 34 | } 35 | } 36 | 37 | /// Make an API call to a network route and retrieve a publisher of your data to bind to other elements of your code. 38 | /// - Parameters: 39 | /// - route: The route defines all aspects of a network call. It represents the target URL and all data required. 40 | /// - model: The type of data model you need to retrieve 41 | /// - Returns: The response of the network call in the form of AnyPublisher of your required type 42 | func publishRequest(route: Route, model: Model.Type) -> AnyPublisher { 43 | let request = route.asURLRequest() 44 | return Future { promise in 45 | URLSession.shared.dataTaskPublisher(for: request) 46 | .tryMap { (data, response) in 47 | try URLResponseHandler(response).validate() 48 | return try self.responseHandler.process(data: data, model: model.self) 49 | }.receive(on: RunLoop.main) 50 | .sink { completion in 51 | switch completion { 52 | case .finished: 53 | break 54 | case .failure(let error): 55 | promise(.failure(error)) 56 | } 57 | } receiveValue: { model in 58 | promise(.success(model)) 59 | }.store(in: &self.cancellables) 60 | }.eraseToAnyPublisher() 61 | 62 | } 63 | 64 | private func performRequest(route: Route, model: Model.Type, completion: @escaping ResponseCompletion) { 65 | let request = route.asURLRequest() 66 | let task = URLSession.shared.dataTask(with: request) { data, response, error in 67 | guard error == nil else { return self.mainQueue.async { completion(nil, error) } } 68 | do { 69 | let result = try self.responseHandler.process(data: data, model: model.self) 70 | self.mainQueue.async { completion(result, nil) } 71 | } catch { 72 | self.mainQueue.async { completion(nil, error) } 73 | } 74 | } 75 | task.resume() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/Networking/Service/ResponseCompletion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResponseCompletion.swift 3 | // URLSessionAsyncNetworking 4 | // 5 | // Created by Ahmed Fathy on 09/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | typealias ResponseCompletion = (Model?, Error?)->Void 11 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/Networking/Service/ResponseHandler/Errors/ResponseHandlerError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResponseHandlerError.swift 3 | // URLSessionAsyncNetworking 4 | // 5 | // Created by Ahmed Fathy on 09/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ResponseHandlerError: Error, LocalizedError { 11 | case missingInputData 12 | case missingReturnData 13 | case APIError(message: String) 14 | } 15 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/Networking/Service/ResponseHandler/Main/ResponseHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResponseHandler.swift 3 | // URLSessionAsyncNetworking 4 | // 5 | // Created by Ahmed Fathy on 09/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Your additional processing goes here 11 | struct ResponseHandler { 12 | /// In this function, you can add any extra processing on your data. It could be as simple as decoding the data with a JSONDecoder or maybe you want to extract the data from a child object because your API returns additional info that you don't need where you make the API call 13 | /// - Parameters: 14 | /// - data: The raw data retreived from the API call without any processing 15 | /// - model: The data type you need to retrieve 16 | /// - Returns: Processed/Decoded response 17 | func process(data: Data?, model: Model.Type) throws -> Model { 18 | guard let data else { throw ResponseHandlerError.missingInputData } 19 | return try JSONDecoder().decode(model.self, from: data) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/Networking/Service/URLResponseHandler/Errors/ClientHTTPResponseError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClientHTTPResponseError.swift 3 | // URLSessionAsyncNetworking 4 | // 5 | // Created by Ahmed Fathy on 10/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | /// An enum contains all client response errors (4xx status code) 11 | enum ClientHTTPResponseError: Int, Error { 12 | // 4xx Client Error 13 | case badRequest = 400 14 | case unauthorized = 401 15 | case paymentRequired = 402 16 | case forbidden = 403 17 | case notFound = 404 18 | case methodNotAllowed = 405 19 | case notAcceptable = 406 20 | case proxyAuthenticationRequired = 407 21 | case requestTimeout = 408 22 | case conflict = 409 23 | case gone = 410 24 | case lengthRequired = 411 25 | case preconditionFailed = 412 26 | case payloadTooLarge = 413 27 | case uriTooLong = 414 28 | case unsupportedMediaType = 415 29 | case rangeNotSatisfiable = 416 30 | case expectationFailed = 417 31 | case imATeapot = 418 32 | case misdirectedRequest = 421 33 | case unprocessableEntity = 422 34 | case locked = 423 35 | case failedDependency = 424 36 | case tooEarly = 425 37 | case upgradeRequired = 426 38 | case preconditionRequired = 428 39 | case tooManyRequests = 429 40 | case requestHeaderFieldsTooLarge = 431 41 | case unavailableForLegalReasons = 451 42 | 43 | var localizedDescription: String { 44 | switch self { 45 | case .badRequest: 46 | return "Bad Request" 47 | case .unauthorized: 48 | return "Unauthorized" 49 | case .paymentRequired: 50 | return "Payment Required" 51 | case .forbidden: 52 | return "Forbidden" 53 | case .notFound: 54 | return "Not Found" 55 | case .methodNotAllowed: 56 | return "Method Not Allowed" 57 | case .notAcceptable: 58 | return "Not Acceptable" 59 | case .proxyAuthenticationRequired: 60 | return "Proxy Authentication Required" 61 | case .requestTimeout: 62 | return "Request Timeout" 63 | case .conflict: 64 | return "Conflict" 65 | case .gone: 66 | return "Gone" 67 | case .lengthRequired: 68 | return "Length Required" 69 | case .preconditionFailed: 70 | return "Precondition Failed" 71 | case .payloadTooLarge: 72 | return "Payload Too Large" 73 | case .uriTooLong: 74 | return "URI Too Long" 75 | case .unsupportedMediaType: 76 | return "Unsupported Media Type" 77 | case .rangeNotSatisfiable: 78 | return "Range Not Satisfiable" 79 | case .expectationFailed: 80 | return "Expectation Failed" 81 | case .imATeapot: 82 | return "I'm a teapot" 83 | case .misdirectedRequest: 84 | return "Misdirected Request" 85 | case .unprocessableEntity: 86 | return "Unprocessable Entity" 87 | case .locked: 88 | return "Locked" 89 | case .failedDependency: 90 | return "Failed Dependency" 91 | case .tooEarly: 92 | return "Too Early" 93 | case .upgradeRequired: 94 | return "Upgrade Required" 95 | case .preconditionRequired: 96 | return "Precondition Required" 97 | case .tooManyRequests: 98 | return "Too Many Requests" 99 | case .requestHeaderFieldsTooLarge: 100 | return "Request Header Fields Too Large" 101 | case .unavailableForLegalReasons: 102 | return "Unavailable For Legal Reasons" 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/Networking/Service/URLResponseHandler/Errors/ServerHTTPResponseError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServerHTTPResponseError.swift 3 | // URLSessionAsyncNetworking 4 | // 5 | // Created by Ahmed Fathy on 10/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | /// An enum contains all server response errors (5xx status code) 11 | enum ServerHTTPResponseError: Int, Error { 12 | // 5xx Server Error 13 | case internalServerError = 500 14 | case notImplemented = 501 15 | case badGateway = 502 16 | case serviceUnavailable = 503 17 | case gatewayTimeout = 504 18 | case httpVersionNotSupported = 505 19 | case variantAlsoNegotiates = 506 20 | case insufficientStorage = 507 21 | case loopDetected = 508 22 | case notExtended = 510 23 | case networkAuthenticationRequired = 511 24 | 25 | var localizedDescription: String { 26 | switch self { 27 | case .internalServerError: 28 | return "Internal Server Error" 29 | case .notImplemented: 30 | return "Not Implemented" 31 | case .badGateway: 32 | return "Bad Gateway" 33 | case .serviceUnavailable: 34 | return "Service Unavailable" 35 | case .gatewayTimeout: 36 | return "Gateway Timeout" 37 | case .httpVersionNotSupported: 38 | return "HTTP Version Not Supported" 39 | case .variantAlsoNegotiates: 40 | return "Variant Also Negotiates" 41 | case .insufficientStorage: 42 | return "Insufficient Storage" 43 | case .loopDetected: 44 | return "Loop Detected" 45 | case .notExtended: 46 | return "Not Extended" 47 | case .networkAuthenticationRequired: 48 | return "Network Authentication Required" 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/Networking/Service/URLResponseHandler/Main/URLResponseHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLResponseHandler.swift 3 | // URLSessionAsyncNetworking 4 | // 5 | // Created by Ahmed Fathy on 10/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | /// This object receives and validates the URLResponse 11 | struct URLResponseHandler { 12 | let response: HTTPURLResponse 13 | 14 | init(_ response: HTTPURLResponse) { 15 | self.response = response 16 | } 17 | 18 | init(_ response: URLResponse) { 19 | self.response = response as! HTTPURLResponse 20 | } 21 | } 22 | 23 | extension URLResponseHandler { 24 | /// Validates the status code and throws an error in case of an Error 25 | func validate() throws { 26 | let statusCode = self.response.statusCode 27 | switch statusCode { 28 | case 400...499: throw ClientHTTPResponseError(rawValue: statusCode) ?? .badRequest 29 | case 500...599: throw ServerHTTPResponseError(rawValue: statusCode) ?? .internalServerError 30 | default: print("Valid Response: \(statusCode)") 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/Networking/URLRequest/URLRequest+AcceptType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLRequest+AcceptType.swift 3 | // URLSessionAsyncNetworking 4 | // 5 | // Created by Ahmed Fathy on 08/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | extension URLRequest { 11 | enum AcceptType: String { 12 | case json = "application/json" 13 | } 14 | 15 | /// Provides a better and cleaner way to access the AcceptType of an API response 16 | var acceptType: AcceptType? { 17 | get { AcceptType(rawValue: self.value(forHTTPHeaderField: "Accept") ?? "") } 18 | set { self.setValue(newValue?.rawValue, forHTTPHeaderField: "Accept")} 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/Networking/URLRequest/URLRequest+Authorization.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLRequest+Authorization.swift 3 | // URLSessionAsyncNetworking 4 | // 5 | // Created by Ahmed Fathy on 08/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | extension URLRequest { 11 | ///Directly set/retrieves the bearer token without needing to add the "Bearer" prefix 12 | var bearerToken: String { 13 | get { self.value(forHTTPHeaderField: "Authorization")?.replacingOccurrences(of: "Bearer ", with: "") ?? "" } 14 | set { self.setValue("Bearer \(newValue)", forHTTPHeaderField: "Authorization")} 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/Networking/URLRequest/URLRequest+Body.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLRequest+Body.swift 3 | // URLSessionAsyncNetworking 4 | // 5 | // Created by Ahmed Fathy on 08/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | extension URLRequest { 11 | /// Directly sets the body parameters as a dictionary 12 | var bodyParameters: HTTPBodyTextFields { 13 | get { (try? JSONSerialization.jsonObject(with: self.httpBody ?? Data()) as? [String: Any]) ?? [:] } 14 | set { 15 | guard !newValue.isEmpty else { return } 16 | self.httpBody = try? JSONSerialization.data(withJSONObject: newValue) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/Networking/URLRequest/URLRequest+ContentType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLRequest+ContentType.swift 3 | // URLSessionAsyncNetworking 4 | // 5 | // Created by Ahmed Fathy on 08/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | extension URLRequest { 11 | 12 | enum ContentType { 13 | case formURLEncoded 14 | case formData 15 | case raw 16 | case binary 17 | case GraphQL 18 | case multipart(boundary: String) 19 | case undefined(stringValue: String) 20 | 21 | var key: String { 22 | switch self { 23 | case .formURLEncoded: return "application/x-www-form-urlencoded" 24 | case .formData: return "form-data" 25 | case .raw: return "raw" 26 | case .binary: return "binary" 27 | case .GraphQL: return "GraphQL" 28 | case .multipart(let boundary): return "multipart/form-data; boundary=\(boundary)" 29 | case .undefined(let stringValue): return stringValue 30 | } 31 | } 32 | 33 | fileprivate static func getType(for key: String) -> ContentType { 34 | if key == "application/x-www-form-urlencoded" { 35 | return .formURLEncoded 36 | } else if key == "form-data" { 37 | return .formData 38 | } else if key == "raw" { 39 | return .raw 40 | } else if key == "binary" { 41 | return .binary 42 | } else if key == "GraphQL" { 43 | return .GraphQL 44 | } else if key.localizedStandardContains("multipart/form-data; boundary=") { 45 | let boundary = key.replacingOccurrences(of: "multipart/form-data; boundary=", with: "") 46 | return .multipart(boundary: boundary) 47 | } else { 48 | return .undefined(stringValue: key) 49 | } 50 | } 51 | } 52 | 53 | /// Provides a better and cleaner way to access the Content Type of request body for an API call 54 | var contentType: ContentType { 55 | get { ContentType.getType(for: self.value(forHTTPHeaderField: "Content-Type") ?? "") } 56 | set { self.setValue(newValue.key, forHTTPHeaderField: "Content-Type") } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/Networking/URLRequest/URLRequest+HTTPMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLRequest+HTTPMethod.swift 3 | // URLSessionAsyncNetworking 4 | // 5 | // Created by Ahmed Fathy on 08/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | extension URLRequest { 11 | 12 | enum HTTPMethod: String { 13 | case get = "GET" 14 | case post = "POST" 15 | case delete = "DELETE" 16 | } 17 | 18 | /// Assign the HTTP Method of a URL Request 19 | var method: HTTPMethod? { 20 | get { HTTPMethod(rawValue: self.httpMethod ?? "") } 21 | set { self.httpMethod = newValue?.rawValue ?? ""} 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworking/Networking/URLRequest/URLRequest+Query.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLRequest+Query.swift 3 | // URLSessionAsyncNetworking 4 | // 5 | // Created by Ahmed Fathy on 08/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | extension URLRequest { 11 | /// Assign the query parameters directly, without needing to access the URL 12 | var queryItems: QueryParameters { 13 | get { 14 | guard let items = URLComponents(string: self.url?.absoluteString ?? "")?.queryItems else { return [:] } 15 | var query = [String: String]() 16 | items.forEach { item in 17 | guard let value = item.value else { return } 18 | query[item.name] = value 19 | } 20 | return query 21 | } 22 | 23 | set { 24 | let items = newValue.map({URLQueryItem(name: $0.key, value: $0.value)}) 25 | guard var url = URLComponents(string: self.url?.absoluteString ?? "") else { return } 26 | url.queryItems = items 27 | self.url = url.url 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworkingTests/URLSessionAsyncNetworkingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSessionAsyncNetworkingTests.swift 3 | // URLSessionAsyncNetworkingTests 4 | // 5 | // Created by Ahmed Fathy on 08/03/2023. 6 | // 7 | 8 | import XCTest 9 | @testable import URLSessionAsyncNetworking 10 | 11 | final class URLSessionAsyncNetworkingTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | // Any test you write for XCTest can be annotated as throws and async. 25 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 26 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 27 | } 28 | 29 | func testPerformanceExample() throws { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworkingUITests/URLSessionAsyncNetworkingUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSessionAsyncNetworkingUITests.swift 3 | // URLSessionAsyncNetworkingUITests 4 | // 5 | // Created by Ahmed Fathy on 08/03/2023. 6 | // 7 | 8 | import XCTest 9 | 10 | final class URLSessionAsyncNetworkingUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use XCTAssert and related functions to verify your tests produce the correct results. 31 | } 32 | 33 | func testLaunchPerformance() throws { 34 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 35 | // This measures how long it takes to launch your application. 36 | measure(metrics: [XCTApplicationLaunchMetric()]) { 37 | XCUIApplication().launch() 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /URLSessionAsyncNetworkingUITests/URLSessionAsyncNetworkingUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSessionAsyncNetworkingUITestsLaunchTests.swift 3 | // URLSessionAsyncNetworkingUITests 4 | // 5 | // Created by Ahmed Fathy on 08/03/2023. 6 | // 7 | 8 | import XCTest 9 | 10 | final class URLSessionAsyncNetworkingUITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | func testLaunch() throws { 21 | let app = XCUIApplication() 22 | app.launch() 23 | 24 | // Insert steps here to perform after app launch but before taking a screenshot, 25 | // such as logging into a test account or navigating somewhere in the app 26 | 27 | let attachment = XCTAttachment(screenshot: app.screenshot()) 28 | attachment.name = "Launch Screen" 29 | attachment.lifetime = .keepAlways 30 | add(attachment) 31 | } 32 | } 33 | --------------------------------------------------------------------------------