diff --git a/Riot/Modules/Onboarding/AuthenticationCoordinator.swift b/Riot/Modules/Onboarding/AuthenticationCoordinator.swift index 9982b2ddf2..9dd0e959ff 100644 --- a/Riot/Modules/Onboarding/AuthenticationCoordinator.swift +++ b/Riot/Modules/Onboarding/AuthenticationCoordinator.swift @@ -131,7 +131,7 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc let parameters = AuthenticationServerSelectionCoordinatorParameters(authenticationService: authenticationService, hasModalPresentation: false) let coordinator = AuthenticationServerSelectionCoordinator(parameters: parameters) - coordinator.completion = { [weak self, weak coordinator] result in + coordinator.callback = { [weak self, weak coordinator] result in guard let self = self, let coordinator = coordinator else { return } self.serverSelectionCoordinator(coordinator, didCompleteWith: result) } @@ -168,7 +168,7 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc registrationFlow: homeserver.registrationFlow, loginMode: homeserver.preferredLoginMode) let coordinator = AuthenticationRegistrationCoordinator(parameters: parameters) - coordinator.completion = { [weak self, weak coordinator] result in + coordinator.callback = { [weak self, weak coordinator] result in guard let self = self, let coordinator = coordinator else { return } self.registrationCoordinator(coordinator, didCompleteWith: result) } @@ -189,8 +189,6 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc @MainActor private func registrationCoordinator(_ coordinator: AuthenticationRegistrationCoordinator, didCompleteWith result: AuthenticationRegistrationCoordinatorResult) { switch result { - case .selectServer: - showServerSelectionScreen() case .completed(let result): handleRegistrationResult(result) } diff --git a/Riot/Modules/Onboarding/OnboardingCoordinator.swift b/Riot/Modules/Onboarding/OnboardingCoordinator.swift index 2edced531c..cf2b524444 100644 --- a/Riot/Modules/Onboarding/OnboardingCoordinator.swift +++ b/Riot/Modules/Onboarding/OnboardingCoordinator.swift @@ -437,7 +437,7 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { let parameters = OnboardingAvatarCoordinatorParameters(userSession: userSession, avatar: selectedAvatar) let coordinator = OnboardingAvatarCoordinator(parameters: parameters) - coordinator.completion = { [weak self, weak coordinator] result in + coordinator.callback = { [weak self, weak coordinator] result in guard let self = self, let coordinator = coordinator else { return } switch result { diff --git a/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationViewModel.swift b/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationViewModel.swift index 6c3607d8c4..b0b32c9360 100644 --- a/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationViewModel.swift +++ b/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationViewModel.swift @@ -27,7 +27,7 @@ class AuthenticationRegistrationViewModel: AuthenticationRegistrationViewModelTy // MARK: Public - @MainActor var completion: ((AuthenticationRegistrationViewModelResult) -> Void)? + @MainActor var callback: ((AuthenticationRegistrationViewModelResult) -> Void)? // MARK: - Setup @@ -44,25 +44,19 @@ class AuthenticationRegistrationViewModel: AuthenticationRegistrationViewModelTy // MARK: - Public override func process(viewAction: AuthenticationRegistrationViewAction) { - Task { - await MainActor.run { - switch viewAction { - case .selectServer: - completion?(.selectServer) - case .validateUsername: - state.hasEditedUsername = true - completion?(.validateUsername(state.bindings.username)) - case .enablePasswordValidation: - state.hasEditedPassword = true - case .clearUsernameError: - guard state.usernameErrorMessage != nil else { return } - state.usernameErrorMessage = nil - case .next: - completion?(.createAccount(username: state.bindings.username, password: state.bindings.password)) - case .continueWithSSO(let id): - break - } - } + switch viewAction { + case .selectServer: + Task { await callback?(.selectServer) } + case .validateUsername: + Task { await validateUsername() } + case .enablePasswordValidation: + Task { await enablePasswordValidation() } + case .clearUsernameError: + Task { await clearUsernameError() } + case .next: + Task { await callback?(.createAccount(username: state.bindings.username, password: state.bindings.password)) } + case .continueWithSSO(let id): + break } } @@ -92,4 +86,27 @@ class AuthenticationRegistrationViewModel: AuthenticationRegistrationViewModelTy state.bindings.alertInfo = AlertInfo(id: type) } } + + // MARK: - Private + + /// Validate the supplied username with the homeserver. + @MainActor private func validateUsername() { + if !state.hasEditedUsername { + state.hasEditedUsername = true + } + + callback?(.validateUsername(state.bindings.username)) + } + + /// Allows password validation to take place. + @MainActor private func enablePasswordValidation() { + guard !state.hasEditedPassword else { return } + state.hasEditedPassword = true + } + + /// Clear any errors being shown in the username text field footer. + @MainActor private func clearUsernameError() { + guard state.usernameErrorMessage != nil else { return } + state.usernameErrorMessage = nil + } } diff --git a/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationViewModelProtocol.swift b/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationViewModelProtocol.swift index 5528aa6887..d20fcfeba9 100644 --- a/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationViewModelProtocol.swift @@ -18,7 +18,7 @@ import Foundation protocol AuthenticationRegistrationViewModelProtocol { - @MainActor var completion: ((AuthenticationRegistrationViewModelResult) -> Void)? { get set } + @MainActor var callback: ((AuthenticationRegistrationViewModelResult) -> Void)? { get set } var context: AuthenticationRegistrationViewModelType.Context { get } /// Update the view with new homeserver information. diff --git a/RiotSwiftUI/Modules/Authentication/Registration/Coordinator/AuthenticationRegistrationCoordinator.swift b/RiotSwiftUI/Modules/Authentication/Registration/Coordinator/AuthenticationRegistrationCoordinator.swift index 1151d1dd99..97609027a1 100644 --- a/RiotSwiftUI/Modules/Authentication/Registration/Coordinator/AuthenticationRegistrationCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/Registration/Coordinator/AuthenticationRegistrationCoordinator.swift @@ -28,8 +28,6 @@ struct AuthenticationRegistrationCoordinatorParameters { } enum AuthenticationRegistrationCoordinatorResult { - /// The user would like to select another server. - case selectServer /// The screen completed with the associated registration result. case completed(RegistrationResult) } @@ -63,7 +61,7 @@ final class AuthenticationRegistrationCoordinator: Coordinator, Presentable { // Must be used only internally var childCoordinators: [Coordinator] = [] - @MainActor var completion: ((AuthenticationRegistrationCoordinatorResult) -> Void)? + @MainActor var callback: ((AuthenticationRegistrationCoordinatorResult) -> Void)? // MARK: - Setup @@ -87,23 +85,8 @@ final class AuthenticationRegistrationCoordinator: Coordinator, Presentable { // MARK: - Public func start() { - Task { - await MainActor.run { - MXLog.debug("[AuthenticationRegistrationCoordinator] did start.") - authenticationRegistrationViewModel.completion = { [weak self] result in - guard let self = self else { return } - MXLog.debug("[AuthenticationRegistrationCoordinator] AuthenticationRegistrationViewModel did complete with result: \(result).") - switch result { - case .selectServer: - self.presentServerSelectionScreen() - case.validateUsername(let username): - self.validateUsername(username) - case .createAccount(let username, let password): - self.createAccount(username: username, password: password) - } - } - } - } + MXLog.debug("[AuthenticationRegistrationCoordinator] did start.") + Task { await setupViewModel() } } func toPresentable() -> UIViewController { @@ -112,6 +95,22 @@ final class AuthenticationRegistrationCoordinator: Coordinator, Presentable { // MARK: - Private + /// Set up the view model. This method is extracted from `start()` so it can run on the `MainActor`. + @MainActor private func setupViewModel() { + authenticationRegistrationViewModel.callback = { [weak self] result in + guard let self = self else { return } + MXLog.debug("[AuthenticationRegistrationCoordinator] AuthenticationRegistrationViewModel did complete with result: \(result).") + switch result { + case .selectServer: + self.presentServerSelectionScreen() + case.validateUsername(let username): + self.validateUsername(username) + case .createAccount(let username, let password): + self.createAccount(username: username, password: password) + } + } + } + /// Show a blocking activity indicator whilst saving. @MainActor private func startLoading(label: String? = nil) { waitingIndicator = indicatorPresenter.present(.loading(label: label ?? VectorL10n.loading, isInteractionBlocking: true)) @@ -160,7 +159,7 @@ final class AuthenticationRegistrationCoordinator: Coordinator, Presentable { let result = try await registrationWizard.createAccount(username: username, password: password, initialDeviceDisplayName: deviceDisplayName) guard !Task.isCancelled else { return } - completion?(.completed(result)) + callback?(.completed(result)) self?.stopLoading() } catch { @@ -211,7 +210,7 @@ final class AuthenticationRegistrationCoordinator: Coordinator, Presentable { let parameters = AuthenticationServerSelectionCoordinatorParameters(authenticationService: authenticationService, hasModalPresentation: true) let coordinator = AuthenticationServerSelectionCoordinator(parameters: parameters) - coordinator.completion = { [weak self, weak coordinator] result in + coordinator.callback = { [weak self, weak coordinator] result in guard let self = self, let coordinator = coordinator else { return } self.serverSelectionCoordinator(coordinator, didCompleteWith: result) } diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionViewModel.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionViewModel.swift index 3fc7131e3a..6945d3bca5 100644 --- a/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionViewModel.swift +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionViewModel.swift @@ -28,7 +28,7 @@ class AuthenticationServerSelectionViewModel: AuthenticationServerSelectionViewM // MARK: Public - var completion: ((AuthenticationServerSelectionViewModelResult) -> Void)? + @MainActor var callback: ((AuthenticationServerSelectionViewModelResult) -> Void)? // MARK: - Setup @@ -41,20 +41,15 @@ class AuthenticationServerSelectionViewModel: AuthenticationServerSelectionViewM // MARK: - Public override func process(viewAction: AuthenticationServerSelectionViewAction) { - Task { - await MainActor.run { - switch viewAction { - case .confirm: - completion?(.confirm(homeserverAddress: state.bindings.homeserverAddress)) - case .dismiss: - completion?(.dismiss) - case .getInTouch: - getInTouch() - case .clearFooterError: - guard state.footerErrorMessage != nil else { return } - withAnimation { state.footerErrorMessage = nil } - } - } + switch viewAction { + case .confirm: + Task { await callback?(.confirm(homeserverAddress: state.bindings.homeserverAddress)) } + case .dismiss: + Task { await callback?(.dismiss) } + case .getInTouch: + Task { await getInTouch() } + case .clearFooterError: + Task { await clearFooterError() } } } @@ -71,6 +66,12 @@ class AuthenticationServerSelectionViewModel: AuthenticationServerSelectionViewM // MARK: - Private + /// Clear any errors shown in the text field footer. + @MainActor private func clearFooterError() { + guard state.footerErrorMessage != nil else { return } + withAnimation { state.footerErrorMessage = nil } + } + /// Opens the EMS link in the user's browser. @MainActor private func getInTouch() { let url = BuildSettings.onboardingHostYourOwnServerLink diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionViewModelProtocol.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionViewModelProtocol.swift index fb7a38779b..29b9e0b329 100644 --- a/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionViewModelProtocol.swift @@ -18,7 +18,7 @@ import Foundation protocol AuthenticationServerSelectionViewModelProtocol { - @MainActor var completion: ((AuthenticationServerSelectionViewModelResult) -> Void)? { get set } + @MainActor var callback: ((AuthenticationServerSelectionViewModelResult) -> Void)? { get set } var context: AuthenticationServerSelectionViewModelType.Context { get } /// Displays an error to the user. diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/Coordinator/AuthenticationServerSelectionCoordinator.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/Coordinator/AuthenticationServerSelectionCoordinator.swift index 54f9c269a0..af1819742e 100644 --- a/RiotSwiftUI/Modules/Authentication/ServerSelection/Coordinator/AuthenticationServerSelectionCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/Coordinator/AuthenticationServerSelectionCoordinator.swift @@ -48,7 +48,7 @@ final class AuthenticationServerSelectionCoordinator: Coordinator, Presentable { // Must be used only internally var childCoordinators: [Coordinator] = [] - @MainActor var completion: ((AuthenticationServerSelectionCoordinatorResult) -> Void)? + @MainActor var callback: ((AuthenticationServerSelectionCoordinatorResult) -> Void)? // MARK: - Setup @@ -70,22 +70,8 @@ final class AuthenticationServerSelectionCoordinator: Coordinator, Presentable { // MARK: - Public func start() { - Task { - await MainActor.run { - MXLog.debug("[AuthenticationServerSelectionCoordinator] did start.") - authenticationServerSelectionViewModel.completion = { [weak self] result in - guard let self = self else { return } - MXLog.debug("[AuthenticationServerSelectionCoordinator] AuthenticationServerSelectionViewModel did complete with result: \(result).") - - switch result { - case .confirm(let homeserverAddress): - self.useHomeserver(homeserverAddress) - case .dismiss: - self.completion?(.dismiss) - } - } - } - } + MXLog.debug("[AuthenticationServerSelectionCoordinator] did start.") + Task { await setupViewModel() } } func toPresentable() -> UIViewController { @@ -94,6 +80,21 @@ final class AuthenticationServerSelectionCoordinator: Coordinator, Presentable { // MARK: - Private + /// Set up the view model. This method is extracted from `start()` so it can run on the `MainActor`. + @MainActor private func setupViewModel() { + authenticationServerSelectionViewModel.callback = { [weak self] result in + guard let self = self else { return } + MXLog.debug("[AuthenticationServerSelectionCoordinator] AuthenticationServerSelectionViewModel did complete with result: \(result).") + + switch result { + case .confirm(let homeserverAddress): + self.useHomeserver(homeserverAddress) + case .dismiss: + self.callback?(.dismiss) + } + } + } + /// Show an activity indicator whilst loading. /// - Parameters: /// - label: The label to show on the indicator. @@ -120,7 +121,7 @@ final class AuthenticationServerSelectionCoordinator: Coordinator, Presentable { try await authenticationService.startFlow(.register, for: homeserverAddress) stopLoading() - completion?(.updated) + callback?(.updated) } catch { stopLoading() diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/Coordinator/OnboardingAvatarCoordinator.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/Coordinator/OnboardingAvatarCoordinator.swift index f312a89063..216a65ea43 100644 --- a/RiotSwiftUI/Modules/Onboarding/Avatar/Coordinator/OnboardingAvatarCoordinator.swift +++ b/RiotSwiftUI/Modules/Onboarding/Avatar/Coordinator/OnboardingAvatarCoordinator.swift @@ -24,7 +24,8 @@ struct OnboardingAvatarCoordinatorParameters { } enum OnboardingAvatarCoordinatorResult { - /// The user has chosen an image (but hasn't yet saved it). + /// The user has chosen an image (but it won't be uploaded until `.complete` is sent). + /// This result is to cache the image in the flow coordinator so it can be restored if the user was to navigate backwards. case selectedAvatar(UIImage?) /// The screen is finished and the next one can be shown. case complete(UserSession) @@ -63,7 +64,7 @@ final class OnboardingAvatarCoordinator: Coordinator, Presentable { // Must be used only internally var childCoordinators: [Coordinator] = [] - var completion: ((OnboardingAvatarCoordinatorResult) -> Void)? + var callback: ((OnboardingAvatarCoordinatorResult) -> Void)? // MARK: - Setup @@ -88,7 +89,7 @@ final class OnboardingAvatarCoordinator: Coordinator, Presentable { func start() { MXLog.debug("[OnboardingAvatarCoordinator] did start.") - onboardingAvatarViewModel.completion = { [weak self] result in + onboardingAvatarViewModel.callback = { [weak self] result in guard let self = self else { return } MXLog.debug("[OnboardingAvatarCoordinator] OnboardingAvatarViewModel did complete with result: \(result).") switch result { @@ -99,7 +100,7 @@ final class OnboardingAvatarCoordinator: Coordinator, Presentable { case .save(let avatar): self.setAvatar(avatar) case .skip: - self.completion?(.complete(self.parameters.userSession)) + self.callback?(.complete(self.parameters.userSession)) } } } @@ -161,7 +162,7 @@ final class OnboardingAvatarCoordinator: Coordinator, Presentable { self.parameters.userSession.account.setUserAvatarUrl(urlString) { [weak self] in guard let self = self else { return } self.stopWaiting() - self.completion?(.complete(self.parameters.userSession)) + self.callback?(.complete(self.parameters.userSession)) } failure: { [weak self] error in guard let self = self else { return } self.stopWaiting() @@ -182,7 +183,7 @@ extension OnboardingAvatarCoordinator: MediaPickerPresenterDelegate { /// so whilst this method may not appear to be called, everything works fine when run on a device. func mediaPickerPresenter(_ presenter: MediaPickerPresenter, didPickImage image: UIImage) { onboardingAvatarViewModel.updateAvatarImage(with: image) - completion?(.selectedAvatar(image)) + callback?(.selectedAvatar(image)) presenter.dismiss(animated: true, completion: nil) } @@ -196,7 +197,7 @@ extension OnboardingAvatarCoordinator: MediaPickerPresenterDelegate { extension OnboardingAvatarCoordinator: CameraPresenterDelegate { func cameraPresenter(_ presenter: CameraPresenter, didSelectImage image: UIImage) { onboardingAvatarViewModel.updateAvatarImage(with: image) - completion?(.selectedAvatar(image)) + callback?(.selectedAvatar(image)) presenter.dismiss(animated: true, completion: nil) } diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarViewModel.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarViewModel.swift index 1a2c8653a7..7d1c23f5cb 100644 --- a/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarViewModel.swift +++ b/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarViewModel.swift @@ -29,7 +29,7 @@ class OnboardingAvatarViewModel: OnboardingAvatarViewModelType, OnboardingAvatar // MARK: Public - var completion: ((OnboardingAvatarViewModelResult) -> Void)? + var callback: ((OnboardingAvatarViewModelResult) -> Void)? // MARK: - Setup @@ -46,13 +46,13 @@ class OnboardingAvatarViewModel: OnboardingAvatarViewModelType, OnboardingAvatar override func process(viewAction: OnboardingAvatarViewAction) { switch viewAction { case .pickImage: - completion?(.pickImage) + callback?(.pickImage) case .takePhoto: - completion?(.takePhoto) + callback?(.takePhoto) case .save: - completion?(.save(state.avatar)) + callback?(.save(state.avatar)) case .skip: - completion?(.skip) + callback?(.skip) } } diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarViewModelProtocol.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarViewModelProtocol.swift index 91323dd6bf..0c3eb58c89 100644 --- a/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarViewModelProtocol.swift @@ -18,7 +18,7 @@ import SwiftUI protocol OnboardingAvatarViewModelProtocol { - var completion: ((OnboardingAvatarViewModelResult) -> Void)? { get set } + var callback: ((OnboardingAvatarViewModelResult) -> Void)? { get set } var context: OnboardingAvatarViewModelType.Context { get } /// Update the view model to show the image that the user has picked. diff --git a/changelog.d/pr-6141.wip b/changelog.d/pr-6141.wip new file mode 100644 index 0000000000..7e19a013d2 --- /dev/null +++ b/changelog.d/pr-6141.wip @@ -0,0 +1 @@ +Onboarding: Rename completion to callback and simplify actor usage