* 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
187 lines
6.1 KiB
Swift
187 lines
6.1 KiB
Swift
//
|
|
// Copyright 2022 New Vector Ltd
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
class NavigationController: ObservableObject, CoordinatorProtocol {
|
|
private var dismissalCallbacks = [UUID: () -> Void]()
|
|
|
|
@Published fileprivate var internalRootCoordinator: AnyCoordinator? {
|
|
didSet {
|
|
if let oldValue {
|
|
oldValue.coordinator.stop()
|
|
}
|
|
|
|
if let internalRootCoordinator {
|
|
logPresentationChange("Set root", internalRootCoordinator)
|
|
internalRootCoordinator.coordinator.start()
|
|
}
|
|
}
|
|
}
|
|
|
|
@Published fileprivate var internalSheetCoordinator: AnyCoordinator? {
|
|
didSet {
|
|
if let oldValue {
|
|
logPresentationChange("Dismiss", oldValue)
|
|
oldValue.coordinator.stop()
|
|
dismissalCallbacks[oldValue.id]?()
|
|
dismissalCallbacks.removeValue(forKey: oldValue.id)
|
|
}
|
|
|
|
if let internalSheetCoordinator {
|
|
logPresentationChange("Present", internalSheetCoordinator)
|
|
internalSheetCoordinator.coordinator.start()
|
|
}
|
|
}
|
|
}
|
|
|
|
@Published fileprivate var internalNavigationStack = [AnyCoordinator]() {
|
|
didSet {
|
|
let diffs = internalNavigationStack.difference(from: oldValue)
|
|
diffs.forEach { change in
|
|
switch change {
|
|
case .insert(_, let anyCoordinator, _):
|
|
logPresentationChange("Push", anyCoordinator)
|
|
anyCoordinator.coordinator.start()
|
|
case .remove(_, let anyCoordinator, _):
|
|
logPresentationChange("Pop", anyCoordinator)
|
|
anyCoordinator.coordinator.stop()
|
|
|
|
dismissalCallbacks[anyCoordinator.id]?()
|
|
dismissalCallbacks.removeValue(forKey: anyCoordinator.id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var rootCoordinator: CoordinatorProtocol? {
|
|
internalRootCoordinator?.coordinator
|
|
}
|
|
|
|
var coordinators: [CoordinatorProtocol] {
|
|
internalNavigationStack.map(\.coordinator)
|
|
}
|
|
|
|
var sheetCoordinator: CoordinatorProtocol? {
|
|
internalSheetCoordinator?.coordinator
|
|
}
|
|
|
|
func setRootCoordinator(_ coordinator: any CoordinatorProtocol) {
|
|
popToRoot(animated: false)
|
|
internalRootCoordinator = AnyCoordinator(coordinator)
|
|
}
|
|
|
|
func push(_ coordinator: any CoordinatorProtocol, dismissalCallback: (() -> Void)? = nil) {
|
|
let anyCoordinator = AnyCoordinator(coordinator)
|
|
|
|
if let dismissalCallback {
|
|
dismissalCallbacks[anyCoordinator.id] = dismissalCallback
|
|
}
|
|
|
|
internalNavigationStack.append(anyCoordinator)
|
|
}
|
|
|
|
func popToRoot(animated: Bool = true) {
|
|
dismissSheet()
|
|
|
|
guard !internalNavigationStack.isEmpty else {
|
|
return
|
|
}
|
|
|
|
if !animated {
|
|
// Disabling animations doesn't work through normal Transactions
|
|
// https://stackoverflow.com/questions/72832243
|
|
UIView.setAnimationsEnabled(false)
|
|
}
|
|
|
|
internalNavigationStack.removeAll()
|
|
|
|
if !animated {
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
|
|
UIView.setAnimationsEnabled(true)
|
|
}
|
|
}
|
|
}
|
|
|
|
func pop() {
|
|
dismissSheet()
|
|
internalNavigationStack.removeLast()
|
|
}
|
|
|
|
func presentSheet(_ coordinator: any CoordinatorProtocol, dismissalCallback: (() -> Void)? = nil) {
|
|
let anyCoordinator = AnyCoordinator(coordinator)
|
|
|
|
if let dismissalCallback {
|
|
dismissalCallbacks[anyCoordinator.id] = dismissalCallback
|
|
}
|
|
|
|
internalSheetCoordinator = anyCoordinator
|
|
}
|
|
|
|
func dismissSheet() {
|
|
internalSheetCoordinator = nil
|
|
}
|
|
|
|
// MARK: - CoordinatorProtocol
|
|
|
|
func toPresentable() -> AnyView {
|
|
AnyView(NavigationControllerView(navigationController: self))
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private func logPresentationChange(_ change: String, _ anyCoordinator: AnyCoordinator) {
|
|
if let navigationCoordinator = anyCoordinator.coordinator as? NavigationController, let rootCoordinator = navigationCoordinator.rootCoordinator {
|
|
MXLog.info("\(change): NavigationController(\(anyCoordinator.id)) - \(rootCoordinator)")
|
|
} else {
|
|
MXLog.info("\(change): \(anyCoordinator.coordinator)(\(anyCoordinator.id))")
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct NavigationControllerView: View {
|
|
@ObservedObject var navigationController: NavigationController
|
|
|
|
var body: some View {
|
|
NavigationStack(path: $navigationController.internalNavigationStack) {
|
|
navigationController.internalRootCoordinator?.coordinator.toPresentable()
|
|
.navigationDestination(for: AnyCoordinator.self) { anyCoordinator in
|
|
anyCoordinator.coordinator.toPresentable()
|
|
}
|
|
}
|
|
.sheet(item: $navigationController.internalSheetCoordinator) { anyCoordinator in
|
|
anyCoordinator.coordinator.toPresentable()
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct AnyCoordinator: Identifiable, Hashable {
|
|
let id = UUID()
|
|
let coordinator: any CoordinatorProtocol
|
|
|
|
init(_ coordinator: any CoordinatorProtocol) {
|
|
self.coordinator = coordinator
|
|
}
|
|
|
|
static func == (lhs: AnyCoordinator, rhs: AnyCoordinator) -> Bool {
|
|
lhs.id == rhs.id
|
|
}
|
|
|
|
func hash(into hasher: inout Hasher) {
|
|
hasher.combine(id)
|
|
}
|
|
}
|