From cd58c0a6f853e0e0c4be3b8b0133ed9863524275 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Mon, 5 Sep 2022 16:46:13 +0200 Subject: [PATCH] Monitor network health in HomeFeature --- .../Sources/HomeFeature/HomeFeature.swift | 82 +++++++++++----- .../Sources/HomeFeature/HomeView.swift | 24 +++++ .../HomeFeatureTests/HomeFeatureTests.swift | 97 ++++++++++++++----- 3 files changed, 156 insertions(+), 47 deletions(-) diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift index 8116fde0..e054e6ac 100644 --- a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift +++ b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift @@ -10,20 +10,23 @@ import XXMessengerClient public struct HomeState: Equatable { public init( failure: String? = nil, - register: RegisterState? = nil, + isNetworkHealthy: Bool? = nil, + isDeletingAccount: Bool = false, alert: AlertState<HomeAction>? = nil, - isDeletingAccount: Bool = false + register: RegisterState? = nil ) { self.failure = failure - self.register = register - self.alert = alert + self.isNetworkHealthy = isNetworkHealthy self.isDeletingAccount = isDeletingAccount + self.alert = alert + self.register = register } public var failure: String? - public var register: RegisterState? - public var alert: AlertState<HomeAction>? + public var isNetworkHealthy: Bool? public var isDeletingAccount: Bool + public var alert: AlertState<HomeAction>? + public var register: RegisterState? } public enum HomeAction: Equatable { @@ -34,6 +37,12 @@ public enum HomeAction: Equatable { case failure(NSError) } + public enum NetworkMonitor: Equatable { + case start + case stop + case health(Bool) + } + public enum DeleteAccount: Equatable { case buttonTapped case confirmed @@ -42,6 +51,7 @@ public enum HomeAction: Equatable { } case messenger(Messenger) + case networkMonitor(NetworkMonitor) case deleteAccount(DeleteAccount) case didDismissAlert case didDismissRegister @@ -82,28 +92,33 @@ extension HomeEnvironment { public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment> { state, action, env in + enum NetworkHealthEffectId {} + switch action { case .messenger(.start): - return .result { - do { - try env.messenger.start() - - if env.messenger.isConnected() == false { - try env.messenger.connect() - } + return .merge( + Effect(value: .networkMonitor(.stop)), + Effect.result { + do { + try env.messenger.start() + + if env.messenger.isConnected() == false { + try env.messenger.connect() + } - if env.messenger.isLoggedIn() == false { - if try env.messenger.isRegistered() == false { - return .success(.messenger(.didStartUnregistered)) + if env.messenger.isLoggedIn() == false { + if try env.messenger.isRegistered() == false { + return .success(.messenger(.didStartUnregistered)) + } + try env.messenger.logIn() } - try env.messenger.logIn() - } - return .success(.messenger(.didStartRegistered)) - } catch { - return .success(.messenger(.failure(error as NSError))) + return .success(.messenger(.didStartRegistered)) + } catch { + return .success(.messenger(.failure(error as NSError))) + } } - } + ) .subscribe(on: env.bgQueue) .receive(on: env.mainQueue) .eraseToEffect() @@ -113,12 +128,33 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment> return .none case .messenger(.didStartRegistered): - return .none + return Effect(value: .networkMonitor(.start)) case .messenger(.failure(let error)): state.failure = error.localizedDescription return .none + case .networkMonitor(.start): + return .run { subscriber in + let callback = HealthCallback { isHealthy in + subscriber.send(.networkMonitor(.health(isHealthy))) + } + let cancellable = env.messenger.cMix()?.addHealthCallback(callback) + return AnyCancellable { cancellable?.cancel() } + } + .cancellable(id: NetworkHealthEffectId.self, cancelInFlight: true) + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + + case .networkMonitor(.stop): + state.isNetworkHealthy = nil + return .cancel(id: NetworkHealthEffectId.self) + + case .networkMonitor(.health(let isHealthy)): + state.isNetworkHealthy = isHealthy + return .none + case .deleteAccount(.buttonTapped): state.alert = .confirmAccountDeletion() return .none diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift index b2c9e660..d105e0bd 100644 --- a/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift +++ b/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift @@ -12,10 +12,12 @@ public struct HomeView: View { struct ViewState: Equatable { var failure: String? + var isNetworkHealthy: Bool? var isDeletingAccount: Bool init(state: HomeState) { failure = state.failure + isNetworkHealthy = state.isNetworkHealthy isDeletingAccount = state.isDeletingAccount } } @@ -37,6 +39,28 @@ public struct HomeView: View { } } + Section { + HStack { + Text("Health") + Spacer() + switch viewStore.isNetworkHealthy { + case .some(true): + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.green) + + case .some(false): + Image(systemName: "xmark.diamond.fill") + .foregroundColor(.red) + + case .none: + Image(systemName: "questionmark.circle") + .foregroundColor(.gray) + } + } + } header: { + Text("Network") + } + Section { Button(role: .destructive) { viewStore.send(.deleteAccount(.buttonTapped)) diff --git a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift index 60c7e305..97ac8900 100644 --- a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift +++ b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift @@ -14,13 +14,11 @@ final class HomeFeatureTests: XCTestCase { environment: .unimplemented ) - let bgQueue = DispatchQueue.test - let mainQueue = DispatchQueue.test var messengerDidStartWithTimeout: [Int] = [] var messengerDidConnect = 0 - store.environment.bgQueue = bgQueue.eraseToAnyScheduler() - store.environment.mainQueue = mainQueue.eraseToAnyScheduler() + store.environment.bgQueue = .immediate + store.environment.mainQueue = .immediate store.environment.messenger.start.run = { messengerDidStartWithTimeout.append($0) } store.environment.messenger.isConnected.run = { false } store.environment.messenger.connect.run = { messengerDidConnect += 1 } @@ -29,13 +27,10 @@ final class HomeFeatureTests: XCTestCase { store.send(.messenger(.start)) - bgQueue.advance() - XCTAssertNoDifference(messengerDidStartWithTimeout, [30_000]) XCTAssertNoDifference(messengerDidConnect, 1) - mainQueue.advance() - + store.receive(.networkMonitor(.stop)) store.receive(.messenger(.didStartUnregistered)) { $0.register = RegisterState() } @@ -48,32 +43,35 @@ final class HomeFeatureTests: XCTestCase { environment: .unimplemented ) - let bgQueue = DispatchQueue.test - let mainQueue = DispatchQueue.test var messengerDidStartWithTimeout: [Int] = [] var messengerDidConnect = 0 var messengerDidLogIn = 0 - store.environment.bgQueue = bgQueue.eraseToAnyScheduler() - store.environment.mainQueue = mainQueue.eraseToAnyScheduler() + store.environment.bgQueue = .immediate + store.environment.mainQueue = .immediate store.environment.messenger.start.run = { messengerDidStartWithTimeout.append($0) } store.environment.messenger.isConnected.run = { false } store.environment.messenger.connect.run = { messengerDidConnect += 1 } store.environment.messenger.isLoggedIn.run = { false } store.environment.messenger.isRegistered.run = { true } store.environment.messenger.logIn.run = { messengerDidLogIn += 1 } + store.environment.messenger.cMix.get = { + var cMix: CMix = .unimplemented + cMix.addHealthCallback.run = { _ in Cancellable {} } + return cMix + } store.send(.messenger(.start)) - bgQueue.advance() - XCTAssertNoDifference(messengerDidStartWithTimeout, [30_000]) XCTAssertNoDifference(messengerDidConnect, 1) XCTAssertNoDifference(messengerDidLogIn, 1) - mainQueue.advance() - + store.receive(.networkMonitor(.stop)) store.receive(.messenger(.didStartRegistered)) + store.receive(.networkMonitor(.start)) + + store.send(.networkMonitor(.stop)) } func testRegisterFinished() { @@ -85,18 +83,21 @@ final class HomeFeatureTests: XCTestCase { environment: .unimplemented ) - let bgQueue = DispatchQueue.test - let mainQueue = DispatchQueue.test var messengerDidStartWithTimeout: [Int] = [] var messengerDidLogIn = 0 - store.environment.bgQueue = bgQueue.eraseToAnyScheduler() - store.environment.mainQueue = mainQueue.eraseToAnyScheduler() + store.environment.bgQueue = .immediate + store.environment.mainQueue = .immediate store.environment.messenger.start.run = { messengerDidStartWithTimeout.append($0) } store.environment.messenger.isConnected.run = { true } store.environment.messenger.isLoggedIn.run = { false } store.environment.messenger.isRegistered.run = { true } store.environment.messenger.logIn.run = { messengerDidLogIn += 1 } + store.environment.messenger.cMix.get = { + var cMix: CMix = .unimplemented + cMix.addHealthCallback.run = { _ in Cancellable {} } + return cMix + } store.send(.register(.finished)) { $0.register = nil @@ -104,14 +105,14 @@ final class HomeFeatureTests: XCTestCase { store.receive(.messenger(.start)) - bgQueue.advance() - XCTAssertNoDifference(messengerDidStartWithTimeout, [30_000]) XCTAssertNoDifference(messengerDidLogIn, 1) - mainQueue.advance() - + store.receive(.networkMonitor(.stop)) store.receive(.messenger(.didStartRegistered)) + store.receive(.networkMonitor(.start)) + + store.send(.networkMonitor(.stop)) } func testMessengerStartFailure() { @@ -130,6 +131,7 @@ final class HomeFeatureTests: XCTestCase { store.send(.messenger(.start)) + store.receive(.networkMonitor(.stop)) store.receive(.messenger(.failure(error as NSError))) { $0.failure = error.localizedDescription } @@ -153,6 +155,7 @@ final class HomeFeatureTests: XCTestCase { store.send(.messenger(.start)) + store.receive(.networkMonitor(.stop)) store.receive(.messenger(.failure(error as NSError))) { $0.failure = error.localizedDescription } @@ -177,6 +180,7 @@ final class HomeFeatureTests: XCTestCase { store.send(.messenger(.start)) + store.receive(.networkMonitor(.stop)) store.receive(.messenger(.failure(error as NSError))) { $0.failure = error.localizedDescription } @@ -202,11 +206,56 @@ final class HomeFeatureTests: XCTestCase { store.send(.messenger(.start)) + store.receive(.networkMonitor(.stop)) store.receive(.messenger(.failure(error as NSError))) { $0.failure = error.localizedDescription } } + func testNetworkMonitorStart() { + let store = TestStore( + initialState: HomeState(), + reducer: homeReducer, + environment: .unimplemented + ) + + var cMixDidAddHealthCallback: [HealthCallback] = [] + var healthCallbackDidCancel = 0 + + store.environment.bgQueue = .immediate + store.environment.mainQueue = .immediate + store.environment.messenger.cMix.get = { + var cMix: CMix = .unimplemented + cMix.addHealthCallback.run = { callback in + cMixDidAddHealthCallback.append(callback) + return Cancellable { healthCallbackDidCancel += 1 } + } + return cMix + } + + store.send(.networkMonitor(.start)) + + XCTAssertNoDifference(cMixDidAddHealthCallback.count, 1) + + cMixDidAddHealthCallback.first?.handle(true) + + store.receive(.networkMonitor(.health(true))) { + $0.isNetworkHealthy = true + } + + cMixDidAddHealthCallback.first?.handle(false) + + store.receive(.networkMonitor(.health(false))) { + $0.isNetworkHealthy = false + } + + store.send(.networkMonitor(.stop)) { + $0.isNetworkHealthy = nil + } + + XCTAssertNoDifference(healthCallbackDidCancel, 1) + } + func testAccountDeletion() { let store = TestStore( initialState: HomeState(), -- GitLab