diff --git a/Example/example-app/Sources/SessionFeature/SessionFeature.swift b/Example/example-app/Sources/SessionFeature/SessionFeature.swift index b07a14cc9dacdb1745beb9bb1e08c437ed45949a..0756f738008e1ea1870ffd1a11a600fccab5e0ca 100644 --- a/Example/example-app/Sources/SessionFeature/SessionFeature.swift +++ b/Example/example-app/Sources/SessionFeature/SessionFeature.swift @@ -7,15 +7,18 @@ public struct SessionState: Equatable { public init( id: UUID, networkFollowerStatus: NetworkFollowerStatus? = nil, + isNetworkHealthy: Bool? = nil, error: ErrorState? = nil ) { self.id = id self.networkFollowerStatus = networkFollowerStatus + self.isNetworkHealthy = isNetworkHealthy self.error = error } public var id: UUID public var networkFollowerStatus: NetworkFollowerStatus? + public var isNetworkHealthy: Bool? public var error: ErrorState? } @@ -25,8 +28,10 @@ public enum SessionAction: Equatable { case didUpdateNetworkFollowerStatus(NetworkFollowerStatus?) case runNetworkFollower(Bool) case networkFollowerDidFail(NSError) - case error(ErrorAction) + case monitorNetworkHealth(Bool) + case didUpdateNetworkHealth(Bool?) case didDismissError + case error(ErrorAction) } public struct SessionEnvironment { @@ -51,6 +56,7 @@ public let sessionReducer = Reducer<SessionState, SessionAction, SessionEnvironm case .viewDidLoad: return .merge([ .init(value: .updateNetworkFollowerStatus), + .init(value: .monitorNetworkHealth(true)), ]) case .updateNetworkFollowerStatus: @@ -91,9 +97,40 @@ public let sessionReducer = Reducer<SessionState, SessionAction, SessionEnvironm state.error = ErrorState(error: error) return .none + case .monitorNetworkHealth(let start): + struct MonitorEffectId: Hashable { + var id: UUID + } + let effectId = MonitorEffectId(id: state.id) + if start { + return Effect.run { subscriber in + var cancellable = env.getClient()?.monitorNetworkHealth { isHealthy in + subscriber.send(.didUpdateNetworkHealth(isHealthy)) + } + return AnyCancellable { + cancellable?.cancel() + } + } + .subscribe(on: env.bgScheduler) + .receive(on: env.mainScheduler) + .eraseToEffect() + .cancellable(id: effectId, cancelInFlight: true) + } else { + return Effect.cancel(id: effectId) + .subscribe(on: env.bgScheduler) + .eraseToEffect() + } + + case .didUpdateNetworkHealth(let isHealthy): + state.isNetworkHealthy = isHealthy + return .none + case .didDismissError: state.error = nil return .none + + case .error(_): + return .none } } diff --git a/Example/example-app/Sources/SessionFeature/SessionView.swift b/Example/example-app/Sources/SessionFeature/SessionView.swift index 74b4d130a242895215b29533679b3fa6d127f6b4..395cfcb160067fedc31ec3ddd5200aa6960864e2 100644 --- a/Example/example-app/Sources/SessionFeature/SessionView.swift +++ b/Example/example-app/Sources/SessionFeature/SessionView.swift @@ -13,9 +13,11 @@ public struct SessionView: View { struct ViewState: Equatable { let networkFollowerStatus: NetworkFollowerStatus? + let isNetworkHealthy: Bool? init(state: SessionState) { networkFollowerStatus = state.networkFollowerStatus + isNetworkHealthy = state.isNetworkHealthy } } @@ -41,6 +43,12 @@ public struct SessionView: View { } header: { Text("Network follower") } + + Section { + NetworkHealthStatusView(status: viewStore.isNetworkHealthy) + } header: { + Text("Network health") + } } .navigationTitle("Session") .task { diff --git a/Example/example-app/Tests/SessionFeatureTests/SessionFeatureTests.swift b/Example/example-app/Tests/SessionFeatureTests/SessionFeatureTests.swift index 092573edced9b499d28433ad51b5a1ecdb8890ae..2ada840d7d9d02bdb1c8732836502ad15d15be33 100644 --- a/Example/example-app/Tests/SessionFeatureTests/SessionFeatureTests.swift +++ b/Example/example-app/Tests/SessionFeatureTests/SessionFeatureTests.swift @@ -7,6 +7,9 @@ import XCTest final class SessionFeatureTests: XCTestCase { func testViewDidLoad() { var networkFollowerStatus: NetworkFollowerStatus! + var didStartMonitoringNetworkHealth = 0 + var didStopMonitoringNetworkHealth = 0 + var networkHealthCallback: ((Bool) -> Void)! let bgScheduler = DispatchQueue.test let mainScheduler = DispatchQueue.test @@ -14,6 +17,13 @@ final class SessionFeatureTests: XCTestCase { env.getClient = { var client = Client.failing client.networkFollower.status.status = { networkFollowerStatus } + client.monitorNetworkHealth.listen = { callback in + networkHealthCallback = callback + didStartMonitoringNetworkHealth += 1 + return Cancellable { + didStopMonitoringNetworkHealth += 1 + } + } return client } env.bgScheduler = bgScheduler.eraseToAnyScheduler() @@ -28,6 +38,7 @@ final class SessionFeatureTests: XCTestCase { store.send(.viewDidLoad) store.receive(.updateNetworkFollowerStatus) + store.receive(.monitorNetworkHealth(true)) networkFollowerStatus = .stopped bgScheduler.advance() @@ -36,6 +47,24 @@ final class SessionFeatureTests: XCTestCase { store.receive(.didUpdateNetworkFollowerStatus(.stopped)) { $0.networkFollowerStatus = .stopped } + + XCTAssertEqual(didStartMonitoringNetworkHealth, 1) + XCTAssertEqual(didStopMonitoringNetworkHealth, 0) + + networkHealthCallback(true) + bgScheduler.advance() + mainScheduler.advance() + + store.receive(.didUpdateNetworkHealth(true)) { + $0.isNetworkHealthy = true + } + + store.send(.monitorNetworkHealth(false)) + + bgScheduler.advance() + + XCTAssertEqual(didStartMonitoringNetworkHealth, 1) + XCTAssertEqual(didStopMonitoringNetworkHealth, 1) } func testStartStopNetworkFollower() {