Skip to content
Snippets Groups Projects
Commit cd58c0a6 authored by Dariusz Rybicki's avatar Dariusz Rybicki
Browse files

Monitor network health in HomeFeature

parent 5e4eec5a
No related branches found
No related tags found
2 merge requests!102Release 1.0.0,!63Messenger example - network status
......@@ -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
......
......@@ -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))
......
......@@ -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(),
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment