From c9c11598b609970973ee16017593fe51df0b5e16 Mon Sep 17 00:00:00 2001 From: Twig Date: Tue, 30 Aug 2022 15:39:40 -0400 Subject: [PATCH 1/3] Adds a convenient request state controller --- Networking.xcodeproj/project.pbxproj | 8 ++ .../NetworkRequestStateController.swift | 116 ++++++++++++++++++ Sources/Networking/Publisher+Result.swift | 22 ++++ 3 files changed, 146 insertions(+) create mode 100644 Sources/Networking/NetworkRequestStateController.swift create mode 100644 Sources/Networking/Publisher+Result.swift diff --git a/Networking.xcodeproj/project.pbxproj b/Networking.xcodeproj/project.pbxproj index 1f582f2..7591bc7 100644 --- a/Networking.xcodeproj/project.pbxproj +++ b/Networking.xcodeproj/project.pbxproj @@ -10,6 +10,8 @@ 4C2CBFAC24D4AA9800920BE2 /* NetworkRequestPerformer+JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2CBFAB24D4AA9800920BE2 /* NetworkRequestPerformer+JSON.swift */; }; F211601924884A4D00B48D52 /* PhotoRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F211601824884A4D00B48D52 /* PhotoRequest.swift */; }; F211601B24884ABF00B48D52 /* Photo.swift in Sources */ = {isa = PBXBuildFile; fileRef = F211601A24884ABF00B48D52 /* Photo.swift */; }; + F264BB4E28BE9D6500969CD1 /* NetworkRequestStateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F264BB4D28BE9D6500969CD1 /* NetworkRequestStateController.swift */; }; + F264BB5028BE9E1700969CD1 /* Publisher+Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = F264BB4F28BE9E1700969CD1 /* Publisher+Result.swift */; }; F2B5BC27248836C500B6A52A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2B5BC26248836C500B6A52A /* AppDelegate.swift */; }; F2B5BC29248836C500B6A52A /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2B5BC28248836C500B6A52A /* SceneDelegate.swift */; }; F2B5BC2B248836C500B6A52A /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2B5BC2A248836C500B6A52A /* ContentView.swift */; }; @@ -41,6 +43,8 @@ 4C2CBFAB24D4AA9800920BE2 /* NetworkRequestPerformer+JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkRequestPerformer+JSON.swift"; sourceTree = ""; }; F211601824884A4D00B48D52 /* PhotoRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoRequest.swift; sourceTree = ""; }; F211601A24884ABF00B48D52 /* Photo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Photo.swift; sourceTree = ""; }; + F264BB4D28BE9D6500969CD1 /* NetworkRequestStateController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkRequestStateController.swift; sourceTree = ""; }; + F264BB4F28BE9E1700969CD1 /* Publisher+Result.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publisher+Result.swift"; sourceTree = ""; }; F2B5BC23248836C500B6A52A /* Networking.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Networking.app; sourceTree = BUILT_PRODUCTS_DIR; }; F2B5BC26248836C500B6A52A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; F2B5BC28248836C500B6A52A /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -100,6 +104,8 @@ F2B5BC54248837B400B6A52A /* NetworkRequestPerformer.swift */, F2B5BC56248837CB00B6A52A /* NetworkController.swift */, 4C2CBFAB24D4AA9800920BE2 /* NetworkRequestPerformer+JSON.swift */, + F264BB4D28BE9D6500969CD1 /* NetworkRequestStateController.swift */, + F264BB4F28BE9E1700969CD1 /* Publisher+Result.swift */, ); path = Networking; sourceTree = ""; @@ -282,7 +288,9 @@ F2B5BC27248836C500B6A52A /* AppDelegate.swift in Sources */, F211601924884A4D00B48D52 /* PhotoRequest.swift in Sources */, F2B5BC512488377600B6A52A /* NetworkResponse.swift in Sources */, + F264BB4E28BE9D6500969CD1 /* NetworkRequestStateController.swift in Sources */, F2B5BC29248836C500B6A52A /* SceneDelegate.swift in Sources */, + F264BB5028BE9E1700969CD1 /* Publisher+Result.swift in Sources */, F2B5BC2B248836C500B6A52A /* ContentView.swift in Sources */, 4C2CBFAC24D4AA9800920BE2 /* NetworkRequestPerformer+JSON.swift in Sources */, F2B5BC4F2488376C00B6A52A /* NetworkRequest.swift in Sources */, diff --git a/Sources/Networking/NetworkRequestStateController.swift b/Sources/Networking/NetworkRequestStateController.swift new file mode 100644 index 0000000..11cee49 --- /dev/null +++ b/Sources/Networking/NetworkRequestStateController.swift @@ -0,0 +1,116 @@ +// +// NetworkRequestStateController.swift +// Networking +// +// Created by Twig on 8/30/22. +// Copyright © 2022 Lickability. All rights reserved. +// + +import Foundation +import Combine + +/// A class responsible for representing the state and value of a network request being made. +public final class NetworkRequestStateController { + + /// The state of a network request's lifecycle. + public enum NetworkRequestState { + + /// A request that has not yet been started. + case notInProgress + + /// A request that has been started, but not completed. + case inProgress + + /// A request that has been completed with an associated result. + case completed(Result) + + /// A `Bool` representing if a request is in progress. + public var isInProgress: Bool { + switch self { + case .notInProgress, .completed: + return false + case .inProgress: + return true + } + } + + /// The completed `LocalizedError`, if one exists. + public var completedError: LocalizedError? { + switch self { + case .notInProgress, .inProgress: + return nil + case let .completed(result): + switch result { + case .success: + return nil + case let .failure(networkError): + return networkError + } + } + } + + /// The completed `NetworkResponse`, if one exists. + public var completedResponse: NetworkResponse? { + switch self { + case .notInProgress, .inProgress: + return nil + case let .completed(result): + switch result { + case let .success(response): + return response + case .failure: + return nil + } + } + } + + /// A `Bool` indicating if the request has finished successfully. + public var didFinishSuccessfully: Bool { + return completedResponse != nil + } + + /// A `Bool` indicating if the request has finished with an error. + public var didFinishUnsuccessfully: Bool { + return completedError != nil + } + } + + /// A `Publisher` that can be subscribed to in order to receive updates about the status of a request. + public private(set) lazy var publisher: AnyPublisher = { + return requestStatePublisher.prepend(.notInProgress).eraseToAnyPublisher() + }() + + private let requestPerformer: NetworkRequestPerformer + private let requestStatePublisher = PassthroughSubject() + private var cancellables = Set() + + /// Initializes the `NetworkRequestStateController` with the specified parameters. + /// - Parameter requestPerformer: The `NetworkRequestPerformer` used to make requests. + public init(requestPerformer: NetworkRequestPerformer) { + self.requestPerformer = requestPerformer + } + + /// Sends a request with the specified parameters. + /// - Parameters: + /// - request: The request to send. + /// - requestBehaviors: Additional behaviors to append to the request. + public func send(request: NetworkRequest, requestBehaviors: [RequestBehavior] = []) { + requestStatePublisher.send(.inProgress) + + requestPerformer.send(request, requestBehaviors: requestBehaviors) + .mapToResult() + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [requestStatePublisher] result in + requestStatePublisher.send(.completed(result)) + }) + .store(in: &cancellables) + } + + /// Resets the state of the `requestStatePublisher` and cancels any in flight requests that may be ongoing. Cancellation is not guaranteed, and requests that are near completion may end up finishing, despite being cancelled. + public func resetState() { + cancellables.forEach { $0.cancel() } + cancellables.removeAll() + + requestStatePublisher.send(.notInProgress) + } +} diff --git a/Sources/Networking/Publisher+Result.swift b/Sources/Networking/Publisher+Result.swift new file mode 100644 index 0000000..7ae7868 --- /dev/null +++ b/Sources/Networking/Publisher+Result.swift @@ -0,0 +1,22 @@ +// +// Publisher+Result.swift +// Networking +// +// Created by Twig on 8/30/22. +// Copyright © 2022 Lickability. All rights reserved. +// + +import Foundation +import Combine + +/// Adds extensions to the `Publisher` type to convert responses into `Result`s. +extension Publisher { + + /// Maps the chain of `Output`/`Failure` responses into a `Result` type that contains the `. + /// - Returns: A `Publisher` that has converted the `Output` or `Failure` into a `Result`. + func mapToResult() -> AnyPublisher, Never> { + map(Result.success) + .catch { Just(.failure($0)) } + .eraseToAnyPublisher() + } +} From 15cfe0963589f240d6403de22018ec3002d10537 Mon Sep 17 00:00:00 2001 From: Twig Date: Tue, 30 Aug 2022 15:45:56 -0400 Subject: [PATCH 2/3] Update Publisher+Result.swift --- Sources/Networking/Publisher+Result.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Networking/Publisher+Result.swift b/Sources/Networking/Publisher+Result.swift index 7ae7868..810de43 100644 --- a/Sources/Networking/Publisher+Result.swift +++ b/Sources/Networking/Publisher+Result.swift @@ -14,7 +14,7 @@ extension Publisher { /// Maps the chain of `Output`/`Failure` responses into a `Result` type that contains the `. /// - Returns: A `Publisher` that has converted the `Output` or `Failure` into a `Result`. - func mapToResult() -> AnyPublisher, Never> { + public func mapToResult() -> AnyPublisher, Never> { map(Result.success) .catch { Just(.failure($0)) } .eraseToAnyPublisher() From 2b22feb3a41989420d124c0aa71107d959e23b10 Mon Sep 17 00:00:00 2001 From: Twig Date: Tue, 30 Aug 2022 15:57:18 -0400 Subject: [PATCH 3/3] Renamed bools --- Sources/Networking/NetworkRequestStateController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Networking/NetworkRequestStateController.swift b/Sources/Networking/NetworkRequestStateController.swift index 11cee49..0aedb59 100644 --- a/Sources/Networking/NetworkRequestStateController.swift +++ b/Sources/Networking/NetworkRequestStateController.swift @@ -65,12 +65,12 @@ public final class NetworkRequestStateController { } /// A `Bool` indicating if the request has finished successfully. - public var didFinishSuccessfully: Bool { + public var didSucceed: Bool { return completedResponse != nil } /// A `Bool` indicating if the request has finished with an error. - public var didFinishUnsuccessfully: Bool { + public var didFail: Bool { return completedError != nil } }