#66 - Rebuild the AppCoordinator on top of a SwiftState FSM

This commit is contained in:
Stefan Ceriu
2022-05-27 13:02:36 +03:00
parent 7881ddc4c3
commit 1e7243fcf9
7 changed files with 230 additions and 26 deletions
+82 -26
View File
@@ -12,6 +12,8 @@ import Kingfisher
class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
private let window: UIWindow
private var stateMachine: AppCoordinatorStateMachine
private let mainNavigationController: UINavigationController
private let splashViewController: UIViewController
@@ -31,6 +33,8 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
var childCoordinators: [Coordinator] = []
init() {
stateMachine = AppCoordinatorStateMachine()
splashViewController = SplashViewController()
mainNavigationController = UINavigationController(rootViewController: splashViewController)
window = UIWindow(frame: UIScreen.main.bounds)
@@ -51,6 +55,8 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
navigationRouter: navigationRouter)
authenticationCoordinator.delegate = self
setupStateMachine()
let loggerConfiguration = MXLogConfiguration()
loggerConfiguration.logLevel = .verbose
MXLog.configure(loggerConfiguration)
@@ -59,22 +65,70 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
}
func start() {
window.makeKeyAndVisible()
authenticationCoordinator.start()
stateMachine.processEvent(.start)
}
// MARK: - AuthenticationCoordinatorDelegate
func authenticationCoordinatorDidStartLoading(_ authenticationCoordinator: AuthenticationCoordinator) {
showLoadingIndicator()
stateMachine.processEvent(.attemptedSignIn)
}
func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, didFailWithError error: AuthenticationCoordinatorError) {
hideLoadingIndicator()
showLoginErrorToast()
stateMachine.processEvent(.failedSigningIn)
}
func authenticationCoordinatorDidSetupClientProxy(_ authenticationCoordinator: AuthenticationCoordinator) {
stateMachine.processEvent(.succeededSigningIn)
}
func authenticationCoordinatorDidTearDownClientProxy(_ authenticationCoordinator: AuthenticationCoordinator) {
stateMachine.processEvent(.succeededSigningOut)
}
// MARK: - Private
// swiftlint:disable cyclomatic_complexity
private func setupStateMachine() {
stateMachine.addTransitionHandler { [weak self] context in
guard let self = self else { return }
switch (context.fromState, context.event, context.toState) {
case (.initial, .start, .signedOut):
self.window.makeKeyAndVisible()
self.authenticationCoordinator.start()
case (.signedOut, .attemptedSignIn, .signingIn):
self.showLoadingIndicator()
case (.signingIn, .failedSigningIn, .signedOut):
self.hideLoadingIndicator()
self.showLoginErrorToast()
case (.signingIn, .succeededSigningIn, .signedIn):
self.hideLoadingIndicator()
self.setupUserSession()
case (.signedIn, .showHomeScreen, .homeScreen):
self.presentHomeScreen()
case(_, _, .roomScreen(let roomId)):
self.presentRoomWithIdentifier(roomId)
case(.roomScreen, .dismissedRoomScreen, .homeScreen):
self.tearDownDismissedRoomScreen()
case (_, .attemptSignOut, .signingOut):
self.authenticationCoordinator.logout()
case (.signingOut, .succeededSigningOut, .signedOut):
self.tearDownUserSession()
case (.signingOut, .failedSigningOut, _):
self.showLogoutErrorToast()
default:
fatalError("Unknown transition: \(context)")
}
}
stateMachine.addErrorHandler { context in
fatalError("Failed transition with context: \(context)")
}
}
// swiftlint:enable cyclomatic_complexity
private func setupUserSession() {
guard let clientProxy = authenticationCoordinator.clientProxy else {
fatalError("User session should be setup at this point")
}
@@ -82,39 +136,34 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
userSession = .init(clientProxy: clientProxy,
mediaProvider: MediaProvider(clientProxy: clientProxy, imageCache: ImageCache.default))
presentHomeScreen()
stateMachine.processEvent(.showHomeScreen)
}
func authenticationCoordinatorDidTearDownClientProxy(_ authenticationCoordinator: AuthenticationCoordinator) {
private func tearDownUserSession() {
if let presentedCoordinator = childCoordinators.first {
remove(childCoordinator: presentedCoordinator)
}
userSession = nil
mainNavigationController.setViewControllers([splashViewController], animated: false)
authenticationCoordinator.start()
}
// MARK: - Private
private func presentHomeScreen() {
hideLoadingIndicator()
guard let userSession = userSession else {
fatalError("User session should be already setup at this point")
}
let parameters = HomeScreenCoordinatorParameters(userSession: userSession,
attributedStringBuilder: AttributedStringBuilder(),
memberDetailProviderManager: memberDetailProviderManager)
let coordinator = HomeScreenCoordinator(parameters: parameters)
coordinator.callback = { [weak self] action in
guard let self = self else { return }
switch action {
case .logout:
self?.authenticationCoordinator.logout()
self.stateMachine.processEvent(.attemptSignOut)
case .selectRoom(let roomIdentifier):
self?.presentRoomWithIdentifier(roomIdentifier)
self.stateMachine.processEvent(.showRoomScreen(roomId: roomIdentifier))
}
}
@@ -123,10 +172,6 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
}
private func presentRoomWithIdentifier(_ roomIdentifier: String) {
guard let userSession = userSession else {
fatalError("User session should be already setup at this point")
}
guard let roomProxy = userSession.clientProxy.rooms.first(where: { $0.id == roomIdentifier }) else {
MXLog.error("Invalid room identifier: \(roomIdentifier)")
return
@@ -147,14 +192,21 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
roomName: roomProxy.displayName ?? roomProxy.name)
let coordinator = RoomScreenCoordinator(parameters: parameters)
self.add(childCoordinator: coordinator)
self.navigationRouter.push(coordinator) { [weak self] in
add(childCoordinator: coordinator)
navigationRouter.push(coordinator) { [weak self] in
guard let self = self else { return }
self.remove(childCoordinator: coordinator)
self.stateMachine.processEvent(.dismissedRoomScreen)
}
}
private func tearDownDismissedRoomScreen() {
guard let coordinator = childCoordinators.last as? RoomScreenCoordinator else {
fatalError("Invalid coordinator hierarchy: \(childCoordinators)")
}
remove(childCoordinator: coordinator)
}
private func showLoadingIndicator() {
loadingIndicator = indicatorPresenter.present(.loading(label: "Loading", isInteractionBlocking: true))
}
@@ -166,4 +218,8 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
private func showLoginErrorToast() {
errorIndicator = indicatorPresenter.present(.success(label: "Failed logging in"))
}
private func showLogoutErrorToast() {
errorIndicator = indicatorPresenter.present(.success(label: "Failed logging out"))
}
}
@@ -0,0 +1,104 @@
//
// AppCoordinatorStateMachine.swift
// ElementX
//
// Created by Stefan Ceriu on 30/05/2022.
// Copyright © 2022 element.io. All rights reserved.
//
import Foundation
import SwiftState
class AppCoordinatorStateMachine {
/// States the AppCoordinator can find itself in
enum State: StateType {
/// The initial state, used before the AppCoordinator starts
case initial
/// Showing the login screen
case signedOut
/// Processing sign in request
case signingIn
/// Successfully signed in
case signedIn
/// Showing the home screen
case homeScreen
/// Showing a particular room's timeline
/// - Parameter roomId: that room's identifier
case roomScreen(roomId: String)
/// Processing a sign out request
case signingOut
}
/// Events that can be triggered on the AppCoordinator state machine
enum Event: EventType {
/// Start AppCoordinator flows, move from initial
case start
/// A sign in request has been started
case attemptedSignIn
/// Signing it succeeded
case succeededSigningIn
/// Signing in failed
case failedSigningIn
/// Request home screen presentation
case showHomeScreen
/// Request sign out
case attemptSignOut
/// Signing out succeeded
case succeededSigningOut
/// Signing out failed
case failedSigningOut
/// Request presentation for a particular room
/// - Parameter roomId:the room identifier
case showRoomScreen(roomId: String)
/// The room screen has been dismissed
case dismissedRoomScreen
}
private let stateMachine: StateMachine<State, Event>
init() {
stateMachine = StateMachine(state: .initial) { machine in
machine.addRoutes(event: .start, transitions: [ .initial => .signedOut ])
machine.addRoutes(event: .attemptedSignIn, transitions: [ .signedOut => .signingIn ])
machine.addRoutes(event: .succeededSigningIn, transitions: [ .signingIn => .signedIn ])
machine.addRoutes(event: .failedSigningIn, transitions: [ .signingIn => .signedOut ])
machine.addRoutes(event: .showHomeScreen, transitions: [ .signedIn => .homeScreen ])
machine.addRoutes(event: .attemptSignOut, transitions: [ .homeScreen => .signingOut ])
machine.addRoutes(event: .succeededSigningOut, transitions: [ .signingOut => .signedOut ])
machine.addRoutes(event: .failedSigningOut, transitions: [ .signingOut => .homeScreen ])
// Transitions with associated values need to be handled through `addRouteMapping`
machine.addRouteMapping { event, fromState, _ in
switch (event, fromState) {
case (.showRoomScreen(let roomId), .homeScreen):
return .roomScreen(roomId: roomId)
case (.dismissedRoomScreen, .roomScreen):
return .homeScreen
default:
return nil
}
}
}
}
/// Attempt to move the state machine to another state through an event
/// It will either invoke the `transitionHandler` or the `errorHandler` depending on its current state
func processEvent(_ event: Event) {
stateMachine.tryEvent(event)
}
/// Registers a callback for processing state machine transitions
func addTransitionHandler(_ handler: @escaping StateMachine<State, Event>.Handler) {
stateMachine.addAnyHandler(.any => .any, handler: handler)
}
/// Registers a callback for processing state machine errors
func addErrorHandler(_ handler: @escaping StateMachine<State, Event>.Handler) {
stateMachine.addErrorHandler(handler: handler)
}
}
+1
View File
@@ -87,6 +87,7 @@ targets:
- package: Kingfisher
- package: Introspect
- package: SwiftyBeaver
- package: SwiftState
sources:
- path: ../Sources