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
@@ -16,90 +16,77 @@
import SwiftUI
struct BugReportCoordinatorParameters {
let bugReportService: BugReportServiceProtocol
let screenshot: UIImage?
enum BugReportCoordinatorResult {
case cancel
case finish
}
final class BugReportCoordinator: Coordinator, Presentable {
// MARK: - Properties
// MARK: Private
private let parameters: BugReportCoordinatorParameters
private let bugReportHostingController: UIViewController
private var bugReportViewModel: BugReportViewModelProtocol
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
private var loadingIndicator: UserIndicator?
private var statusIndicator: UserIndicator?
// MARK: Public
struct BugReportCoordinatorParameters {
let bugReportService: BugReportServiceProtocol
let userNotificationController: UserNotificationControllerProtocol
let screenshot: UIImage?
let isModallyPresented: Bool
}
// Must be used only internally
var childCoordinators: [Coordinator] = []
var completion: (() -> Void)?
// MARK: - Setup
final class BugReportCoordinator: CoordinatorProtocol {
private let parameters: BugReportCoordinatorParameters
private var viewModel: BugReportViewModelProtocol
var completion: ((BugReportCoordinatorResult) -> Void)?
init(parameters: BugReportCoordinatorParameters) {
self.parameters = parameters
let viewModel = BugReportViewModel(bugReportService: parameters.bugReportService,
screenshot: parameters.screenshot)
let view = BugReportScreen(context: viewModel.context)
bugReportViewModel = viewModel
bugReportHostingController = UIHostingController(rootView: view)
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: bugReportHostingController)
viewModel = BugReportViewModel(bugReportService: parameters.bugReportService,
screenshot: parameters.screenshot,
isModallyPresented: parameters.isModallyPresented)
}
// MARK: - Public
func start() {
MXLog.debug("Did start.")
bugReportViewModel.callback = { [weak self] result in
viewModel.callback = { [weak self] result in
guard let self else { return }
MXLog.debug("BugReportViewModel did complete with result: \(result).")
switch result {
case .cancel:
self.completion?(.cancel)
case .submitStarted:
self.startLoading()
case .submitFinished:
self.stopLoading()
self.completion?()
self.completion?(.finish)
case .submitFailed(let error):
self.stopLoading()
self.showError(label: error.localizedDescription)
}
}
}
func toPresentable() -> UIViewController {
bugReportHostingController
}
func stop() {
stopLoading()
}
func toPresentable() -> AnyView {
AnyView(BugReportScreen(context: viewModel.context))
}
// 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))
static let loadingIndicatorIdentifier = "BugReportLoading"
private func startLoading(label: String = ElementL10n.loading) {
parameters.userNotificationController.submitNotification(UserNotification(id: Self.loadingIndicatorIdentifier,
type: .modal,
title: label,
persistent: true))
}
/// Hide the currently displayed activity indicator.
private func stopLoading() {
loadingIndicator = nil
parameters.userNotificationController.retractNotificationWithId(Self.loadingIndicatorIdentifier)
}
/// Show error indicator
private func showError(label: String) {
statusIndicator = indicatorPresenter.present(.error(label: label))
parameters.userNotificationController.submitNotification(UserNotification(title: label))
}
}
@@ -22,6 +22,7 @@ import UIKit
// MARK: View model
enum BugReportViewModelAction {
case cancel
case submitStarted
case submitFinished
case submitFailed(error: Error)
@@ -32,6 +33,7 @@ enum BugReportViewModelAction {
struct BugReportViewState: BindableState {
var screenshot: UIImage?
var bindings: BugReportViewStateBindings
let isModallyPresented: Bool
}
struct BugReportViewStateBindings {
@@ -40,6 +42,7 @@ struct BugReportViewStateBindings {
}
enum BugReportViewAction {
case cancel
case submit
case toggleSendLogs
case removeScreenshot
@@ -16,18 +16,41 @@
import SwiftUI
@available(iOS 14, *)
typealias BugReportViewModelType = StateStoreViewModel<BugReportViewState,
BugReportViewAction>
@available(iOS 14, *)
class BugReportViewModel: BugReportViewModelType, BugReportViewModelProtocol {
// MARK: - Properties
typealias BugReportViewModelType = StateStoreViewModel<BugReportViewState, BugReportViewAction>
class BugReportViewModel: BugReportViewModelType, BugReportViewModelProtocol {
let bugReportService: BugReportServiceProtocol
var callback: ((BugReportViewModelAction) -> Void)?
init(bugReportService: BugReportServiceProtocol,
screenshot: UIImage?,
isModallyPresented: Bool) {
self.bugReportService = bugReportService
let bindings = BugReportViewStateBindings(reportText: "", sendingLogsEnabled: true)
super.init(initialViewState: BugReportViewState(screenshot: screenshot,
bindings: bindings,
isModallyPresented: isModallyPresented))
}
// MARK: - Public
override func process(viewAction: BugReportViewAction) async {
switch viewAction {
case .cancel:
callback?(.cancel)
case .submit:
await submitBugReport()
case .toggleSendLogs:
context.sendingLogsEnabled.toggle()
case .removeScreenshot:
state.screenshot = nil
}
}
// MARK: Private
func submitBugReport() async {
private func submitBugReport() async {
callback?(.submitStarted)
do {
var files: [URL] = []
@@ -54,31 +77,4 @@ class BugReportViewModel: BugReportViewModelType, BugReportViewModelProtocol {
callback?(.submitFailed(error: error))
}
}
// MARK: Public
var callback: ((BugReportViewModelAction) -> Void)?
// MARK: - Setup
init(bugReportService: BugReportServiceProtocol,
screenshot: UIImage?) {
self.bugReportService = bugReportService
let bindings = BugReportViewStateBindings(reportText: "", sendingLogsEnabled: true)
super.init(initialViewState: BugReportViewState(screenshot: screenshot,
bindings: bindings))
}
// MARK: - Public
override func process(viewAction: BugReportViewAction) async {
switch viewAction {
case .submit:
await submitBugReport()
case .toggleSendLogs:
context.sendingLogsEnabled.toggle()
case .removeScreenshot:
state.screenshot = nil
}
}
}
@@ -19,6 +19,5 @@ import Foundation
@MainActor
protocol BugReportViewModelProtocol {
var callback: ((BugReportViewModelAction) -> Void)? { get set }
@available(iOS 14, *)
var context: BugReportViewModelType.Context { get }
}
@@ -48,6 +48,16 @@ struct BugReportScreen: View {
.padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 16)
}
.navigationTitle(ElementL10n.titleActivityBugReport)
.toolbar {
if context.viewState.isModallyPresented {
ToolbarItem(placement: .cancellationAction) {
Button(ElementL10n.actionCancel) {
context.send(viewAction: .cancel)
}
}
}
}
.interactiveDismissDisabled()
}
}
@@ -136,7 +146,7 @@ struct BugReport_Previews: PreviewProvider {
@ViewBuilder
static var body: some View {
let viewModel = BugReportViewModel(bugReportService: MockBugReportService(), screenshot: Asset.Images.appLogo.image)
let viewModel = BugReportViewModel(bugReportService: MockBugReportService(), screenshot: Asset.Images.appLogo.image, isModallyPresented: false)
BugReportScreen(context: viewModel.context)
.previewInterfaceOrientation(.portrait)
}