mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
#66 - Rebuild the AppCoordinator on top of a SwiftState FSM
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -87,6 +87,7 @@ targets:
|
||||
- package: Kingfisher
|
||||
- package: Introspect
|
||||
- package: SwiftyBeaver
|
||||
- package: SwiftState
|
||||
|
||||
sources:
|
||||
- path: ../Sources
|
||||
|
||||
Reference in New Issue
Block a user