mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
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:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user