mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Memory management (#503)
* Correctly tear down the user session on signing out * Fix session verification <-> user session retain cycle, visible range debouncer leak * Manually clean up coordinators retained within SwiftUI's NavigationStacks * Slightly refactor the timeline content menu builder and prevent it from retaining the view model. Cleanup now unnecessarily optional RoomScreenCoordinator instance vars * Move coordinator dismissal logic to the navigation modules
This commit is contained in:
@@ -29,8 +29,9 @@ class AppCoordinator: AppCoordinatorProtocol {
|
|||||||
|
|
||||||
private var userSession: UserSessionProtocol! {
|
private var userSession: UserSessionProtocol! {
|
||||||
didSet {
|
didSet {
|
||||||
deobserveUserSessionChanges()
|
userSessionCancellables.removeAll()
|
||||||
if let userSession, !userSession.isSoftLogout {
|
|
||||||
|
if userSession != nil {
|
||||||
configureNotificationManager()
|
configureNotificationManager()
|
||||||
observeUserSessionChanges()
|
observeUserSessionChanges()
|
||||||
}
|
}
|
||||||
@@ -145,14 +146,16 @@ class AppCoordinator: AppCoordinatorProtocol {
|
|||||||
self.setupUserSession()
|
self.setupUserSession()
|
||||||
case (_, .signOut, .signingOut):
|
case (_, .signOut, .signingOut):
|
||||||
self.showLoadingIndicator()
|
self.showLoadingIndicator()
|
||||||
self.tearDownUserSession()
|
self.logout(isSoftLogout: false)
|
||||||
case (.signingOut, .completedSigningOut, .signedOut):
|
case (.signingOut, .completedSigningOut, .signedOut):
|
||||||
|
self.tearDownUserSession()
|
||||||
self.presentSplashScreen()
|
self.presentSplashScreen()
|
||||||
self.hideLoadingIndicator()
|
self.hideLoadingIndicator()
|
||||||
case (_, .remoteSignOut(let isSoft), .remoteSigningOut):
|
case (_, .remoteSignOut(let isSoft), .remoteSigningOut):
|
||||||
self.showLoadingIndicator()
|
self.showLoadingIndicator()
|
||||||
self.tearDownUserSession(isSoftLogout: isSoft)
|
self.logout(isSoftLogout: isSoft)
|
||||||
case (.remoteSigningOut(let isSoft), .completedSigningOut, .signedOut):
|
case (.remoteSigningOut(let isSoft), .completedSigningOut, .signedOut):
|
||||||
|
self.tearDownUserSession()
|
||||||
self.presentSplashScreen(isSoftLogout: isSoft)
|
self.presentSplashScreen(isSoftLogout: isSoft)
|
||||||
self.hideLoadingIndicator()
|
self.hideLoadingIndicator()
|
||||||
default:
|
default:
|
||||||
@@ -251,34 +254,34 @@ class AppCoordinator: AppCoordinatorProtocol {
|
|||||||
navigationRootCoordinator.setRootCoordinator(navigationSplitCoordinator)
|
navigationRootCoordinator.setRootCoordinator(navigationSplitCoordinator)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func tearDownUserSession(isSoftLogout: Bool = false) {
|
private func logout(isSoftLogout: Bool) {
|
||||||
userSession.clientProxy.stopSync()
|
userSession.clientProxy.stopSync()
|
||||||
userSessionFlowCoordinator?.stop()
|
userSessionFlowCoordinator?.stop()
|
||||||
|
|
||||||
deobserveUserSessionChanges()
|
|
||||||
|
|
||||||
guard !isSoftLogout else {
|
guard !isSoftLogout else {
|
||||||
stateMachine.processEvent(.completedSigningOut)
|
stateMachine.processEvent(.completedSigningOut)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
showLoadingIndicator()
|
|
||||||
|
|
||||||
// first log out from the server
|
// first log out from the server
|
||||||
_ = await userSession.clientProxy.logout()
|
_ = await userSession.clientProxy.logout()
|
||||||
|
|
||||||
// regardless of the result, clear user data
|
// regardless of the result, clear user data
|
||||||
userSessionStore.logout(userSession: userSession)
|
userSessionStore.logout(userSession: userSession)
|
||||||
userSession = nil
|
|
||||||
notificationManager?.delegate = nil
|
|
||||||
notificationManager = nil
|
|
||||||
|
|
||||||
stateMachine.processEvent(.completedSigningOut)
|
stateMachine.processEvent(.completedSigningOut)
|
||||||
|
|
||||||
hideLoadingIndicator()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func tearDownUserSession() {
|
||||||
|
userSession = nil
|
||||||
|
|
||||||
|
userSessionFlowCoordinator = nil
|
||||||
|
|
||||||
|
notificationManager?.delegate = nil
|
||||||
|
notificationManager = nil
|
||||||
|
}
|
||||||
|
|
||||||
private func presentSplashScreen(isSoftLogout: Bool = false) {
|
private func presentSplashScreen(isSoftLogout: Bool = false) {
|
||||||
navigationRootCoordinator.setRootCoordinator(SplashScreenCoordinator())
|
navigationRootCoordinator.setRootCoordinator(SplashScreenCoordinator())
|
||||||
@@ -340,10 +343,6 @@ class AppCoordinator: AppCoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
.store(in: &userSessionCancellables)
|
.store(in: &userSessionCancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func deobserveUserSessionChanges() {
|
|
||||||
userSessionCancellables.removeAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Toasts and loading indicators
|
// MARK: Toasts and loading indicators
|
||||||
|
|
||||||
|
|||||||
@@ -28,13 +28,12 @@ class NavigationSplitCoordinator: CoordinatorProtocol, ObservableObject, CustomS
|
|||||||
didSet {
|
didSet {
|
||||||
if let oldValue {
|
if let oldValue {
|
||||||
logPresentationChange("Remove sidebar", oldValue)
|
logPresentationChange("Remove sidebar", oldValue)
|
||||||
oldValue.coordinator.stop()
|
oldValue.tearDown()
|
||||||
oldValue.dismissalCallback?()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let sidebarModule {
|
if let sidebarModule {
|
||||||
logPresentationChange("Set sidebar", sidebarModule)
|
logPresentationChange("Set sidebar", sidebarModule)
|
||||||
sidebarModule.coordinator.start()
|
sidebarModule.coordinator?.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCompactLayoutComponents()
|
updateCompactLayoutComponents()
|
||||||
@@ -50,13 +49,12 @@ class NavigationSplitCoordinator: CoordinatorProtocol, ObservableObject, CustomS
|
|||||||
didSet {
|
didSet {
|
||||||
if let oldValue {
|
if let oldValue {
|
||||||
logPresentationChange("Remove detail", oldValue)
|
logPresentationChange("Remove detail", oldValue)
|
||||||
oldValue.coordinator.stop()
|
oldValue.tearDown()
|
||||||
oldValue.dismissalCallback?()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let detailModule {
|
if let detailModule {
|
||||||
logPresentationChange("Set detail", detailModule)
|
logPresentationChange("Set detail", detailModule)
|
||||||
detailModule.coordinator.start()
|
detailModule.coordinator?.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCompactLayoutComponents()
|
updateCompactLayoutComponents()
|
||||||
@@ -72,13 +70,12 @@ class NavigationSplitCoordinator: CoordinatorProtocol, ObservableObject, CustomS
|
|||||||
didSet {
|
didSet {
|
||||||
if let oldValue {
|
if let oldValue {
|
||||||
logPresentationChange("Remove sheet", oldValue)
|
logPresentationChange("Remove sheet", oldValue)
|
||||||
oldValue.coordinator.stop()
|
oldValue.tearDown()
|
||||||
oldValue.dismissalCallback?()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let sheetModule {
|
if let sheetModule {
|
||||||
logPresentationChange("Set sheet", sheetModule)
|
logPresentationChange("Set sheet", sheetModule)
|
||||||
sheetModule.coordinator.start()
|
sheetModule.coordinator?.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCompactLayoutComponents()
|
updateCompactLayoutComponents()
|
||||||
@@ -94,13 +91,12 @@ class NavigationSplitCoordinator: CoordinatorProtocol, ObservableObject, CustomS
|
|||||||
didSet {
|
didSet {
|
||||||
if let oldValue {
|
if let oldValue {
|
||||||
logPresentationChange("Remove fullscreen cover", oldValue)
|
logPresentationChange("Remove fullscreen cover", oldValue)
|
||||||
oldValue.coordinator.stop()
|
oldValue.tearDown()
|
||||||
oldValue.dismissalCallback?()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let fullScreenCoverModule {
|
if let fullScreenCoverModule {
|
||||||
logPresentationChange("Set fullscreen cover", fullScreenCoverModule)
|
logPresentationChange("Set fullscreen cover", fullScreenCoverModule)
|
||||||
fullScreenCoverModule.coordinator.start()
|
fullScreenCoverModule.coordinator?.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCompactLayoutComponents()
|
updateCompactLayoutComponents()
|
||||||
@@ -123,7 +119,7 @@ class NavigationSplitCoordinator: CoordinatorProtocol, ObservableObject, CustomS
|
|||||||
@Published internal var compactLayoutStackModules: [NavigationModule] = []
|
@Published internal var compactLayoutStackModules: [NavigationModule] = []
|
||||||
|
|
||||||
var compactLayoutStackCoordinators: [any CoordinatorProtocol] {
|
var compactLayoutStackCoordinators: [any CoordinatorProtocol] {
|
||||||
compactLayoutStackModules.map(\.coordinator)
|
compactLayoutStackModules.compactMap(\.coordinator)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Default NavigationSplitCoordinator initialiser
|
/// Default NavigationSplitCoordinator initialiser
|
||||||
@@ -190,6 +186,10 @@ class NavigationSplitCoordinator: CoordinatorProtocol, ObservableObject, CustomS
|
|||||||
AnyView(NavigationSplitCoordinatorView(navigationSplitCoordinator: self))
|
AnyView(NavigationSplitCoordinatorView(navigationSplitCoordinator: self))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stop() {
|
||||||
|
releaseAllCoordinatorReferences()
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - CustomStringConvertible
|
// MARK: - CustomStringConvertible
|
||||||
|
|
||||||
var description: String {
|
var description: String {
|
||||||
@@ -207,8 +207,27 @@ class NavigationSplitCoordinator: CoordinatorProtocol, ObservableObject, CustomS
|
|||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
|
/// The NavigationStack has a tendency to hold on to path items for longer than needed. We work around that by manually nilling the coordinator
|
||||||
|
/// when a NavigationModule is dismissed. As the NavigationModule is just a wrapper multiple instances of it continuing living is of no consequence
|
||||||
|
/// https://stackoverflow.com/questions/73885353/found-a-strange-behaviour-of-state-when-combined-to-the-new-navigation-stack/
|
||||||
|
///
|
||||||
|
/// For added complexity, the NavigationSplitCoordinator has an internal compact layout NavigationStack for which we need to manually nil things again
|
||||||
|
private func releaseAllCoordinatorReferences() {
|
||||||
|
sidebarModule?.coordinator = nil
|
||||||
|
detailModule?.coordinator = nil
|
||||||
|
sheetModule?.coordinator = nil
|
||||||
|
fullScreenCoverModule?.coordinator = nil
|
||||||
|
|
||||||
|
compactLayoutRootModule?.coordinator = nil
|
||||||
|
compactLayoutStackModules.forEach { module in
|
||||||
|
module.coordinator = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func logPresentationChange(_ change: String, _ module: NavigationModule) {
|
private func logPresentationChange(_ change: String, _ module: NavigationModule) {
|
||||||
MXLog.info("\(self) \(change): \(module.coordinator)")
|
if let coordinator = module.coordinator {
|
||||||
|
MXLog.info("\(self) \(change): \(coordinator)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// We need to update the compact layout whenever anything changes within the split coordinator or
|
/// We need to update the compact layout whenever anything changes within the split coordinator or
|
||||||
@@ -335,11 +354,11 @@ private struct NavigationSplitCoordinatorView: View {
|
|||||||
// Embedded NavigationStackCoordinators will present their sheets
|
// Embedded NavigationStackCoordinators will present their sheets
|
||||||
// through the NavigationSplitCoordinator as well.
|
// through the NavigationSplitCoordinator as well.
|
||||||
.sheet(item: $navigationSplitCoordinator.sheetModule) { module in
|
.sheet(item: $navigationSplitCoordinator.sheetModule) { module in
|
||||||
module.coordinator.toPresentable()
|
module.coordinator?.toPresentable()
|
||||||
.tint(.element.accent)
|
.tint(.element.accent)
|
||||||
}
|
}
|
||||||
.fullScreenCover(item: $navigationSplitCoordinator.fullScreenCoverModule) { module in
|
.fullScreenCover(item: $navigationSplitCoordinator.fullScreenCoverModule) { module in
|
||||||
module.coordinator.toPresentable()
|
module.coordinator?.toPresentable()
|
||||||
.tint(.element.accent)
|
.tint(.element.accent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -347,9 +366,9 @@ private struct NavigationSplitCoordinatorView: View {
|
|||||||
/// The NavigationStack that will be used in compact layouts
|
/// The NavigationStack that will be used in compact layouts
|
||||||
var navigationStack: some View {
|
var navigationStack: some View {
|
||||||
NavigationStack(path: $navigationSplitCoordinator.compactLayoutStackModules) {
|
NavigationStack(path: $navigationSplitCoordinator.compactLayoutStackModules) {
|
||||||
navigationSplitCoordinator.compactLayoutRootModule?.coordinator.toPresentable()
|
navigationSplitCoordinator.compactLayoutRootModule?.coordinator?.toPresentable()
|
||||||
.navigationDestination(for: NavigationModule.self) { module in
|
.navigationDestination(for: NavigationModule.self) { module in
|
||||||
module.coordinator.toPresentable()
|
module.coordinator?.toPresentable()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -358,20 +377,20 @@ private struct NavigationSplitCoordinatorView: View {
|
|||||||
var navigationSplitView: some View {
|
var navigationSplitView: some View {
|
||||||
NavigationSplitView(columnVisibility: $columnVisibility) {
|
NavigationSplitView(columnVisibility: $columnVisibility) {
|
||||||
if let sidebarModule = navigationSplitCoordinator.sidebarModule {
|
if let sidebarModule = navigationSplitCoordinator.sidebarModule {
|
||||||
sidebarModule.coordinator.toPresentable()
|
sidebarModule.coordinator?.toPresentable()
|
||||||
} else {
|
} else {
|
||||||
navigationSplitCoordinator.placeholderModule.coordinator.toPresentable()
|
navigationSplitCoordinator.placeholderModule.coordinator?.toPresentable()
|
||||||
}
|
}
|
||||||
} detail: {
|
} detail: {
|
||||||
if let detailModule = navigationSplitCoordinator.detailModule {
|
if let detailModule = navigationSplitCoordinator.detailModule {
|
||||||
detailModule.coordinator.toPresentable()
|
detailModule.coordinator?.toPresentable()
|
||||||
} else {
|
} else {
|
||||||
navigationSplitCoordinator.placeholderModule.coordinator.toPresentable()
|
navigationSplitCoordinator.placeholderModule.coordinator?.toPresentable()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationSplitViewStyle(.balanced)
|
.navigationSplitViewStyle(.balanced)
|
||||||
.navigationDestination(for: NavigationModule.self) { module in
|
.navigationDestination(for: NavigationModule.self) { module in
|
||||||
module.coordinator.toPresentable()
|
module.coordinator?.toPresentable()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -386,13 +405,12 @@ class NavigationStackCoordinator: ObservableObject, CoordinatorProtocol, CustomS
|
|||||||
didSet {
|
didSet {
|
||||||
if let oldValue {
|
if let oldValue {
|
||||||
logPresentationChange("Remove root", oldValue)
|
logPresentationChange("Remove root", oldValue)
|
||||||
oldValue.coordinator.stop()
|
oldValue.tearDown()
|
||||||
oldValue.dismissalCallback?()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let rootModule {
|
if let rootModule {
|
||||||
logPresentationChange("Set root", rootModule)
|
logPresentationChange("Set root", rootModule)
|
||||||
rootModule.coordinator.start()
|
rootModule.coordinator?.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -406,13 +424,12 @@ class NavigationStackCoordinator: ObservableObject, CoordinatorProtocol, CustomS
|
|||||||
didSet {
|
didSet {
|
||||||
if let oldValue {
|
if let oldValue {
|
||||||
logPresentationChange("Remove sheet", oldValue)
|
logPresentationChange("Remove sheet", oldValue)
|
||||||
oldValue.coordinator.stop()
|
oldValue.tearDown()
|
||||||
oldValue.dismissalCallback?()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let sheetModule {
|
if let sheetModule {
|
||||||
logPresentationChange("Set sheet", sheetModule)
|
logPresentationChange("Set sheet", sheetModule)
|
||||||
sheetModule.coordinator.start()
|
sheetModule.coordinator?.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -433,13 +450,12 @@ class NavigationStackCoordinator: ObservableObject, CoordinatorProtocol, CustomS
|
|||||||
didSet {
|
didSet {
|
||||||
if let oldValue {
|
if let oldValue {
|
||||||
logPresentationChange("Remove fullscreen cover", oldValue)
|
logPresentationChange("Remove fullscreen cover", oldValue)
|
||||||
oldValue.coordinator.stop()
|
oldValue.tearDown()
|
||||||
oldValue.dismissalCallback?()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let fullScreenCoverModule {
|
if let fullScreenCoverModule {
|
||||||
logPresentationChange("Set fullscreen cover", fullScreenCoverModule)
|
logPresentationChange("Set fullscreen cover", fullScreenCoverModule)
|
||||||
fullScreenCoverModule.coordinator.start()
|
fullScreenCoverModule.coordinator?.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -461,11 +477,10 @@ class NavigationStackCoordinator: ObservableObject, CoordinatorProtocol, CustomS
|
|||||||
switch change {
|
switch change {
|
||||||
case .insert(_, let module, _):
|
case .insert(_, let module, _):
|
||||||
logPresentationChange("Push", module)
|
logPresentationChange("Push", module)
|
||||||
module.coordinator.start()
|
module.coordinator?.start()
|
||||||
case .remove(_, let module, _):
|
case .remove(_, let module, _):
|
||||||
logPresentationChange("Pop", module)
|
logPresentationChange("Pop", module)
|
||||||
module.coordinator.stop()
|
module.tearDown()
|
||||||
module.dismissalCallback?()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -473,7 +488,7 @@ class NavigationStackCoordinator: ObservableObject, CoordinatorProtocol, CustomS
|
|||||||
|
|
||||||
// The current navigation stack. Excludes the rootCoordinator
|
// The current navigation stack. Excludes the rootCoordinator
|
||||||
var stackCoordinators: [any CoordinatorProtocol] {
|
var stackCoordinators: [any CoordinatorProtocol] {
|
||||||
stackModules.map(\.coordinator)
|
stackModules.compactMap(\.coordinator)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If this NavigationStackCoordinator will be embedded into a NavigationSplitCoordinator pass it here
|
/// If this NavigationStackCoordinator will be embedded into a NavigationSplitCoordinator pass it here
|
||||||
@@ -592,7 +607,9 @@ class NavigationStackCoordinator: ObservableObject, CoordinatorProtocol, CustomS
|
|||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func logPresentationChange(_ change: String, _ module: NavigationModule) {
|
private func logPresentationChange(_ change: String, _ module: NavigationModule) {
|
||||||
MXLog.info("\(self) \(change): \(module.coordinator)")
|
if let coordinator = module.coordinator {
|
||||||
|
MXLog.info("\(self) \(change): \(coordinator)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -601,17 +618,17 @@ private struct NavigationStackCoordinatorView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack(path: $navigationStackCoordinator.stackModules) {
|
NavigationStack(path: $navigationStackCoordinator.stackModules) {
|
||||||
navigationStackCoordinator.rootModule?.coordinator.toPresentable()
|
navigationStackCoordinator.rootModule?.coordinator?.toPresentable()
|
||||||
.navigationDestination(for: NavigationModule.self) { module in
|
.navigationDestination(for: NavigationModule.self) { module in
|
||||||
module.coordinator.toPresentable()
|
module.coordinator?.toPresentable()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(item: $navigationStackCoordinator.sheetModule) { module in
|
.sheet(item: $navigationStackCoordinator.sheetModule) { module in
|
||||||
module.coordinator.toPresentable()
|
module.coordinator?.toPresentable()
|
||||||
.tint(.element.accent)
|
.tint(.element.accent)
|
||||||
}
|
}
|
||||||
.fullScreenCover(item: $navigationStackCoordinator.fullScreenCoverModule) { module in
|
.fullScreenCover(item: $navigationStackCoordinator.fullScreenCoverModule) { module in
|
||||||
module.coordinator.toPresentable()
|
module.coordinator?.toPresentable()
|
||||||
.tint(.element.accent)
|
.tint(.element.accent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,15 @@ import Foundation
|
|||||||
|
|
||||||
/// A CoordinatorProtocol wrapper and type erasing component that allows
|
/// A CoordinatorProtocol wrapper and type erasing component that allows
|
||||||
/// dynamically presenting arbitrary screens
|
/// dynamically presenting arbitrary screens
|
||||||
struct NavigationModule: Identifiable, Hashable {
|
@MainActor
|
||||||
|
class NavigationModule: Identifiable, Hashable {
|
||||||
let id = UUID()
|
let id = UUID()
|
||||||
let coordinator: any CoordinatorProtocol
|
|
||||||
|
/// The NavigationStack has a tendency to hold on to path items for longer than needed. We work around that by manually nilling the coordinator
|
||||||
|
/// when a NavigationModule is dismissed, reason why this is an optional property
|
||||||
|
/// As the NavigationModule is just a wrapper multiple instances of it continuing living is of no consequence
|
||||||
|
/// https://stackoverflow.com/questions/73885353/found-a-strange-behaviour-of-state-when-combined-to-the-new-navigation-stack/
|
||||||
|
var coordinator: (any CoordinatorProtocol)?
|
||||||
let dismissalCallback: (() -> Void)?
|
let dismissalCallback: (() -> Void)?
|
||||||
|
|
||||||
init(_ coordinator: any CoordinatorProtocol, dismissalCallback: (() -> Void)? = nil) {
|
init(_ coordinator: any CoordinatorProtocol, dismissalCallback: (() -> Void)? = nil) {
|
||||||
@@ -28,11 +34,17 @@ struct NavigationModule: Identifiable, Hashable {
|
|||||||
self.dismissalCallback = dismissalCallback
|
self.dismissalCallback = dismissalCallback
|
||||||
}
|
}
|
||||||
|
|
||||||
static func == (lhs: NavigationModule, rhs: NavigationModule) -> Bool {
|
func tearDown() {
|
||||||
|
coordinator?.stop()
|
||||||
|
dismissalCallback?()
|
||||||
|
coordinator = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
nonisolated static func == (lhs: NavigationModule, rhs: NavigationModule) -> Bool {
|
||||||
lhs.id == rhs.id
|
lhs.id == rhs.id
|
||||||
}
|
}
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
nonisolated func hash(into hasher: inout Hasher) {
|
||||||
hasher.combine(id)
|
hasher.combine(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,13 +20,12 @@ class NavigationRootCoordinator: ObservableObject, CoordinatorProtocol, CustomSt
|
|||||||
@Published fileprivate var rootModule: NavigationModule? {
|
@Published fileprivate var rootModule: NavigationModule? {
|
||||||
didSet {
|
didSet {
|
||||||
if let oldValue {
|
if let oldValue {
|
||||||
oldValue.coordinator.stop()
|
oldValue.tearDown()
|
||||||
oldValue.dismissalCallback?()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let rootModule {
|
if let rootModule {
|
||||||
logPresentationChange("Set root", rootModule)
|
logPresentationChange("Set root", rootModule)
|
||||||
rootModule.coordinator.start()
|
rootModule.coordinator?.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,7 +65,9 @@ class NavigationRootCoordinator: ObservableObject, CoordinatorProtocol, CustomSt
|
|||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func logPresentationChange(_ change: String, _ module: NavigationModule) {
|
private func logPresentationChange(_ change: String, _ module: NavigationModule) {
|
||||||
MXLog.info("\(self) \(change): \(module.coordinator)")
|
if let coordinator = module.coordinator {
|
||||||
|
MXLog.info("\(self) \(change): \(coordinator)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +76,7 @@ private struct NavigationRootCoordinatorView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
rootCoordinator.rootModule?.coordinator.toPresentable()
|
rootCoordinator.rootModule?.coordinator?.toPresentable()
|
||||||
}
|
}
|
||||||
.animation(.elementDefault, value: rootCoordinator.rootModule)
|
.animation(.elementDefault, value: rootCoordinator.rootModule)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,8 +35,6 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
|||||||
|
|
||||||
var callback: ((HomeScreenViewModelAction) -> Void)?
|
var callback: ((HomeScreenViewModelAction) -> Void)?
|
||||||
|
|
||||||
// MARK: - Setup
|
|
||||||
|
|
||||||
// swiftlint:disable:next function_body_length
|
// swiftlint:disable:next function_body_length
|
||||||
init(userSession: UserSessionProtocol, attributedStringBuilder: AttributedStringBuilderProtocol) {
|
init(userSession: UserSessionProtocol, attributedStringBuilder: AttributedStringBuilderProtocol) {
|
||||||
self.userSession = userSession
|
self.userSession = userSession
|
||||||
@@ -65,8 +63,8 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
|||||||
visibleItemRangePublisher
|
visibleItemRangePublisher
|
||||||
.debounce(for: 0.1, scheduler: DispatchQueue.main)
|
.debounce(for: 0.1, scheduler: DispatchQueue.main)
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.sink { range in
|
.sink { [weak self] range in
|
||||||
self.updateVisibleRange(range)
|
self?.updateVisibleRange(range)
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
@@ -93,7 +91,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
|||||||
visibleRoomsSummaryProvider.roomListPublisher)
|
visibleRoomsSummaryProvider.roomListPublisher)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] state, totalCount, rooms in
|
.sink { [weak self] state, totalCount, rooms in
|
||||||
guard let self = self else { return }
|
guard let self else { return }
|
||||||
|
|
||||||
let isLoadingData = state != .live && (totalCount == 0 || rooms.count != totalCount)
|
let isLoadingData = state != .live && (totalCount == 0 || rooms.count != totalCount)
|
||||||
let hasNoRooms = state == .live && totalCount == 0
|
let hasNoRooms = state == .live && totalCount == 0
|
||||||
|
|||||||
@@ -25,15 +25,11 @@ struct RoomScreenCoordinatorParameters {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class RoomScreenCoordinator: CoordinatorProtocol {
|
final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||||
private var parameters: RoomScreenCoordinatorParameters?
|
private var parameters: RoomScreenCoordinatorParameters
|
||||||
|
|
||||||
private var viewModel: RoomScreenViewModelProtocol?
|
private var viewModel: RoomScreenViewModelProtocol
|
||||||
private var navigationStackCoordinator: NavigationStackCoordinator {
|
private var navigationStackCoordinator: NavigationStackCoordinator {
|
||||||
guard let parameters else {
|
parameters.navigationStackCoordinator
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
|
|
||||||
return parameters.navigationStackCoordinator
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init(parameters: RoomScreenCoordinatorParameters) {
|
init(parameters: RoomScreenCoordinatorParameters) {
|
||||||
@@ -49,7 +45,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
// MARK: - Public
|
// MARK: - Public
|
||||||
|
|
||||||
func start() {
|
func start() {
|
||||||
viewModel?.callback = { [weak self] action in
|
viewModel.callback = { [weak self] action in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
@@ -64,18 +60,11 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func stop() {
|
func stop() {
|
||||||
viewModel?.context.send(viewAction: .markRoomAsRead)
|
viewModel.context.send(viewAction: .markRoomAsRead)
|
||||||
viewModel?.stop()
|
|
||||||
viewModel = nil
|
|
||||||
parameters = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func toPresentable() -> AnyView {
|
func toPresentable() -> AnyView {
|
||||||
guard let context = viewModel?.context else {
|
AnyView(RoomScreen(context: viewModel.context))
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
|
|
||||||
return AnyView(RoomScreen(context: context))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
@@ -91,14 +80,9 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func displayEmojiPickerScreen(for itemId: String) {
|
private func displayEmojiPickerScreen(for itemId: String) {
|
||||||
guard let emojiProvider = parameters?.emojiProvider,
|
|
||||||
let timelineController = parameters?.timelineController else {
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
|
|
||||||
let emojiPickerNavigationStackCoordinator = NavigationStackCoordinator()
|
let emojiPickerNavigationStackCoordinator = NavigationStackCoordinator()
|
||||||
|
|
||||||
let params = EmojiPickerScreenCoordinatorParameters(emojiProvider: emojiProvider,
|
let params = EmojiPickerScreenCoordinatorParameters(emojiProvider: parameters.emojiProvider,
|
||||||
itemId: itemId)
|
itemId: itemId)
|
||||||
let coordinator = EmojiPickerScreenCoordinator(parameters: params)
|
let coordinator = EmojiPickerScreenCoordinator(parameters: params)
|
||||||
coordinator.callback = { [weak self] action in
|
coordinator.callback = { [weak self] action in
|
||||||
@@ -107,7 +91,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
MXLog.debug("Selected \(emoji) for \(itemId)")
|
MXLog.debug("Selected \(emoji) for \(itemId)")
|
||||||
self?.navigationStackCoordinator.setSheetCoordinator(nil)
|
self?.navigationStackCoordinator.setSheetCoordinator(nil)
|
||||||
Task {
|
Task {
|
||||||
await timelineController.sendReaction(emoji, to: itemId)
|
await self?.parameters.timelineController.sendReaction(emoji, to: itemId)
|
||||||
}
|
}
|
||||||
case .dismiss:
|
case .dismiss:
|
||||||
self?.navigationStackCoordinator.setSheetCoordinator(nil)
|
self?.navigationStackCoordinator.setSheetCoordinator(nil)
|
||||||
@@ -121,14 +105,9 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func displayRoomDetails() {
|
private func displayRoomDetails() {
|
||||||
guard let roomProxy = parameters?.roomProxy,
|
|
||||||
let mediaProvider = parameters?.mediaProvider else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let params = RoomDetailsCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
let params = RoomDetailsCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
||||||
roomProxy: roomProxy,
|
roomProxy: parameters.roomProxy,
|
||||||
mediaProvider: mediaProvider)
|
mediaProvider: parameters.mediaProvider)
|
||||||
let coordinator = RoomDetailsCoordinator(parameters: params)
|
let coordinator = RoomDetailsCoordinator(parameters: params)
|
||||||
coordinator.callback = { [weak self] _ in
|
coordinator.callback = { [weak self] _ in
|
||||||
self?.navigationStackCoordinator.pop()
|
self?.navigationStackCoordinator.pop()
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ enum RoomScreenViewAction {
|
|||||||
case cancelEdit
|
case cancelEdit
|
||||||
/// Mark the entire room as read - this is heavy handed as a starting point for now.
|
/// Mark the entire room as read - this is heavy handed as a starting point for now.
|
||||||
case markRoomAsRead
|
case markRoomAsRead
|
||||||
|
case contextMenuAction(itemID: String, action: TimelineItemContextMenuAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RoomScreenViewState: BindableState {
|
struct RoomScreenViewState: BindableState {
|
||||||
@@ -56,7 +57,7 @@ struct RoomScreenViewState: BindableState {
|
|||||||
var showLoading = false
|
var showLoading = false
|
||||||
var bindings: RoomScreenViewStateBindings
|
var bindings: RoomScreenViewStateBindings
|
||||||
|
|
||||||
var contextMenuBuilder: (@MainActor (_ itemId: String) -> TimelineItemContextMenu)?
|
var contextMenuActionProvider: (@MainActor (_ itemId: String) -> TimelineItemContextMenuActions?)?
|
||||||
|
|
||||||
var composerMode: RoomScreenComposerMode = .default
|
var composerMode: RoomScreenComposerMode = .default
|
||||||
|
|
||||||
|
|||||||
@@ -28,9 +28,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
|
|
||||||
private let timelineController: RoomTimelineControllerProtocol
|
private let timelineController: RoomTimelineControllerProtocol
|
||||||
private let timelineViewFactory: RoomTimelineViewFactoryProtocol
|
private let timelineViewFactory: RoomTimelineViewFactoryProtocol
|
||||||
|
|
||||||
// MARK: - Setup
|
|
||||||
|
|
||||||
init(timelineController: RoomTimelineControllerProtocol,
|
init(timelineController: RoomTimelineControllerProtocol,
|
||||||
timelineViewFactory: RoomTimelineViewFactoryProtocol,
|
timelineViewFactory: RoomTimelineViewFactoryProtocol,
|
||||||
mediaProvider: MediaProviderProtocol,
|
mediaProvider: MediaProviderProtocol,
|
||||||
@@ -72,7 +70,13 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
state.contextMenuBuilder = buildContextMenuForItemId(_:)
|
state.contextMenuActionProvider = { [weak self] itemId -> TimelineItemContextMenuActions? in
|
||||||
|
guard let self else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.contextMenuActionsForItemId(itemId)
|
||||||
|
}
|
||||||
|
|
||||||
buildTimelineViews()
|
buildTimelineViews()
|
||||||
}
|
}
|
||||||
@@ -109,13 +113,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
state.bindings.composerText = ""
|
state.bindings.composerText = ""
|
||||||
case .markRoomAsRead:
|
case .markRoomAsRead:
|
||||||
await markRoomAsRead()
|
await markRoomAsRead()
|
||||||
|
case .contextMenuAction(let itemID, let action):
|
||||||
|
processContentMenuAction(action, itemID: itemID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func stop() {
|
|
||||||
cancellables.removeAll()
|
|
||||||
state.contextMenuBuilder = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
@@ -197,22 +198,16 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
|
|
||||||
// MARK: ContextMenus
|
// MARK: ContextMenus
|
||||||
|
|
||||||
private func buildContextMenuForItemId(_ itemId: String) -> TimelineItemContextMenu {
|
private func contextMenuActionsForItemId(_ itemId: String) -> TimelineItemContextMenuActions? {
|
||||||
TimelineItemContextMenu(contextMenuActions: contextMenuActionsForItemId(itemId)) { [weak self] action in
|
|
||||||
self?.processContentMenuAction(action, itemId: itemId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func contextMenuActionsForItemId(_ itemId: String) -> TimelineItemContextMenuActions {
|
|
||||||
guard let timelineItem = timelineController.timelineItems.first(where: { $0.id == itemId }),
|
guard let timelineItem = timelineController.timelineItems.first(where: { $0.id == itemId }),
|
||||||
let item = timelineItem as? EventBasedTimelineItemProtocol else {
|
let item = timelineItem as? EventBasedTimelineItemProtocol else {
|
||||||
// Don't show a context menu for non-event based items.
|
// Don't show a context menu for non-event based items.
|
||||||
return .empty
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if timelineItem is StateRoomTimelineItem {
|
if timelineItem is StateRoomTimelineItem {
|
||||||
// Don't show a context menu for state events.
|
// Don't show a context menu for state events.
|
||||||
return .empty
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var actions: [TimelineItemContextMenuAction] = [
|
var actions: [TimelineItemContextMenuAction] = [
|
||||||
@@ -238,8 +233,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
}
|
}
|
||||||
|
|
||||||
// swiftlint:disable:next cyclomatic_complexity
|
// swiftlint:disable:next cyclomatic_complexity
|
||||||
private func processContentMenuAction(_ action: TimelineItemContextMenuAction, itemId: String) {
|
private func processContentMenuAction(_ action: TimelineItemContextMenuAction, itemID: String) {
|
||||||
guard let timelineItem = timelineController.timelineItems.first(where: { $0.id == itemId }),
|
guard let timelineItem = timelineController.timelineItems.first(where: { $0.id == itemID }),
|
||||||
let item = timelineItem as? EventBasedTimelineItemProtocol else {
|
let item = timelineItem as? EventBasedTimelineItemProtocol else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -265,7 +260,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
}
|
}
|
||||||
case .redact:
|
case .redact:
|
||||||
Task {
|
Task {
|
||||||
await timelineController.redact(itemId)
|
await timelineController.redact(itemID)
|
||||||
}
|
}
|
||||||
case .reply:
|
case .reply:
|
||||||
state.bindings.composerFocused = true
|
state.bindings.composerFocused = true
|
||||||
|
|||||||
@@ -20,6 +20,4 @@ import Foundation
|
|||||||
protocol RoomScreenViewModelProtocol {
|
protocol RoomScreenViewModelProtocol {
|
||||||
var callback: ((RoomScreenViewModelAction) -> Void)? { get set }
|
var callback: ((RoomScreenViewModelAction) -> Void)? { get set }
|
||||||
var context: RoomScreenViewModelType.Context { get }
|
var context: RoomScreenViewModelType.Context { get }
|
||||||
|
|
||||||
func stop()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,17 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct TimelineItemContextMenuActions {
|
struct TimelineItemContextMenuActions {
|
||||||
static var empty: TimelineItemContextMenuActions { .init(actions: [], debugActions: []) }
|
|
||||||
|
|
||||||
let actions: [TimelineItemContextMenuAction]
|
let actions: [TimelineItemContextMenuAction]
|
||||||
let debugActions: [TimelineItemContextMenuAction]
|
let debugActions: [TimelineItemContextMenuAction]
|
||||||
var isEmpty: Bool { actions.isEmpty && debugActions.isEmpty }
|
|
||||||
|
init?(actions: [TimelineItemContextMenuAction], debugActions: [TimelineItemContextMenuAction]) {
|
||||||
|
if actions.isEmpty, debugActions.isEmpty {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.actions = actions
|
||||||
|
self.debugActions = debugActions
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TimelineItemContextMenuAction: Identifiable, Hashable {
|
enum TimelineItemContextMenuAction: Identifiable, Hashable {
|
||||||
@@ -52,16 +58,11 @@ public struct TimelineItemContextMenu: View {
|
|||||||
let callback: (TimelineItemContextMenuAction) -> Void
|
let callback: (TimelineItemContextMenuAction) -> Void
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
if contextMenuActions.isEmpty {
|
viewsForActions(contextMenuActions.actions)
|
||||||
// When there are no actions make sure then menu isn't shown.
|
Menu {
|
||||||
EmptyView()
|
viewsForActions(contextMenuActions.debugActions)
|
||||||
} else {
|
} label: {
|
||||||
viewsForActions(contextMenuActions.actions)
|
Label("Developer", systemImage: "hammer")
|
||||||
Menu {
|
|
||||||
viewsForActions(contextMenuActions.debugActions)
|
|
||||||
} label: {
|
|
||||||
Label("Developer", systemImage: "hammer")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class TimelineTableViewController: UIViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var contextMenuBuilder: (@MainActor (_ itemId: String) -> TimelineItemContextMenu)?
|
var contextMenuActionProvider: (@MainActor (_ itemId: String) -> TimelineItemContextMenuActions?)?
|
||||||
|
|
||||||
@Binding private var scrollToBottomButtonVisible: Bool
|
@Binding private var scrollToBottomButtonVisible: Bool
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ class TimelineTableViewController: UIViewController {
|
|||||||
// A local reference to avoid capturing self in the cell configuration.
|
// A local reference to avoid capturing self in the cell configuration.
|
||||||
let coordinator = self.coordinator
|
let coordinator = self.coordinator
|
||||||
let opacity = self.opacity(for: timelineItem)
|
let opacity = self.opacity(for: timelineItem)
|
||||||
let contextMenuBuilder = self.contextMenuBuilder
|
let contextMenuActionProvider = self.contextMenuActionProvider
|
||||||
|
|
||||||
cell.item = timelineItem
|
cell.item = timelineItem
|
||||||
cell.contentConfiguration = UIHostingConfiguration {
|
cell.contentConfiguration = UIHostingConfiguration {
|
||||||
@@ -198,7 +198,11 @@ class TimelineTableViewController: UIViewController {
|
|||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.opacity(opacity)
|
.opacity(opacity)
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
contextMenuBuilder?(timelineItem.id)
|
contextMenuActionProvider?(timelineItem.id).map { actions in
|
||||||
|
TimelineItemContextMenu(contextMenuActions: actions) { action in
|
||||||
|
coordinator.send(viewAction: .contextMenuAction(itemID: timelineItem.id, action: action))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
coordinator.send(viewAction: .itemAppeared(id: timelineItem.id))
|
coordinator.send(viewAction: .itemAppeared(id: timelineItem.id))
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ struct TimelineView: UIViewControllerRepresentable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Doesn't have an equatable conformance :(
|
// Doesn't have an equatable conformance :(
|
||||||
tableViewController.contextMenuBuilder = context.viewState.contextMenuBuilder
|
tableViewController.contextMenuActionProvider = context.viewState.contextMenuActionProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
func send(viewAction: RoomScreenViewAction) {
|
func send(viewAction: RoomScreenViewAction) {
|
||||||
|
|||||||
@@ -66,10 +66,10 @@ class UserSession: UserSessionProtocol {
|
|||||||
|
|
||||||
self.sessionVerificationController = sessionVerificationController
|
self.sessionVerificationController = sessionVerificationController
|
||||||
|
|
||||||
sessionVerificationController.callbacks.sink { callback in
|
sessionVerificationController.callbacks.sink { [weak self] callback in
|
||||||
switch callback {
|
switch callback {
|
||||||
case .finished:
|
case .finished:
|
||||||
self.callbacks.send(.didVerifySession)
|
self?.callbacks.send(.didVerifySession)
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user