Skip to content

Commit

Permalink
Merge pull request #9 from Lickability/feature/add-request-state-cont…
Browse files Browse the repository at this point in the history
…roller

Adds Convenience RequestStateController
  • Loading branch information
Twigz authored Aug 30, 2022
2 parents 70ca4b6 + 2b22feb commit 0ed53ac
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 0 deletions.
8 changes: 8 additions & 0 deletions Networking.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -41,6 +43,8 @@
4C2CBFAB24D4AA9800920BE2 /* NetworkRequestPerformer+JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkRequestPerformer+JSON.swift"; sourceTree = "<group>"; };
F211601824884A4D00B48D52 /* PhotoRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoRequest.swift; sourceTree = "<group>"; };
F211601A24884ABF00B48D52 /* Photo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Photo.swift; sourceTree = "<group>"; };
F264BB4D28BE9D6500969CD1 /* NetworkRequestStateController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkRequestStateController.swift; sourceTree = "<group>"; };
F264BB4F28BE9E1700969CD1 /* Publisher+Result.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publisher+Result.swift"; sourceTree = "<group>"; };
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 = "<group>"; };
F2B5BC28248836C500B6A52A /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -100,6 +104,8 @@
F2B5BC54248837B400B6A52A /* NetworkRequestPerformer.swift */,
F2B5BC56248837CB00B6A52A /* NetworkController.swift */,
4C2CBFAB24D4AA9800920BE2 /* NetworkRequestPerformer+JSON.swift */,
F264BB4D28BE9D6500969CD1 /* NetworkRequestStateController.swift */,
F264BB4F28BE9E1700969CD1 /* Publisher+Result.swift */,
);
path = Networking;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down
116 changes: 116 additions & 0 deletions Sources/Networking/NetworkRequestStateController.swift
Original file line number Diff line number Diff line change
@@ -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<NetworkResponse, NetworkError>)

/// 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 didSucceed: Bool {
return completedResponse != nil
}

/// A `Bool` indicating if the request has finished with an error.
public var didFail: 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<NetworkRequestState, Never> = {
return requestStatePublisher.prepend(.notInProgress).eraseToAnyPublisher()
}()

private let requestPerformer: NetworkRequestPerformer
private let requestStatePublisher = PassthroughSubject<NetworkRequestState, Never>()
private var cancellables = Set<AnyCancellable>()

/// 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)
}
}
22 changes: 22 additions & 0 deletions Sources/Networking/Publisher+Result.swift
Original file line number Diff line number Diff line change
@@ -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 `<Output/Failure>.
/// - Returns: A `Publisher` that has converted the `Output` or `Failure` into a `Result`.
public func mapToResult() -> AnyPublisher<Result<Output, Failure>, Never> {
map(Result.success)
.catch { Just(.failure($0)) }
.eraseToAnyPublisher()
}
}

0 comments on commit 0ed53ac

Please sign in to comment.