Fix state machine crashes & background tasks (#343)

* Fix `UIApplication.shared` after moving to SwiftUI app

* Do not autoplay videos on background

* Move app state changes into the app coordinator

* Add application background task, move into the suspended state more accurately

* Add changelog

* Fix most of the linter errors

* Strip suspended state from state machine

* Fix build

* Clear audio session warning

* Update AppCoordinator.swift

* Update AppCoordinator.swift

* Swift format
This commit is contained in:
ismailgulek
2022-11-28 18:42:49 +03:00
committed by GitHub
parent bd530333df
commit cb5db22b7f
10 changed files with 131 additions and 97 deletions
@@ -22,6 +22,14 @@ class AppCoordinator: AppCoordinatorProtocol {
private let stateMachine: AppCoordinatorStateMachine
private let navigationController: NavigationController
private let userSessionStore: UserSessionStoreProtocol
/// Common background task to resume long-running tasks in the background.
/// When this task expiring, we'll try to suspend the state machine by `suspend` event.
private var backgroundTask: BackgroundTaskProtocol?
private var isSuspended = false {
didSet {
MXLog.debug("didSet to: \(isSuspended)")
}
}
private var userSession: UserSessionProtocol! {
didSet {
@@ -52,7 +60,9 @@ class AppCoordinator: AppCoordinatorProtocol {
ServiceLocator.shared.register(userNotificationController: UserNotificationController(rootCoordinator: navigationController))
backgroundTaskService = UIKitBackgroundTaskService(withApplication: UIApplication.shared)
backgroundTaskService = UIKitBackgroundTaskService {
UIApplication.shared
}
userSessionStore = UserSessionStore(backgroundTaskService: backgroundTaskService)
@@ -61,6 +71,8 @@ class AppCoordinator: AppCoordinatorProtocol {
setupLogging()
Bundle.elementFallbackLanguage = "en"
startObservingApplicationState()
// Benchmark.trackingEnabled = true
}
@@ -324,6 +336,52 @@ class AppCoordinator: AppCoordinatorProtocol {
private func showLoginErrorToast() {
ServiceLocator.shared.userNotificationController.submitNotification(UserNotification(title: "Failed logging in"))
}
// MARK: - Application State
private func pause() {
userSession?.clientProxy.stopSync()
}
private func resume() {
userSession?.clientProxy.startSync()
}
private func startObservingApplicationState() {
NotificationCenter.default.addObserver(self,
selector: #selector(applicationWillResignActive),
name: UIApplication.willResignActiveNotification,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(applicationDidBecomeActive),
name: UIApplication.didBecomeActiveNotification,
object: nil)
}
@objc
private func applicationWillResignActive() {
guard backgroundTask == nil else {
return
}
backgroundTask = backgroundTaskService.startBackgroundTask(withName: "SuspendApp: \(UUID().uuidString)") { [weak self] in
self?.pause()
self?.backgroundTask = nil
self?.isSuspended = true
}
}
@objc
private func applicationDidBecomeActive() {
backgroundTask?.stop()
backgroundTask = nil
if isSuspended {
isSuspended = false
resume()
}
}
}
// MARK: - AuthenticationCoordinatorDelegate
@@ -62,27 +62,38 @@ class AppCoordinatorStateMachine {
private let stateMachine: StateMachine<State, Event>
init() {
stateMachine = StateMachine(state: .initial) { machine in
machine.addRoutes(event: .startWithAuthentication, transitions: [.initial => .signedOut])
machine.addRoutes(event: .succeededSigningIn, transitions: [.signedOut => .signedIn])
machine.addRoutes(event: .startWithExistingSession, transitions: [.initial => .restoringSession])
machine.addRoutes(event: .succeededRestoringSession, transitions: [.restoringSession => .signedIn])
machine.addRoutes(event: .failedRestoringSession, transitions: [.restoringSession => .signedOut])
machine.addRoutes(event: .signOut, transitions: [.any => .signingOut])
machine.addRoutes(event: .completedSigningOut, transitions: [.signingOut => .signedOut])
// Transitions with associated values need to be handled through `addRouteMapping`
machine.addRouteMapping { event, fromState, _ in
switch (event, fromState) {
case (.remoteSignOut(let isSoft), _):
return .remoteSigningOut(isSoft: isSoft)
case (.completedSigningOut, .remoteSigningOut):
return .signedOut
default:
return nil
}
stateMachine = StateMachine(state: .initial)
configure()
}
private func configure() {
stateMachine.addRoutes(event: .startWithAuthentication, transitions: [.initial => .signedOut])
stateMachine.addRoutes(event: .succeededSigningIn, transitions: [.signedOut => .signedIn])
stateMachine.addRoutes(event: .startWithExistingSession, transitions: [.initial => .restoringSession])
stateMachine.addRoutes(event: .succeededRestoringSession, transitions: [.restoringSession => .signedIn])
stateMachine.addRoutes(event: .failedRestoringSession, transitions: [.restoringSession => .signedOut])
stateMachine.addRoutes(event: .signOut, transitions: [.any => .signingOut])
stateMachine.addRoutes(event: .completedSigningOut, transitions: [.signingOut => .signedOut])
// Transitions with associated values need to be handled through `addRouteMapping`
stateMachine.addRouteMapping { event, fromState, _ in
switch (event, fromState) {
case (.remoteSignOut(let isSoft), _):
return .remoteSigningOut(isSoft: isSoft)
case (.completedSigningOut, .remoteSigningOut):
return .signedOut
default:
return nil
}
}
addTransitionHandler { context in
if let event = context.event {
MXLog.info("Transitioning from `\(context.fromState)` to `\(context.toState)` with event `\(event)`")
} else {
MXLog.info("Transitioning from \(context.fromState)` to `\(context.toState)`")
}
}
}