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

Start & stop network follower from SessionView

parent 503cba3f
No related branches found
No related tags found
1 merge request!11[Example App] Monitor network health
......@@ -135,10 +135,15 @@ let package = Package(
.target(
name: "SessionFeature",
dependencies: [
.target(name: "ErrorFeature"),
.product(
name: "ComposableArchitecture",
package: "swift-composable-architecture"
),
.product(
name: "ComposablePresentation",
package: "swift-composable-presentation"
),
.product(
name: "ElixxirDAppsSDK",
package: "elixxir-dapps-sdk-swift"
......
......@@ -42,7 +42,9 @@ extension AppEnvironment {
error: ErrorEnvironment()
),
session: SessionEnvironment(
getClient: { clientSubject.value }
getClient: { clientSubject.value },
bgScheduler: bgScheduler,
mainScheduler: mainScheduler
)
)
}
......
import Combine
import ComposableArchitecture
import ElixxirDAppsSDK
import ErrorFeature
public struct SessionState: Equatable {
public init(
id: UUID
id: UUID,
networkFollowerStatus: NetworkFollowerStatus? = nil,
error: ErrorState? = nil
) {
self.id = id
self.networkFollowerStatus = networkFollowerStatus
self.error = error
}
public var id: UUID
public var networkFollowerStatus: NetworkFollowerStatus?
public var error: ErrorState?
}
public enum SessionAction: Equatable {
case viewDidLoad
case updateNetworkFollowerStatus
case didUpdateNetworkFollowerStatus(NetworkFollowerStatus?)
case runNetworkFollower(Bool)
case networkFollowerDidFail(NSError)
case error(ErrorAction)
case didDismissError
}
public struct SessionEnvironment {
public init(
getClient: @escaping () -> Client?
getClient: @escaping () -> Client?,
bgScheduler: AnySchedulerOf<DispatchQueue>,
mainScheduler: AnySchedulerOf<DispatchQueue>
) {
self.getClient = getClient
self.bgScheduler = bgScheduler
self.mainScheduler = mainScheduler
}
public var getClient: () -> Client?
public var bgScheduler: AnySchedulerOf<DispatchQueue>
public var mainScheduler: AnySchedulerOf<DispatchQueue>
}
public let sessionReducer = Reducer<SessionState, SessionAction, SessionEnvironment>
{ state, action, env in
switch action {
case .viewDidLoad:
return .merge([
.init(value: .updateNetworkFollowerStatus),
])
case .updateNetworkFollowerStatus:
return Effect.future { fulfill in
let status = env.getClient()?.networkFollower.status()
fulfill(.success(.didUpdateNetworkFollowerStatus(status)))
}
.subscribe(on: env.bgScheduler)
.receive(on: env.mainScheduler)
.eraseToEffect()
case .didUpdateNetworkFollowerStatus(let status):
state.networkFollowerStatus = status
return .none
case .runNetworkFollower(let start):
state.networkFollowerStatus = start ? .starting : .stopping
return Effect.run { subscriber in
do {
if start {
try env.getClient()?.networkFollower.start(timeoutMS: 30_000)
} else {
try env.getClient()?.networkFollower.stop()
}
} catch {
subscriber.send(.networkFollowerDidFail(error as NSError))
}
let status = env.getClient()?.networkFollower.status()
subscriber.send(.didUpdateNetworkFollowerStatus(status))
subscriber.send(completion: .finished)
return AnyCancellable {}
}
.subscribe(on: env.bgScheduler)
.receive(on: env.mainScheduler)
.eraseToEffect()
case .networkFollowerDidFail(let error):
state.error = ErrorState(error: error)
return .none
case .didDismissError:
state.error = nil
return .none
}
}
......@@ -36,7 +100,9 @@ public let sessionReducer = Reducer<SessionState, SessionAction, SessionEnvironm
#if DEBUG
extension SessionEnvironment {
public static let failing = SessionEnvironment(
getClient: { .failing }
getClient: { .failing },
bgScheduler: .failing,
mainScheduler: .failing
)
}
#endif
import ComposableArchitecture
import ComposablePresentation
import ElixxirDAppsSDK
import ErrorFeature
import SwiftUI
public struct SessionView: View {
......@@ -9,16 +12,50 @@ public struct SessionView: View {
let store: Store<SessionState, SessionAction>
struct ViewState: Equatable {
init(state: SessionState) {}
let networkFollowerStatus: NetworkFollowerStatus?
init(state: SessionState) {
networkFollowerStatus = state.networkFollowerStatus
}
}
public var body: some View {
WithViewStore(store.scope(state: ViewState.init)) { viewStore in
Text("SessionView")
.navigationTitle("Session")
.task {
viewStore.send(.viewDidLoad)
Form {
Section {
NetworkFollowerStatusView(status: viewStore.networkFollowerStatus)
Button {
viewStore.send(.runNetworkFollower(true))
} label: {
Text("Start")
}
.disabled(viewStore.networkFollowerStatus != .stopped)
Button {
viewStore.send(.runNetworkFollower(false))
} label: {
Text("Stop")
}
.disabled(viewStore.networkFollowerStatus != .running)
} header: {
Text("Network follower")
}
}
.navigationTitle("Session")
.task {
viewStore.send(.viewDidLoad)
}
.sheet(
store.scope(
state: \.error,
action: SessionAction.error
),
onDismiss: {
viewStore.send(.didDismissError)
},
content: ErrorView.init(store:)
)
}
}
}
......
import ComposableArchitecture
import ElixxirDAppsSDK
import ErrorFeature
import XCTest
@testable import SessionFeature
final class SessionFeatureTests: XCTestCase {
func testViewDidLoad() throws {
func testViewDidLoad() {
var networkFollowerStatus: NetworkFollowerStatus!
let bgScheduler = DispatchQueue.test
let mainScheduler = DispatchQueue.test
var env = SessionEnvironment.failing
env.getClient = {
var client = Client.failing
client.networkFollower.status.status = { networkFollowerStatus }
return client
}
env.bgScheduler = bgScheduler.eraseToAnyScheduler()
env.mainScheduler = mainScheduler.eraseToAnyScheduler()
let store = TestStore(
initialState: SessionState(id: UUID()),
reducer: sessionReducer,
environment: .failing
environment: env
)
store.send(.viewDidLoad)
store.receive(.updateNetworkFollowerStatus)
networkFollowerStatus = .stopped
bgScheduler.advance()
mainScheduler.advance()
store.receive(.didUpdateNetworkFollowerStatus(.stopped)) {
$0.networkFollowerStatus = .stopped
}
}
func testStartStopNetworkFollower() {
var networkFollowerStatus: NetworkFollowerStatus!
var didStartNetworkFollowerWithTimeout = [Int]()
var didStopNetworkFollower = 0
var networkFollowerStartError: NSError?
let bgScheduler = DispatchQueue.test
let mainScheduler = DispatchQueue.test
var env = SessionEnvironment.failing
env.getClient = {
var client = Client.failing
client.networkFollower.status.status = {
networkFollowerStatus
}
client.networkFollower.start.start = {
didStartNetworkFollowerWithTimeout.append($0)
if let error = networkFollowerStartError {
throw error
}
}
client.networkFollower.stop.stop = {
didStopNetworkFollower += 1
}
return client
}
env.bgScheduler = bgScheduler.eraseToAnyScheduler()
env.mainScheduler = mainScheduler.eraseToAnyScheduler()
let store = TestStore(
initialState: SessionState(id: UUID()),
reducer: sessionReducer,
environment: env
)
store.send(.runNetworkFollower(true)) {
$0.networkFollowerStatus = .starting
}
networkFollowerStatus = .running
bgScheduler.advance()
mainScheduler.advance()
XCTAssertEqual(didStartNetworkFollowerWithTimeout, [30_000])
XCTAssertEqual(didStopNetworkFollower, 0)
store.receive(.didUpdateNetworkFollowerStatus(.running)) {
$0.networkFollowerStatus = .running
}
store.send(.runNetworkFollower(false)) {
$0.networkFollowerStatus = .stopping
}
networkFollowerStatus = .stopped
bgScheduler.advance()
mainScheduler.advance()
XCTAssertEqual(didStartNetworkFollowerWithTimeout, [30_000])
XCTAssertEqual(didStopNetworkFollower, 1)
store.receive(.didUpdateNetworkFollowerStatus(.stopped)) {
$0.networkFollowerStatus = .stopped
}
store.send(.runNetworkFollower(true)) {
$0.networkFollowerStatus = .starting
}
networkFollowerStartError = NSError(domain: "test", code: 1234)
networkFollowerStatus = .stopped
bgScheduler.advance()
mainScheduler.advance()
XCTAssertEqual(didStartNetworkFollowerWithTimeout, [30_000, 30_000])
XCTAssertEqual(didStopNetworkFollower, 1)
store.receive(.networkFollowerDidFail(networkFollowerStartError!)) {
$0.error = ErrorState(error: networkFollowerStartError!)
}
store.receive(.didUpdateNetworkFollowerStatus(.stopped)) {
$0.networkFollowerStatus = .stopped
}
store.send(.didDismissError) {
$0.error = nil
}
}
}
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