Fixes #317 - Adopt a split layout for iPad and Mac apps

Rename navigation components: SplitScreenCoordinator -> NavigationSplitCoordinator, StackScreenCoordinator -> NavigationStackCoordinator and SingleScreenCoordinator -> NavigationRootCoordinator
[0c161039] Tweak navigation logging
[826c19cf] Move the navigation dismissal callbacks to the NavigationModule, add SingleScreenCoordinator tests
[b8830d9c] Add tests
[252ad119] Merge the StackScreenCoordinator and SplitScreenCoordinators into a single file and stop publicly exposing their internal workings. Add more documentation.
[37671699] Cleanup navigation logging
[51406184] Use the parent SplitScreenCoordinator to present embedded StackScreenCoordinator sheets
[b94b04c9] Retract the room "syncing" indicator when dismissing a room
[1467b0ac] Correctly move to the no room selected state when popping in compact layouts
[10bf2ad8] Allow nilling root coordinators, replace present/dismiss sheet with setSheetCoordinator(?)
[33716784] Add single screen coordinator fade transition animation
[3cbe65e7] Prevent the timeline table view from being reused between different rooms
[9c94c50b] Move files around
[c10b6bc5] Adapt the user session state machine to the split layout
[7115a319] Fix unit and UI tests
[1ece59e8] Fix login flows
[6884dc3b] Use modules everywhere the underlying object is a NavigationModule
[ab08d44c] Rename navigation components to: SingleScreenCoordinator, SplitScreenCoordinator and StackScreenCoordinator
[ada2be57] Add SplitNavigationController

* Remove the navigationRootCoordinator from the UserSessionFlowCoordinator
This commit is contained in:
Stefan Ceriu
2022-12-12 12:31:27 +02:00
committed by GitHub
parent 5ef53e9220
commit cd40da6c3f
22 changed files with 1407 additions and 541 deletions
@@ -24,18 +24,26 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
private let stateMachine: UserSessionFlowCoordinatorStateMachine
private let userSession: UserSessionProtocol
private let navigationController: NavigationController
private let navigationSplitCoordinator: NavigationSplitCoordinator
private let bugReportService: BugReportServiceProtocol
private let emojiProvider: EmojiProviderProtocol = EmojiProvider()
private let sidebarNavigationStackCoordinator: NavigationStackCoordinator
private let detailNavigationStackCoordinator: NavigationStackCoordinator
var callback: ((UserSessionFlowCoordinatorAction) -> Void)?
init(userSession: UserSessionProtocol, navigationController: NavigationController, bugReportService: BugReportServiceProtocol) {
init(userSession: UserSessionProtocol, navigationSplitCoordinator: NavigationSplitCoordinator, bugReportService: BugReportServiceProtocol) {
stateMachine = UserSessionFlowCoordinatorStateMachine()
self.userSession = userSession
self.navigationController = navigationController
self.navigationSplitCoordinator = navigationSplitCoordinator
self.bugReportService = bugReportService
sidebarNavigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator)
detailNavigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator)
navigationSplitCoordinator.setSidebarCoordinator(sidebarNavigationStackCoordinator)
setupStateMachine()
}
@@ -50,7 +58,7 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
}
func tryDisplayingRoomScreen(roomId: String) {
stateMachine.processEvent(.showRoomScreen(roomId: roomId))
stateMachine.processEvent(.selectRoom(roomId: roomId))
}
// MARK: - Private
@@ -61,27 +69,32 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
guard let self else { return }
switch (context.fromState, context.event, context.toState) {
case (.initial, .start, .homeScreen):
case (.initial, .start, .roomList):
self.presentHomeScreen()
case(.homeScreen, .showRoomScreen, .roomScreen(let roomId)):
self.presentRoomWithIdentifier(roomId)
case(.roomScreen, .dismissedRoomScreen, .homeScreen):
break
case(.roomList(let currentRoomId), .selectRoom, .roomList(let selectedRoomId)):
guard let selectedRoomId,
selectedRoomId != currentRoomId else {
return
}
case (.homeScreen, .showSessionVerificationScreen, .sessionVerificationScreen):
self.presentRoomWithIdentifier(selectedRoomId)
case(.roomList, .deselectRoom, .roomList):
break
case (.roomList, .showSessionVerificationScreen, .sessionVerificationScreen):
self.presentSessionVerification()
case (.sessionVerificationScreen, .dismissedSessionVerificationScreen, .homeScreen):
case (.sessionVerificationScreen, .dismissedSessionVerificationScreen, .roomList):
break
case (.homeScreen, .showSettingsScreen, .settingsScreen):
case (.roomList, .showSettingsScreen, .settingsScreen):
self.presentSettingsScreen()
case (.settingsScreen, .dismissedSettingsScreen, .homeScreen):
case (.settingsScreen, .dismissedSettingsScreen, .roomList):
break
case (.homeScreen, .feedbackScreen, .feedbackScreen):
case (.roomList, .feedbackScreen, .feedbackScreen):
self.presentFeedbackScreen()
case (.feedbackScreen, .dismissedFeedbackScreen, .homeScreen):
case (.feedbackScreen, .dismissedFeedbackScreen, .roomList):
break
default:
@@ -100,7 +113,7 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
let parameters = HomeScreenCoordinatorParameters(userSession: userSession,
attributedStringBuilder: AttributedStringBuilder(),
bugReportService: bugReportService,
navigationController: navigationController)
navigationStackCoordinator: detailNavigationStackCoordinator)
let coordinator = HomeScreenCoordinator(parameters: parameters)
coordinator.callback = { [weak self] action in
@@ -108,7 +121,7 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
switch action {
case .presentRoomScreen(let roomIdentifier):
self.stateMachine.processEvent(.showRoomScreen(roomId: roomIdentifier))
self.stateMachine.processEvent(.selectRoom(roomId: roomIdentifier))
case .presentSettingsScreen:
self.stateMachine.processEvent(.showSettingsScreen)
case .presentFeedbackScreen:
@@ -119,8 +132,8 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
self.callback?(.signOut)
}
}
navigationController.setRootCoordinator(coordinator)
sidebarNavigationStackCoordinator.setRootCoordinator(coordinator)
}
// MARK: Rooms
@@ -145,7 +158,7 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
mediaProvider: userSession.mediaProvider,
roomProxy: roomProxy)
let parameters = RoomScreenCoordinatorParameters(navigationController: navigationController,
let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: detailNavigationStackCoordinator,
timelineController: timelineController,
mediaProvider: userSession.mediaProvider,
roomName: roomProxy.displayName ?? roomProxy.name,
@@ -153,9 +166,17 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
emojiProvider: emojiProvider)
let coordinator = RoomScreenCoordinator(parameters: parameters)
navigationController.push(coordinator) { [weak self] in
detailNavigationStackCoordinator.setRootCoordinator(coordinator)
navigationSplitCoordinator.setDetailCoordinator(detailNavigationStackCoordinator) { [weak self, roomIdentifier] in
guard let self else { return }
self.stateMachine.processEvent(.dismissedRoomScreen)
// Move the state machine to no room selected if the room currently being dimissed
// is the same as the one selected in the state machine.
// This generally happens when popping the room screen while in a compact layout
if case let .roomList(selectedRoomId) = self.stateMachine.state, selectedRoomId == roomIdentifier {
self.stateMachine.processEvent(.deselectRoom)
self.detailNavigationStackCoordinator.setRootCoordinator(nil)
}
}
}
}
@@ -163,11 +184,11 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
// MARK: Settings
private func presentSettingsScreen() {
let settingsNavigationController = NavigationController()
let settingsNavigationStackCoordinator = NavigationStackCoordinator()
let userNotificationController = UserNotificationController(rootCoordinator: settingsNavigationController)
let userNotificationController = UserNotificationController(rootCoordinator: settingsNavigationStackCoordinator)
let parameters = SettingsCoordinatorParameters(navigationController: settingsNavigationController,
let parameters = SettingsCoordinatorParameters(navigationStackCoordinator: settingsNavigationStackCoordinator,
userNotificationController: userNotificationController,
userSession: userSession,
bugReportService: bugReportService)
@@ -176,16 +197,16 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
guard let self else { return }
switch action {
case .dismiss:
self.navigationController.dismissSheet()
self.navigationSplitCoordinator.setSheetCoordinator(nil)
case .logout:
self.navigationController.dismissSheet()
self.navigationSplitCoordinator.setSheetCoordinator(nil)
self.callback?(.signOut)
}
}
settingsNavigationController.setRootCoordinator(settingsCoordinator)
settingsNavigationStackCoordinator.setRootCoordinator(settingsCoordinator)
navigationController.presentSheet(userNotificationController) { [weak self] in
navigationSplitCoordinator.setSheetCoordinator(userNotificationController) { [weak self] in
self?.stateMachine.processEvent(.dismissedSettingsScreen)
}
}
@@ -202,10 +223,10 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
let coordinator = SessionVerificationCoordinator(parameters: parameters)
coordinator.callback = { [weak self] in
self?.navigationController.dismissSheet()
self?.navigationSplitCoordinator.setSheetCoordinator(nil)
}
navigationController.presentSheet(coordinator) { [weak self] in
navigationSplitCoordinator.setSheetCoordinator(coordinator) { [weak self] in
self?.stateMachine.processEvent(.dismissedSessionVerificationScreen)
}
}
@@ -213,9 +234,9 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
// MARK: Bug reporting
private func presentFeedbackScreen(for image: UIImage? = nil) {
let feedbackNavigationController = NavigationController()
let feedbackNavigationStackCoordinator = NavigationStackCoordinator()
let userNotificationController = UserNotificationController(rootCoordinator: feedbackNavigationController)
let userNotificationController = UserNotificationController(rootCoordinator: feedbackNavigationStackCoordinator)
let parameters = BugReportCoordinatorParameters(bugReportService: bugReportService,
userNotificationController: userNotificationController,
@@ -223,12 +244,12 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
isModallyPresented: true)
let coordinator = BugReportCoordinator(parameters: parameters)
coordinator.completion = { [weak self] _ in
self?.navigationController.dismissSheet()
self?.navigationSplitCoordinator.setSheetCoordinator(nil)
}
feedbackNavigationController.setRootCoordinator(coordinator)
feedbackNavigationStackCoordinator.setRootCoordinator(coordinator)
navigationController.presentSheet(userNotificationController) { [weak self] in
navigationSplitCoordinator.setSheetCoordinator(userNotificationController) { [weak self] in
self?.stateMachine.processEvent(.dismissedFeedbackScreen)
}
}
@@ -24,20 +24,16 @@ class UserSessionFlowCoordinatorStateMachine {
case initial
/// Showing the home screen
case homeScreen
/// Showing a particular room's timeline
/// - Parameter roomId: that room's identifier
case roomScreen(roomId: String)
case roomList(selectedRoomId: String?)
/// Showing the session verification flows
case sessionVerificationScreen(selectedRoomId: String?)
/// Showing the session verification flows
case sessionVerificationScreen
/// Showing the session verification flows
case feedbackScreen
case feedbackScreen(selectedRoomId: String?)
/// Showing the settings screen
case settingsScreen
case settingsScreen(selectedRoomId: String?)
}
/// Events that can be triggered on the AppCoordinator state machine
@@ -47,9 +43,9 @@ class UserSessionFlowCoordinatorStateMachine {
/// Request presentation for a particular room
/// - Parameter roomId:the room identifier
case showRoomScreen(roomId: String)
case selectRoom(roomId: String)
/// The room screen has been dismissed
case dismissedRoomScreen
case deselectRoom
/// Request presentation of the settings screen
case showSettingsScreen
@@ -69,35 +65,39 @@ class UserSessionFlowCoordinatorStateMachine {
private let stateMachine: StateMachine<State, Event>
var state: UserSessionFlowCoordinatorStateMachine.State {
stateMachine.state
}
init() {
stateMachine = StateMachine(state: .initial)
configure()
}
private func configure() {
stateMachine.addRoutes(event: .start, transitions: [.initial => .homeScreen])
stateMachine.addRoutes(event: .start, transitions: [.initial => .roomList(selectedRoomId: nil)])
stateMachine.addRouteMapping { event, fromState, _ in
switch (event, fromState) {
case (.showRoomScreen(let roomId), .homeScreen):
return .roomScreen(roomId: roomId)
case (.dismissedRoomScreen, .roomScreen):
return .homeScreen
case (.selectRoom(let roomId), .roomList):
return .roomList(selectedRoomId: roomId)
case (.deselectRoom, .roomList):
return .roomList(selectedRoomId: nil)
case (.showSettingsScreen, .homeScreen):
return .settingsScreen
case (.dismissedSettingsScreen, .settingsScreen):
return .homeScreen
case (.showSettingsScreen, .roomList(let selectedRoomId)):
return .settingsScreen(selectedRoomId: selectedRoomId)
case (.dismissedSettingsScreen, .settingsScreen(let selectedRoomId)):
return .roomList(selectedRoomId: selectedRoomId)
case (.feedbackScreen, .homeScreen):
return .feedbackScreen
case (.dismissedFeedbackScreen, .feedbackScreen):
return .homeScreen
case (.feedbackScreen, .roomList(let selectedRoomId)):
return .feedbackScreen(selectedRoomId: selectedRoomId)
case (.dismissedFeedbackScreen, .feedbackScreen(let selectedRoomId)):
return .roomList(selectedRoomId: selectedRoomId)
case (.showSessionVerificationScreen, .homeScreen):
return .sessionVerificationScreen
case (.dismissedSessionVerificationScreen, .sessionVerificationScreen):
return .homeScreen
case (.showSessionVerificationScreen, .roomList(let selectedRoomId)):
return .sessionVerificationScreen(selectedRoomId: selectedRoomId)
case (.dismissedSessionVerificationScreen, .sessionVerificationScreen(let selectedRoomId)):
return .roomList(selectedRoomId: selectedRoomId)
default:
return nil
@@ -132,8 +132,8 @@ class UserSessionFlowCoordinatorStateMachine {
/// Flag indicating the machine is displaying room screen with given room identifier
func isDisplayingRoomScreen(withRoomId roomId: String) -> Bool {
switch stateMachine.state {
case .roomScreen(let displayedRoomId):
return roomId == displayedRoomId
case .roomList(let selectedRoomId):
return roomId == selectedRoomId
default:
return false
}