SwiftUI NavigationController and UserNotificationControllers (#309)

* Fixes #286 - Adopted the new SwiftUI NavigationStack based NavigationController throughout the application
* Fixes #315 - Implemented new user notification components on top of SwiftUI and the new navigation flows
* Add home screen fade animation between skeletons and real rooms
* Bump the danger-swift version used on the CI and swiftlint with it
* Renamed Splash to Onboarding, Empty to Splash
This commit is contained in:
Stefan Ceriu
2022-11-16 15:37:34 +02:00
committed by GitHub
parent af85c770da
commit 2fd0491a18
108 changed files with 1627 additions and 3720 deletions
@@ -27,18 +27,18 @@ enum MockServerSelectionScreenState: CaseIterable {
switch self {
case .matrix:
return ServerSelectionViewModel(homeserverAddress: "https://matrix.org",
hasModalPresentation: true)
isModallyPresented: true)
case .emptyAddress:
return ServerSelectionViewModel(homeserverAddress: "",
hasModalPresentation: true)
isModallyPresented: true)
case .invalidAddress:
let viewModel = ServerSelectionViewModel(homeserverAddress: "thisisbad",
hasModalPresentation: true)
isModallyPresented: true)
viewModel.displayError(.footerMessage(ElementL10n.unknownError))
return viewModel
case .nonModal:
return ServerSelectionViewModel(homeserverAddress: "https://matrix.org",
hasModalPresentation: false)
isModallyPresented: false)
}
}
}
@@ -19,8 +19,9 @@ import SwiftUI
struct ServerSelectionCoordinatorParameters {
/// The service used to authenticate the user.
let authenticationService: AuthenticationServiceProxyProtocol
let userNotificationController: UserNotificationControllerProtocol
/// Whether the screen is presented modally or within a navigation stack.
let hasModalPresentation: Bool
let isModallyPresented: Bool
}
enum ServerSelectionCoordinatorAction {
@@ -28,45 +29,25 @@ enum ServerSelectionCoordinatorAction {
case dismiss
}
final class ServerSelectionCoordinator: Coordinator, Presentable {
// MARK: - Properties
// MARK: Private
final class ServerSelectionCoordinator: CoordinatorProtocol {
private let parameters: ServerSelectionCoordinatorParameters
private let serverSelectionHostingController: UIViewController
private var serverSelectionViewModel: ServerSelectionViewModelProtocol
private let userNotificationController: UserNotificationControllerProtocol
private var viewModel: ServerSelectionViewModelProtocol
private var authenticationService: AuthenticationServiceProxyProtocol { parameters.authenticationService }
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
private var loadingIndicator: UserIndicator?
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
var callback: (@MainActor (ServerSelectionCoordinatorAction) -> Void)?
// MARK: - Setup
init(parameters: ServerSelectionCoordinatorParameters) {
self.parameters = parameters
let viewModel = ServerSelectionViewModel(homeserverAddress: parameters.authenticationService.homeserver.address,
hasModalPresentation: parameters.hasModalPresentation)
let view = ServerSelectionScreen(context: viewModel.context)
serverSelectionViewModel = viewModel
serverSelectionHostingController = UIHostingController(rootView: view)
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: serverSelectionHostingController)
viewModel = ServerSelectionViewModel(homeserverAddress: parameters.authenticationService.homeserver.address,
isModallyPresented: parameters.isModallyPresented)
userNotificationController = parameters.userNotificationController
}
// MARK: - Public
func start() {
MXLog.debug("Did start.")
serverSelectionViewModel.callback = { [weak self] action in
viewModel.callback = { [weak self] action in
guard let self else { return }
MXLog.debug("ServerSelectionViewModel did callback with action: \(action).")
@@ -79,27 +60,24 @@ final class ServerSelectionCoordinator: Coordinator, Presentable {
}
}
func toPresentable() -> UIViewController {
serverSelectionHostingController
}
func stop() {
stopLoading()
}
// MARK: - Private
/// Show an activity indicator whilst loading.
/// - Parameters:
/// - label: The label to show on the indicator.
/// - isInteractionBlocking: Whether the indicator should block any user interaction.
private func startLoading(label: String = ElementL10n.loading, isInteractionBlocking: Bool = true) {
loadingIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking))
func toPresentable() -> AnyView {
AnyView(ServerSelectionScreen(context: viewModel.context))
}
// MARK: - Private
private func startLoading(label: String = ElementL10n.loading) {
userNotificationController.submitNotification(UserNotification(type: .modal,
title: label,
persistent: true))
}
/// Hide the currently displayed activity indicator.
private func stopLoading() {
loadingIndicator = nil
userNotificationController.retractAllNotifications()
}
/// Updates the login flow using the supplied homeserver address, or shows an error when this isn't possible.
@@ -122,9 +100,9 @@ final class ServerSelectionCoordinator: Coordinator, Presentable {
private func handleError(_ error: AuthenticationServiceError) {
switch error {
case .invalidServer, .invalidHomeserverAddress:
serverSelectionViewModel.displayError(.footerMessage(ElementL10n.loginErrorHomeserverNotFound))
viewModel.displayError(.footerMessage(ElementL10n.loginErrorHomeserverNotFound))
default:
serverSelectionViewModel.displayError(.footerMessage(ElementL10n.unknownError))
viewModel.displayError(.footerMessage(ElementL10n.unknownError))
}
}
}
@@ -33,7 +33,7 @@ struct ServerSelectionViewState: BindableState {
/// An error message to be shown in the text field footer.
var footerErrorMessage: String?
/// Whether the screen is presented modally or within a navigation stack.
var hasModalPresentation: Bool
var isModallyPresented: Bool
/// The message to show in the text field footer.
var footerMessage: String {
@@ -42,7 +42,7 @@ struct ServerSelectionViewState: BindableState {
/// The title shown on the confirm button.
var buttonTitle: String {
hasModalPresentation ? ElementL10n.actionConfirm : ElementL10n.actionNext
isModallyPresented ? ElementL10n.actionConfirm : ElementL10n.actionNext
}
/// The text field is showing an error.
@@ -28,13 +28,13 @@ class ServerSelectionViewModel: ServerSelectionViewModelType, ServerSelectionVie
var callback: (@MainActor (ServerSelectionViewModelAction) -> Void)?
// MARK: - Setup
init(homeserverAddress: String, hasModalPresentation: Bool) {
init(homeserverAddress: String, isModallyPresented: Bool) {
let bindings = ServerSelectionBindings(homeserverAddress: homeserverAddress)
super.init(initialViewState: ServerSelectionViewState(bindings: bindings,
hasModalPresentation: hasModalPresentation))
isModallyPresented: isModallyPresented))
}
// MARK: - Public
override func process(viewAction: ServerSelectionViewAction) async {
@@ -44,6 +44,7 @@ struct ServerSelectionScreen: View {
.background(Color.element.background, ignoresSafeAreaEdges: .all)
.toolbar { toolbar }
.alert(item: $context.alertInfo) { $0.alert }
.interactiveDismissDisabled()
}
/// The title, message and icon at the top of the screen.
@@ -91,7 +92,7 @@ struct ServerSelectionScreen: View {
@ToolbarContentBuilder
var toolbar: some ToolbarContent {
ToolbarItem(placement: .cancellationAction) {
if context.viewState.hasModalPresentation {
if context.viewState.isModallyPresented {
Button { context.send(viewAction: .dismiss) } label: {
Text(ElementL10n.actionCancel)
}