From c01c6adf3803c5af70ca8fa789847ad60c018919 Mon Sep 17 00:00:00 2001 From: Tomas Cejka Date: Wed, 6 Sep 2023 22:15:20 +0200 Subject: [PATCH 01/29] [chore] fix swiftlint warnings --- .../xcschemes/Networking.xcscheme | 2 +- .../xcshareddata/swiftpm/Package.resolved | 14 ++ NetworkingSampleApp/.swiftlint.yml | 3 +- .../project.pbxproj | 30 ++++- .../xcschemes/NetworkingSampleApp.xcscheme | 2 +- Package.swift | 14 +- Plugins/SwiftLintXcodeNetworking/Plugin.swift | 27 ++++ Sources/Networking/.swiftlint.yml | 121 ++++++++++++++++++ Sources/Networking/Core/APIManager.swift | 2 +- .../Networking/Core/DownloadAPIManager.swift | 2 +- .../Networking/Core/DownloadAPIManaging.swift | 2 +- .../Core/EndpointIdentifiable.swift | 4 +- .../Core/MockResponseProvider.swift | 3 +- .../Networking/Core/ResponseProviding.swift | 2 +- .../Networking/Core/RetryConfiguration.swift | 3 +- Sources/Networking/Core/Retryable.swift | 2 +- .../Upload/MultipartFormData+BodyPart.swift | 2 +- .../MultipartFormData+EncodingError.swift | 8 +- .../Core/Upload/MultipartFormData.swift | 2 +- .../Upload/MultipartFormDataEncoder.swift | 15 +-- .../Upload/MultipartFormDataEncoding.swift | 2 +- .../Core/Upload/UploadAPIManaging.swift | 2 +- .../Core/Upload/UploadTask+State.swift | 2 +- .../Networking/Core/Upload/UploadTask.swift | 3 +- .../Networking/Core/Upload/Uploadable.swift | 2 +- Sources/Networking/Misc/ArrayEncoding.swift | 2 +- Sources/Networking/Misc/ArrayParameter.swift | 4 +- Sources/Networking/Misc/Counter.swift | 2 +- .../Misc/MockResponseProviderError.swift | 2 +- .../Misc/ThreadSafeDictionary.swift | 4 +- .../Misc/URLSessionTask+AsyncResponse.swift | 2 +- .../Misc/URLSessionTask+DownloadState.swift | 4 +- .../Authorization/AuthorizationData.swift | 6 +- .../Authorization/AuthorizationError.swift | 2 +- .../Authorization/AuthorizationManaging.swift | 2 +- .../AuthorizationStorageManaging.swift | 4 +- .../AuthorizationTokenInterceptor.swift | 2 +- .../Interceptors/LoggingInterceptor.swift | 7 +- .../EndpointRequestStorageProcessor.swift | 2 +- .../Processors/StatusCodeProcessor.swift | 2 +- .../Utils/Sequence+Convenience.swift | 2 +- .../Networking/Utils/URL+Convenience.swift | 4 +- 42 files changed, 258 insertions(+), 66 deletions(-) create mode 100644 Networking.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 Plugins/SwiftLintXcodeNetworking/Plugin.swift create mode 100755 Sources/Networking/.swiftlint.yml diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Networking.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Networking.xcscheme index fb598c66..ebd1c787 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/Networking.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/Networking.xcscheme @@ -1,6 +1,6 @@ [Command] { + return [ + .buildCommand( + displayName: "Linting \(target.name)", + executable: try context.tool(named: "swiftlint").path, + arguments: [ + "lint", + "--config", + "\(target.directory.string)/.swiftlint.yml", + target.directory.string // only lint the files in the target directory + ], + environment: [:] + ) + ] + } +} diff --git a/Sources/Networking/.swiftlint.yml b/Sources/Networking/.swiftlint.yml new file mode 100755 index 00000000..8015693b --- /dev/null +++ b/Sources/Networking/.swiftlint.yml @@ -0,0 +1,121 @@ +# +# SwiftLint configuration file commonly used in STRV (v 0.1.2) +# +# Feel free to send pull request or suggest improvements! +# + +analyzer_rules: + - unused_import + +# +# Rule identifiers to exclude from running. +# +disabled_rules: + +# +# Some rules are only opt-in. Find all the available rules by running: swiftlint rules +# +opt_in_rules: + - empty_count + - force_unwrapping + - conditional_returns_on_newline + - private_action + - private_outlet + - implicitly_unwrapped_optional + - empty_string + - extension_access_modifier + - closure_spacing + - fatal_error_message + - first_where + - toggle_bool + - modifier_order + - contains_over_first_not_nil + - convenience_type + - fallthrough + - unavailable_function + - strict_fileprivate + - explicit_init + - redundant_objc_attribute + - unowned_variable_capture + - strong_iboutlet + +# +# Paths to include during linting. `--path` is ignored if present. +# +included: + - ./ + - ../Sources + - ../Tests + +# +# Paths to ignore during linting. Takes precedence over `included`. +# +excluded: + - Carthage + - Pods + - Tests + - Scripts + - vendor + - fastlane + - R.generated.swift + +# +# Configure individual rules below. +# +vertical_whitespace: + max_empty_lines: 2 + +force_cast: error + +identifier_name: + excluded: + - id + +empty_count: + severity: warning + +trailing_whitespace: + ignores_empty_lines: true + +line_length: + ignores_function_declarations: true + ignores_comments: true + ignores_urls: true + error: 300 + warning: 200 + +function_body_length: + error: 150 + warning: 100 + +type_body_length: + error: 900 + warning: 600 + +file_length: + error: 1000 + warning: 700 + + +# Custom rules definitions +custom_rules: + comments_space: # From https://github.com/brandenr/swiftlintconfig + name: "Space After Comment" + regex: "(^ *//\\w+)" + message: "There should be a space after //" + severity: warning + + force_https: # From https://github.com/Twigz/Game + name: "Force HTTPS over HTTP" + regex: "((?i)http(?!s))" + match_kinds: string + message: "HTTPS should be favored over HTTP" + severity: warning + + double_space: # From https://github.com/IBM-Swift/Package-Builder + include: "*.swift" + name: "Double space" + regex: "([a-z,A-Z] \\s+)" + message: "Double space between keywords" + match_kinds: keyword + severity: warning diff --git a/Sources/Networking/Core/APIManager.swift b/Sources/Networking/Core/APIManager.swift index 481a2587..238fe93c 100644 --- a/Sources/Networking/Core/APIManager.swift +++ b/Sources/Networking/Core/APIManager.swift @@ -1,6 +1,6 @@ // // APIManager.swift -// +// // // Created by Matej Molnár on 24.11.2022. // diff --git a/Sources/Networking/Core/DownloadAPIManager.swift b/Sources/Networking/Core/DownloadAPIManager.swift index c61a7b6f..4b106848 100644 --- a/Sources/Networking/Core/DownloadAPIManager.swift +++ b/Sources/Networking/Core/DownloadAPIManager.swift @@ -1,6 +1,6 @@ // // DownloadAPIManager.swift -// +// // // Created by Matej Molnár on 07.03.2023. // diff --git a/Sources/Networking/Core/DownloadAPIManaging.swift b/Sources/Networking/Core/DownloadAPIManaging.swift index bf3dbd5a..5b0fb33c 100644 --- a/Sources/Networking/Core/DownloadAPIManaging.swift +++ b/Sources/Networking/Core/DownloadAPIManaging.swift @@ -1,6 +1,6 @@ // // DownloadAPIManaging.swift -// +// // // Created by Dominika Gajdová on 12.05.2023. // diff --git a/Sources/Networking/Core/EndpointIdentifiable.swift b/Sources/Networking/Core/EndpointIdentifiable.swift index f692aa76..5434e4d5 100644 --- a/Sources/Networking/Core/EndpointIdentifiable.swift +++ b/Sources/Networking/Core/EndpointIdentifiable.swift @@ -85,8 +85,7 @@ private extension EndpointIdentifiable { // the items need to be sorted because the final identifier should be the same no matter the order of query items in the URL if let queryItems = urlComponents.queryItems? .sorted(by: { $0.name < $1.name }) - .flatMap({ [$0.name, $0.value ?? ""] }) - { + .flatMap({ [$0.name, $0.value ?? ""] }) { components.append(contentsOf: queryItems) } @@ -96,4 +95,3 @@ private extension EndpointIdentifiable { return components } } - diff --git a/Sources/Networking/Core/MockResponseProvider.swift b/Sources/Networking/Core/MockResponseProvider.swift index 13463f62..307c471c 100644 --- a/Sources/Networking/Core/MockResponseProvider.swift +++ b/Sources/Networking/Core/MockResponseProvider.swift @@ -1,6 +1,6 @@ // // MockResponseProvider.swift -// +// // // Created by Matej Molnár on 04.01.2023. // @@ -71,6 +71,7 @@ private extension MockResponseProvider { } // return previous response, if no more stored indexed api calls + // swiftlint:disable:next empty_count if count > 0, let data = NSDataAsset(name: "\(sessionId)_\(request.identifier)_\(count - 1)", bundle: bundle)?.data { return try decoder.decode(EndpointRequestStorageModel.self, from: data) } diff --git a/Sources/Networking/Core/ResponseProviding.swift b/Sources/Networking/Core/ResponseProviding.swift index a40034a0..f6a374bb 100644 --- a/Sources/Networking/Core/ResponseProviding.swift +++ b/Sources/Networking/Core/ResponseProviding.swift @@ -1,6 +1,6 @@ // // ResponseProviding.swift -// +// // // Created by Matej Molnár on 04.01.2023. // diff --git a/Sources/Networking/Core/RetryConfiguration.swift b/Sources/Networking/Core/RetryConfiguration.swift index 61480e7b..e793f5d0 100644 --- a/Sources/Networking/Core/RetryConfiguration.swift +++ b/Sources/Networking/Core/RetryConfiguration.swift @@ -20,8 +20,7 @@ public struct RetryConfiguration { public init( retries: Int, delay: DelayConfiguration, - retryHandler: @escaping (Error) -> Bool) - { + retryHandler: @escaping (Error) -> Bool) { self.retries = retries self.delay = delay self.retryHandler = retryHandler diff --git a/Sources/Networking/Core/Retryable.swift b/Sources/Networking/Core/Retryable.swift index 42106a59..7ad94f52 100644 --- a/Sources/Networking/Core/Retryable.swift +++ b/Sources/Networking/Core/Retryable.swift @@ -1,6 +1,6 @@ // // Retryable.swift -// +// // // Created by Dominika Gajdová on 09.05.2023. // diff --git a/Sources/Networking/Core/Upload/MultipartFormData+BodyPart.swift b/Sources/Networking/Core/Upload/MultipartFormData+BodyPart.swift index 86da8894..043b0c09 100644 --- a/Sources/Networking/Core/Upload/MultipartFormData+BodyPart.swift +++ b/Sources/Networking/Core/Upload/MultipartFormData+BodyPart.swift @@ -1,6 +1,6 @@ // // MultipartFormData+BodyPart.swift -// +// // // Created by Tony Ngo on 18.06.2023. // diff --git a/Sources/Networking/Core/Upload/MultipartFormData+EncodingError.swift b/Sources/Networking/Core/Upload/MultipartFormData+EncodingError.swift index d081c49c..9ea1273c 100644 --- a/Sources/Networking/Core/Upload/MultipartFormData+EncodingError.swift +++ b/Sources/Networking/Core/Upload/MultipartFormData+EncodingError.swift @@ -1,6 +1,6 @@ // // MultipartFormData+EncodingError.swift -// +// // // Created by Tony Ngo on 19.06.2023. // @@ -10,10 +10,10 @@ import Foundation public extension MultipartFormData { enum EncodingError: LocalizedError { case invalidFileUrl(URL) - case invalidFileName(at: URL) + case invalidFileName(for: URL) case missingFileSize(for: URL) case dataStreamReadFailed(with: Error) - case dataStreamWriteFailed(at: URL) - case fileAlreadyExists(at: URL) + case dataStreamWriteFailed(for: URL) + case fileAlreadyExists(for: URL) } } diff --git a/Sources/Networking/Core/Upload/MultipartFormData.swift b/Sources/Networking/Core/Upload/MultipartFormData.swift index ade36a46..4cd84401 100644 --- a/Sources/Networking/Core/Upload/MultipartFormData.swift +++ b/Sources/Networking/Core/Upload/MultipartFormData.swift @@ -1,6 +1,6 @@ // // MultipartFormData.swift -// +// // // Created by Tony Ngo on 18.06.2023. // diff --git a/Sources/Networking/Core/Upload/MultipartFormDataEncoder.swift b/Sources/Networking/Core/Upload/MultipartFormDataEncoder.swift index a37fecf8..d5fc6b76 100644 --- a/Sources/Networking/Core/Upload/MultipartFormDataEncoder.swift +++ b/Sources/Networking/Core/Upload/MultipartFormDataEncoder.swift @@ -1,6 +1,6 @@ // // MultipartFormDataEncoder.swift -// +// // // Created by Tony Ngo on 18.06.2023. // @@ -31,14 +31,9 @@ open class MultipartFormDataEncoder { } } - -/** - -The main reason why there are methods to encode data & encode file is similar to `uploadTask(with:from:)` and `uploadTask(with:fromFile:)` ig one could convert the content of the file to Data using Data(contentsOf:) and use the first method to send data. One has the data available in memory while the second reads the data directly from the file thus doesn't load the data into memory so it is more efficient. - */ - // MARK: - MultipartFormDataEncoding extension MultipartFormDataEncoder: MultipartFormDataEncoding { + /// The main reason why there are methods to encode data & encode file is similar to `uploadTask(with:from:)` and `uploadTask(with:fromFile:)` ig one could convert the content of the file to Data using Data(contentsOf:) and use the first method to send data. One has the data available in memory while the second reads the data directly from the file thus doesn't load the data into memory so it is more efficient. public func encode(_ multipartFormData: MultipartFormData) throws -> Data { var encoded = Data() @@ -64,11 +59,11 @@ extension MultipartFormDataEncoder: MultipartFormDataEncoding { } guard !fileManager.fileExists(at: fileUrl) else { - throw MultipartFormData.EncodingError.fileAlreadyExists(at: fileUrl) + throw MultipartFormData.EncodingError.fileAlreadyExists(for: fileUrl) } guard let outputStream = OutputStream(url: fileUrl, append: false) else { - throw MultipartFormData.EncodingError.dataStreamWriteFailed(at: fileUrl) + throw MultipartFormData.EncodingError.dataStreamWriteFailed(for: fileUrl) } try encode(multipartFormData, into: outputStream) @@ -158,7 +153,7 @@ private extension MultipartFormDataEncoder { // Encode headers in a deterministic manner for easier testing let encodedHeaders = contentHeaders .sorted(by: { $0.key.rawValue < $1.key.rawValue }) - .map { "\($0.key.rawValue): \($0.value)"} + .map { "\($0.key.rawValue): \($0.value)" } .joined(separator: "\(crlf)") encoded.append(encodedHeaders) diff --git a/Sources/Networking/Core/Upload/MultipartFormDataEncoding.swift b/Sources/Networking/Core/Upload/MultipartFormDataEncoding.swift index 79e510c9..6050f454 100644 --- a/Sources/Networking/Core/Upload/MultipartFormDataEncoding.swift +++ b/Sources/Networking/Core/Upload/MultipartFormDataEncoding.swift @@ -1,6 +1,6 @@ // // MultipartFormDataEncoding.swift -// +// // // Created by Tony Ngo on 18.06.2023. // diff --git a/Sources/Networking/Core/Upload/UploadAPIManaging.swift b/Sources/Networking/Core/Upload/UploadAPIManaging.swift index db867d62..ac0c5d05 100644 --- a/Sources/Networking/Core/Upload/UploadAPIManaging.swift +++ b/Sources/Networking/Core/Upload/UploadAPIManaging.swift @@ -1,6 +1,6 @@ // // UploadAPIManaging.swift -// +// // // Created by Tony Ngo on 12.06.2023. // diff --git a/Sources/Networking/Core/Upload/UploadTask+State.swift b/Sources/Networking/Core/Upload/UploadTask+State.swift index ef7030ca..8b5bd2e5 100644 --- a/Sources/Networking/Core/Upload/UploadTask+State.swift +++ b/Sources/Networking/Core/Upload/UploadTask+State.swift @@ -1,6 +1,6 @@ // // UploadTask+State.swift -// +// // // Created by Tony Ngo on 12.06.2023. // diff --git a/Sources/Networking/Core/Upload/UploadTask.swift b/Sources/Networking/Core/Upload/UploadTask.swift index 740694c9..35d79f61 100644 --- a/Sources/Networking/Core/Upload/UploadTask.swift +++ b/Sources/Networking/Core/Upload/UploadTask.swift @@ -1,6 +1,6 @@ // // UploadTask.swift -// +// // // Created by Tony Ngo on 12.06.2023. // @@ -10,6 +10,7 @@ import Foundation /// Represents and manages an upload task and provides its state. public struct UploadTask { + // swiftlint:disable:next type_name public typealias ID = String /// The session task this object represents. diff --git a/Sources/Networking/Core/Upload/Uploadable.swift b/Sources/Networking/Core/Upload/Uploadable.swift index 290db57e..c974b93a 100644 --- a/Sources/Networking/Core/Upload/Uploadable.swift +++ b/Sources/Networking/Core/Upload/Uploadable.swift @@ -1,6 +1,6 @@ // // Uploadable.swift -// +// // // Created by Tony Ngo on 13.06.2023. // diff --git a/Sources/Networking/Misc/ArrayEncoding.swift b/Sources/Networking/Misc/ArrayEncoding.swift index bfd673d8..a3926a2c 100644 --- a/Sources/Networking/Misc/ArrayEncoding.swift +++ b/Sources/Networking/Misc/ArrayEncoding.swift @@ -1,6 +1,6 @@ // // ArrayEncoding.swift -// +// // // Created by Dominika Gajdová on 08.05.2023. // diff --git a/Sources/Networking/Misc/ArrayParameter.swift b/Sources/Networking/Misc/ArrayParameter.swift index b8f3265c..dfe9b930 100644 --- a/Sources/Networking/Misc/ArrayParameter.swift +++ b/Sources/Networking/Misc/ArrayParameter.swift @@ -1,6 +1,6 @@ // -// File.swift -// +// ArrayParameter.swift +// // // Created by Dominika Gajdová on 08.05.2023. // diff --git a/Sources/Networking/Misc/Counter.swift b/Sources/Networking/Misc/Counter.swift index 12e90087..8862c504 100644 --- a/Sources/Networking/Misc/Counter.swift +++ b/Sources/Networking/Misc/Counter.swift @@ -1,6 +1,6 @@ // // Counter.swift -// +// // // Created by Matej Molnár on 14.12.2022. // diff --git a/Sources/Networking/Misc/MockResponseProviderError.swift b/Sources/Networking/Misc/MockResponseProviderError.swift index 730024b8..0319461a 100644 --- a/Sources/Networking/Misc/MockResponseProviderError.swift +++ b/Sources/Networking/Misc/MockResponseProviderError.swift @@ -1,6 +1,6 @@ // // MockResponseProviderError.swift -// +// // // Created by Matej Molnár on 04.01.2023. // diff --git a/Sources/Networking/Misc/ThreadSafeDictionary.swift b/Sources/Networking/Misc/ThreadSafeDictionary.swift index 5d81e04a..864ffed5 100644 --- a/Sources/Networking/Misc/ThreadSafeDictionary.swift +++ b/Sources/Networking/Misc/ThreadSafeDictionary.swift @@ -1,6 +1,6 @@ // // ThreadSafeDictionary.swift -// +// // // Created by Dominika Gajdová on 25.05.2023. // @@ -20,7 +20,7 @@ actor ThreadSafeDictionary { } func set(value: Value?, for task: Key) { - values[task] = value + values[task] = value } /// Updates the property of a given keyPath. diff --git a/Sources/Networking/Misc/URLSessionTask+AsyncResponse.swift b/Sources/Networking/Misc/URLSessionTask+AsyncResponse.swift index 8451765d..d75e7c84 100644 --- a/Sources/Networking/Misc/URLSessionTask+AsyncResponse.swift +++ b/Sources/Networking/Misc/URLSessionTask+AsyncResponse.swift @@ -1,6 +1,6 @@ // // URLSessionTask+AsyncResponse.swift -// +// // // Created by Dominika Gajdová on 12.05.2023. // diff --git a/Sources/Networking/Misc/URLSessionTask+DownloadState.swift b/Sources/Networking/Misc/URLSessionTask+DownloadState.swift index 239d19e5..0417013e 100644 --- a/Sources/Networking/Misc/URLSessionTask+DownloadState.swift +++ b/Sources/Networking/Misc/URLSessionTask+DownloadState.swift @@ -1,6 +1,6 @@ // -// DownloadState.swift -// +// URLSessionTask+DownloadState.swift +// // // Created by Matej Molnár on 07.03.2023. // diff --git a/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationData.swift b/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationData.swift index 259fecb0..e6d0a6b5 100644 --- a/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationData.swift +++ b/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationData.swift @@ -1,6 +1,6 @@ // // AuthorizationData.swift -// +// // // Created by Dominika Gajdová on 20.12.2022. // @@ -23,8 +23,8 @@ public struct AuthorizationData { } // MARK: Computed propeties -extension AuthorizationData { - public var isExpired: Bool { +public extension AuthorizationData { + var isExpired: Bool { guard let expiresIn else { /// If there is no information about expiration, always assume it is not expired. return false diff --git a/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationError.swift b/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationError.swift index 271526b7..7ac788ba 100644 --- a/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationError.swift +++ b/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationError.swift @@ -1,6 +1,6 @@ // // AuthorizationError.swift -// +// // // Created by Dominika Gajdová on 02.01.2023. // diff --git a/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationManaging.swift b/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationManaging.swift index 95dd0a18..f177adb9 100644 --- a/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationManaging.swift +++ b/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationManaging.swift @@ -1,6 +1,6 @@ // // AuthorizationManaging.swift -// +// // // Created by Dominika Gajdová on 20.12.2022. // diff --git a/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationStorageManaging.swift b/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationStorageManaging.swift index f779e95d..4939e932 100644 --- a/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationStorageManaging.swift +++ b/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationStorageManaging.swift @@ -1,6 +1,6 @@ // // AuthorizationStorageManaging.swift -// +// // // Created by Dominika Gajdová on 20.12.2022. // @@ -9,7 +9,7 @@ import Foundation /// Basic operations to store `AuthorizationData` /// To keep consistency all operations are async -public protocol AuthorizationStorageManaging { +public protocol AuthorizationStorageManaging { func saveData(_ data: AuthorizationData) async throws func getData() async throws -> AuthorizationData func deleteData() async throws diff --git a/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationTokenInterceptor.swift b/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationTokenInterceptor.swift index 693616cc..2c2c5d29 100644 --- a/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationTokenInterceptor.swift +++ b/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationTokenInterceptor.swift @@ -1,6 +1,6 @@ // // AuthorizationTokenInterceptor.swift -// +// // // Created by Dominika Gajdová on 08.12.2022. // diff --git a/Sources/Networking/Modifiers/Interceptors/LoggingInterceptor.swift b/Sources/Networking/Modifiers/Interceptors/LoggingInterceptor.swift index fe12d8d2..4cb36cff 100644 --- a/Sources/Networking/Modifiers/Interceptors/LoggingInterceptor.swift +++ b/Sources/Networking/Modifiers/Interceptors/LoggingInterceptor.swift @@ -1,6 +1,6 @@ // // LoggingInterceptor.swift -// +// // // Created by Matej Molnár on 01.12.2022. // @@ -73,9 +73,7 @@ private extension LoggingInterceptor { let requestBody = request.httpBody, let object = try? JSONSerialization.jsonObject(with: requestBody, options: []), let data = try? JSONSerialization.data(withJSONObject: object, options: [.prettyPrinted]), - let body = String(data: data, encoding: .utf8) - { - // swiftlint:disable:previous opening_brace + let body = String(data: data, encoding: .utf8) { os_log("👉 Body: %{public}@", type: .info, body) } @@ -131,4 +129,3 @@ private extension LoggingInterceptor { os_log("❌❌❌ ERROR END ❌❌❌", type: .error) } } - diff --git a/Sources/Networking/Modifiers/Processors/EndpointRequestStorageProcessor/EndpointRequestStorageProcessor.swift b/Sources/Networking/Modifiers/Processors/EndpointRequestStorageProcessor/EndpointRequestStorageProcessor.swift index 6b91677f..91ec57a2 100644 --- a/Sources/Networking/Modifiers/Processors/EndpointRequestStorageProcessor/EndpointRequestStorageProcessor.swift +++ b/Sources/Networking/Modifiers/Processors/EndpointRequestStorageProcessor/EndpointRequestStorageProcessor.swift @@ -1,6 +1,6 @@ // // EndpointRequestStorageProcessor.swift -// +// // // Created by Matej Molnár on 12.12.2022. // diff --git a/Sources/Networking/Modifiers/Processors/StatusCodeProcessor.swift b/Sources/Networking/Modifiers/Processors/StatusCodeProcessor.swift index e4af464c..cd907b5a 100644 --- a/Sources/Networking/Modifiers/Processors/StatusCodeProcessor.swift +++ b/Sources/Networking/Modifiers/Processors/StatusCodeProcessor.swift @@ -1,6 +1,6 @@ // // StatusCodeProcessor.swift -// +// // // Created by Matej Molnár on 01.12.2022. // diff --git a/Sources/Networking/Utils/Sequence+Convenience.swift b/Sources/Networking/Utils/Sequence+Convenience.swift index cf97ed41..9ac25667 100644 --- a/Sources/Networking/Utils/Sequence+Convenience.swift +++ b/Sources/Networking/Utils/Sequence+Convenience.swift @@ -1,6 +1,6 @@ // // Sequence+Convenience.swift -// +// // // Created by Tomas Cejka on 15.11.2022. // diff --git a/Sources/Networking/Utils/URL+Convenience.swift b/Sources/Networking/Utils/URL+Convenience.swift index 92bf9fbe..f578517a 100644 --- a/Sources/Networking/Utils/URL+Convenience.swift +++ b/Sources/Networking/Utils/URL+Convenience.swift @@ -1,6 +1,6 @@ // // URL+Convenience.swift -// +// // // Created by Tony Ngo on 18.06.2023. // @@ -18,7 +18,7 @@ public extension URL { } var fileSize: Int? { - guard let resources = try? resourceValues(forKeys:[.fileSizeKey]) else { + guard let resources = try? resourceValues(forKeys: [.fileSizeKey]) else { return nil } return resources.fileSize From 39dbca1b08cc0af37dba721d7a668f97bc898ff6 Mon Sep 17 00:00:00 2001 From: Tomas Cejka Date: Wed, 6 Sep 2023 22:18:57 +0200 Subject: [PATCH 02/29] [fix] networking plugin command --- Plugins/SwiftLintXcodeNetworking/Plugin.swift | 4 +++- Sources/Networking/Core/Upload/MultipartFormData.swift | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Plugins/SwiftLintXcodeNetworking/Plugin.swift b/Plugins/SwiftLintXcodeNetworking/Plugin.swift index 8bd0bfaf..1b9c0b5a 100644 --- a/Plugins/SwiftLintXcodeNetworking/Plugin.swift +++ b/Plugins/SwiftLintXcodeNetworking/Plugin.swift @@ -10,7 +10,7 @@ import PackagePlugin @main struct SwiftLintPlugins: BuildToolPlugin { func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { - return [ + [ .buildCommand( displayName: "Linting \(target.name)", executable: try context.tool(named: "swiftlint").path, @@ -18,6 +18,8 @@ struct SwiftLintPlugins: BuildToolPlugin { "lint", "--config", "\(target.directory.string)/.swiftlint.yml", + "--cache-path", + "\(context.pluginWorkDirectory.string)/cache", target.directory.string // only lint the files in the target directory ], environment: [:] diff --git a/Sources/Networking/Core/Upload/MultipartFormData.swift b/Sources/Networking/Core/Upload/MultipartFormData.swift index 4cd84401..03a86fed 100644 --- a/Sources/Networking/Core/Upload/MultipartFormData.swift +++ b/Sources/Networking/Core/Upload/MultipartFormData.swift @@ -71,7 +71,7 @@ public extension MultipartFormData { let fileName = fileName ?? fileUrl.lastPathComponent guard !fileName.isEmpty && !fileUrl.pathExtension.isEmpty else { - throw EncodingError.invalidFileName(at: fileUrl) + throw EncodingError.invalidFileName(for: fileUrl) } guard From 434e0e4bc09108f1ba8a9ee441369f0927922104 Mon Sep 17 00:00:00 2001 From: Tomas Cejka Date: Mon, 11 Sep 2023 12:46:10 +0200 Subject: [PATCH 03/29] [chore] fix swiftlint warning in sample app & tests --- .../API/Routers/SampleAuthRouter.swift | 1 - .../API/Routers/SampleDownloadRouter.swift | 2 +- .../API/Routers/SampleUploadRouter.swift | 1 + .../API/Routers/SampleUserRouter.swift | 3 +- .../API/SampleAPIConstants.swift | 3 +- .../Scenes/Download/DownloadsView.swift | 2 +- .../Scenes/Download/DownloadsViewModel.swift | 2 +- .../AssociatedArrayQueryTests.swift | 24 +++++++------ .../AuthorizationTokenInterceptorTests.swift | 15 ++++---- ...EndpointRequestStorageProcessorTests.swift | 10 +++--- .../NetworkingTests/ErrorProcessorTests.swift | 9 ++--- .../MockResponseProviderTests.swift | 34 ++++++++++--------- .../MultipartFormDataEncoderTests.swift | 5 +-- .../StatusCodeProcessorTests.swift | 4 ++- 14 files changed, 62 insertions(+), 53 deletions(-) diff --git a/NetworkingSampleApp/NetworkingSampleApp/API/Routers/SampleAuthRouter.swift b/NetworkingSampleApp/NetworkingSampleApp/API/Routers/SampleAuthRouter.swift index a28b4864..929ab06d 100644 --- a/NetworkingSampleApp/NetworkingSampleApp/API/Routers/SampleAuthRouter.swift +++ b/NetworkingSampleApp/NetworkingSampleApp/API/Routers/SampleAuthRouter.swift @@ -15,7 +15,6 @@ enum SampleAuthRouter: Requestable { case status var baseURL: URL { - /// sample API host // swiftlint:disable:next force_unwrapping return URL(string: SampleAPIConstants.authHost)! } diff --git a/NetworkingSampleApp/NetworkingSampleApp/API/Routers/SampleDownloadRouter.swift b/NetworkingSampleApp/NetworkingSampleApp/API/Routers/SampleDownloadRouter.swift index 850f4794..d09a4751 100644 --- a/NetworkingSampleApp/NetworkingSampleApp/API/Routers/SampleDownloadRouter.swift +++ b/NetworkingSampleApp/NetworkingSampleApp/API/Routers/SampleDownloadRouter.swift @@ -1,6 +1,6 @@ // // SampleDownloadRouter.swift -// +// // // Created by Matej Molnár on 07.03.2023. // diff --git a/NetworkingSampleApp/NetworkingSampleApp/API/Routers/SampleUploadRouter.swift b/NetworkingSampleApp/NetworkingSampleApp/API/Routers/SampleUploadRouter.swift index 49c2f4b8..fc4643dd 100644 --- a/NetworkingSampleApp/NetworkingSampleApp/API/Routers/SampleUploadRouter.swift +++ b/NetworkingSampleApp/NetworkingSampleApp/API/Routers/SampleUploadRouter.swift @@ -14,6 +14,7 @@ enum SampleUploadRouter: Requestable { case multipart(boundary: String) var baseURL: URL { + // swiftlint:disable:next force_unwrapping URL(string: SampleAPIConstants.uploadHost)! } diff --git a/NetworkingSampleApp/NetworkingSampleApp/API/Routers/SampleUserRouter.swift b/NetworkingSampleApp/NetworkingSampleApp/API/Routers/SampleUserRouter.swift index c5ca2d68..14711006 100644 --- a/NetworkingSampleApp/NetworkingSampleApp/API/Routers/SampleUserRouter.swift +++ b/NetworkingSampleApp/NetworkingSampleApp/API/Routers/SampleUserRouter.swift @@ -9,14 +9,13 @@ import Foundation import Networking /// Implementation of sample API router -enum SampleUserRouter: Requestable { +enum SampleUserRouter: Requestable { case users(page: Int) case user(userId: Int) case createUser(user: SampleUserRequest) case registerUser(user: SampleUserAuthRequest) var baseURL: URL { - /// sample API host // swiftlint:disable:next force_unwrapping URL(string: SampleAPIConstants.userHost)! } diff --git a/NetworkingSampleApp/NetworkingSampleApp/API/SampleAPIConstants.swift b/NetworkingSampleApp/NetworkingSampleApp/API/SampleAPIConstants.swift index 15e5ed0d..9c7efc2f 100644 --- a/NetworkingSampleApp/NetworkingSampleApp/API/SampleAPIConstants.swift +++ b/NetworkingSampleApp/NetworkingSampleApp/API/SampleAPIConstants.swift @@ -11,8 +11,9 @@ import Foundation enum SampleAPIConstants { static let userHost = "https://reqres.in/api" static let authHost = "https://nonexistentmockauth.com/api" + // swiftlint:disable:next force_https static let uploadHost = "https://httpbin.org" static let validEmail = "eve.holt@reqres.in" static let validPassword = "cityslicka" - static let videoUrl = "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4" + static let videoUrl = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4" } diff --git a/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadsView.swift b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadsView.swift index abd14cf5..7c6571b4 100644 --- a/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadsView.swift +++ b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadsView.swift @@ -1,6 +1,6 @@ // // DownloadsView.swift -// +// // // Created by Matej Molnár on 07.03.2023. // diff --git a/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadsViewModel.swift b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadsViewModel.swift index ba616989..14651401 100644 --- a/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadsViewModel.swift +++ b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadsViewModel.swift @@ -1,6 +1,6 @@ // // DownloadsViewModel.swift -// +// // // Created by Matej Molnár on 07.03.2023. // diff --git a/Tests/NetworkingTests/AssociatedArrayQueryTests.swift b/Tests/NetworkingTests/AssociatedArrayQueryTests.swift index e12c9512..c89421e9 100644 --- a/Tests/NetworkingTests/AssociatedArrayQueryTests.swift +++ b/Tests/NetworkingTests/AssociatedArrayQueryTests.swift @@ -1,6 +1,6 @@ // -// File.swift -// +// AssociatedArrayQueryTests.swift +// // // Created by Dominika Gajdová on 08.05.2023. // @@ -15,10 +15,14 @@ final class AssociatedArrayQueryTests: XCTestCase { case arraySeparated case both - var baseURL: URL { URL(string: "http://someurl.com")! } + var baseURL: URL { + // swiftlint:disable:next force_unwrapping + URL(string: "https://someurl.com")! + } + var path: String { "" } - var urlParameters: [String: Any]? { + var urlParameters: [String: Any]? { switch self { case .single: return ["filter": 1] @@ -37,25 +41,23 @@ final class AssociatedArrayQueryTests: XCTestCase { func testMultipleKeyParamaterURLCreation() async throws { let urlRequest1 = try TestRouter.single.asRequest() - XCTAssertEqual("http://someurl.com/?filter=1", urlRequest1.url?.absoluteString ?? "") + XCTAssertEqual("https://someurl.com/?filter=1", urlRequest1.url?.absoluteString ?? "") let urlRequest2 = try TestRouter.arrayIndividual.asRequest() - XCTAssertEqual("http://someurl.com/?filter=1&filter=2&filter=3", urlRequest2.url?.absoluteString ?? "") + XCTAssertEqual("https://someurl.com/?filter=1&filter=2&filter=3", urlRequest2.url?.absoluteString ?? "") let urlRequest3 = try TestRouter.arraySeparated.asRequest() - XCTAssertEqual("http://someurl.com/?filter=1,2,3", urlRequest3.url?.absoluteString ?? "") + XCTAssertEqual("https://someurl.com/?filter=1,2,3", urlRequest3.url?.absoluteString ?? "") let urlRequest4 = try TestRouter.both.asRequest() if let url = urlRequest4.url, let components = URLComponents(url: url, resolvingAgainstBaseURL: true), let queryItems = components.queryItems, - let parameters = TestRouter.both.urlParameters - { - let result = parameters.allSatisfy { (key, value) in + let parameters = TestRouter.both.urlParameters { + let result = parameters.allSatisfy { (key, _) in queryItems.contains(where: { $0.name == key }) } - XCTAssertTrue(result) } else { XCTFail("Invalid request url and/or query parameters.") diff --git a/Tests/NetworkingTests/AuthorizationTokenInterceptorTests.swift b/Tests/NetworkingTests/AuthorizationTokenInterceptorTests.swift index 7bac3d57..af36eb57 100644 --- a/Tests/NetworkingTests/AuthorizationTokenInterceptorTests.swift +++ b/Tests/NetworkingTests/AuthorizationTokenInterceptorTests.swift @@ -1,6 +1,6 @@ // // AuthorizationTokenInterceptorTests.swift -// +// // // Created by Matej Molnár on 02.02.2023. // @@ -26,7 +26,7 @@ final class AuthorizationTokenInterceptorTests: XCTestCase { let adaptedRequest = try await authTokenInterceptor.adapt(request, for: endpointRequest) - XCTAssertEqual(adaptedRequest.allHTTPHeaderFields![HTTPHeader.HeaderField.authorization.rawValue], "Bearer \(validAuthData.accessToken)") + XCTAssertEqual(adaptedRequest.allHTTPHeaderFields?[HTTPHeader.HeaderField.authorization.rawValue], "Bearer \(validAuthData.accessToken)") } func testFailedRequestAuthorization() async throws { @@ -40,7 +40,7 @@ final class AuthorizationTokenInterceptorTests: XCTestCase { do { _ = try await authTokenInterceptor.adapt(request, for: endpointRequest) } catch { - XCTAssertEqual(error as! AuthorizationError, AuthorizationError.missingAuthorizationData) + XCTAssertEqual(error as? AuthorizationError, AuthorizationError.missingAuthorizationData) } } @@ -77,7 +77,7 @@ final class AuthorizationTokenInterceptorTests: XCTestCase { let adaptedRequest = try await authTokenInterceptor.adapt(request, for: endpointRequest) - XCTAssertEqual(adaptedRequest.allHTTPHeaderFields![HTTPHeader.HeaderField.authorization.rawValue], "Bearer \(refreshedAuthData.accessToken)") + XCTAssertEqual(adaptedRequest.allHTTPHeaderFields?[HTTPHeader.HeaderField.authorization.rawValue], "Bearer \(refreshedAuthData.accessToken)") } func testFailedTokenRefresh() async throws { @@ -94,7 +94,7 @@ final class AuthorizationTokenInterceptorTests: XCTestCase { do { _ = try await authTokenInterceptor.adapt(request, for: endpointRequest) } catch { - XCTAssertEqual(error as! AuthorizationError, AuthorizationError.expiredAccessToken) + XCTAssertEqual(error as? AuthorizationError, AuthorizationError.expiredAccessToken) } } @@ -121,7 +121,7 @@ final class AuthorizationTokenInterceptorTests: XCTestCase { group.addTask { do { let request = try await authTokenInterceptor.adapt(request, for: endpointRequest) - XCTAssertEqual(request.allHTTPHeaderFields![HTTPHeader.HeaderField.authorization.rawValue], "Bearer \(refreshedAuthData.accessToken)") + XCTAssertEqual(request.allHTTPHeaderFields?[HTTPHeader.HeaderField.authorization.rawValue], "Bearer \(refreshedAuthData.accessToken)") } catch { XCTAssert(false, "function shouldn't throw and error: \(error)") } @@ -151,7 +151,7 @@ final class AuthorizationTokenInterceptorTests: XCTestCase { _ = try await authTokenInterceptor.adapt(request, for: endpointRequest) XCTAssert(false, "function didn't throw an error even though it should have") } catch { - XCTAssertEqual(error as! AuthorizationError, AuthorizationError.expiredAccessToken) + XCTAssertEqual(error as? AuthorizationError, AuthorizationError.expiredAccessToken) } } } @@ -202,6 +202,7 @@ private enum MockRouter: Requestable { case testAuthenticationNotRequired var baseURL: URL { + // swiftlint:disable:next force_unwrapping URL(string: "test.com")! } diff --git a/Tests/NetworkingTests/EndpointRequestStorageProcessorTests.swift b/Tests/NetworkingTests/EndpointRequestStorageProcessorTests.swift index c42eaa18..9d51b57a 100644 --- a/Tests/NetworkingTests/EndpointRequestStorageProcessorTests.swift +++ b/Tests/NetworkingTests/EndpointRequestStorageProcessorTests.swift @@ -1,6 +1,6 @@ // // EndpointRequestStorageProcessorTests.swift -// +// // // Created by Matej Molnár on 12.12.2022. // @@ -51,14 +51,14 @@ final class EndpointRequestStorageProcessorTests: XCTestCase { return .post } } - var urlParameters: [String : Any]? { + var urlParameters: [String: Any]? { switch self { case .testStoringGet, .testStoringPost, .testStoringImage, .testStoringError: return ["query": "mock"] } } - var headers: [String : String]? { + var headers: [String: String]? { switch self { case .testStoringGet, .testStoringPost, .testStoringImage, .testStoringError: return ["mockRequestHeader": "mock"] @@ -278,13 +278,13 @@ final class EndpointRequestStorageProcessorTests: XCTestCase { ) } + // swiftlint:enable force_unwrapping static var allTests = [ ("testResponseStaysTheSameAfterStoringData", testResponseStaysTheSameAfterStoringData), ("testStoredDataForGetRequestWithJSONResponse", testStoredDataForGetRequestWithJSONResponse), ("testStoredDataForGetRequestWithImageResponse", testStoredDataForGetRequestWithImageResponse), ("testStoredDataForGetRequestWithErrorResponse", testStoredDataForGetRequestWithErrorResponse), - ("testStoredDataForPostRequest", testStoredDataForPostRequest), - + ("testStoredDataForPostRequest", testStoredDataForPostRequest) ] } diff --git a/Tests/NetworkingTests/ErrorProcessorTests.swift b/Tests/NetworkingTests/ErrorProcessorTests.swift index a9400f39..f9e954ad 100644 --- a/Tests/NetworkingTests/ErrorProcessorTests.swift +++ b/Tests/NetworkingTests/ErrorProcessorTests.swift @@ -1,6 +1,6 @@ // -// File.swift -// +// ErrorProcessorTests.swift +// // // Created by Dominika Gajdová on 05.12.2022. // @@ -36,9 +36,10 @@ final class ErrorProcessorTests: XCTestCase { // Our mocked error processors don't utilise the endpointRequest parameter so we can use the same mocked endpointRequest for all tests private let mockEndpointRequest = EndpointRequest(MockRouter.testErrorProcessing, sessionId: "sessionId_error_process") - // swiftlint:disable:next force_unwrapping + private var testUrl: URL { - URL(string: "http://sometesturl.com")! + // swiftlint:disable:next force_unwrapping + URL(string: "https://sometesturl.com")! } func test_errorProcessing_process_mappingUnacceptableToSimpleErrorShouldSucceed() { diff --git a/Tests/NetworkingTests/MockResponseProviderTests.swift b/Tests/NetworkingTests/MockResponseProviderTests.swift index 4edd8096..c3ca6027 100644 --- a/Tests/NetworkingTests/MockResponseProviderTests.swift +++ b/Tests/NetworkingTests/MockResponseProviderTests.swift @@ -1,6 +1,6 @@ // // MockResponseProviderTests.swift -// +// // // Created by Matej Molnár on 05.01.2023. // @@ -12,22 +12,24 @@ final class MockResponseProviderTests: XCTestCase { // swiftlint:disable:next force_unwrapping private lazy var mockUrlRequest = URLRequest(url: URL(string: "https://reqres.in/api/users?page=2")!) private let mockSessionId = "2023-01-04T16:15:29Z" + private let mockHeaderFields = [ - "Server" : "cloudflare", - "Etag" : "W/\"406-ut0vzoCuidvyMf8arZpMpJ6ZRDw\"", - "x-powered-by" : "Express", - "nel" : "{\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}", - "Content-Encoding" : "br", - "Vary" : "Accept-Encoding", - "report-to" : "{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v3?s=5XGHUrnfYDsl7guBAx0nFk7LTbUgOLjp5%2BGMkSPetC5OrW6fKlUc1NBBtOKHKe9yWrcbXkF4TQe8jsv1c4KggYW1q4pYf5G2rQvA8XACg1znl6MbWiNj1w2wOg%3D%3D\"}],\"group\":\"cf-nel\",\"max_age\":604800}", - "Content-Type" : "application/json; charset=utf-8", - "cf-cache-status" : "HIT", - "Cache-Control" : "max-age=14400", - "Access-Control-Allow-Origin" : "*", - "cf-ray" : "784545f34d2f27bc-PRG", - "Date" : "Wed, 04 Jan 2023 16:15:29 GMT", - "Via" : "1.1 vegur", - "Age" : "6306" + "Server": "cloudflare", + "Etag": "W/\"406-ut0vzoCuidvyMf8arZpMpJ6ZRDw\"", + "x-powered-by": "Express", + "nel": "{\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}", + "Content-Encoding": "br", + "Vary": "Accept-Encoding", + // swiftlint:disable:next line_length + "report-to": "{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v3?s=5XGHUrnfYDsl7guBAx0nFk7LTbUgOLjp5%2BGMkSPetC5OrW6fKlUc1NBBtOKHKe9yWrcbXkF4TQe8jsv1c4KggYW1q4pYf5G2rQvA8XACg1znl6MbWiNj1w2wOg%3D%3D\"}],\"group\":\"cf-nel\",\"max_age\":604800}", + "Content-Type": "application/json; charset=utf-8", + "cf-cache-status": "HIT", + "Cache-Control": "max-age=14400", + "Access-Control-Allow-Origin": "*", + "cf-ray": "784545f34d2f27bc-PRG", + "Date": "Wed, 04 Jan 2023 16:15:29 GMT", + "Via": "1.1 vegur", + "Age": "6306" ] func testLoadingData() async throws { diff --git a/Tests/NetworkingTests/MultipartFormDataEncoderTests.swift b/Tests/NetworkingTests/MultipartFormDataEncoderTests.swift index aca40bf7..80e8a252 100644 --- a/Tests/NetworkingTests/MultipartFormDataEncoderTests.swift +++ b/Tests/NetworkingTests/MultipartFormDataEncoderTests.swift @@ -1,6 +1,6 @@ // // MultipartFormDataEncoderTests.swift -// +// // // Created by Tony Ngo on 18.06.2023. // @@ -77,6 +77,7 @@ final class MultipartFormDataEncoderTests: XCTestCase { func test_encode_throwsInvalidFileUrl() { let sut = makeSUT() let formData = MultipartFormData() + // swiftlint:disable:next force_unwrapping let tmpFileUrl = URL(string: "invalid/path")! do { @@ -84,7 +85,7 @@ final class MultipartFormDataEncoderTests: XCTestCase { XCTFail("Encoding should have failed.") } catch MultipartFormData.EncodingError.invalidFileUrl { } catch { - XCTFail("Should have failed with MultipartFormData.EncodingError.fileAlreadyExists") + XCTFail("Should have failed with MultipartFormData.EncodingError.invalidFileUrl") } } diff --git a/Tests/NetworkingTests/StatusCodeProcessorTests.swift b/Tests/NetworkingTests/StatusCodeProcessorTests.swift index c40a033d..62c8d6cc 100644 --- a/Tests/NetworkingTests/StatusCodeProcessorTests.swift +++ b/Tests/NetworkingTests/StatusCodeProcessorTests.swift @@ -1,6 +1,6 @@ // // StatusCodeProcessorTests.swift -// +// // // Created by Matej Molnár on 01.12.2022. // @@ -120,9 +120,11 @@ final class StatusCodeProcessorTests: XCTestCase { // MARK: - Factory methods to create mock objects private extension StatusCodeProcessorTests { + func createMockResponseParams( _ router: MockRouter, statusCode: HTTPStatusCode + // swiftlint:disable:next large_tuple ) -> (response: Response, urlRequest: URLRequest, endpointRequest: EndpointRequest) { let mockEndpointRequest = EndpointRequest(router, sessionId: sessionId) let mockURLRequest = URLRequest(url: router.baseURL) From 7011e5d86b3aea22b078a5f4fa841ceabec682f9 Mon Sep 17 00:00:00 2001 From: Tomas Cejka Date: Mon, 11 Sep 2023 20:34:45 +0200 Subject: [PATCH 04/29] [chore] fix swiftlint package setup --- .../.swiftlint.yml => .swiftlint.yml | 0 .../xcshareddata/swiftpm/Package.resolved | 78 ++++++++++- .../project.pbxproj | 18 +-- Package.swift | 17 +-- Plugins/SwiftLintXcodeNetworking/Plugin.swift | 29 ----- Sources/Networking/.swiftlint.yml | 121 ------------------ 6 files changed, 88 insertions(+), 175 deletions(-) rename NetworkingSampleApp/.swiftlint.yml => .swiftlint.yml (100%) delete mode 100644 Plugins/SwiftLintXcodeNetworking/Plugin.swift delete mode 100755 Sources/Networking/.swiftlint.yml diff --git a/NetworkingSampleApp/.swiftlint.yml b/.swiftlint.yml similarity index 100% rename from NetworkingSampleApp/.swiftlint.yml rename to .swiftlint.yml diff --git a/Networking.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Networking.xcworkspace/xcshareddata/swiftpm/Package.resolved index deb15a9b..39e2c5d7 100644 --- a/Networking.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Networking.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,13 +1,85 @@ { "pins" : [ { - "identity" : "swiftlintplugin", + "identity" : "collectionconcurrencykit", "kind" : "remoteSourceControl", - "location" : "https://github.com/lukepistrol/SwiftLintPlugin", + "location" : "https://github.com/JohnSundell/CollectionConcurrencyKit.git", "state" : { - "revision" : "b1090ecd269dddd96bda0df24ca3f1aa78f33578", + "revision" : "b4f23e24b5a1bff301efc5e70871083ca029ff95", + "version" : "0.2.0" + } + }, + { + "identity" : "cryptoswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/krzyzanowskim/CryptoSwift.git", + "state" : { + "revision" : "32f641cf24fc7abc1c591a2025e9f2f572648b0f", + "version" : "1.7.2" + } + }, + { + "identity" : "sourcekitten", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jpsim/SourceKitten.git", + "state" : { + "revision" : "b6dc09ee51dfb0c66e042d2328c017483a1a5d56", + "version" : "0.34.1" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531", + "version" : "1.2.3" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "59ed009d2c4a5a6b78f75a25679b6417ac040dcf", + "version" : "509.0.0-swift-DEVELOPMENT-SNAPSHOT-2023-07-04-a" + } + }, + { + "identity" : "swiftlint", + "kind" : "remoteSourceControl", + "location" : "https://github.com/realm/SwiftLint.git", + "state" : { + "revision" : "9eaecbedce469a51bd8487effbd4ab46ec8384ae", "version" : "0.52.4" } + }, + { + "identity" : "swiftytexttable", + "kind" : "remoteSourceControl", + "location" : "https://github.com/scottrhoyt/SwiftyTextTable.git", + "state" : { + "revision" : "c6df6cf533d120716bff38f8ff9885e1ce2a4ac3", + "version" : "0.9.0" + } + }, + { + "identity" : "swxmlhash", + "kind" : "remoteSourceControl", + "location" : "https://github.com/drmohundro/SWXMLHash.git", + "state" : { + "revision" : "a853604c9e9a83ad9954c7e3d2a565273982471f", + "version" : "7.0.2" + } + }, + { + "identity" : "yams", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jpsim/Yams.git", + "state" : { + "revision" : "0d9ee7ea8c4ebd4a489ad7a73d5c6cad55d6fed3", + "version" : "5.0.6" + } } ], "version" : 2 diff --git a/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.pbxproj b/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.pbxproj index 5599fc2e..5f2d4591 100644 --- a/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.pbxproj +++ b/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.pbxproj @@ -278,7 +278,7 @@ buildRules = ( ); dependencies = ( - 230A9F9C2AA8FD8500773B2F /* PBXTargetDependency */, + 23204AB02AAF381000F8D6A7 /* PBXTargetDependency */, ); name = NetworkingSampleApp; packageProductDependencies = ( @@ -313,7 +313,7 @@ ); mainGroup = 23A575A825F8B9DA00617551; packageReferences = ( - 230A9F9A2AA8FD5F00773B2F /* XCRemoteSwiftPackageReference "SwiftLintPlugin" */, + 23204AAE2AAF380700F8D6A7 /* XCRemoteSwiftPackageReference "SwiftLint" */, ); productRefGroup = 23A575B225F8B9DA00617551 /* Products */; projectDirPath = ""; @@ -380,9 +380,9 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 230A9F9C2AA8FD8500773B2F /* PBXTargetDependency */ = { + 23204AB02AAF381000F8D6A7 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - productRef = 230A9F9B2AA8FD8500773B2F /* SwiftLint */; + productRef = 23204AAF2AAF381000F8D6A7 /* SwiftLintPlugin */; }; /* End PBXTargetDependency section */ @@ -578,9 +578,9 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 230A9F9A2AA8FD5F00773B2F /* XCRemoteSwiftPackageReference "SwiftLintPlugin" */ = { + 23204AAE2AAF380700F8D6A7 /* XCRemoteSwiftPackageReference "SwiftLint" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/lukepistrol/SwiftLintPlugin"; + repositoryURL = "https://github.com/realm/SwiftLint.git"; requirement = { kind = upToNextMajorVersion; minimumVersion = 0.52.4; @@ -593,10 +593,10 @@ isa = XCSwiftPackageProductDependency; productName = Networking; }; - 230A9F9B2AA8FD8500773B2F /* SwiftLint */ = { + 23204AAF2AAF381000F8D6A7 /* SwiftLintPlugin */ = { isa = XCSwiftPackageProductDependency; - package = 230A9F9A2AA8FD5F00773B2F /* XCRemoteSwiftPackageReference "SwiftLintPlugin" */; - productName = "plugin:SwiftLint"; + package = 23204AAE2AAF380700F8D6A7 /* XCRemoteSwiftPackageReference "SwiftLint" */; + productName = "plugin:SwiftLintPlugin"; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/Package.swift b/Package.swift index 48b253c0..3bf58b1f 100644 --- a/Package.swift +++ b/Package.swift @@ -17,28 +17,19 @@ let package = Package( targets: ["Networking"] ) ], + dependencies: [.package(url: "https://github.com/realm/SwiftLint.git", exact: "0.52.4")], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages this package depends on. .target( name: "Networking", - plugins: ["SwiftLintXcodeNetworking"] + plugins: [.plugin(name: "SwiftLintPlugin", package: "SwiftLint")] ), .testTarget( name: "NetworkingTests", dependencies: ["Networking"], - resources: [.process("Resources")] - ), - .binaryTarget( - name: "SwiftLintBinaryNetworking", - url: "https://github.com/realm/SwiftLint/releases/download/0.52.4/SwiftLintBinary-macos.artifactbundle.zip", - checksum: "8a8095e6235a07d00f34a9e500e7568b359f6f66a249f36d12cd846017a8c6f5" - ), - // 2. Define the SPM plugin. - .plugin( - name: "SwiftLintXcodeNetworking", - capability: .buildTool(), - dependencies: ["SwiftLintBinaryNetworking"] + resources: [.process("Resources")], + plugins: [.plugin(name: "SwiftLintPlugin", package: "SwiftLint")] ) ] ) diff --git a/Plugins/SwiftLintXcodeNetworking/Plugin.swift b/Plugins/SwiftLintXcodeNetworking/Plugin.swift deleted file mode 100644 index 1b9c0b5a..00000000 --- a/Plugins/SwiftLintXcodeNetworking/Plugin.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// Plugin.swift -// -// -// Created by Tomas Cejka on 06.09.2023. -// - -import PackagePlugin - -@main -struct SwiftLintPlugins: BuildToolPlugin { - func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { - [ - .buildCommand( - displayName: "Linting \(target.name)", - executable: try context.tool(named: "swiftlint").path, - arguments: [ - "lint", - "--config", - "\(target.directory.string)/.swiftlint.yml", - "--cache-path", - "\(context.pluginWorkDirectory.string)/cache", - target.directory.string // only lint the files in the target directory - ], - environment: [:] - ) - ] - } -} diff --git a/Sources/Networking/.swiftlint.yml b/Sources/Networking/.swiftlint.yml deleted file mode 100755 index 8015693b..00000000 --- a/Sources/Networking/.swiftlint.yml +++ /dev/null @@ -1,121 +0,0 @@ -# -# SwiftLint configuration file commonly used in STRV (v 0.1.2) -# -# Feel free to send pull request or suggest improvements! -# - -analyzer_rules: - - unused_import - -# -# Rule identifiers to exclude from running. -# -disabled_rules: - -# -# Some rules are only opt-in. Find all the available rules by running: swiftlint rules -# -opt_in_rules: - - empty_count - - force_unwrapping - - conditional_returns_on_newline - - private_action - - private_outlet - - implicitly_unwrapped_optional - - empty_string - - extension_access_modifier - - closure_spacing - - fatal_error_message - - first_where - - toggle_bool - - modifier_order - - contains_over_first_not_nil - - convenience_type - - fallthrough - - unavailable_function - - strict_fileprivate - - explicit_init - - redundant_objc_attribute - - unowned_variable_capture - - strong_iboutlet - -# -# Paths to include during linting. `--path` is ignored if present. -# -included: - - ./ - - ../Sources - - ../Tests - -# -# Paths to ignore during linting. Takes precedence over `included`. -# -excluded: - - Carthage - - Pods - - Tests - - Scripts - - vendor - - fastlane - - R.generated.swift - -# -# Configure individual rules below. -# -vertical_whitespace: - max_empty_lines: 2 - -force_cast: error - -identifier_name: - excluded: - - id - -empty_count: - severity: warning - -trailing_whitespace: - ignores_empty_lines: true - -line_length: - ignores_function_declarations: true - ignores_comments: true - ignores_urls: true - error: 300 - warning: 200 - -function_body_length: - error: 150 - warning: 100 - -type_body_length: - error: 900 - warning: 600 - -file_length: - error: 1000 - warning: 700 - - -# Custom rules definitions -custom_rules: - comments_space: # From https://github.com/brandenr/swiftlintconfig - name: "Space After Comment" - regex: "(^ *//\\w+)" - message: "There should be a space after //" - severity: warning - - force_https: # From https://github.com/Twigz/Game - name: "Force HTTPS over HTTP" - regex: "((?i)http(?!s))" - match_kinds: string - message: "HTTPS should be favored over HTTP" - severity: warning - - double_space: # From https://github.com/IBM-Swift/Package-Builder - include: "*.swift" - name: "Double space" - regex: "([a-z,A-Z] \\s+)" - message: "Double space between keywords" - match_kinds: keyword - severity: warning From 4d36864f7fadafe78ba2882af46f2050e3a4c580 Mon Sep 17 00:00:00 2001 From: Tomas Cejka Date: Mon, 11 Sep 2023 21:29:42 +0200 Subject: [PATCH 05/29] [chore] sample app doesn't have tests --- .../project.pbxproj | 10 --------- .../NetworkingSampleAppTests/Info.plist | 22 ------------------- 2 files changed, 32 deletions(-) delete mode 100644 NetworkingSampleApp/NetworkingSampleAppTests/Info.plist diff --git a/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.pbxproj b/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.pbxproj index 5f2d4591..75cdff2a 100644 --- a/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.pbxproj +++ b/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.pbxproj @@ -51,7 +51,6 @@ 23A575C025F8B9DB00617551 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 23A575C225F8B9DB00617551 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 23C16B79276B5AFD00C0B4F1 /* ios-networking */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "ios-networking"; path = ..; sourceTree = ""; }; - 23D8FA2D261DA21200DE1800 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 23EA9CE9292FB70A00B8E418 /* SampleUserRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleUserRouter.swift; sourceTree = ""; }; 23EA9CEA292FB70A00B8E418 /* SampleAPIConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleAPIConstants.swift; sourceTree = ""; }; 23EA9CEB292FB70A00B8E418 /* SampleAPIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleAPIError.swift; sourceTree = ""; }; @@ -104,7 +103,6 @@ children = ( 23C16B78276B5AFD00C0B4F1 /* Packages */, 23A575B325F8B9DA00617551 /* NetworkingSampleApp */, - 23D8FA2A261DA21200DE1800 /* NetworkingSampleAppTests */, 23A575B225F8B9DA00617551 /* Products */, 23A575E725F8BA3600617551 /* Frameworks */, ); @@ -159,14 +157,6 @@ name = Packages; sourceTree = ""; }; - 23D8FA2A261DA21200DE1800 /* NetworkingSampleAppTests */ = { - isa = PBXGroup; - children = ( - 23D8FA2D261DA21200DE1800 /* Info.plist */, - ); - path = NetworkingSampleAppTests; - sourceTree = ""; - }; 23EA9CE7292FB70A00B8E418 /* API */ = { isa = PBXGroup; children = ( diff --git a/NetworkingSampleApp/NetworkingSampleAppTests/Info.plist b/NetworkingSampleApp/NetworkingSampleAppTests/Info.plist deleted file mode 100644 index 64d65ca4..00000000 --- a/NetworkingSampleApp/NetworkingSampleAppTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - From ac836177b8590aa3886d0ad1614074ded1b23f38 Mon Sep 17 00:00:00 2001 From: Tomas Cejka Date: Mon, 11 Sep 2023 21:48:03 +0200 Subject: [PATCH 06/29] [fix] tests, remove swiftlint from package test target --- Package.swift | 3 +-- Tests/NetworkingTests/AssociatedArrayQueryTests.swift | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 3bf58b1f..f9eeb0c3 100644 --- a/Package.swift +++ b/Package.swift @@ -28,8 +28,7 @@ let package = Package( .testTarget( name: "NetworkingTests", dependencies: ["Networking"], - resources: [.process("Resources")], - plugins: [.plugin(name: "SwiftLintPlugin", package: "SwiftLint")] + resources: [.process("Resources")] ) ] ) diff --git a/Tests/NetworkingTests/AssociatedArrayQueryTests.swift b/Tests/NetworkingTests/AssociatedArrayQueryTests.swift index c89421e9..d50eb605 100644 --- a/Tests/NetworkingTests/AssociatedArrayQueryTests.swift +++ b/Tests/NetworkingTests/AssociatedArrayQueryTests.swift @@ -17,7 +17,7 @@ final class AssociatedArrayQueryTests: XCTestCase { var baseURL: URL { // swiftlint:disable:next force_unwrapping - URL(string: "https://someurl.com")! + URL(string: "https://someurl.com/")! } var path: String { "" } From e3eb5ace9245ccbaab1be3044f0f2718f504d1c9 Mon Sep 17 00:00:00 2001 From: Tomas Cejka Date: Tue, 12 Sep 2023 14:02:42 +0200 Subject: [PATCH 07/29] [chore] add linter to networking tests --- Package.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index f9eeb0c3..3bf58b1f 100644 --- a/Package.swift +++ b/Package.swift @@ -28,7 +28,8 @@ let package = Package( .testTarget( name: "NetworkingTests", dependencies: ["Networking"], - resources: [.process("Resources")] + resources: [.process("Resources")], + plugins: [.plugin(name: "SwiftLintPlugin", package: "SwiftLint")] ) ] ) From 59640ce7530071fc07a8b24f923fa0dc95778c92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kodes=CC=8C?= Date: Tue, 12 Sep 2023 15:17:14 +0200 Subject: [PATCH 08/29] [chore]: Update swiftlint config --- .swiftlint.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index 8015693b..e459d116 100755 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -43,9 +43,9 @@ opt_in_rules: # Paths to include during linting. `--path` is ignored if present. # included: - - ./ - - ../Sources - - ../Tests + - NetworkingSampleApp + - Sources + - Tests # # Paths to ignore during linting. Takes precedence over `included`. @@ -53,7 +53,6 @@ included: excluded: - Carthage - Pods - - Tests - Scripts - vendor - fastlane From 073b9944c3945607574b38b14f9b1c32656a9a2d Mon Sep 17 00:00:00 2001 From: Tomas Cejka Date: Tue, 3 Oct 2023 11:14:11 +0200 Subject: [PATCH 09/29] [chore] update swiftlint, tools version, remove unnecessary return --- .../xcschemes/Networking.xcscheme | 2 +- .../xcshareddata/swiftpm/Package.resolved | 8 +++---- .../project.pbxproj | 6 +++-- .../xcschemes/NetworkingSampleApp.xcscheme | 2 +- .../API/Routers/SampleAuthRouter.swift | 22 ++++++++--------- .../API/Routers/SampleDownloadRouter.swift | 4 ++-- .../API/Routers/SampleUploadRouter.swift | 6 ++--- .../API/Routers/SampleUserRouter.swift | 24 +++++++++---------- .../Download/DownloadProgressViewModel.swift | 10 ++++---- .../AssociatedArrayQueryTests.swift | 8 +++---- .../AuthorizationTokenInterceptorTests.swift | 4 ++-- .../EndpointIdentifiableTests.swift | 20 ++++++++-------- ...EndpointRequestStorageProcessorTests.swift | 20 ++++++++-------- .../NetworkingTests/ErrorProcessorTests.swift | 6 ++--- .../StatusCodeProcessorTests.swift | 12 +++++----- 15 files changed, 78 insertions(+), 76 deletions(-) diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Networking.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Networking.xcscheme index ebd1c787..a5adf7f8 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/Networking.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/Networking.xcscheme @@ -1,6 +1,6 @@ ? { switch self { case .emptyAcceptStatuses: - return nil + nil case .regularAcceptStatuses: - return HTTPStatusCode.successAndRedirectCodes + HTTPStatusCode.successAndRedirectCodes case .irregularAcceptStatuses: - return 400 ..< 500 + 400 ..< 500 } } } From cde0508200cd4f0d5462a7fdad92c933e3da25fe Mon Sep 17 00:00:00 2001 From: Tomas Cejka Date: Tue, 3 Oct 2023 11:14:28 +0200 Subject: [PATCH 10/29] [chore] update swiftlint, tools version --- Package.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 3bf58b1f..231c4530 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.7 +// swift-tools-version:5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -17,7 +17,7 @@ let package = Package( targets: ["Networking"] ) ], - dependencies: [.package(url: "https://github.com/realm/SwiftLint.git", exact: "0.52.4")], + dependencies: [.package(url: "https://github.com/realm/SwiftLint.git", exact: "0.53.0")], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages this package depends on. From 324ec96c8182716ac49dc3902c2fa0bc4ded2c9e Mon Sep 17 00:00:00 2001 From: Tomas Cejka Date: Mon, 9 Oct 2023 15:30:58 +0200 Subject: [PATCH 11/29] [chore] update swiftlint version --- .../NetworkingSampleApp.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.pbxproj b/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.pbxproj index 45788dab..9e84c92a 100644 --- a/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.pbxproj +++ b/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.pbxproj @@ -575,7 +575,7 @@ repositoryURL = "https://github.com/realm/SwiftLint.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.52.4; + minimumVersion = 0.53.0; }; }; /* End XCRemoteSwiftPackageReference section */ From 84704b88bb6bfccb60d689ffbbb4a1e91990ea01 Mon Sep 17 00:00:00 2001 From: Matej Molnar Date: Wed, 6 Dec 2023 08:31:02 +0100 Subject: [PATCH 12/29] [feat] load active download tasks on appear --- .../NetworkingSampleApp/Scenes/Download/DownloadsView.swift | 3 +++ .../Scenes/Download/DownloadsViewModel.swift | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadsView.swift b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadsView.swift index 7c6571b4..a822ad7b 100644 --- a/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadsView.swift +++ b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadsView.swift @@ -36,5 +36,8 @@ struct DownloadsView: View { } } .navigationTitle("Downloads") + .onAppear { + viewModel.loadTasks() + } } } diff --git a/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadsViewModel.swift b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadsViewModel.swift index 14651401..ddfd3fab 100644 --- a/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadsViewModel.swift +++ b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadsViewModel.swift @@ -20,6 +20,12 @@ final class DownloadsViewModel: ObservableObject { await downloadItem() } } + + func loadTasks() { + Task { + tasks = await downloadAPIManager.allTasks + } + } } private extension DownloadsViewModel { From 8a4f65ba8b05b9f965cb64a3813be921084b713a Mon Sep 17 00:00:00 2001 From: Matej Molnar Date: Wed, 6 Dec 2023 19:34:22 +0100 Subject: [PATCH 13/29] [feat] update downloads UI --- .../project.pbxproj | 4 +- .../Download/DownloadProgressView.swift | 105 ++++++++++-------- .../Download/DownloadProgressViewModel.swift | 3 + .../Scenes/Download/DownloadsView.swift | 46 ++++---- 4 files changed, 90 insertions(+), 68 deletions(-) diff --git a/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.pbxproj b/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.pbxproj index 9e84c92a..d66a7d88 100644 --- a/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.pbxproj +++ b/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.pbxproj @@ -206,10 +206,10 @@ 58C3E75B29B78ED3004FD1CD /* Download */ = { isa = PBXGroup; children = ( - DD6E48722A0E24D30025AD05 /* DownloadProgressView.swift */, - 58C3E75C29B78ED3004FD1CD /* DownloadsView.swift */, 58C3E75D29B78ED3004FD1CD /* DownloadsViewModel.swift */, + 58C3E75C29B78ED3004FD1CD /* DownloadsView.swift */, 58C3E76429B7D709004FD1CD /* DownloadProgressViewModel.swift */, + DD6E48722A0E24D30025AD05 /* DownloadProgressView.swift */, ); path = Download; sourceTree = ""; diff --git a/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadProgressView.swift b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadProgressView.swift index 162f9f92..440ca53e 100644 --- a/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadProgressView.swift +++ b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadProgressView.swift @@ -11,19 +11,10 @@ struct DownloadProgressView: View { @StateObject var viewModel: DownloadProgressViewModel var body: some View { - VStack(alignment: .leading, spacing: 8) { - content - } - .task { - await viewModel.startObservingDownloadProgress() - } - .padding(10) - .background( - Color.white - .cornerRadius(15) - .shadow(radius: 10) - ) - .padding(15) + content + .task { + await viewModel.startObservingDownloadProgress() + } } } @@ -31,45 +22,65 @@ struct DownloadProgressView: View { private extension DownloadProgressView { @ViewBuilder var content: some View { - Text(viewModel.state.title) - .padding(.bottom, 8) - - Text("Status: \(viewModel.state.statusTitle)") - Text("\(String(format: "%.1f", viewModel.state.percentCompleted))% of \(String(format: "%.1f", viewModel.state.totalMegaBytes))MB") - - if let errorTitle = viewModel.state.errorTitle { - Text("Error: \(errorTitle)") - } - - if let fileURL = viewModel.state.fileURL { - Text("FileURL: \(fileURL)") - } - - downloadState - } - - @ViewBuilder - var downloadState: some View { - if viewModel.state.status != .completed { - HStack { - Button { - viewModel.suspend() - } label: { - Text("Suspend") + VStack(alignment: .leading, spacing: 8) { + Text(viewModel.state.title) + .truncationMode(.middle) + .lineLimit(1) + .padding(.bottom, 8) + + Group { + if let errorTitle = viewModel.state.errorTitle { + Text("Error: \(errorTitle)") + } else { + Text("Status: \(viewModel.state.statusTitle)") } - Button { - viewModel.resume() - } label: { - Text("Resume") + if let fileURL = viewModel.state.fileURL { + Text("FileURL: \(fileURL)") } - - Button { - viewModel.cancel() - } label: { - Text("Cancel") + + HStack { + ProgressView(value: viewModel.state.percentCompleted, total: 100) + .progressViewStyle(.linear) + .frame(width: 150) + + Text("\(String(format: "%.1f", viewModel.state.megaBytesCompleted))MB") + .font(.footnote) + .foregroundColor(.gray) + + Spacer() + + button( + symbol: viewModel.state.status == .suspended ? "play" : "pause", + color: .blue, + action: { viewModel.state.status == .suspended ? viewModel.resume() : viewModel.suspend() } + ) + + button( + symbol: "x", + color: .red, + action: { viewModel.cancel() } + ) } } + .font(.footnote) + .foregroundColor(.gray) } } + + func button(symbol: String, color: Color, action: @escaping () -> Void) -> some View { + Button( + action: action, + label: { + Image(systemName: symbol) + .symbolVariant(.circle.fill) + .font(.title2) + .symbolRenderingMode(.hierarchical) + .foregroundStyle(color) + } + ) + .buttonStyle(.plain) + .contentShape(Circle()) + } } + diff --git a/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadProgressViewModel.swift b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadProgressViewModel.swift index 4a3b3f18..56d8deb7 100644 --- a/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadProgressViewModel.swift +++ b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadProgressViewModel.swift @@ -56,6 +56,9 @@ struct DownloadProgressState { var totalMegaBytes: Double = 0 var errorTitle: String? var fileURL: String? + var megaBytesCompleted: Double { + totalMegaBytes * (percentCompleted / 100) + } } // MARK: URLSessionTask states diff --git a/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadsView.swift b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadsView.swift index a822ad7b..81cfeb79 100644 --- a/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadsView.swift +++ b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadsView.swift @@ -10,29 +10,33 @@ import Networking struct DownloadsView: View { @StateObject private var viewModel = DownloadsViewModel() - + var body: some View { - VStack { - HStack { - TextField("File URL", text: $viewModel.urlText, axis: .vertical) - .textFieldStyle(.roundedBorder) - - Button { - viewModel.startDownload() - } label: { - Text("Download") + Form { + Section( + content: { + TextField("Download URL", text: $viewModel.urlText, axis: .vertical) + }, + header: { + Text("URL") + }, + footer: { + Button("Download") { + viewModel.startDownload() + } + .buttonStyle(.borderedProminent) + .frame(maxWidth: .infinity) } - .buttonStyle(.bordered) - } - .padding(.horizontal, 15) - - ScrollView { - LazyVStack { - ForEach(viewModel.tasks, id: \.taskIdentifier) { task in - DownloadProgressView(viewModel: .init(task: task)) + ) + + if !viewModel.tasks.isEmpty { + Section("Active downloads") { + List { + ForEach(viewModel.tasks, id: \.taskIdentifier) { task in + DownloadProgressView(viewModel: .init(task: task)) + } } } - .padding(.vertical, 5) } } .navigationTitle("Downloads") @@ -41,3 +45,7 @@ struct DownloadsView: View { } } } + +#Preview { + DownloadsView() +} From 07ad22a73726e1dbcb21a79cc273844ffbfcdc59 Mon Sep 17 00:00:00 2001 From: Matej Molnar Date: Mon, 11 Dec 2023 17:36:53 +0100 Subject: [PATCH 14/29] [feat] add TaskButton and use it in Downloads + Uploads --- .../project.pbxproj | 12 +++++ .../Download/DownloadProgressView.swift | 32 +++-------- .../Scenes/Upload/UploadItemView.swift | 41 ++++---------- .../UIElements/TaskButton.swift | 54 +++++++++++++++++++ 4 files changed, 81 insertions(+), 58 deletions(-) create mode 100644 NetworkingSampleApp/NetworkingSampleApp/UIElements/TaskButton.swift diff --git a/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.pbxproj b/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.pbxproj index d66a7d88..b15fb889 100644 --- a/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.pbxproj +++ b/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 23EA9CF9292FB70A00B8E418 /* SampleUserResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EA9CEF292FB70A00B8E418 /* SampleUserResponse.swift */; }; 23EA9CFA292FB70A00B8E418 /* SampleUserAuthRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EA9CF1292FB70A00B8E418 /* SampleUserAuthRequest.swift */; }; 23EA9CFB292FB70A00B8E418 /* SampleUserRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EA9CF2292FB70A00B8E418 /* SampleUserRequest.swift */; }; + 587CD0EF2B27713700E3CB71 /* TaskButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587CD0EE2B27713700E3CB71 /* TaskButton.swift */; }; 58C3E75E29B78EE6004FD1CD /* DownloadsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3E75C29B78ED3004FD1CD /* DownloadsView.swift */; }; 58C3E75F29B78EE8004FD1CD /* DownloadsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3E75D29B78ED3004FD1CD /* DownloadsViewModel.swift */; }; 58C3E76129B79259004FD1CD /* SampleDownloadRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3E76029B79259004FD1CD /* SampleDownloadRouter.swift */; }; @@ -59,6 +60,7 @@ 23EA9CEF292FB70A00B8E418 /* SampleUserResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleUserResponse.swift; sourceTree = ""; }; 23EA9CF1292FB70A00B8E418 /* SampleUserAuthRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleUserAuthRequest.swift; sourceTree = ""; }; 23EA9CF2292FB70A00B8E418 /* SampleUserRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleUserRequest.swift; sourceTree = ""; }; + 587CD0EE2B27713700E3CB71 /* TaskButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskButton.swift; sourceTree = ""; }; 58C3E75C29B78ED3004FD1CD /* DownloadsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsView.swift; sourceTree = ""; }; 58C3E75D29B78ED3004FD1CD /* DownloadsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsViewModel.swift; sourceTree = ""; }; 58C3E76029B79259004FD1CD /* SampleDownloadRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleDownloadRouter.swift; sourceTree = ""; }; @@ -119,6 +121,7 @@ 23A575B325F8B9DA00617551 /* NetworkingSampleApp */ = { isa = PBXGroup; children = ( + 587CD0ED2B27712B00E3CB71 /* UIElements */, DD6E48742A0E2CC70025AD05 /* Extensions */, 58FB80CC29895A8D0031FC59 /* Resources */, 23EA9CE7292FB70A00B8E418 /* API */, @@ -203,6 +206,14 @@ path = Requests; sourceTree = ""; }; + 587CD0ED2B27712B00E3CB71 /* UIElements */ = { + isa = PBXGroup; + children = ( + 587CD0EE2B27713700E3CB71 /* TaskButton.swift */, + ); + path = UIElements; + sourceTree = ""; + }; 58C3E75B29B78ED3004FD1CD /* Download */ = { isa = PBXGroup; children = ( @@ -332,6 +343,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 587CD0EF2B27713700E3CB71 /* TaskButton.swift in Sources */, 58E4E0EF29843B42000ACBC0 /* NetworkingSampleApp.swift in Sources */, DD6E48762A0E2CD30025AD05 /* DownloadAPIManager+SharedInstance.swift in Sources */, DDE8884529476AC300DD3BFF /* SampleRefreshTokenRequest.swift in Sources */, diff --git a/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadProgressView.swift b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadProgressView.swift index 440ca53e..4ba52c60 100644 --- a/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadProgressView.swift +++ b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadProgressView.swift @@ -50,37 +50,17 @@ private extension DownloadProgressView { Spacer() - button( - symbol: viewModel.state.status == .suspended ? "play" : "pause", - color: .blue, - action: { viewModel.state.status == .suspended ? viewModel.resume() : viewModel.suspend() } - ) + TaskButton(config: viewModel.state.status == .suspended ? .play : .pause) { + viewModel.state.status == .suspended ? viewModel.resume() : viewModel.suspend() + } - button( - symbol: "x", - color: .red, - action: { viewModel.cancel() } - ) + TaskButton(config: .cancel) { + viewModel.cancel() + } } } .font(.footnote) .foregroundColor(.gray) } } - - func button(symbol: String, color: Color, action: @escaping () -> Void) -> some View { - Button( - action: action, - label: { - Image(systemName: symbol) - .symbolVariant(.circle.fill) - .font(.title2) - .symbolRenderingMode(.hierarchical) - .foregroundStyle(color) - } - ) - .buttonStyle(.plain) - .contentShape(Circle()) - } } - diff --git a/NetworkingSampleApp/NetworkingSampleApp/Scenes/Upload/UploadItemView.swift b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Upload/UploadItemView.swift index 0c7c9938..e6042871 100644 --- a/NetworkingSampleApp/NetworkingSampleApp/Scenes/Upload/UploadItemView.swift +++ b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Upload/UploadItemView.swift @@ -25,24 +25,18 @@ struct UploadItemView: View { if !viewModel.isCancelled && !viewModel.isRetryable && !viewModel.isCompleted { HStack { - button( - symbol: viewModel.isPaused ? "play" : "pause", - color: .blue, - action: { viewModel.isPaused ? viewModel.resume() : viewModel.pause() } - ) + TaskButton(config: viewModel.isPaused ? .play : .pause) { + viewModel.isPaused ? viewModel.resume() : viewModel.pause() + } - button( - symbol: "x", - color: .red, - action: { viewModel.cancel() } - ) + TaskButton(config: .cancel) { + viewModel.cancel() + } } } else if viewModel.isRetryable { - button( - symbol: "repeat", - color: .blue, - action: { viewModel.retry() } - ) + TaskButton(config: .retry) { + viewModel.retry() + } } } @@ -56,20 +50,3 @@ struct UploadItemView: View { .task { await viewModel.observeProgress() } } } - -private extension UploadItemView { - func button(symbol: String, color: Color, action: @escaping () -> Void) -> some View { - Button( - action: action, - label: { - Image(systemName: symbol) - .symbolVariant(.circle.fill) - .font(.title2) - .symbolRenderingMode(.hierarchical) - .foregroundStyle(color) - } - ) - .buttonStyle(.plain) - .contentShape(Circle()) - } -} diff --git a/NetworkingSampleApp/NetworkingSampleApp/UIElements/TaskButton.swift b/NetworkingSampleApp/NetworkingSampleApp/UIElements/TaskButton.swift new file mode 100644 index 00000000..8b04de06 --- /dev/null +++ b/NetworkingSampleApp/NetworkingSampleApp/UIElements/TaskButton.swift @@ -0,0 +1,54 @@ +// +// TaskButton.swift +// NetworkingSampleApp +// +// Created by Matej Molnár on 11.12.2023. +// + +import SwiftUI + +struct TaskButton: View { + enum Config { + case play, pause, cancel, retry + + var imageName: String { + switch self { + case .play: + return "play" + case .pause: + return "pause" + case .retry: + return "repeat" + case .cancel: + return "x" + } + } + + var color: Color { + switch self { + case .play, .pause, .retry: + return .blue + case .cancel: + return .red + } + } + } + + let config: Config + let action: () -> Void + + var body: some View { + Button( + action: action, + label: { + Image(systemName: config.imageName) + .symbolVariant(.circle.fill) + .font(.title2) + .symbolRenderingMode(.hierarchical) + .foregroundStyle(config.color) + } + ) + .buttonStyle(.plain) + .contentShape(Circle()) + } +} From c5255c1f0935b5e8c9e8347a41acfb7caf2f5e95 Mon Sep 17 00:00:00 2001 From: Matej Molnar Date: Mon, 11 Dec 2023 17:53:12 +0100 Subject: [PATCH 15/29] [feat] use ByteCountFormatter instead of manual calculations --- .../Scenes/Download/DownloadProgressView.swift | 2 +- .../Scenes/Download/DownloadProgressViewModel.swift | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadProgressView.swift b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadProgressView.swift index 4ba52c60..d91ae67b 100644 --- a/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadProgressView.swift +++ b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadProgressView.swift @@ -44,7 +44,7 @@ private extension DownloadProgressView { .progressViewStyle(.linear) .frame(width: 150) - Text("\(String(format: "%.1f", viewModel.state.megaBytesCompleted))MB") + Text(viewModel.state.downloadedBytes) .font(.footnote) .foregroundColor(.gray) diff --git a/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadProgressViewModel.swift b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadProgressViewModel.swift index 56d8deb7..9349e11b 100644 --- a/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadProgressViewModel.swift +++ b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Download/DownloadProgressViewModel.swift @@ -24,7 +24,7 @@ final class DownloadProgressViewModel: ObservableObject { for try await downloadState in stream { var newState = DownloadProgressState() newState.percentCompleted = downloadState.fractionCompleted * 100 - newState.totalMegaBytes = Double(downloadState.totalBytes) / 1_000_000 + newState.downloadedBytes = ByteCountFormatter.megaBytesFormatter.string(fromByteCount: downloadState.downloadedBytes) newState.status = downloadState.taskState newState.statusTitle = downloadState.taskState.title newState.errorTitle = downloadState.error?.localizedDescription @@ -53,12 +53,9 @@ struct DownloadProgressState { var status: URLSessionTask.State = .running var statusTitle: String = "" var percentCompleted: Double = 0 - var totalMegaBytes: Double = 0 + var downloadedBytes: String = "" var errorTitle: String? var fileURL: String? - var megaBytesCompleted: Double { - totalMegaBytes * (percentCompleted / 100) - } } // MARK: URLSessionTask states From bd44a8534f5f54aabdc56b0eeed6f674067b3b2e Mon Sep 17 00:00:00 2001 From: Matej Molnar Date: Tue, 12 Dec 2023 12:07:06 +0100 Subject: [PATCH 16/29] [chore] remove unnecessary return statements --- .../UIElements/TaskButton.swift | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/NetworkingSampleApp/NetworkingSampleApp/UIElements/TaskButton.swift b/NetworkingSampleApp/NetworkingSampleApp/UIElements/TaskButton.swift index 8b04de06..e70c64df 100644 --- a/NetworkingSampleApp/NetworkingSampleApp/UIElements/TaskButton.swift +++ b/NetworkingSampleApp/NetworkingSampleApp/UIElements/TaskButton.swift @@ -13,23 +13,19 @@ struct TaskButton: View { var imageName: String { switch self { - case .play: - return "play" - case .pause: - return "pause" - case .retry: - return "repeat" - case .cancel: - return "x" + case .play: "play" + case .pause: "pause" + case .retry: "repeat" + case .cancel: "x" } } var color: Color { switch self { case .play, .pause, .retry: - return .blue - case .cancel: - return .red + .blue + case .cancel: + .red } } } From fa87d4ca9fe97c01b46ec234e7f121d43d150cb6 Mon Sep 17 00:00:00 2001 From: Matej Molnar Date: Wed, 13 Dec 2023 09:56:10 +0100 Subject: [PATCH 17/29] [feat] add documentation to readme --- README.md | 324 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 320 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ce74f99d..a93d740a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,323 @@ # Networking -The streamlined library for efficient API call management. This lightweight solution leverages the power of Swift concurrency by building upon URL sessions. -The library is thoughtfully documented using the DocC documentation format, ensuring comprehensive and accessible documentation for developers. +[![Coverage](https://img.shields.io/badge/Coverage-100%25-darkgreen?style=flat-square)] +[![Platforms](https://img.shields.io/badge/Platforms-iOS_iPadOS_macOS_watchOS-lightgrey?style=flat-square)] +[![Swift](https://img.shields.io/badge/Swift-5.9+-blue?style=flat-square)] -## Supported features -TBD +A networking layer using native `UrlSession` and Swift concurrency. + +## Requirements + +- iOS/iPadOS 15.0+, macOS 12.0+, watchOS 9.0+ +- Xcode 14+ +- Swift 5.9+ + +## Installation + +You can install the library with [Swift Package Manager](https://swift.org/package-manager/). Once you have your Swift package set up, adding Dependency Injection as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`. + +```swift +dependencies: [ + .package(url: "https://github.com/strvcom/ios-networking.git", .upToNextMajor(from: "0.0.4")) +] +``` + +## Overview +Heavily inspired by Moya, the networking layer's philosophy is focused on creating individual endpoint routers, transforming them into a valid URLRequest objects and applying optional adapters and processors in the network call pipeline. + +## Router +By conforming to the ``Requestable`` protocol, you can define endpoint definitions containing the elementary HTTP request components necessary to create valid HTTP requests. +
**Recommendation:** Follow the `Router` naming convention to explicitly indicate the usage of a router pattern. + +### Example +```swift +enum UserRouter { + case getUser + case updateUser(UpdateUserRequest) +} + +extension UserRouter: Requestable { + // The base URL address used for the HTTP call. + var baseURL: URL { + URL(string: Constants.baseHost)! + } + + // Path will be appended to the base URL. + var path: String { + switch self { + case .getUser, .updateUser: + return "/user" + } + } + + // HTTPMethod used for each endpoint. + var method: HTTPMethod { + switch self { + case .getUser: + return .get + case .updateUser: + return .post + } + } + + // Optional body data encoded in JSON by default. + var dataType: RequestDataType? { + switch self { + case .getUser: + return nil + case let .updateUser(data): + return .encodable(data) + } + } + + // Optional authentication requirement if AuthorizationInterceptor is used. + var isAuthenticationRequired: Bool { + switch self { + case .getUser, .updateUser: + return true + } + } +} +``` + +Some of the properties have default implementations defined in the `Requestable+Convenience` extension. + +## APIManager +APIManager is responsible for the creation and management of a network call. It conforms to the ``APIManaging`` protocol which allows you to define your own custom APIManager if needed. + +There are two ways to initialise the ``APIManager`` object: +1. Using URLSession as the response provider. +```swift +init( + urlSession: URLSession = .init(configuration: .default), + requestAdapters: [RequestAdapting] = [], + responseProcessors: [ResponseProcessing] = [StatusCodeProcessor.shared], + errorProcessors: [ErrorProcessing] = [] +) +``` + +2. Using custom response provider by conforming to ``ResponseProviding``. An example of a custom provider is ``MockResponseProvider``, which can be used for UI tests to interact with mocked data saved through "EndpointRequestStorageProcessor". To utilize them, simply move the stored session folder into the Asset catalogue. + +```swift +init( + responseProvider: ResponseProviding, + requestAdapters: [RequestAdapting] = [], + responseProcessors: [ResponseProcessing] = [StatusCodeProcessor.shared], + errorProcessors: [ErrorProcessing] = [] +) +``` + +Adapters and processors are passed during initialisation and cannot be changed afterwards. + +There are two methods provided by the ``APIManaging`` protocol: + +1. Result is URLSession's default (data, response) tuple. +```swift +func request( + _ endpoint: Requestable, + retryConfiguration: RetryConfiguration? +) async throws -> Response +``` +2. Result is custom decodable object. +```swift +func request( + _ endpoint: Requestable, + decoder: JSONDecoder, + retryConfiguration: RetryConfiguration? +) async throws -> DecodableResponse +``` + +### Example +In the most simple form, the network request looks like this: + +```swift +try await apiManager.request(UserRouter.getUser) +``` + +If you specify object type, the APIManager will automatically perform the decoding (given the received JSON correctly maps to the decodable). You can also specify a custom json decoder. + +```swift +let userResponse: UserResponse = try await apiManager.request(UserRouter.getUser) +``` + +Provide a custom after failure ``RetryConfiguration``, specifying the count of retries, delay and a handler that determines whether the request should be tried again. Otherwise, ``RetryConfiguration/default`` configuration is used. + +```swift +let retryConfiguration = RetryConfiguration(retries: 2, delay: .constant(1)) { error in + // custom logic here +} +let userResponse: UserResponse = try await apiManager.request( + UserRouter.getUser, + retryConfiguration: retryConfiguration +) +``` + +## DownloadAPIManager +DownloadAPIManager is responsible for the creation and management of a network file download. It conforms to the ``DownloadAPIManaging`` protocol which allows you to define your own custom DownloadAPIManager if needed. Multiple parallel downloads are supported. + +The initialisation is equivalent to APIManager, except the session is created for the user based on a given `URLSessionConfiguration`: +```swift +init( + urlSessionConfiguration: URLSessionConfiguration = .default, + requestAdapters: [RequestAdapting] = [], + responseProcessors: [ResponseProcessing] = [StatusCodeProcessor.shared], + errorProcessors: [ErrorProcessing] = [] +) +``` + +Adapters and processors are passed during initialisation and cannot be changed afterwards. + +The DownloadAPIManager contains a public property that enables you to keep track of current tasks in progress. +```swift +var allTasks: [URLSessionDownloadTask] { get async } +``` +There are three methods provided by the ``DownloadAPIManaging`` protocol: + +1. Request download for a given endpoint. Returns a standard (URLSessionDownloadTask, Response) result for the HTTP handshake. This result is not the actual downloaded file, but the HTTP response received after the download is initiated. +```swift +func downloadRequest( + _ endpoint: Requestable, + resumableData: Data? = nil, + retryConfiguration: RetryConfiguration? +) async throws -> DownloadResult +``` + +2. Get progress async stream for a given task to observe task download progress and state. +```swift +func progressStream(for task: URLSessionTask) -> AsyncStream +``` + +The `DownloadState` struct provides you with information about the download itself, including bytes downloaded, total byte size of the file being downloaded or the error if any occurs. + +3. Invalidate download session in case DownloadAPIManager is not used as singleton to prevent memory leaks. +```swift +func invalidateSession(shouldFinishTasks: Bool = false) +``` +DownloadAPIManager is not deallocated from memory since URLSession is holding a reference to it. If you wish to use new instances of the DownloadAPIManager, don't forget to invalidate the session if it is not needed anymore. + +## Retry ability +Both APIManager and DownloadAPIManager allow for configurable retry mechanism. + +```swift +let retryConfiguration = RetryConfiguration(retries: 2, delay: .constant(1)) { error in + // custom logic here that determines whether the request should be retried + // e.g you can only retry with 5xx http error codes +} +``` + +## Modifiers +Modifiers are useful pieces of code that modify request/response in the network request pipeline. +![Interceptors diagram](interceptors-diagram.png) + +There are three types you can leverage:
+ +``RequestAdapting`` + +Adapters are request transformable components that perform operations on the URLRequest before it is dispatched. They are used to further customise HTTP requests before they are carried out by editing the URLRequest (e.g updating headers). + +``ResponseProcessing`` + +Response processors are handling the ``Response`` received after a successful network request. + +``ErrorProcessing`` + +Error processors are handling the ``Error`` received after a failed network request. + +``RequestInterceptor`` + +Interceptors handle both adapting and response/error processing. + +By conforming to these protocols, you can create your own adaptors/processors/interceptors. In the following part, modifiers provided by Networking are introduced. + +## Request Interceptors + +### Logging +Networking provides a default ``LoggingInterceptor`` which internally uses `os_log` to pretty print requests/responses. You can utilise it to get logging console output either for requests, responses or both. + +```swift +APIManager( + // + requestAdapters: [LoggingInterceptor.shared], + responseProcessors: [LoggingInterceptor.shared], + errorProcessors: [LoggingInterceptor.shared] + // +) +``` + +### Authorization +Networking provides a default authorization handling for OAuth scenarios. Use the default ``AuthorizationTokenInterceptor`` with the APIManager to obtain the behaviour of JWT Bearer authorization header injection and access token expiration refresh flow. + +Start by implementing an authorization manager by conforming to ``AuthorizationManaging``. This manager requires you to provide storage defined by ``AuthorizationStorageManaging`` (where OAuth credentials will be stored) and a refresh method that will perform the refresh token network call to obtain a new OAuth pair. Optionally, you can provide custom implementations for ``AuthorizationManaging/authorizeRequest(_:)`` (by default, this method sets the authorization header) or access token getter (by default, this method returns the access token saved in provided storage). + +```swift +let authorizationManager = CustomAuthorizationManager() +let authorizationInterceptor = AuthorizationTokenInterceptor(authorizationManager: authorizationManager) +APIManager( + // + requestAdapters: [authorizationInterceptor], + responseProcessors: [authorizationInterceptor], + // +) +``` + +```swift +final class CustomAuthorizationManager: AuthorizationManaging { + let storage: AuthorizationStorageManaging = CustomAuthorizationStorageManager() + + /// For refresh token logic, create a new instance of APIManager + /// without injecting `AuthorizationTokenInterceptor` to avoid cycling during refreshes. + private let apiManager: APIManager = APIManager() + + func refreshAuthorizationData(with refreshToken: String) async throws -> Networking.AuthorizationData { + // Perform a network request to obtain refreshed OAuth credentials. + } +} +``` + +## Processors + +### Status Code +Each ``Requestable`` endpoint definition contains an ``Requestable/acceptableStatusCodes`` range of acceptable status codes. By default, these are set to `200..<400`. Networking provides a default status code processor that makes sure the received response's HTTP code is an acceptable one, otherwise an ``NetworkError/unacceptableStatusCode(statusCode:acceptedStatusCodes:response:)`` error is thrown. + +```swift +APIManager( + // + responseProcessors: [StatusCodeProcessor.shared], + // +) +``` + +### Storage +Networking provides an ``EndpointRequestStorageProcessor`` which allows for requests and responses to be saved locally into the file system. Requests are stored in a sequential manner. Each session is kept in its own dedicated folder. The ``EndpointRequestStorageModel`` includes both successful and erroneous data. + +Initialise by optionally providing a `FileManager` instance, `JSONEncoder` to be used during request/response data encoding and a configuration. The configuration allows you to set optionally a multiPeerSharing configuration if you wish to utilize the multipeer connectivity feature for sharing the ``EndpointRequestStorageModel`` with devices using the `MultipeerConnectivity` framework. + +```swift +init( + fileManager: FileManager = .default, + jsonEncoder: JSONEncoder? = nil, + config: Config = .default +) +``` + +## Associated array query parameters +When specifying urlParameters in the endpoint definition, use an ``ArrayParameter`` to define multiple values for a single URL query parameter. The struct lets you decide which ``ArrayEncoding`` will be used during the creation of the URL. + +There are two currently supported encodings: + +1. Comma separated +```swift +http://example.com?filter=1,2,3 +``` + +2. Individual (default) +```swift +http://example.com?filter=1&filter=2&filter=3 +``` + +### Example +```swift +var urlParameters: [String: Any]? { + ["filter": ArrayParameter([1, 2, 3], arrayEncoding: .individual)] +} +``` From 0ad081061ac77f02695fab98b5e3705593a7537f Mon Sep 17 00:00:00 2001 From: Matej Molnar Date: Wed, 13 Dec 2023 10:24:26 +0100 Subject: [PATCH 18/29] [feat] add UploadAPIManager documentation --- README.md | 40 +++++++++++++++++++ .../Documentation.docc/Documentation.md | 40 +++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/README.md b/README.md index a93d740a..d80b703a 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,46 @@ func invalidateSession(shouldFinishTasks: Bool = false) ``` DownloadAPIManager is not deallocated from memory since URLSession is holding a reference to it. If you wish to use new instances of the DownloadAPIManager, don't forget to invalidate the session if it is not needed anymore. +## UploadAPIManager +Similarly to DownloadAPIManager we have an UploadAPIManager responsible for the creation and management of a network file uploads. It conforms to the ``UploadAPIManaging`` protocol which allows you to define your own custom UploadAPIManager if needed. Multiple parallel uploads are supported. + +The initialisation is equivalent to APIManager, except the session is created for the user based on a given `URLSessionConfiguration`: +```swift +init( + urlSessionConfiguration: URLSessionConfiguration = .default, + requestAdapters: [RequestAdapting] = [], + responseProcessors: [ResponseProcessing] = [StatusCodeProcessor.shared], + errorProcessors: [ErrorProcessing] = [] +) +``` + +Adapters and processors are passed during initialisation and cannot be changed afterwards. + +The UploadAPIManager contains a public property that enables you to keep track of current tasks in progress. +```swift +var activeTasks: [UploadTask] { get async } +``` +``UploadAPIManaging`` defines three methods for upload based on the upload type `Data`, file `URL` and `MultipartFormData`. Each of these methods return an `UploadTask`. +An `UploadTask` is a struct which under the hood represents + manages a URLSessionUploadTask and provides its state. + +After firing an upload by one of these three methods, you can get a StateStream either from the `UploadTask` itself or from the manager with the following method. +```swift +func stateStream(for uploadTaskId: UploadTask.ID) async -> StateStream +``` +The `StateStream` is a typealias for `AsyncPublisher>`. +The `UploadTask.State` struct provides you with information about the upload itself, including bytes uploaded, total byte size of the file being uploaded or the error if any occurs. + +The manager also allows for retries of uploads. +```swift + func retry(taskId: String) async throws +``` + +You should invalidate upload session in case UploadAPIManager is not used as singleton to prevent memory leaks. +```swift +func invalidateSession(shouldFinishTasks: Bool = false) +``` +UploadAPIManager is not deallocated from memory since URLSession is holding a reference to it. If you wish to use new instances of the UploadAPIManager, don't forget to invalidate the session if it is not needed anymore. + ## Retry ability Both APIManager and DownloadAPIManager allow for configurable retry mechanism. diff --git a/Sources/Networking/Documentation.docc/Documentation.md b/Sources/Networking/Documentation.docc/Documentation.md index 584af800..09c6f61d 100644 --- a/Sources/Networking/Documentation.docc/Documentation.md +++ b/Sources/Networking/Documentation.docc/Documentation.md @@ -175,6 +175,46 @@ func invalidateSession(shouldFinishTasks: Bool = false) ``` DownloadAPIManager is not deallocated from memory since URLSession is holding a reference to it. If you wish to use new instances of the DownloadAPIManager, don't forget to invalidate the session if it is not needed anymore. +## UploadAPIManager +Similarly to DownloadAPIManager we have an UploadAPIManager responsible for the creation and management of network file uploads. It conforms to the ``UploadAPIManaging`` protocol which allows you to define your own custom UploadAPIManager if needed. Multiple parallel uploads are supported. + +The initialisation is equivalent to APIManager, except the session is created for the user based on a given `URLSessionConfiguration`: +```swift +init( + urlSessionConfiguration: URLSessionConfiguration = .default, + requestAdapters: [RequestAdapting] = [], + responseProcessors: [ResponseProcessing] = [StatusCodeProcessor.shared], + errorProcessors: [ErrorProcessing] = [] +) +``` + +Adapters and processors are passed during initialisation and cannot be changed afterwards. + +The UploadAPIManager contains a public property that enables you to keep track of current tasks in progress. +```swift +var activeTasks: [UploadTask] { get async } +``` +``UploadAPIManaging`` defines three methods for upload based on the upload type `Data`, file `URL` and `MultipartFormData`. Each of these methods return an `UploadTask`. +An `UploadTask` is a struct which under the hood represents + manages a URLSessionUploadTask and provides its state. + +After firing an upload by one of these three methods, you can get a StateStream either from the `UploadTask` itself or from the manager with the following method. +```swift +func stateStream(for uploadTaskId: UploadTask.ID) async -> StateStream +``` +The `StateStream` is a typealias for `AsyncPublisher>`. +The `UploadTask.State` struct provides you with information about the upload itself, including bytes uploaded, total byte size of the file being uploaded or the error if any occurs. + +The manager also allows for retries of uploads. +```swift + func retry(taskId: String) async throws +``` + +You should invalidate upload session in case UploadAPIManager is not used as singleton to prevent memory leaks. +```swift +func invalidateSession(shouldFinishTasks: Bool = false) +``` +UploadAPIManager is not deallocated from memory since URLSession is holding a reference to it. If you wish to use new instances of the UploadAPIManager, don't forget to invalidate the session if it is not needed anymore. + ## Retry ability Both APIManager and DownloadAPIManager allow for configurable retry mechanism. From 3cb54c023f0f641b71a7f7fed7970e739aca839b Mon Sep 17 00:00:00 2001 From: Matej Molnar Date: Mon, 18 Dec 2023 15:11:21 +0100 Subject: [PATCH 19/29] [feat] make properties private --- .../NetworkingSampleApp/UIElements/TaskButton.swift | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/NetworkingSampleApp/NetworkingSampleApp/UIElements/TaskButton.swift b/NetworkingSampleApp/NetworkingSampleApp/UIElements/TaskButton.swift index e70c64df..a92b5d92 100644 --- a/NetworkingSampleApp/NetworkingSampleApp/UIElements/TaskButton.swift +++ b/NetworkingSampleApp/NetworkingSampleApp/UIElements/TaskButton.swift @@ -24,14 +24,19 @@ struct TaskButton: View { switch self { case .play, .pause, .retry: .blue - case .cancel: + case .cancel: .red } } } - let config: Config - let action: () -> Void + private let config: Config + private let action: () -> Void + + init(config: Config, action: @escaping () -> Void) { + self.config = config + self.action = action + } var body: some View { Button( From e6e5fc762fa3087b01c90ed6cb11969ddd911134 Mon Sep 17 00:00:00 2001 From: Matej Molnar Date: Mon, 11 Dec 2023 14:41:29 +0100 Subject: [PATCH 20/29] [feat] add Users screen with more samples --- .../project.pbxproj | 28 +++- .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 ++ .../Responses/SampleCreateUserResponse.swift | 16 +++ .../API/Responses/SampleUserResponse.swift | 14 -- .../API/Responses/SampleUsersResponse.swift | 7 +- .../API/Routers/SampleUserRouter.swift | 23 ++-- .../NetworkingSampleApp/ContentView.swift | 3 + .../Scenes/Users/User.swift | 25 ++++ .../Scenes/Users/UsersView.swift | 120 ++++++++++++++++++ .../Scenes/Users/UsersViewModel.swift | 82 ++++++++++++ 11 files changed, 296 insertions(+), 37 deletions(-) create mode 100644 NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 NetworkingSampleApp/NetworkingSampleApp/API/Responses/SampleCreateUserResponse.swift delete mode 100644 NetworkingSampleApp/NetworkingSampleApp/API/Responses/SampleUserResponse.swift create mode 100644 NetworkingSampleApp/NetworkingSampleApp/Scenes/Users/User.swift create mode 100644 NetworkingSampleApp/NetworkingSampleApp/Scenes/Users/UsersView.swift create mode 100644 NetworkingSampleApp/NetworkingSampleApp/Scenes/Users/UsersViewModel.swift diff --git a/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.pbxproj b/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.pbxproj index b15fb889..50dd980f 100644 --- a/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.pbxproj +++ b/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.pbxproj @@ -15,14 +15,17 @@ 23EA9CF6292FB70A00B8E418 /* SampleAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EA9CEB292FB70A00B8E418 /* SampleAPIError.swift */; }; 23EA9CF7292FB70A00B8E418 /* SampleUserAuthResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EA9CED292FB70A00B8E418 /* SampleUserAuthResponse.swift */; }; 23EA9CF8292FB70A00B8E418 /* SampleUsersResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EA9CEE292FB70A00B8E418 /* SampleUsersResponse.swift */; }; - 23EA9CF9292FB70A00B8E418 /* SampleUserResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EA9CEF292FB70A00B8E418 /* SampleUserResponse.swift */; }; + 23EA9CF9292FB70A00B8E418 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EA9CEF292FB70A00B8E418 /* User.swift */; }; 23EA9CFA292FB70A00B8E418 /* SampleUserAuthRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EA9CF1292FB70A00B8E418 /* SampleUserAuthRequest.swift */; }; 23EA9CFB292FB70A00B8E418 /* SampleUserRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EA9CF2292FB70A00B8E418 /* SampleUserRequest.swift */; }; 587CD0EF2B27713700E3CB71 /* TaskButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587CD0EE2B27713700E3CB71 /* TaskButton.swift */; }; + 587CD0EC2B271CF800E3CB71 /* SampleCreateUserResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587CD0EB2B271CF800E3CB71 /* SampleCreateUserResponse.swift */; }; 58C3E75E29B78EE6004FD1CD /* DownloadsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3E75C29B78ED3004FD1CD /* DownloadsView.swift */; }; 58C3E75F29B78EE8004FD1CD /* DownloadsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3E75D29B78ED3004FD1CD /* DownloadsViewModel.swift */; }; 58C3E76129B79259004FD1CD /* SampleDownloadRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3E76029B79259004FD1CD /* SampleDownloadRouter.swift */; }; 58C3E76529B7D709004FD1CD /* DownloadProgressViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3E76429B7D709004FD1CD /* DownloadProgressViewModel.swift */; }; + 58D6976F2B21FF8300E6C529 /* UsersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D6976E2B21FF8300E6C529 /* UsersView.swift */; }; + 58D697712B21FF8E00E6C529 /* UsersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D697702B21FF8E00E6C529 /* UsersViewModel.swift */; }; 58E4E0ED2982D884000ACBC0 /* SampleAuthorizationStorageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E4E0EC2982D884000ACBC0 /* SampleAuthorizationStorageManager.swift */; }; 58E4E0EF29843B42000ACBC0 /* NetworkingSampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E4E0EE29843B42000ACBC0 /* NetworkingSampleApp.swift */; }; 58E4E0F129850E86000ACBC0 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E4E0F029850E86000ACBC0 /* ContentView.swift */; }; @@ -57,14 +60,17 @@ 23EA9CEB292FB70A00B8E418 /* SampleAPIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleAPIError.swift; sourceTree = ""; }; 23EA9CED292FB70A00B8E418 /* SampleUserAuthResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleUserAuthResponse.swift; sourceTree = ""; }; 23EA9CEE292FB70A00B8E418 /* SampleUsersResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleUsersResponse.swift; sourceTree = ""; }; - 23EA9CEF292FB70A00B8E418 /* SampleUserResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleUserResponse.swift; sourceTree = ""; }; + 23EA9CEF292FB70A00B8E418 /* User.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; 23EA9CF1292FB70A00B8E418 /* SampleUserAuthRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleUserAuthRequest.swift; sourceTree = ""; }; 23EA9CF2292FB70A00B8E418 /* SampleUserRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleUserRequest.swift; sourceTree = ""; }; + 587CD0EB2B271CF800E3CB71 /* SampleCreateUserResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleCreateUserResponse.swift; sourceTree = ""; }; 587CD0EE2B27713700E3CB71 /* TaskButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskButton.swift; sourceTree = ""; }; 58C3E75C29B78ED3004FD1CD /* DownloadsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsView.swift; sourceTree = ""; }; 58C3E75D29B78ED3004FD1CD /* DownloadsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsViewModel.swift; sourceTree = ""; }; 58C3E76029B79259004FD1CD /* SampleDownloadRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleDownloadRouter.swift; sourceTree = ""; }; 58C3E76429B7D709004FD1CD /* DownloadProgressViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadProgressViewModel.swift; sourceTree = ""; }; + 58D6976E2B21FF8300E6C529 /* UsersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsersView.swift; sourceTree = ""; }; + 58D697702B21FF8E00E6C529 /* UsersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsersViewModel.swift; sourceTree = ""; }; 58E4E0EC2982D884000ACBC0 /* SampleAuthorizationStorageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleAuthorizationStorageManager.swift; sourceTree = ""; }; 58E4E0EE29843B42000ACBC0 /* NetworkingSampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkingSampleApp.swift; sourceTree = ""; }; 58E4E0F029850E86000ACBC0 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -145,6 +151,7 @@ 23A575ED25F8BF0E00617551 /* Scenes */ = { isa = PBXGroup; children = ( + 58D6976D2B21FF6A00E6C529 /* Users */, 58FB80C5298521DA0031FC59 /* Authorization */, 58C3E75B29B78ED3004FD1CD /* Download */, B52674BB2A370D0D006D3B9C /* Upload */, @@ -191,7 +198,7 @@ children = ( 23EA9CED292FB70A00B8E418 /* SampleUserAuthResponse.swift */, 23EA9CEE292FB70A00B8E418 /* SampleUsersResponse.swift */, - 23EA9CEF292FB70A00B8E418 /* SampleUserResponse.swift */, + 587CD0EB2B271CF800E3CB71 /* SampleCreateUserResponse.swift */, ); path = Responses; sourceTree = ""; @@ -225,6 +232,16 @@ path = Download; sourceTree = ""; }; + 58D6976D2B21FF6A00E6C529 /* Users */ = { + isa = PBXGroup; + children = ( + 23EA9CEF292FB70A00B8E418 /* User.swift */, + 58D697702B21FF8E00E6C529 /* UsersViewModel.swift */, + 58D6976E2B21FF8300E6C529 /* UsersView.swift */, + ); + path = Users; + sourceTree = ""; + }; 58FB80C5298521DA0031FC59 /* Authorization */ = { isa = PBXGroup; children = ( @@ -356,7 +373,7 @@ 58E4E0F129850E86000ACBC0 /* ContentView.swift in Sources */, B52674BD2A370D1D006D3B9C /* UploadService.swift in Sources */, 58C3E76529B7D709004FD1CD /* DownloadProgressViewModel.swift in Sources */, - 23EA9CF9292FB70A00B8E418 /* SampleUserResponse.swift in Sources */, + 23EA9CF9292FB70A00B8E418 /* User.swift in Sources */, DDD3AD1F2950E794006CB777 /* SampleAuthRouter.swift in Sources */, DD887780293E33850065ED03 /* SampleErrorProcessor.swift in Sources */, 23EA9CFB292FB70A00B8E418 /* SampleUserRequest.swift in Sources */, @@ -364,13 +381,16 @@ 58C3E76129B79259004FD1CD /* SampleDownloadRouter.swift in Sources */, 23EA9CF4292FB70A00B8E418 /* SampleUserRouter.swift in Sources */, B52674BF2A370D33006D3B9C /* UploadItem.swift in Sources */, + 58D6976F2B21FF8300E6C529 /* UsersView.swift in Sources */, 23EA9CF5292FB70A00B8E418 /* SampleAPIConstants.swift in Sources */, 58FB80C7298521FF0031FC59 /* AuthorizationView.swift in Sources */, DD410D6F293F2E6E006D8E31 /* AuthorizationViewModel.swift in Sources */, B52674BA2A370C15006D3B9C /* SampleUploadRouter.swift in Sources */, + 58D697712B21FF8E00E6C529 /* UsersViewModel.swift in Sources */, B52674C72A371046006D3B9C /* UploadItemView.swift in Sources */, 58C3E75F29B78EE8004FD1CD /* DownloadsViewModel.swift in Sources */, 58C3E75E29B78EE6004FD1CD /* DownloadsView.swift in Sources */, + 587CD0EC2B271CF800E3CB71 /* SampleCreateUserResponse.swift in Sources */, B52674C32A370E35006D3B9C /* UploadItemViewModel.swift in Sources */, B52674C12A370DFF006D3B9C /* UploadsViewModel.swift in Sources */, B52674C52A37102D006D3B9C /* UploadsView.swift in Sources */, diff --git a/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/NetworkingSampleApp/NetworkingSampleApp/API/Responses/SampleCreateUserResponse.swift b/NetworkingSampleApp/NetworkingSampleApp/API/Responses/SampleCreateUserResponse.swift new file mode 100644 index 00000000..23b41f4a --- /dev/null +++ b/NetworkingSampleApp/NetworkingSampleApp/API/Responses/SampleCreateUserResponse.swift @@ -0,0 +1,16 @@ +// +// SampleCreateUserResponse.swift +// NetworkingSampleApp +// +// Created by Matej Molnár on 11.12.2023. +// + +import Foundation + +/// Data structure of sample API create user response +struct SampleCreateUserResponse: Codable { + let id: String + let name: String + let job: String + let createdAt: Date +} diff --git a/NetworkingSampleApp/NetworkingSampleApp/API/Responses/SampleUserResponse.swift b/NetworkingSampleApp/NetworkingSampleApp/API/Responses/SampleUserResponse.swift deleted file mode 100644 index 32aa9036..00000000 --- a/NetworkingSampleApp/NetworkingSampleApp/API/Responses/SampleUserResponse.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// SampleUserResponse.swift -// Networking sample app -// -// Created by Tomas Cejka on 07.04.2021. -// - -import Foundation - -/// Data structure of sample API user response -struct SampleUserResponse: Codable { - let id: Int - let email: String? -} diff --git a/NetworkingSampleApp/NetworkingSampleApp/API/Responses/SampleUsersResponse.swift b/NetworkingSampleApp/NetworkingSampleApp/API/Responses/SampleUsersResponse.swift index 0f01a5f5..b93bfaaa 100644 --- a/NetworkingSampleApp/NetworkingSampleApp/API/Responses/SampleUsersResponse.swift +++ b/NetworkingSampleApp/NetworkingSampleApp/API/Responses/SampleUsersResponse.swift @@ -7,8 +7,7 @@ import Foundation -/// Data structure of sample API user list response -struct SampleUsersResponse: Codable { - let page: Int - let data: [SampleUserResponse] +/// Data structure of sample API get user response +struct SampleUserResponse: Codable { + let data: User } diff --git a/NetworkingSampleApp/NetworkingSampleApp/API/Routers/SampleUserRouter.swift b/NetworkingSampleApp/NetworkingSampleApp/API/Routers/SampleUserRouter.swift index c3916f80..7a33b08f 100644 --- a/NetworkingSampleApp/NetworkingSampleApp/API/Routers/SampleUserRouter.swift +++ b/NetworkingSampleApp/NetworkingSampleApp/API/Routers/SampleUserRouter.swift @@ -13,7 +13,6 @@ enum SampleUserRouter: Requestable { case users(page: Int) case user(userId: Int) case createUser(user: SampleUserRequest) - case registerUser(user: SampleUserAuthRequest) var baseURL: URL { // swiftlint:disable:next force_unwrapping @@ -25,25 +24,23 @@ enum SampleUserRouter: Requestable { case .users, .createUser: "users" case let .user(userId): - "user/\(userId)" - case .registerUser: - "register" + return "users/\(userId)" } } var urlParameters: [String: Any]? { switch self { case let .users(page): - ["page": page] - case .createUser, .registerUser, .user: - nil + return ["page": page] + case .createUser, .user: + return nil } } var method: HTTPMethod { switch self { - case .createUser, .registerUser: - .post + case .createUser: + return .post case .users, .user: .get } @@ -52,9 +49,7 @@ enum SampleUserRouter: Requestable { var dataType: RequestDataType? { switch self { case let .createUser(user): - .encodable(user) - case let .registerUser(user): - .encodable(user) + return .encodable(user) case .users, .user: nil } @@ -62,10 +57,8 @@ enum SampleUserRouter: Requestable { var isAuthenticationRequired: Bool { switch self { - case .registerUser: - false case .createUser, .users, .user: - true + return false } } } diff --git a/NetworkingSampleApp/NetworkingSampleApp/ContentView.swift b/NetworkingSampleApp/NetworkingSampleApp/ContentView.swift index 44bf5700..dbe824c5 100644 --- a/NetworkingSampleApp/NetworkingSampleApp/ContentView.swift +++ b/NetworkingSampleApp/NetworkingSampleApp/ContentView.swift @@ -8,6 +8,7 @@ import SwiftUI enum NetworkingFeature: String, Hashable, CaseIterable { + case users case authorization case downloads case uploads @@ -24,6 +25,8 @@ struct ContentView: View { .navigationTitle("Examples") .navigationDestination(for: NetworkingFeature.self) { feature in switch feature { + case .users: + UsersView() case .authorization: AuthorizationView() case .downloads: diff --git a/NetworkingSampleApp/NetworkingSampleApp/Scenes/Users/User.swift b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Users/User.swift new file mode 100644 index 00000000..f58b7bab --- /dev/null +++ b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Users/User.swift @@ -0,0 +1,25 @@ +// +// SampleUserResponse.swift +// Networking sample app +// +// Created by Tomas Cejka on 07.04.2021. +// + +import Foundation + +/// Data structure of sample API user response +struct User: Codable, Identifiable { + enum CodingKeys: String, CodingKey { + case id + case email + case firstName = "first_name" + case lastName = "last_name" + case avatarURL = "avatar" + } + + let id: Int + let email: String + let firstName: String + let lastName: String + let avatarURL: URL +} diff --git a/NetworkingSampleApp/NetworkingSampleApp/Scenes/Users/UsersView.swift b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Users/UsersView.swift new file mode 100644 index 00000000..4bb07581 --- /dev/null +++ b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Users/UsersView.swift @@ -0,0 +1,120 @@ +// +// UsersView.swift +// NetworkingSampleApp +// +// Created by Matej Molnár on 07.12.2023. +// + +import SwiftUI + +struct UsersView: View { + @StateObject private var viewModel = UsersViewModel() + + @State private var fromUserID: Int = 1 + @State private var toUserID: Int = 3 + @State private var parallelise = false + @State private var userName: String = "" + @State private var userJob: String = "" + + var body: some View { + Form { + getUserView + + createUserView + } + .navigationTitle("Users") + } +} + +private extension UsersView { + var getUserView: some View { + Group { + Section { + HStack { + Text("From:") + + TextField("From user ID", value: $fromUserID, formatter: NumberFormatter()) + } + + HStack { + Text("To:") + + TextField("To user ID", value: $toUserID, formatter: NumberFormatter()) + } + + Toggle("Parallelise", isOn: $parallelise) + } header: { + Text("Get User by ID") + } footer: { + Button("Get Users") { + viewModel.getUsers( + in: fromUserID...toUserID, + parallelFetch: parallelise + ) + } + .buttonStyle(.borderedProminent) + .frame(maxWidth: .infinity) + } + + if !viewModel.users.isEmpty { + Section("Users") { + ForEach(viewModel.users) { user in + userCell(user) + } + } + } + } + } + + var createUserView: some View { + Group { + Section { + TextField("Name", text: $userName) + TextField("Job", text: $userJob) + } header: { + Text("Create User with parameters") + } footer: { + Button("Create User") { + viewModel.createUser(name: userName, job: userJob) + } + .buttonStyle(.borderedProminent) + .frame(maxWidth: .infinity) + } + + if let createdUser = viewModel.createdUser { + Section("Created User") { + Text("ID: \(createdUser.id)") + Text("Name: \(createdUser.name)") + Text("Job: \(createdUser.job)") + Text("Created at: \(createdUser.createdAt.formatted())") + } + } + } + } + + func userCell(_ user: User) -> some View { + HStack(alignment: .center) { + AsyncImage(url: user.avatarURL) { image in + image + .resizable() + } placeholder: { + Color.gray + } + .frame(width: 70, height: 70) + .clipShape(Circle()) + + VStack(alignment: .leading) { + Text(user.firstName + " " + user.lastName) + .font(.subheadline) + + Text(user.email) + .font(.footnote) + .foregroundStyle(.gray) + } + } + } +} + +#Preview { + UsersView() +} diff --git a/NetworkingSampleApp/NetworkingSampleApp/Scenes/Users/UsersViewModel.swift b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Users/UsersViewModel.swift new file mode 100644 index 00000000..4d3e020f --- /dev/null +++ b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Users/UsersViewModel.swift @@ -0,0 +1,82 @@ +// +// UsersViewModel.swift +// NetworkingSampleApp +// +// Created by Matej Molnár on 07.12.2023. +// + +import Foundation +import Networking + +@MainActor +final class UsersViewModel: ObservableObject { + @Published var users = [User]() + @Published var createdUser: SampleCreateUserResponse? + + /// Custom decoder needed for decoding `createdAt` parameter of SampleCreateUserResponse. + private let responseDecoder: JSONDecoder = { + let decoder = JSONDecoder() + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + decoder.dateDecodingStrategy = .formatted(dateFormatter) + return decoder + }() + + private lazy var apiManager: APIManager = { + var responseProcessors: [ResponseProcessing] = [ + LoggingInterceptor.shared, + StatusCodeProcessor.shared + ] + var errorProcessors: [ErrorProcessing] = [LoggingInterceptor.shared] + +#if DEBUG + responseProcessors.append(EndpointRequestStorageProcessor.shared) + errorProcessors.append(EndpointRequestStorageProcessor.shared) +#endif + + return APIManager( + requestAdapters: [LoggingInterceptor.shared], + responseProcessors: responseProcessors, + errorProcessors: errorProcessors + ) + }() +} + +extension UsersViewModel { + func getUsers(in range: ClosedRange, parallelFetch: Bool) { + Task { + users = [] + + if parallelFetch { + // Fire all user requests parallelly in a group, assign it to users array after all of them are completed. + users = try await withThrowingTaskGroup(of: User.self) { group in + for id in range { + group.addTask { + let response: SampleUserResponse = try await self.apiManager.request(SampleUserRouter.user(userId: id)) + return response.data + } + } + + return try await group.reduce(into: [User]()) { $0.append($1) } + } + } else { + // Fetch user add it to users array and wait for 0.5 seconds, before fetching the next one. + for id in range { + let response: SampleUserResponse = try await apiManager.request(SampleUserRouter.user(userId: id)) + users.append(response.data) + try await Task.sleep(for: .seconds(0.5)) + } + } + } + } + + func createUser(name: String, job: String) { + Task { + createdUser = try await self.apiManager.request( + SampleUserRouter.createUser(user: .init(name: name, job: job)), + decoder: responseDecoder, + retryConfiguration: .default + ) + } + } +} From 172e368796085b01c6dc5bd1963d0aeac803ef2d Mon Sep 17 00:00:00 2001 From: Matej Molnar Date: Mon, 11 Dec 2023 14:58:16 +0100 Subject: [PATCH 21/29] [fix] rebase issues --- .../NetworkingSampleApp/API/Routers/SampleUserRouter.swift | 6 +++--- .../NetworkingSampleApp/Scenes/Users/UsersView.swift | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/NetworkingSampleApp/NetworkingSampleApp/API/Routers/SampleUserRouter.swift b/NetworkingSampleApp/NetworkingSampleApp/API/Routers/SampleUserRouter.swift index 7a33b08f..af572743 100644 --- a/NetworkingSampleApp/NetworkingSampleApp/API/Routers/SampleUserRouter.swift +++ b/NetworkingSampleApp/NetworkingSampleApp/API/Routers/SampleUserRouter.swift @@ -22,7 +22,7 @@ enum SampleUserRouter: Requestable { var path: String { switch self { case .users, .createUser: - "users" + return "users" case let .user(userId): return "users/\(userId)" } @@ -42,7 +42,7 @@ enum SampleUserRouter: Requestable { case .createUser: return .post case .users, .user: - .get + return .get } } @@ -51,7 +51,7 @@ enum SampleUserRouter: Requestable { case let .createUser(user): return .encodable(user) case .users, .user: - nil + return nil } } diff --git a/NetworkingSampleApp/NetworkingSampleApp/Scenes/Users/UsersView.swift b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Users/UsersView.swift index 4bb07581..64d4da1f 100644 --- a/NetworkingSampleApp/NetworkingSampleApp/Scenes/Users/UsersView.swift +++ b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Users/UsersView.swift @@ -48,7 +48,7 @@ private extension UsersView { } footer: { Button("Get Users") { viewModel.getUsers( - in: fromUserID...toUserID, + in: fromUserID...toUserID, parallelFetch: parallelise ) } From b7565d22f8c778c96fe30860ff06ec29ec6183c5 Mon Sep 17 00:00:00 2001 From: Matej Molnar Date: Tue, 12 Dec 2023 12:25:26 +0100 Subject: [PATCH 22/29] [chore] remove unnecessary return statements --- .../API/Routers/SampleUserRouter.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/NetworkingSampleApp/NetworkingSampleApp/API/Routers/SampleUserRouter.swift b/NetworkingSampleApp/NetworkingSampleApp/API/Routers/SampleUserRouter.swift index af572743..2af800db 100644 --- a/NetworkingSampleApp/NetworkingSampleApp/API/Routers/SampleUserRouter.swift +++ b/NetworkingSampleApp/NetworkingSampleApp/API/Routers/SampleUserRouter.swift @@ -22,43 +22,43 @@ enum SampleUserRouter: Requestable { var path: String { switch self { case .users, .createUser: - return "users" + "users" case let .user(userId): - return "users/\(userId)" + "users/\(userId)" } } var urlParameters: [String: Any]? { switch self { case let .users(page): - return ["page": page] + ["page": page] case .createUser, .user: - return nil + nil } } var method: HTTPMethod { switch self { case .createUser: - return .post + .post case .users, .user: - return .get + .get } } var dataType: RequestDataType? { switch self { case let .createUser(user): - return .encodable(user) + .encodable(user) case .users, .user: - return nil + nil } } var isAuthenticationRequired: Bool { switch self { case .createUser, .users, .user: - return false + false } } } From 524e2af95f34d4cc9ccd8b94d18b42eb78168b2b Mon Sep 17 00:00:00 2001 From: Matej Molnar Date: Sun, 7 Jan 2024 23:30:53 +0100 Subject: [PATCH 23/29] [feat] update readme --- README.md | 203 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 166 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index d80b703a..e2802e90 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,36 @@ [![Platforms](https://img.shields.io/badge/Platforms-iOS_iPadOS_macOS_watchOS-lightgrey?style=flat-square)] [![Swift](https://img.shields.io/badge/Swift-5.9+-blue?style=flat-square)] -A networking layer using native `UrlSession` and Swift concurrency. - -## Requirements +A networking layer using native `URLSession` and Swift concurrency. + +- [Requirements](#requirements) +- [Installation](#installation) +- [Overview](#overview) +- [Basics](#basics) + - [Making requests](#makingRequests) + - [Downloading files](#downloadingFiles) + - [Uploading files](#uploadingFiles) + - [Request authorization](#requestAuthorization) +- [Requestable](#requestable) +- [APIManager](#apiManager) +- [DownloadAPIManager](#downloadManager) +- [UploadAPIManager](#uploadManager) +- [Modifiers](#modifiers) +- [Interceptors](#interceptors) + - [LoggingInterceptor](#loggingInterceptor) + - [AuthorizationTokenInterceptor](#authorizationTokenInterceptor) +- [Processors](#processors) + - [StatusCodeProcessor](#statusCodeProcessor) + - [EndpointRequestStorageProcessor](#endpointRequestStorageProcessor) +- [Associated array query parameters](#arrayQueryParameters) + +## Requirements - iOS/iPadOS 15.0+, macOS 12.0+, watchOS 9.0+ - Xcode 14+ - Swift 5.9+ -## Installation +## Installation You can install the library with [Swift Package Manager](https://swift.org/package-manager/). Once you have your Swift package set up, adding Dependency Injection as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`. @@ -22,10 +43,113 @@ dependencies: [ ] ``` -## Overview -Heavily inspired by Moya, the networking layer's philosophy is focused on creating individual endpoint routers, transforming them into a valid URLRequest objects and applying optional adapters and processors in the network call pipeline. +## Overview +Heavily inspired by Moya, the networking layer's philosophy is focused on creating individual endpoint routers, transforming them into a valid URLRequest objects and applying optional adapters and processors in the network call pipeline utilising native `URLSession` under the hood. + +## Basics + +### Making requests +There is no 1 line way of making a request from scratch in order to ensure consistency and better structure. First we need to define a Router by conforming to [Requestable](#requestable) protocol. Which in the simplest form can look like this: +```swift +enum UserRouter: Requestable { + case getUser + + var baseURL: URL { + URL(string: "https://reqres.in/api")! + } + + var path: String { + switch self { + case .getUser: "/user" + } + } + + var method: HTTPMethod { + switch self { + case .getUser: .get + } + } +} +``` -## Router +Then we can make a request on an [APIManager](#apiManager) instance, which is responsible for handling the whole request flow. +```swift +let response = try await APIManager().request(UserRouter.getUser) +``` +If you specify object type, the [APIManager](#apiManager) will automatically perform the decoding (given the received JSON correctly maps to the decodable). You can also specify a custom json decoder. + +```swift +let userResponse: UserResponse = try await apiManager.request(UserRouter.getUser) +``` + +### Downloading files +Downloads are being handled by a designated [DownloadAPIManager](#downloadManager). Here is an example of a basic form of file download from a `URL`. It returns a tuple of `URLSessionDownloadTask` and `Response` (result for the HTTP handshake). +```swift +let (task, response) = try await DownloadAPIManager().request(url: URL) +``` + +You can then observe the download progress for a given `URLSessionDownloadTask` +```swift +for try await downloadState in downloadAPIManager.shared.progressStream(for: task) { + ... +} +``` + +In case you need to provide some specific info in the request, you can define a type conforming to [Requestable](#requestable) protocol and pass that to the [DownloadAPIManager](#downloadManager) instead of the `URL`. + +### Uploading files +Uploads are being handled by a designated [UploadAPIManager](#uploadManager). Here is an example of a basic form of file upload to a `URL`. It returns an `UploadTask` which is a struct that represents + manages a `URLSessionUploadTask` and provides its state. +```swift +let uploadTask = try await uploadManager.upload(.file(fileUrl), to: "https://upload.com/file") +``` + +You can then observe the upload progress for a given `UploadTask` +```swift +for await uploadState in await uploadManager.stateStream(for: task.id) { +... +} +``` + +In case you need to provide some specific info in the request, you can define a type conforming to [Requestable](#requestable) protocol and pass that to the [UploadAPIManager](#uploadManager) instead of the upload `URL`. + +### Request authorization +Networking provides a default authorization handling for OAuth scenarios. In order to utilise this we +have to first create our own implementation of `AuthorizationManaging` which we inject into to [AuthorizationTokenInterceptor](#authorizationTokenInterceptor) and then pass +it to the [APIManager](#apiManager) as both adapter and processor. + +```swift +let authManager = AuthorizationManager() +let authorizationInterceptor = AuthorizationTokenInterceptor(authorizationManager: authManager) +let apiManager = APIManager( + requestAdapters: [authorizationInterceptor], + responseProcessors: [authorizationInterceptor] + ) +``` + +After login we have to save the `AuthorizationData` to the `AuthorizationManager`. + +``` +let response: UserAuthResponse = try await apiManager.request( + UserRouter.loginUser(request) +) +try await authManager.storage.saveData(response.authData) +``` + +Then we can simply define which request should be authorised via `isAuthenticationRequired` property of [Requestable](#requestable) protocol. + +```swift +extension UserRouter: Requestable { + ... + var isAuthenticationRequired: Bool { + switch self { + case .getUser, .updateUser: + return true + } + } +} +``` + +## Requestable By conforming to the ``Requestable`` protocol, you can define endpoint definitions containing the elementary HTTP request components necessary to create valid HTTP requests.
**Recommendation:** Follow the `Router` naming convention to explicitly indicate the usage of a router pattern. @@ -39,7 +163,7 @@ enum UserRouter { extension UserRouter: Requestable { // The base URL address used for the HTTP call. var baseURL: URL { - URL(string: Constants.baseHost)! + URL(string: "https://reqres.in/api")! } // Path will be appended to the base URL. @@ -82,7 +206,7 @@ extension UserRouter: Requestable { Some of the properties have default implementations defined in the `Requestable+Convenience` extension. -## APIManager +## APIManager APIManager is responsible for the creation and management of a network call. It conforms to the ``APIManaging`` protocol which allows you to define your own custom APIManager if needed. There are two ways to initialise the ``APIManager`` object: @@ -128,17 +252,7 @@ func request( ``` ### Example -In the most simple form, the network request looks like this: -```swift -try await apiManager.request(UserRouter.getUser) -``` - -If you specify object type, the APIManager will automatically perform the decoding (given the received JSON correctly maps to the decodable). You can also specify a custom json decoder. - -```swift -let userResponse: UserResponse = try await apiManager.request(UserRouter.getUser) -``` Provide a custom after failure ``RetryConfiguration``, specifying the count of retries, delay and a handler that determines whether the request should be tried again. Otherwise, ``RetryConfiguration/default`` configuration is used. @@ -152,7 +266,7 @@ let userResponse: UserResponse = try await apiManager.request( ) ``` -## DownloadAPIManager +## DownloadAPIManager DownloadAPIManager is responsible for the creation and management of a network file download. It conforms to the ``DownloadAPIManaging`` protocol which allows you to define your own custom DownloadAPIManager if needed. Multiple parallel downloads are supported. The initialisation is equivalent to APIManager, except the session is created for the user based on a given `URLSessionConfiguration`: @@ -173,13 +287,19 @@ var allTasks: [URLSessionDownloadTask] { get async } ``` There are three methods provided by the ``DownloadAPIManaging`` protocol: -1. Request download for a given endpoint. Returns a standard (URLSessionDownloadTask, Response) result for the HTTP handshake. This result is not the actual downloaded file, but the HTTP response received after the download is initiated. +1. Request download for a given endpoint or a simple URL. Returns a standard (URLSessionDownloadTask, Response) result for the HTTP handshake. This result is not the actual downloaded file, but the HTTP response received after the download is initiated. ```swift -func downloadRequest( - _ endpoint: Requestable, - resumableData: Data? = nil, - retryConfiguration: RetryConfiguration? -) async throws -> DownloadResult + func downloadRequest( + _ endpoint: Requestable, + resumableData: Data? = nil, + retryConfiguration: RetryConfiguration? = .default + ) async throws -> DownloadResult { + + func downloadRequest( + _ fileURL: URL, + resumableData: Data? = nil, + retryConfiguration: RetryConfiguration? = .default + ) async throws -> DownloadResult { ``` 2. Get progress async stream for a given task to observe task download progress and state. @@ -195,7 +315,7 @@ func invalidateSession(shouldFinishTasks: Bool = false) ``` DownloadAPIManager is not deallocated from memory since URLSession is holding a reference to it. If you wish to use new instances of the DownloadAPIManager, don't forget to invalidate the session if it is not needed anymore. -## UploadAPIManager +## UploadAPIManager Similarly to DownloadAPIManager we have an UploadAPIManager responsible for the creation and management of a network file uploads. It conforms to the ``UploadAPIManaging`` protocol which allows you to define your own custom UploadAPIManager if needed. Multiple parallel uploads are supported. The initialisation is equivalent to APIManager, except the session is created for the user based on a given `URLSessionConfiguration`: @@ -214,7 +334,16 @@ The UploadAPIManager contains a public property that enables you to keep track o ```swift var activeTasks: [UploadTask] { get async } ``` -``UploadAPIManaging`` defines three methods for upload based on the upload type `Data`, file `URL` and `MultipartFormData`. Each of these methods return an `UploadTask`. + +You can start an upload by calling the `upload` function by passing `UploadType` which defines three types of resources for upload `Data`, file `URL` and `MultipartFormData`. + +```swift +func upload( + _ type: UploadType, + to endpoint: Requestable +) async throws -> UploadTask +``` + An `UploadTask` is a struct which under the hood represents + manages a URLSessionUploadTask and provides its state. After firing an upload by one of these three methods, you can get a StateStream either from the `UploadTask` itself or from the manager with the following method. @@ -245,7 +374,7 @@ let retryConfiguration = RetryConfiguration(retries: 2, delay: .constant(1)) { e } ``` -## Modifiers +## Modifiers Modifiers are useful pieces of code that modify request/response in the network request pipeline. ![Interceptors diagram](interceptors-diagram.png) @@ -269,9 +398,9 @@ Interceptors handle both adapting and response/error processing. By conforming to these protocols, you can create your own adaptors/processors/interceptors. In the following part, modifiers provided by Networking are introduced. -## Request Interceptors +## Interceptors -### Logging +### LoggingInterceptor Networking provides a default ``LoggingInterceptor`` which internally uses `os_log` to pretty print requests/responses. You can utilise it to get logging console output either for requests, responses or both. ```swift @@ -284,7 +413,7 @@ APIManager( ) ``` -### Authorization +### AuthorizationTokenInterceptor Networking provides a default authorization handling for OAuth scenarios. Use the default ``AuthorizationTokenInterceptor`` with the APIManager to obtain the behaviour of JWT Bearer authorization header injection and access token expiration refresh flow. Start by implementing an authorization manager by conforming to ``AuthorizationManaging``. This manager requires you to provide storage defined by ``AuthorizationStorageManaging`` (where OAuth credentials will be stored) and a refresh method that will perform the refresh token network call to obtain a new OAuth pair. Optionally, you can provide custom implementations for ``AuthorizationManaging/authorizeRequest(_:)`` (by default, this method sets the authorization header) or access token getter (by default, this method returns the access token saved in provided storage). @@ -314,9 +443,9 @@ final class CustomAuthorizationManager: AuthorizationManaging { } ``` -## Processors +## Processors -### Status Code +### StatusCodeProcessor Each ``Requestable`` endpoint definition contains an ``Requestable/acceptableStatusCodes`` range of acceptable status codes. By default, these are set to `200..<400`. Networking provides a default status code processor that makes sure the received response's HTTP code is an acceptable one, otherwise an ``NetworkError/unacceptableStatusCode(statusCode:acceptedStatusCodes:response:)`` error is thrown. ```swift @@ -327,10 +456,10 @@ APIManager( ) ``` -### Storage +### EndpointRequestStorageProcessor Networking provides an ``EndpointRequestStorageProcessor`` which allows for requests and responses to be saved locally into the file system. Requests are stored in a sequential manner. Each session is kept in its own dedicated folder. The ``EndpointRequestStorageModel`` includes both successful and erroneous data. -Initialise by optionally providing a `FileManager` instance, `JSONEncoder` to be used during request/response data encoding and a configuration. The configuration allows you to set optionally a multiPeerSharing configuration if you wish to utilize the multipeer connectivity feature for sharing the ``EndpointRequestStorageModel`` with devices using the `MultipeerConnectivity` framework. +Initialise by optionally providing a `FileManager` instance, `JSONEncoder` to be used during request/response data encoding and a configuration. The configuration allows you to set optionally a multiPeerSharing configuration if you wish to utilise the multipeer connectivity feature for sharing the ``EndpointRequestStorageModel`` with devices using the `MultipeerConnectivity` framework. ```swift init( @@ -340,7 +469,7 @@ init( ) ``` -## Associated array query parameters +## Associated array query parameters When specifying urlParameters in the endpoint definition, use an ``ArrayParameter`` to define multiple values for a single URL query parameter. The struct lets you decide which ``ArrayEncoding`` will be used during the creation of the URL. There are two currently supported encodings: From ce21b81659382cc7dafe4dd81abdfb6f3e3981f5 Mon Sep 17 00:00:00 2001 From: Matej Molnar Date: Sun, 7 Jan 2024 23:34:02 +0100 Subject: [PATCH 24/29] [fix] README icons --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e2802e90..4121ab12 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Networking -[![Coverage](https://img.shields.io/badge/Coverage-100%25-darkgreen?style=flat-square)] -[![Platforms](https://img.shields.io/badge/Platforms-iOS_iPadOS_macOS_watchOS-lightgrey?style=flat-square)] -[![Swift](https://img.shields.io/badge/Swift-5.9+-blue?style=flat-square)] +![Coverage](https://img.shields.io/badge/Coverage-100%25-darkgreen?style=flat-square) +![Platforms](https://img.shields.io/badge/Platforms-iOS_iPadOS_macOS_watchOS-lightgrey?style=flat-square) +![Swift](https://img.shields.io/badge/Swift-5.9+-blue?style=flat-square) A networking layer using native `URLSession` and Swift concurrency. From b7ac79f024426377e4635fdd746fa6eb75361005 Mon Sep 17 00:00:00 2001 From: Matej Molnar Date: Mon, 8 Jan 2024 13:04:14 +0100 Subject: [PATCH 25/29] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4121ab12..3d293842 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ A networking layer using native `URLSession` and Swift concurrency. - [EndpointRequestStorageProcessor](#endpointRequestStorageProcessor) - [Associated array query parameters](#arrayQueryParameters) -## Requirements +## Requirements - iOS/iPadOS 15.0+, macOS 12.0+, watchOS 9.0+ - Xcode 14+ From e1d46751d1ef5016d714e86b38f7dd2bfed341d0 Mon Sep 17 00:00:00 2001 From: Matej Molnar Date: Mon, 8 Jan 2024 13:12:27 +0100 Subject: [PATCH 26/29] [chore] remove tags --- README.md | 78 +++++++++++++++++++++++++++---------------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 3d293842..14f4c55f 100644 --- a/README.md +++ b/README.md @@ -10,22 +10,22 @@ A networking layer using native `URLSession` and Swift concurrency. - [Installation](#installation) - [Overview](#overview) - [Basics](#basics) - - [Making requests](#makingRequests) - - [Downloading files](#downloadingFiles) - - [Uploading files](#uploadingFiles) - - [Request authorization](#requestAuthorization) + - [Making requests](#making-requests) + - [Downloading files](#downloading-files) + - [Uploading files](#uploading-files) + - [Request authorization](#request-authorization) - [Requestable](#requestable) -- [APIManager](#apiManager) -- [DownloadAPIManager](#downloadManager) -- [UploadAPIManager](#uploadManager) +- [APIManager](#apimanager) +- [DownloadAPIManager](#downloadapimanager) +- [UploadAPIManager](#uploadapimanager) - [Modifiers](#modifiers) - [Interceptors](#interceptors) - - [LoggingInterceptor](#loggingInterceptor) - - [AuthorizationTokenInterceptor](#authorizationTokenInterceptor) + - [LoggingInterceptor](#logginginterceptor) + - [AuthorizationTokenInterceptor](#authorizationtokeninterceptor) - [Processors](#processors) - - [StatusCodeProcessor](#statusCodeProcessor) - - [EndpointRequestStorageProcessor](#endpointRequestStorageProcessor) -- [Associated array query parameters](#arrayQueryParameters) + - [StatusCodeProcessor](#statuscodeprocessor) + - [EndpointRequestStorageProcessor](#endpointrequeststorageprocessor) +- [Associated array query parameters](#associated-array-query-parameters) ## Requirements @@ -33,7 +33,7 @@ A networking layer using native `URLSession` and Swift concurrency. - Xcode 14+ - Swift 5.9+ -## Installation +## Installation You can install the library with [Swift Package Manager](https://swift.org/package-manager/). Once you have your Swift package set up, adding Dependency Injection as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`. @@ -43,12 +43,12 @@ dependencies: [ ] ``` -## Overview +## Overview Heavily inspired by Moya, the networking layer's philosophy is focused on creating individual endpoint routers, transforming them into a valid URLRequest objects and applying optional adapters and processors in the network call pipeline utilising native `URLSession` under the hood. -## Basics +## Basics -### Making requests +### Making requests There is no 1 line way of making a request from scratch in order to ensure consistency and better structure. First we need to define a Router by conforming to [Requestable](#requestable) protocol. Which in the simplest form can look like this: ```swift enum UserRouter: Requestable { @@ -72,18 +72,18 @@ enum UserRouter: Requestable { } ``` -Then we can make a request on an [APIManager](#apiManager) instance, which is responsible for handling the whole request flow. +Then we can make a request on an [APIManager](#apimanager) instance, which is responsible for handling the whole request flow. ```swift let response = try await APIManager().request(UserRouter.getUser) ``` -If you specify object type, the [APIManager](#apiManager) will automatically perform the decoding (given the received JSON correctly maps to the decodable). You can also specify a custom json decoder. +If you specify object type, the [APIManager](#apimanager) will automatically perform the decoding (given the received JSON correctly maps to the decodable). You can also specify a custom json decoder. ```swift let userResponse: UserResponse = try await apiManager.request(UserRouter.getUser) ``` -### Downloading files -Downloads are being handled by a designated [DownloadAPIManager](#downloadManager). Here is an example of a basic form of file download from a `URL`. It returns a tuple of `URLSessionDownloadTask` and `Response` (result for the HTTP handshake). +### Downloading files +Downloads are being handled by a designated [DownloadAPIManager](#downloadapimanager). Here is an example of a basic form of file download from a `URL`. It returns a tuple of `URLSessionDownloadTask` and `Response` (result for the HTTP handshake). ```swift let (task, response) = try await DownloadAPIManager().request(url: URL) ``` @@ -95,10 +95,10 @@ for try await downloadState in downloadAPIManager.shared.progressStream(for: tas } ``` -In case you need to provide some specific info in the request, you can define a type conforming to [Requestable](#requestable) protocol and pass that to the [DownloadAPIManager](#downloadManager) instead of the `URL`. +In case you need to provide some specific info in the request, you can define a type conforming to [Requestable](#requestable) protocol and pass that to the [DownloadAPIManager](#downloadapimanager) instead of the `URL`. -### Uploading files -Uploads are being handled by a designated [UploadAPIManager](#uploadManager). Here is an example of a basic form of file upload to a `URL`. It returns an `UploadTask` which is a struct that represents + manages a `URLSessionUploadTask` and provides its state. +### Uploading files +Uploads are being handled by a designated [UploadAPIManager](#uploadapimanager). Here is an example of a basic form of file upload to a `URL`. It returns an `UploadTask` which is a struct that represents + manages a `URLSessionUploadTask` and provides its state. ```swift let uploadTask = try await uploadManager.upload(.file(fileUrl), to: "https://upload.com/file") ``` @@ -110,12 +110,12 @@ for await uploadState in await uploadManager.stateStream(for: task.id) { } ``` -In case you need to provide some specific info in the request, you can define a type conforming to [Requestable](#requestable) protocol and pass that to the [UploadAPIManager](#uploadManager) instead of the upload `URL`. +In case you need to provide some specific info in the request, you can define a type conforming to [Requestable](#requestable) protocol and pass that to the [UploadAPIManager](#uploadapimanager) instead of the upload `URL`. -### Request authorization +### Request authorization Networking provides a default authorization handling for OAuth scenarios. In order to utilise this we -have to first create our own implementation of `AuthorizationManaging` which we inject into to [AuthorizationTokenInterceptor](#authorizationTokenInterceptor) and then pass -it to the [APIManager](#apiManager) as both adapter and processor. +have to first create our own implementation of `AuthorizationManaging` which we inject into to [AuthorizationTokenInterceptor](#authorizationtokeninterceptor) and then pass +it to the [APIManager](#apimanager) as both adapter and processor. ```swift let authManager = AuthorizationManager() @@ -149,7 +149,7 @@ extension UserRouter: Requestable { } ``` -## Requestable +## Requestable By conforming to the ``Requestable`` protocol, you can define endpoint definitions containing the elementary HTTP request components necessary to create valid HTTP requests.
**Recommendation:** Follow the `Router` naming convention to explicitly indicate the usage of a router pattern. @@ -206,7 +206,7 @@ extension UserRouter: Requestable { Some of the properties have default implementations defined in the `Requestable+Convenience` extension. -## APIManager +## APIManager APIManager is responsible for the creation and management of a network call. It conforms to the ``APIManaging`` protocol which allows you to define your own custom APIManager if needed. There are two ways to initialise the ``APIManager`` object: @@ -266,7 +266,7 @@ let userResponse: UserResponse = try await apiManager.request( ) ``` -## DownloadAPIManager +## DownloadAPIManager DownloadAPIManager is responsible for the creation and management of a network file download. It conforms to the ``DownloadAPIManaging`` protocol which allows you to define your own custom DownloadAPIManager if needed. Multiple parallel downloads are supported. The initialisation is equivalent to APIManager, except the session is created for the user based on a given `URLSessionConfiguration`: @@ -315,7 +315,7 @@ func invalidateSession(shouldFinishTasks: Bool = false) ``` DownloadAPIManager is not deallocated from memory since URLSession is holding a reference to it. If you wish to use new instances of the DownloadAPIManager, don't forget to invalidate the session if it is not needed anymore. -## UploadAPIManager +## UploadAPIManager Similarly to DownloadAPIManager we have an UploadAPIManager responsible for the creation and management of a network file uploads. It conforms to the ``UploadAPIManaging`` protocol which allows you to define your own custom UploadAPIManager if needed. Multiple parallel uploads are supported. The initialisation is equivalent to APIManager, except the session is created for the user based on a given `URLSessionConfiguration`: @@ -374,7 +374,7 @@ let retryConfiguration = RetryConfiguration(retries: 2, delay: .constant(1)) { e } ``` -## Modifiers +## Modifiers Modifiers are useful pieces of code that modify request/response in the network request pipeline. ![Interceptors diagram](interceptors-diagram.png) @@ -398,9 +398,9 @@ Interceptors handle both adapting and response/error processing. By conforming to these protocols, you can create your own adaptors/processors/interceptors. In the following part, modifiers provided by Networking are introduced. -## Interceptors +## Interceptors -### LoggingInterceptor +### LoggingInterceptor Networking provides a default ``LoggingInterceptor`` which internally uses `os_log` to pretty print requests/responses. You can utilise it to get logging console output either for requests, responses or both. ```swift @@ -413,7 +413,7 @@ APIManager( ) ``` -### AuthorizationTokenInterceptor +### AuthorizationTokenInterceptor Networking provides a default authorization handling for OAuth scenarios. Use the default ``AuthorizationTokenInterceptor`` with the APIManager to obtain the behaviour of JWT Bearer authorization header injection and access token expiration refresh flow. Start by implementing an authorization manager by conforming to ``AuthorizationManaging``. This manager requires you to provide storage defined by ``AuthorizationStorageManaging`` (where OAuth credentials will be stored) and a refresh method that will perform the refresh token network call to obtain a new OAuth pair. Optionally, you can provide custom implementations for ``AuthorizationManaging/authorizeRequest(_:)`` (by default, this method sets the authorization header) or access token getter (by default, this method returns the access token saved in provided storage). @@ -443,9 +443,9 @@ final class CustomAuthorizationManager: AuthorizationManaging { } ``` -## Processors +## Processors -### StatusCodeProcessor +### StatusCodeProcessor Each ``Requestable`` endpoint definition contains an ``Requestable/acceptableStatusCodes`` range of acceptable status codes. By default, these are set to `200..<400`. Networking provides a default status code processor that makes sure the received response's HTTP code is an acceptable one, otherwise an ``NetworkError/unacceptableStatusCode(statusCode:acceptedStatusCodes:response:)`` error is thrown. ```swift @@ -456,7 +456,7 @@ APIManager( ) ``` -### EndpointRequestStorageProcessor +### EndpointRequestStorageProcessor Networking provides an ``EndpointRequestStorageProcessor`` which allows for requests and responses to be saved locally into the file system. Requests are stored in a sequential manner. Each session is kept in its own dedicated folder. The ``EndpointRequestStorageModel`` includes both successful and erroneous data. Initialise by optionally providing a `FileManager` instance, `JSONEncoder` to be used during request/response data encoding and a configuration. The configuration allows you to set optionally a multiPeerSharing configuration if you wish to utilise the multipeer connectivity feature for sharing the ``EndpointRequestStorageModel`` with devices using the `MultipeerConnectivity` framework. @@ -469,7 +469,7 @@ init( ) ``` -## Associated array query parameters +## Associated array query parameters When specifying urlParameters in the endpoint definition, use an ``ArrayParameter`` to define multiple values for a single URL query parameter. The struct lets you decide which ``ArrayEncoding`` will be used during the creation of the URL. There are two currently supported encodings: From 88b2e5622d9edeb1f4497112ddee86434fab2076 Mon Sep 17 00:00:00 2001 From: Matej Molnar Date: Mon, 8 Jan 2024 13:16:03 +0100 Subject: [PATCH 27/29] [fix] README image not being displayed --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 14f4c55f..a61cefc2 100644 --- a/README.md +++ b/README.md @@ -376,7 +376,7 @@ let retryConfiguration = RetryConfiguration(retries: 2, delay: .constant(1)) { e ## Modifiers Modifiers are useful pieces of code that modify request/response in the network request pipeline. -![Interceptors diagram](interceptors-diagram.png) +![Interceptors diagram](Sources/Networking/Documentation.docc/Resources/interceptors-diagram.png) There are three types you can leverage:
From 75b029d0c86208bda98b20473367ab698f8c12ca Mon Sep 17 00:00:00 2001 From: Tomas Cejka Date: Thu, 11 Jan 2024 17:54:04 +0100 Subject: [PATCH 28/29] [feat] add support for iOS 14 --- Package.swift | 2 +- Sources/Networking/Core/APIManager.swift | 17 +++++++++++++++-- .../Networking/Core/DownloadAPIManager.swift | 8 +++++++- .../Core/Upload/UploadAPIManager.swift | 5 +++++ .../Core/Upload/UploadAPIManaging.swift | 2 ++ Sources/Networking/Core/Upload/UploadTask.swift | 1 + 6 files changed, 31 insertions(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index 231c4530..152471e9 100644 --- a/Package.swift +++ b/Package.swift @@ -6,7 +6,7 @@ import PackageDescription let package = Package( name: "Networking", platforms: [ - .iOS(SupportedPlatform.IOSVersion.v15), + .iOS(SupportedPlatform.IOSVersion.v14), .macOS(SupportedPlatform.MacOSVersion.v12), .watchOS(SupportedPlatform.WatchOSVersion.v9) ], diff --git a/Sources/Networking/Core/APIManager.swift b/Sources/Networking/Core/APIManager.swift index 238fe93c..226c6560 100644 --- a/Sources/Networking/Core/APIManager.swift +++ b/Sources/Networking/Core/APIManager.swift @@ -23,7 +23,14 @@ open class APIManager: APIManaging, Retryable { errorProcessors: [ErrorProcessing] = [] ) { /// generate session id in readable format - sessionId = Date().ISO8601Format() + if #unavailable(iOS 15) { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + sessionId = dateFormatter.string(from: Date()) + } else { + sessionId = Date().ISO8601Format() + } + self.responseProvider = urlSession self.requestAdapters = requestAdapters self.responseProcessors = responseProcessors @@ -37,7 +44,13 @@ open class APIManager: APIManaging, Retryable { errorProcessors: [ErrorProcessing] = [] ) { /// generate session id in readable format - sessionId = Date().ISO8601Format() + if #unavailable(iOS 15) { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + sessionId = dateFormatter.string(from: Date()) + } else { + sessionId = Date().ISO8601Format() + } self.responseProvider = responseProvider self.requestAdapters = requestAdapters self.responseProcessors = responseProcessors diff --git a/Sources/Networking/Core/DownloadAPIManager.swift b/Sources/Networking/Core/DownloadAPIManager.swift index 4b106848..05d96b13 100644 --- a/Sources/Networking/Core/DownloadAPIManager.swift +++ b/Sources/Networking/Core/DownloadAPIManager.swift @@ -34,7 +34,13 @@ open class DownloadAPIManager: NSObject, Retryable { errorProcessors: [ErrorProcessing] = [] ) { /// generate session id in readable format - sessionId = Date().ISO8601Format() + if #unavailable(iOS 15) { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + sessionId = dateFormatter.string(from: Date()) + } else { + sessionId = Date().ISO8601Format() + } self.requestAdapters = requestAdapters self.responseProcessors = responseProcessors diff --git a/Sources/Networking/Core/Upload/UploadAPIManager.swift b/Sources/Networking/Core/Upload/UploadAPIManager.swift index 501d85d5..a553845a 100644 --- a/Sources/Networking/Core/Upload/UploadAPIManager.swift +++ b/Sources/Networking/Core/Upload/UploadAPIManager.swift @@ -9,6 +9,7 @@ import Combine import Foundation /// Default upload API manager +@available(iOS 15.0, *) open class UploadAPIManager: NSObject { // MARK: - Public Properties public var activeTasks: [UploadTask] { @@ -60,6 +61,7 @@ open class UploadAPIManager: NSObject { } // MARK: URLSessionDataDelegate +@available(iOS 15.0, *) extension UploadAPIManager: URLSessionDataDelegate { public func urlSession( _ session: URLSession, @@ -92,6 +94,7 @@ extension UploadAPIManager: URLSessionDataDelegate { } // MARK: - URLSessionTaskDelegate +@available(iOS 15.0, *) extension UploadAPIManager: URLSessionTaskDelegate { public func urlSession( _ session: URLSession, @@ -130,6 +133,7 @@ extension UploadAPIManager: URLSessionTaskDelegate { } // MARK: - UploadAPIManaging +@available(iOS 15.0, *) extension UploadAPIManager: UploadAPIManaging { public func invalidateSession(shouldFinishTasks: Bool) { if shouldFinishTasks { @@ -216,6 +220,7 @@ extension UploadAPIManager: UploadAPIManaging { } // MARK: - Private API +@available(iOS 15.0, *) private extension UploadAPIManager { @discardableResult func uploadRequest( diff --git a/Sources/Networking/Core/Upload/UploadAPIManaging.swift b/Sources/Networking/Core/Upload/UploadAPIManaging.swift index ac0c5d05..2fe2c0a3 100644 --- a/Sources/Networking/Core/Upload/UploadAPIManaging.swift +++ b/Sources/Networking/Core/Upload/UploadAPIManaging.swift @@ -8,6 +8,7 @@ import Combine import Foundation +@available(iOS 15.0, *) public protocol UploadAPIManaging { typealias StateStream = AsyncPublisher> @@ -72,6 +73,7 @@ public protocol UploadAPIManaging { func invalidateSession(shouldFinishTasks: Bool) } +@available(iOS 15.0, *) public extension UploadAPIManaging { /// Initiates a `multipart/form-data` upload request to the specified `endpoint`. /// diff --git a/Sources/Networking/Core/Upload/UploadTask.swift b/Sources/Networking/Core/Upload/UploadTask.swift index 35d79f61..1e09622e 100644 --- a/Sources/Networking/Core/Upload/UploadTask.swift +++ b/Sources/Networking/Core/Upload/UploadTask.swift @@ -63,6 +63,7 @@ public extension UploadTask { } // MARK: - Internal API +@available(iOS 15.0, *) extension UploadTask { /// The identifier of the underlying `URLSessionUploadTask`. var taskIdentifier: Int { From aa99c1398e52be5d54471cc2e905404be530d883 Mon Sep 17 00:00:00 2001 From: Tomas Cejka Date: Tue, 16 Jan 2024 09:47:34 +0100 Subject: [PATCH 29/29] [chore] typo fix --- Sources/Networking/Core/Upload/MultipartFormDataEncoder.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Networking/Core/Upload/MultipartFormDataEncoder.swift b/Sources/Networking/Core/Upload/MultipartFormDataEncoder.swift index d5fc6b76..79cbf04b 100644 --- a/Sources/Networking/Core/Upload/MultipartFormDataEncoder.swift +++ b/Sources/Networking/Core/Upload/MultipartFormDataEncoder.swift @@ -33,7 +33,7 @@ open class MultipartFormDataEncoder { // MARK: - MultipartFormDataEncoding extension MultipartFormDataEncoder: MultipartFormDataEncoding { - /// The main reason why there are methods to encode data & encode file is similar to `uploadTask(with:from:)` and `uploadTask(with:fromFile:)` ig one could convert the content of the file to Data using Data(contentsOf:) and use the first method to send data. One has the data available in memory while the second reads the data directly from the file thus doesn't load the data into memory so it is more efficient. + /// The main reason why there are methods to encode data & encode file is similar to `uploadTask(with:from:)` and `uploadTask(with:fromFile:)` if one could convert the content of the file to Data using Data(contentsOf:) and use the first method to send data. One has the data available in memory while the second reads the data directly from the file thus doesn't load the data into memory so it is more efficient. public func encode(_ multipartFormData: MultipartFormData) throws -> Data { var encoded = Data()