diff --git a/ElementX/Sources/Other/Extensions/Publisher.swift b/ElementX/Sources/Other/Extensions/Publisher.swift index ebd939558..274f5921e 100644 --- a/ElementX/Sources/Other/Extensions/Publisher.swift +++ b/ElementX/Sources/Other/Extensions/Publisher.swift @@ -23,17 +23,3 @@ extension Publisher where Self.Failure == Never { } } } - -extension Published.Publisher { - /// Returns the next output from the publisher skipping the current value stored into it (which is readable from the @Published property itself). - /// - Returns: the next output from the publisher - var nextValue: Output? { - get async { - var iterator = values.makeAsyncIterator() - - // skips the publisher's current value - _ = await iterator.next() - return await iterator.next() - } - } -} diff --git a/ElementX/Sources/Other/SwiftUI/ViewModel/StateStoreViewModel.swift b/ElementX/Sources/Other/SwiftUI/ViewModel/StateStoreViewModel.swift index 7b7f561e7..9dad366d4 100644 --- a/ElementX/Sources/Other/SwiftUI/ViewModel/StateStoreViewModel.swift +++ b/ElementX/Sources/Other/SwiftUI/ViewModel/StateStoreViewModel.swift @@ -89,14 +89,14 @@ class StateStoreViewModel { .sink { [weak self] action in guard let self else { return } - Task { await self.process(viewAction: action) } + self.process(viewAction: action) } .store(in: &cancellables) } /// Override to handles incoming `ViewAction`s from the `ViewModel`. /// - Parameter viewAction: The `ViewAction` to be processed in `ViewModel` implementation. - func process(viewAction: ViewAction) async { + func process(viewAction: ViewAction) { // Default implementation, -no-op } } diff --git a/ElementX/Sources/Screens/AnalyticsPrompt/AnalyticsPromptViewModel.swift b/ElementX/Sources/Screens/AnalyticsPrompt/AnalyticsPromptViewModel.swift index 0386831ae..cf30d804a 100644 --- a/ElementX/Sources/Screens/AnalyticsPrompt/AnalyticsPromptViewModel.swift +++ b/ElementX/Sources/Screens/AnalyticsPrompt/AnalyticsPromptViewModel.swift @@ -32,7 +32,7 @@ class AnalyticsPromptViewModel: AnalyticsPromptViewModelType, AnalyticsPromptVie // MARK: - Public - override func process(viewAction: AnalyticsPromptViewAction) async { + override func process(viewAction: AnalyticsPromptViewAction) { switch viewAction { case .enable: callback?(.enable) diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginViewModel.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginViewModel.swift index 364b06dbf..5a2668633 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginViewModel.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginViewModel.swift @@ -28,7 +28,7 @@ class LoginViewModel: LoginViewModelType, LoginViewModelProtocol { super.init(initialViewState: viewState) } - override func process(viewAction: LoginViewAction) async { + override func process(viewAction: LoginViewAction) { switch viewAction { case .selectServer: callback?(.selectServer) diff --git a/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionViewModel.swift b/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionViewModel.swift index ff11fde97..0e96e9409 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionViewModel.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelection/ServerSelectionViewModel.swift @@ -28,7 +28,7 @@ class ServerSelectionViewModel: ServerSelectionViewModelType, ServerSelectionVie isModallyPresented: isModallyPresented)) } - override func process(viewAction: ServerSelectionViewAction) async { + override func process(viewAction: ServerSelectionViewAction) { switch viewAction { case .confirm: callback?(.confirm(homeserverAddress: state.bindings.homeserverAddress)) diff --git a/ElementX/Sources/Screens/Authentication/SoftLogout/SoftLogoutViewModel.swift b/ElementX/Sources/Screens/Authentication/SoftLogout/SoftLogoutViewModel.swift index 2e9a210e4..bac33b985 100644 --- a/ElementX/Sources/Screens/Authentication/SoftLogout/SoftLogoutViewModel.swift +++ b/ElementX/Sources/Screens/Authentication/SoftLogout/SoftLogoutViewModel.swift @@ -33,7 +33,7 @@ class SoftLogoutViewModel: SoftLogoutViewModelType, SoftLogoutViewModelProtocol super.init(initialViewState: viewState) } - override func process(viewAction: SoftLogoutViewAction) async { + override func process(viewAction: SoftLogoutViewAction) { switch viewAction { case .login: callback?(.login(state.bindings.password)) diff --git a/ElementX/Sources/Screens/BugReport/BugReportCoordinator.swift b/ElementX/Sources/Screens/BugReport/BugReportCoordinator.swift index 583c2b3ec..416ab676d 100644 --- a/ElementX/Sources/Screens/BugReport/BugReportCoordinator.swift +++ b/ElementX/Sources/Screens/BugReport/BugReportCoordinator.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import Combine import SwiftUI enum BugReportCoordinatorResult { @@ -34,6 +35,7 @@ struct BugReportCoordinatorParameters { final class BugReportCoordinator: CoordinatorProtocol { private let parameters: BugReportCoordinatorParameters private var viewModel: BugReportViewModelProtocol + private var cancellables: Set = .init() var completion: ((BugReportCoordinatorResult) -> Void)? @@ -50,22 +52,25 @@ final class BugReportCoordinator: CoordinatorProtocol { // MARK: - Public func start() { - viewModel.callback = { [weak self] result in - guard let self else { return } - MXLog.info("BugReportViewModel did complete with result: \(result).") - switch result { - case .cancel: - self.completion?(.cancel) - case let .submitStarted(progressTracker): - self.startLoading(label: L10n.commonSending, progressPublisher: progressTracker) - case .submitFinished: - self.stopLoading() - self.completion?(.finish) - case .submitFailed(let error): - self.stopLoading() - self.showError(label: error.localizedDescription) + viewModel + .actions + .sink { [weak self] result in + guard let self else { return } + MXLog.info("BugReportViewModel did complete with result: \(result).") + switch result { + case .cancel: + self.completion?(.cancel) + case let .submitStarted(progressTracker): + self.startLoading(label: L10n.commonSending, progressPublisher: progressTracker) + case .submitFinished: + self.stopLoading() + self.completion?(.finish) + case .submitFailed(let error): + self.stopLoading() + self.showError(label: error.localizedDescription) + } } - } + .store(in: &cancellables) } func stop() { diff --git a/ElementX/Sources/Screens/BugReport/BugReportViewModel.swift b/ElementX/Sources/Screens/BugReport/BugReportViewModel.swift index a48013371..1482f0095 100644 --- a/ElementX/Sources/Screens/BugReport/BugReportViewModel.swift +++ b/ElementX/Sources/Screens/BugReport/BugReportViewModel.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import Combine import SwiftUI typealias BugReportViewModelType = StateStoreViewModel @@ -22,8 +23,11 @@ class BugReportViewModel: BugReportViewModelType, BugReportViewModelProtocol { private let bugReportService: BugReportServiceProtocol private let userID: String private let deviceID: String? + private let actionsSubject: PassthroughSubject = .init() - var callback: ((BugReportViewModelAction) -> Void)? + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } init(bugReportService: BugReportServiceProtocol, userID: String, @@ -42,12 +46,12 @@ class BugReportViewModel: BugReportViewModelType, BugReportViewModelProtocol { // MARK: - Public - override func process(viewAction: BugReportViewAction) async { + override func process(viewAction: BugReportViewAction) { switch viewAction { case .cancel: - callback?(.cancel) + actionsSubject.send(.cancel) case .submit: - await submitBugReport() + Task { await submitBugReport() } case .removeScreenshot: state.screenshot = nil case let .attachScreenshot(image): @@ -59,7 +63,7 @@ class BugReportViewModel: BugReportViewModelType, BugReportViewModelProtocol { private func submitBugReport() async { let progressTracker = ProgressTracker() - callback?(.submitStarted(progressTracker: progressTracker)) + actionsSubject.send(.submitStarted(progressTracker: progressTracker)) do { var files: [URL] = [] if let screenshot = context.viewState.screenshot { @@ -78,10 +82,10 @@ class BugReportViewModel: BugReportViewModelType, BugReportViewModelProtocol { let result = try await bugReportService.submitBugReport(bugReport, progressListener: progressTracker) MXLog.info("SubmitBugReport succeeded, result: \(result.reportUrl)") - callback?(.submitFinished) + actionsSubject.send(.submitFinished) } catch { MXLog.error("SubmitBugReport failed: \(error)") - callback?(.submitFailed(error: error)) + actionsSubject.send(.submitFailed(error: error)) } } } diff --git a/ElementX/Sources/Screens/BugReport/BugReportViewModelProtocol.swift b/ElementX/Sources/Screens/BugReport/BugReportViewModelProtocol.swift index 3910d6cfb..db0474618 100644 --- a/ElementX/Sources/Screens/BugReport/BugReportViewModelProtocol.swift +++ b/ElementX/Sources/Screens/BugReport/BugReportViewModelProtocol.swift @@ -14,10 +14,10 @@ // limitations under the License. // -import Foundation +import Combine @MainActor protocol BugReportViewModelProtocol { - var callback: ((BugReportViewModelAction) -> Void)? { get set } + var actions: AnyPublisher { get } var context: BugReportViewModelType.Context { get } } diff --git a/ElementX/Sources/Screens/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift b/ElementX/Sources/Screens/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift index 4c3d60710..5cc56cad0 100644 --- a/ElementX/Sources/Screens/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift +++ b/ElementX/Sources/Screens/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift @@ -34,7 +34,7 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve .store(in: &cancellables) } - override func process(viewAction: DeveloperOptionsScreenViewAction) async { + override func process(viewAction: DeveloperOptionsScreenViewAction) { switch viewAction { case .changedShouldCollapseRoomStateEvents: ServiceLocator.shared.settings.shouldCollapseRoomStateEvents = state.bindings.shouldCollapseRoomStateEvents diff --git a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift index 8e0e3ded6..36b52cfae 100644 --- a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift +++ b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift @@ -32,11 +32,13 @@ class EmojiPickerScreenViewModel: EmojiPickerScreenViewModelType, EmojiPickerScr // MARK: - Public - override func process(viewAction: EmojiPickerScreenViewAction) async { + override func process(viewAction: EmojiPickerScreenViewAction) { switch viewAction { case let .search(searchString: searchString): - let categories = await emojiProvider.getCategories(searchString: searchString) - state.categories = convert(emojiCategories: categories) + Task { + let categories = await emojiProvider.getCategories(searchString: searchString) + state.categories = convert(emojiCategories: categories) + } case let .emojiTapped(emoji: emoji): callback?(.emojiSelected(emoji: emoji.value)) case .dismiss: diff --git a/ElementX/Sources/Screens/FilePreview/FilePreviewViewModel.swift b/ElementX/Sources/Screens/FilePreview/FilePreviewViewModel.swift index 3e2b52ca4..b4448d42d 100644 --- a/ElementX/Sources/Screens/FilePreview/FilePreviewViewModel.swift +++ b/ElementX/Sources/Screens/FilePreview/FilePreviewViewModel.swift @@ -25,7 +25,7 @@ class FilePreviewViewModel: FilePreviewViewModelType, FilePreviewViewModelProtoc super.init(initialViewState: FilePreviewViewState(mediaFile: mediaFile, title: title)) } - override func process(viewAction: FilePreviewViewAction) async { + override func process(viewAction: FilePreviewViewAction) { switch viewAction { case .cancel: callback?(.cancel) diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index aefa6a544..059864b70 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -133,7 +133,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol // MARK: - Public - override func process(viewAction: HomeScreenViewAction) async { + override func process(viewAction: HomeScreenViewAction) { switch viewAction { case .selectRoom(let roomIdentifier): callback?(.presentRoom(roomIdentifier: roomIdentifier)) diff --git a/ElementX/Sources/Screens/MediaPickerPreviewScreen/MediaPickerPreviewScreenViewModel.swift b/ElementX/Sources/Screens/MediaPickerPreviewScreen/MediaPickerPreviewScreenViewModel.swift index 1d04d6581..fd7f57d1d 100644 --- a/ElementX/Sources/Screens/MediaPickerPreviewScreen/MediaPickerPreviewScreenViewModel.swift +++ b/ElementX/Sources/Screens/MediaPickerPreviewScreen/MediaPickerPreviewScreenViewModel.swift @@ -25,7 +25,7 @@ class MediaPickerPreviewScreenViewModel: MediaPickerPreviewScreenViewModelType, super.init(initialViewState: MediaPickerPreviewScreenViewState(url: url, title: title)) } - override func process(viewAction: MediaPickerPreviewScreenViewAction) async { + override func process(viewAction: MediaPickerPreviewScreenViewAction) { switch viewAction { case .send: callback?(.send) diff --git a/ElementX/Sources/Screens/OnboardingScreen/OnboardingViewModel.swift b/ElementX/Sources/Screens/OnboardingScreen/OnboardingViewModel.swift index f1dd386a8..e29b890ca 100644 --- a/ElementX/Sources/Screens/OnboardingScreen/OnboardingViewModel.swift +++ b/ElementX/Sources/Screens/OnboardingScreen/OnboardingViewModel.swift @@ -26,7 +26,7 @@ class OnboardingViewModel: OnboardingViewModelType, OnboardingViewModelProtocol super.init(initialViewState: OnboardingViewState()) } - override func process(viewAction: OnboardingViewAction) async { + override func process(viewAction: OnboardingViewAction) { switch viewAction { case .login: callback?(.login) diff --git a/ElementX/Sources/Screens/ReportContent/ReportContentCoordinator.swift b/ElementX/Sources/Screens/ReportContent/ReportContentCoordinator.swift index 70f389bb0..4e2bbe852 100644 --- a/ElementX/Sources/Screens/ReportContent/ReportContentCoordinator.swift +++ b/ElementX/Sources/Screens/ReportContent/ReportContentCoordinator.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import Combine import SwiftUI struct ReportContentCoordinatorParameters { @@ -31,6 +32,7 @@ enum ReportContentCoordinatorAction { final class ReportContentCoordinator: CoordinatorProtocol { private let parameters: ReportContentCoordinatorParameters private var viewModel: ReportContentViewModelProtocol + private var cancellables: Set = .init() var callback: ((ReportContentCoordinatorAction) -> Void)? @@ -43,21 +45,23 @@ final class ReportContentCoordinator: CoordinatorProtocol { // MARK: - Public func start() { - viewModel.callback = { [weak self] action in - guard let self else { return } - switch action { - case .submitStarted: - self.startLoading() - case let .submitFailed(error): - self.stopLoading() - self.showError(description: error.localizedDescription) - case .submitFinished: - self.stopLoading() - self.callback?(.finish) - case .cancel: - self.callback?(.cancel) + viewModel.actions + .sink { [weak self] action in + guard let self else { return } + switch action { + case .submitStarted: + self.startLoading() + case let .submitFailed(error): + self.stopLoading() + self.showError(description: error.localizedDescription) + case .submitFinished: + self.stopLoading() + self.callback?(.finish) + case .cancel: + self.callback?(.cancel) + } } - } + .store(in: &cancellables) } func stop() { diff --git a/ElementX/Sources/Screens/ReportContent/ReportContentViewModel.swift b/ElementX/Sources/Screens/ReportContent/ReportContentViewModel.swift index d039c06f4..0793d56d2 100644 --- a/ElementX/Sources/Screens/ReportContent/ReportContentViewModel.swift +++ b/ElementX/Sources/Screens/ReportContent/ReportContentViewModel.swift @@ -14,16 +14,20 @@ // limitations under the License. // +import Combine import SwiftUI typealias ReportContentViewModelType = StateStoreViewModel class ReportContentViewModel: ReportContentViewModelType, ReportContentViewModelProtocol { - var callback: ((ReportContentViewModelAction) -> Void)? - private let itemID: String private let senderID: String private let roomProxy: RoomProxyProtocol + private let actionsSubject: PassthroughSubject = .init() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } init(itemID: String, senderID: String, roomProxy: RoomProxyProtocol) { self.itemID = itemID @@ -35,34 +39,34 @@ class ReportContentViewModel: ReportContentViewModelType, ReportContentViewModel // MARK: - Public - override func process(viewAction: ReportContentViewAction) async { + override func process(viewAction: ReportContentViewAction) { switch viewAction { case .cancel: - callback?(.cancel) + actionsSubject.send(.cancel) case .submit: - await submitReport() + Task { await submitReport() } } } // MARK: Private private func submitReport() async { - callback?(.submitStarted) + actionsSubject.send(.submitStarted) if case let .failure(error) = await roomProxy.reportContent(itemID, reason: state.bindings.reasonText) { MXLog.error("Submit Report Content failed: \(error)") - callback?(.submitFailed(error: error)) + actionsSubject.send(.submitFailed(error: error)) return } // Ignore the sender if the user wants to. if state.bindings.ignoreUser, case let .failure(error) = await roomProxy.ignoreUser(senderID) { MXLog.error("Ignore user failed: \(error)") - callback?(.submitFailed(error: error)) + actionsSubject.send(.submitFailed(error: error)) return } MXLog.info("Submit Report Content succeeded") - callback?(.submitFinished) + actionsSubject.send(.submitFinished) } } diff --git a/ElementX/Sources/Screens/ReportContent/ReportContentViewModelProtocol.swift b/ElementX/Sources/Screens/ReportContent/ReportContentViewModelProtocol.swift index 6b04a6dc0..5b030f3c7 100644 --- a/ElementX/Sources/Screens/ReportContent/ReportContentViewModelProtocol.swift +++ b/ElementX/Sources/Screens/ReportContent/ReportContentViewModelProtocol.swift @@ -14,10 +14,11 @@ // limitations under the License. // +import Combine import Foundation @MainActor protocol ReportContentViewModelProtocol { - var callback: ((ReportContentViewModelAction) -> Void)? { get set } + var actions: AnyPublisher { get } var context: ReportContentViewModelType.Context { get } } diff --git a/ElementX/Sources/Screens/RoomDetails/RoomDetailsViewModel.swift b/ElementX/Sources/Screens/RoomDetails/RoomDetailsViewModel.swift index 07c3b9d12..640fb2339 100644 --- a/ElementX/Sources/Screens/RoomDetails/RoomDetailsViewModel.swift +++ b/ElementX/Sources/Screens/RoomDetails/RoomDetailsViewModel.swift @@ -70,7 +70,7 @@ class RoomDetailsViewModel: RoomDetailsViewModelType, RoomDetailsViewModelProtoc // MARK: - Public - override func process(viewAction: RoomDetailsViewAction) async { + override func process(viewAction: RoomDetailsViewAction) { switch viewAction { case .processTapPeople: callback?(.requestMemberDetailsPresentation(members)) @@ -81,15 +81,15 @@ class RoomDetailsViewModel: RoomDetailsViewModelType, RoomDetailsViewModelProtoc } state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(state: roomProxy.isPublic ? .public : .private) case .confirmLeave: - await leaveRoom() + Task { await leaveRoom() } case .processTapIgnore: state.bindings.ignoreUserRoomAlertItem = .init(action: .ignore) case .processTapUnignore: state.bindings.ignoreUserRoomAlertItem = .init(action: .unignore) case .ignoreConfirmed: - await ignore() + Task { await ignore() } case .unignoreConfirmed: - await unignore() + Task { await unignore() } } } diff --git a/ElementX/Sources/Screens/RoomMemberDetails/RoomMemberDetailsViewModel.swift b/ElementX/Sources/Screens/RoomMemberDetails/RoomMemberDetailsViewModel.swift index a40aba97e..605d8216e 100644 --- a/ElementX/Sources/Screens/RoomMemberDetails/RoomMemberDetailsViewModel.swift +++ b/ElementX/Sources/Screens/RoomMemberDetails/RoomMemberDetailsViewModel.swift @@ -32,21 +32,22 @@ class RoomMemberDetailsViewModel: RoomMemberDetailsViewModelType, RoomMemberDeta // MARK: - Public - override func process(viewAction: RoomMemberDetailsViewAction) async { + override func process(viewAction: RoomMemberDetailsViewAction) { switch viewAction { case .showUnignoreAlert: state.bindings.ignoreUserAlert = .init(action: .unignore) case .showIgnoreAlert: state.bindings.ignoreUserAlert = .init(action: .ignore) case .ignoreConfirmed: - await ignoreUser() + Task { await ignoreUser() } case .unignoreConfirmed: - await unignoreUser() + Task { await unignoreUser() } } } // MARK: - Private + @MainActor private func ignoreUser() async { state.isProcessingIgnoreRequest = true let result = await roomMemberProxy.ignoreUser() @@ -59,6 +60,7 @@ class RoomMemberDetailsViewModel: RoomMemberDetailsViewModelType, RoomMemberDeta } } + @MainActor private func unignoreUser() async { state.isProcessingIgnoreRequest = true let result = await roomMemberProxy.unignoreUser() diff --git a/ElementX/Sources/Screens/RoomMembers/RoomMembersListViewModel.swift b/ElementX/Sources/Screens/RoomMembers/RoomMembersListViewModel.swift index 17499faa8..085b94225 100644 --- a/ElementX/Sources/Screens/RoomMembers/RoomMembersListViewModel.swift +++ b/ElementX/Sources/Screens/RoomMembers/RoomMembersListViewModel.swift @@ -35,7 +35,7 @@ class RoomMembersListViewModel: RoomMembersListViewModelType, RoomMembersListVie // MARK: - Public - override func process(viewAction: RoomMembersListViewAction) async { + override func process(viewAction: RoomMembersListViewAction) { switch viewAction { case .selectMember(let id): guard let member = members.first(where: { $0.userID == id }) else { diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index ed621e354..e3cdb3bbc 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -87,33 +87,33 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol var callback: ((RoomScreenViewModelAction) -> Void)? // swiftlint:disable:next cyclomatic_complexity - override func process(viewAction: RoomScreenViewAction) async { + override func process(viewAction: RoomScreenViewAction) { switch viewAction { case .displayRoomDetails: callback?(.displayRoomDetails) case .paginateBackwards: - await paginateBackwards() + Task { await paginateBackwards() } case .itemAppeared(let id): - await timelineController.processItemAppearance(id) + Task { await timelineController.processItemAppearance(id) } case .itemDisappeared(let id): - await timelineController.processItemDisappearance(id) + Task { await timelineController.processItemDisappearance(id) } case .itemTapped(let id): - await itemTapped(with: id) + Task { await itemTapped(with: id) } case .itemDoubleTapped(let id): itemDoubleTapped(with: id) case .linkClicked(let url): MXLog.warning("Link clicked: \(url)") case .sendMessage: - await sendCurrentMessage() + Task { await sendCurrentMessage() } case .sendReaction(let emoji, let itemId): - await timelineController.sendReaction(emoji, to: itemId) + Task { await timelineController.sendReaction(emoji, to: itemId) } case .cancelReply: state.composerMode = .default case .cancelEdit: state.composerMode = .default state.bindings.composerText = "" case .markRoomAsRead: - await markRoomAsRead() + Task { await markRoomAsRead() } case .contextMenuAction(let itemID, let action): processContentMenuAction(action, itemID: itemID) case .displayCameraPicker: diff --git a/ElementX/Sources/Screens/SessionVerification/SessionVerificationViewModel.swift b/ElementX/Sources/Screens/SessionVerification/SessionVerificationViewModel.swift index d1c60337c..1768fa125 100644 --- a/ElementX/Sources/Screens/SessionVerification/SessionVerificationViewModel.swift +++ b/ElementX/Sources/Screens/SessionVerification/SessionVerificationViewModel.swift @@ -63,7 +63,7 @@ class SessionVerificationViewModel: SessionVerificationViewModelType, SessionVer .store(in: &cancellables) } - override func process(viewAction: SessionVerificationViewAction) async { + override func process(viewAction: SessionVerificationViewAction) { switch viewAction { case .requestVerification: stateMachine.processEvent(.requestVerification) diff --git a/ElementX/Sources/Screens/Settings/SettingsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/SettingsScreenViewModel.swift index 862b39c20..55f0fe403 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreenViewModel.swift @@ -73,7 +73,7 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo .store(in: &cancellables) } - override func process(viewAction: SettingsScreenViewAction) async { + override func process(viewAction: SettingsScreenViewAction) { switch viewAction { case .close: callback?(.close) diff --git a/ElementX/Sources/Screens/StartChat/StartChatViewModel.swift b/ElementX/Sources/Screens/StartChat/StartChatViewModel.swift index e35fb0985..1210e1f05 100644 --- a/ElementX/Sources/Screens/StartChat/StartChatViewModel.swift +++ b/ElementX/Sources/Screens/StartChat/StartChatViewModel.swift @@ -36,7 +36,7 @@ class StartChatViewModel: StartChatViewModelType, StartChatViewModelProtocol { // MARK: - Public - override func process(viewAction: StartChatViewAction) async { + override func process(viewAction: StartChatViewAction) { switch viewAction { case .close: callback?(.close) diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 353ebd3da..d2008fd25 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -322,7 +322,7 @@ class MockScreen: Identifiable { let coordinator = StartChatCoordinator(parameters: .init(userSession: MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider()))) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator - case .startChatSearchingNonExistingID: + case .startChatSearchingNonexistentID: let navigationStackCoordinator = NavigationStackCoordinator() let clientProxy = MockClientProxy(userID: "@mock:client.com") clientProxy.searchUsersResult = .success(.init(results: [.mockAlice], limited: true)) diff --git a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift index b0156bda7..ceaa68cde 100644 --- a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift +++ b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift @@ -50,7 +50,7 @@ enum UITestsScreenIdentifier: String { case reportContent case startChat case startChatWithSearchResults - case startChatSearchingNonExistingID + case startChatSearchingNonexistentID } extension UITestsScreenIdentifier: CustomStringConvertible { diff --git a/IntegrationTests/SupportingFiles/IntegrationTests.xctestplan b/IntegrationTests/SupportingFiles/IntegrationTests.xctestplan index 97171ce3a..5619366d9 100644 --- a/IntegrationTests/SupportingFiles/IntegrationTests.xctestplan +++ b/IntegrationTests/SupportingFiles/IntegrationTests.xctestplan @@ -9,6 +9,25 @@ } ], "defaultOptions" : { + "environmentVariableEntries" : [ + { + "key" : "INTEGRATION_TESTS_HOST", + "value" : "${INTEGRATION_TESTS_HOST}" + }, + { + "key" : "INTEGRATION_TESTS_PASSWORD", + "value" : "${INTEGRATION_TESTS_PASSWORD}" + }, + { + "key" : "INTEGRATION_TESTS_USERNAME", + "value" : "${INTEGRATION_TESTS_USERNAME}" + } + ], + "targetForVariableExpansion" : { + "containerPath" : "container:ElementX.xcodeproj", + "identifier" : "D3DB351B7FBE0F49649171FC", + "name" : "IntegrationTests" + }, "testTimeoutsEnabled" : true }, "testTargets" : [ diff --git a/IntegrationTests/SupportingFiles/target.yml b/IntegrationTests/SupportingFiles/target.yml index 405ce20d1..90092169d 100644 --- a/IntegrationTests/SupportingFiles/target.yml +++ b/IntegrationTests/SupportingFiles/target.yml @@ -14,10 +14,6 @@ schemes: run: config: Debug disableMainThreadChecker: false - environmentVariables: - INTEGRATION_TESTS_HOST: ${INTEGRATION_TESTS_HOST} - INTEGRATION_TESTS_USERNAME: ${INTEGRATION_TESTS_USERNAME} - INTEGRATION_TESTS_PASSWORD: ${INTEGRATION_TESTS_PASSWORD} test: config: Debug disableMainThreadChecker: false diff --git a/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateCoordinator.swift b/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateCoordinator.swift index 2b404659c..737079a68 100644 --- a/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateCoordinator.swift +++ b/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateCoordinator.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import Combine import SwiftUI struct TemplateCoordinatorParameters { @@ -30,8 +31,12 @@ enum TemplateCoordinatorAction { final class TemplateCoordinator: CoordinatorProtocol { private let parameters: TemplateCoordinatorParameters private var viewModel: TemplateViewModelProtocol + private let actionsSubject: PassthroughSubject = .init() + private var cancellables: Set = .init() - var callback: ((TemplateCoordinatorAction) -> Void)? + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } init(parameters: TemplateCoordinatorParameters) { self.parameters = parameters @@ -40,16 +45,17 @@ final class TemplateCoordinator: CoordinatorProtocol { } func start() { - viewModel.callback = { [weak self] action in + viewModel.actions.sink { [weak self] action in guard let self else { return } switch action { case .accept: MXLog.info("User accepted the prompt.") - self.callback?(.accept) + self.actionsSubject.send(.accept) case .cancel: - self.callback?(.cancel) + self.actionsSubject.send(.cancel) } } + .store(in: &cancellables) } func toPresentable() -> AnyView { diff --git a/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateViewModel.swift b/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateViewModel.swift index 8ab2ebe8f..c8e9f822d 100644 --- a/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateViewModel.swift +++ b/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateViewModel.swift @@ -14,12 +14,17 @@ // limitations under the License. // +import Combine import SwiftUI typealias TemplateViewModelType = StateStoreViewModel class TemplateViewModel: TemplateViewModelType, TemplateViewModelProtocol { - var callback: ((TemplateViewModelAction) -> Void)? + private var actionsSubject: PassthroughSubject = .init() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } init(promptType: TemplatePromptType, initialCount: Int = 0) { super.init(initialViewState: TemplateViewState(promptType: promptType, count: 0)) @@ -27,12 +32,12 @@ class TemplateViewModel: TemplateViewModelType, TemplateViewModelProtocol { // MARK: - Public - override func process(viewAction: TemplateViewAction) async { + override func process(viewAction: TemplateViewAction) { switch viewAction { case .accept: - callback?(.accept) + actionsSubject.send(.accept) case .cancel: - callback?(.cancel) + actionsSubject.send(.cancel) case .incrementCount: state.count += 1 case .decrementCount: diff --git a/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateViewModelProtocol.swift b/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateViewModelProtocol.swift index 40fe26120..47c5fd323 100644 --- a/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateViewModelProtocol.swift +++ b/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateViewModelProtocol.swift @@ -14,10 +14,10 @@ // limitations under the License. // -import Foundation +import Combine @MainActor protocol TemplateViewModelProtocol { - var callback: ((TemplateViewModelAction) -> Void)? { get set } + var actions: AnyPublisher { get } var context: TemplateViewModelType.Context { get } } diff --git a/Tools/Scripts/Templates/SimpleScreenExample/Tests/Unit/TemplateViewModelTests.swift b/Tools/Scripts/Templates/SimpleScreenExample/Tests/Unit/TemplateViewModelTests.swift index ab6ca0df1..950cf9a80 100644 --- a/Tools/Scripts/Templates/SimpleScreenExample/Tests/Unit/TemplateViewModelTests.swift +++ b/Tools/Scripts/Templates/SimpleScreenExample/Tests/Unit/TemplateViewModelTests.swift @@ -38,15 +38,12 @@ class TemplateScreenViewModelTests: XCTestCase { func testCounter() async throws { context.send(viewAction: .incrementCount) - await Task.yield() XCTAssertEqual(context.viewState.count, 1) context.send(viewAction: .incrementCount) - await Task.yield() XCTAssertEqual(context.viewState.count, 2) context.send(viewAction: .decrementCount) - await Task.yield() XCTAssertEqual(context.viewState.count, 1) } } diff --git a/UITests/Sources/BugReportUITests.swift b/UITests/Sources/BugReportUITests.swift index 724f0b32c..d6459a12c 100644 --- a/UITests/Sources/BugReportUITests.swift +++ b/UITests/Sources/BugReportUITests.swift @@ -25,21 +25,6 @@ class BugReportUITests: XCTestCase { app.assertScreenshot(.bugReport, step: 0) } - func testToggleSendingLogs() { - let app = Application.launch(.bugReport) - - // Don't know why, but there's an issue on CI where the toggle is tapped but doesn't respond. Waiting for - // it fixes this (even it it already exists). Reproducible by running the test after quitting the simulator. - let sendingLogsToggle = app.switches[A11yIdentifiers.bugReportScreen.sendLogs] - XCTAssertTrue(sendingLogsToggle.waitForExistence(timeout: 1)) - XCTAssertTrue(sendingLogsToggle.isOn) - - sendingLogsToggle.tap() - - XCTAssertFalse(sendingLogsToggle.isOn) - app.assertScreenshot(.bugReport, step: 1) - } - func testReportText() { let app = Application.launch(.bugReport) diff --git a/UITests/Sources/ReportContentScreenUITests.swift b/UITests/Sources/ReportContentScreenUITests.swift index 8a89e210a..659ce4eb1 100644 --- a/UITests/Sources/ReportContentScreenUITests.swift +++ b/UITests/Sources/ReportContentScreenUITests.swift @@ -22,19 +22,4 @@ class ReportContentScreenUITests: XCTestCase { let app = Application.launch(.reportContent) app.assertScreenshot(.reportContent, step: 0) } - - func testToggleIgnoreUser() { - let app = Application.launch(.reportContent) - - // Don't know why, but there's an issue on CI where the toggle is tapped but doesn't respond. Waiting for - // it fixes this (even it it already exists). Reproducible by running the test after quitting the simulator. - let sendingLogsToggle = app.switches[A11yIdentifiers.reportContent.ignoreUser] - XCTAssertTrue(sendingLogsToggle.waitForExistence(timeout: 1)) - XCTAssertFalse(sendingLogsToggle.isOn) - - sendingLogsToggle.tap() - - XCTAssertTrue(sendingLogsToggle.isOn) - app.assertScreenshot(.reportContent, step: 1) - } } diff --git a/UITests/Sources/StartChatScreenUITests.swift b/UITests/Sources/StartChatScreenUITests.swift index 386e17f72..ef50318dd 100644 --- a/UITests/Sources/StartChatScreenUITests.swift +++ b/UITests/Sources/StartChatScreenUITests.swift @@ -50,7 +50,7 @@ class StartChatScreenUITests: XCTestCase { } func testSearchExactNotExistingMatrixID() { - let app = Application.launch(.startChatSearchingInexistingID) + let app = Application.launch(.startChatSearchingNonexistentID) let searchField = app.searchFields.firstMatch searchField.clearAndTypeText("@a:b.com") XCTAssertFalse(app.staticTexts[A11yIdentifiers.startChatScreen.searchNoResults].waitForExistence(timeout: 1.0)) diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.bugReport-1.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.bugReport-1.png deleted file mode 100644 index 58d165db6..000000000 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.bugReport-1.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3d3a511a1c225751c5fe3bf6e296c12612e28efa7f101b533191fa862eace69a -size 114029 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.reportContent-1.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.reportContent-1.png deleted file mode 100644 index ecb2d81f9..000000000 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.reportContent-1.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:06d95ccd192c2c9550e4a405b76352d8f75d86a1d72cbc42dfab15743bcac2de -size 96766 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.bugReport-1.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.bugReport-1.png deleted file mode 100644 index 15e7d200c..000000000 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.bugReport-1.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6ed199f6c4b98bf600364b87010655c454a2dfeee9edc455e37988b60386e696 -size 146593 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.reportContent-1.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.reportContent-1.png deleted file mode 100644 index adeb41a6c..000000000 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.reportContent-1.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:63627788babeceb6940989acf8e07c7244d50b4ab84c97bc74b4e19b9ee6ce47 -size 120785 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.bugReport-1.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.bugReport-1.png deleted file mode 100644 index 6e4a82f9c..000000000 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.bugReport-1.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:22868875281c5277f94754837ecb9b85bcbdd5149c25552819e4e794395374e5 -size 144520 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.reportContent-1.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.reportContent-1.png deleted file mode 100644 index 3f5b5df37..000000000 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.reportContent-1.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:324756300a6ca0abca19892b5ff4fea9bcd1718fb9375ed87de8e9e981a06908 -size 118557 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.bugReport-1.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.bugReport-1.png deleted file mode 100644 index 3b30fd23e..000000000 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.bugReport-1.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c1d1fa4ff4b3a3981f78f3ac41e62d110d38f1bc465bb41b77683544fc0bda1d -size 210079 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.reportContent-1.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.reportContent-1.png deleted file mode 100644 index 77848b368..000000000 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.reportContent-1.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b4120dca6e5eb55094f9e301bb3fa6e538b6f9a1226f29a2ab4832886661b429 -size 163825 diff --git a/UnitTests/Sources/BugReportViewModelTests.swift b/UnitTests/Sources/BugReportViewModelTests.swift index 1b1d507b1..878f8e6ac 100644 --- a/UnitTests/Sources/BugReportViewModelTests.swift +++ b/UnitTests/Sources/BugReportViewModelTests.swift @@ -46,7 +46,6 @@ class BugReportViewModelTests: XCTestCase { let context = viewModel.context context.send(viewAction: .removeScreenshot) - try await Task.sleep(nanoseconds: 100_000_000) XCTAssertNil(context.viewState.screenshot) } @@ -58,7 +57,6 @@ class BugReportViewModelTests: XCTestCase { let context = viewModel.context XCTAssertNil(context.viewState.screenshot) context.send(viewAction: .attachScreenshot(UIImage.actions)) - try await Task.sleep(nanoseconds: 100_000_000) XCTAssert(context.viewState.screenshot == UIImage.actions) } @@ -70,19 +68,20 @@ class BugReportViewModelTests: XCTestCase { deviceID: nil, screenshot: nil, isModallyPresented: false) let context = viewModel.context - var isSuccess = false - viewModel.callback = { result in - switch result { - case .submitFinished: - isSuccess = true - default: break - } - } context.send(viewAction: .submit) - try await Task.sleep(for: .milliseconds(100)) + + _ = await viewModel + .actions + .values + .first { + guard case .submitFinished = $0 else { + return false + } + return true + } + XCTAssert(mockService.submitBugReportProgressListenerCallsCount == 1) XCTAssert(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport == BugReport(userID: "@mock.client.com", deviceID: nil, text: "", includeLogs: true, includeCrashLog: true, githubLabels: [], files: [])) - XCTAssertTrue(isSuccess) } func testSendReportWithError() async throws { @@ -95,20 +94,19 @@ class BugReportViewModelTests: XCTestCase { deviceID: nil, screenshot: nil, isModallyPresented: false) let context = viewModel.context - var isFailure = false - - viewModel.callback = { result in - switch result { - case .submitFailed: - isFailure = true - default: break - } - } - context.send(viewAction: .submit) - try await Task.sleep(for: .milliseconds(100)) + + _ = await viewModel + .actions + .values + .first { + guard case .submitFailed = $0 else { + return false + } + return true + } + XCTAssert(mockService.submitBugReportProgressListenerCallsCount == 1) XCTAssert(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport == BugReport(userID: "@mock.client.com", deviceID: nil, text: "", includeLogs: true, includeCrashLog: true, githubLabels: [], files: [])) - XCTAssertTrue(isFailure) } } diff --git a/UnitTests/Sources/Extensions/AsyncSequence.swift b/UnitTests/Sources/Extensions/AsyncSequence.swift new file mode 100644 index 000000000..3de8a6e02 --- /dev/null +++ b/UnitTests/Sources/Extensions/AsyncSequence.swift @@ -0,0 +1,21 @@ +// +// Copyright 2023 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. +// + +extension AsyncSequence { + func first() async rethrows -> Self.Element? { + try await first { _ in true } + } +} diff --git a/UnitTests/Sources/Extensions/Publisher.swift b/UnitTests/Sources/Extensions/Publisher.swift new file mode 100644 index 000000000..cfc848528 --- /dev/null +++ b/UnitTests/Sources/Extensions/Publisher.swift @@ -0,0 +1,31 @@ +// +// Copyright 2023 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 Combine + +extension Published.Publisher { + /// Returns the next output from the publisher skipping the current value stored into it (which is readable from the @Published property itself). + /// - Returns: the next output from the publisher + var nextValue: Output? { + get async { + var iterator = values.makeAsyncIterator() + + // skips the publisher's current value + _ = await iterator.next() + return await iterator.next() + } + } +} diff --git a/UnitTests/Sources/Extensions/ViewModelContext.swift b/UnitTests/Sources/Extensions/ViewModelContext.swift new file mode 100644 index 000000000..725eebbbe --- /dev/null +++ b/UnitTests/Sources/Extensions/ViewModelContext.swift @@ -0,0 +1,24 @@ +// +// Copyright 2023 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. +// + +@testable import ElementX + +extension ViewModelContext { + @discardableResult + func nextViewState() async -> ViewState? { + await $viewState.nextValue + } +} diff --git a/UnitTests/Sources/FilePreviewViewModelTests.swift b/UnitTests/Sources/FilePreviewViewModelTests.swift index 97c77d489..d5a5f1978 100644 --- a/UnitTests/Sources/FilePreviewViewModelTests.swift +++ b/UnitTests/Sources/FilePreviewViewModelTests.swift @@ -23,12 +23,12 @@ class FilePreviewScreenViewModelTests: XCTestCase { var viewModel: FilePreviewViewModelProtocol! var context: FilePreviewViewModelType.Context! - @MainActor override func setUpWithError() throws { + override func setUpWithError() throws { viewModel = FilePreviewViewModel(mediaFile: .unmanaged(url: URL(staticString: "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"))) context = viewModel.context } - @MainActor func testCancel() async throws { + func testCancel() async throws { var correctResult = false viewModel.callback = { result in switch result { diff --git a/UnitTests/Sources/HomeScreenViewModelTests.swift b/UnitTests/Sources/HomeScreenViewModelTests.swift index 5dc466f39..f5bf00c7a 100644 --- a/UnitTests/Sources/HomeScreenViewModelTests.swift +++ b/UnitTests/Sources/HomeScreenViewModelTests.swift @@ -18,18 +18,19 @@ import XCTest @testable import ElementX +@MainActor class HomeScreenViewModelTests: XCTestCase { var viewModel: HomeScreenViewModelProtocol! var context: HomeScreenViewModelType.Context! - @MainActor override func setUpWithError() throws { + override func setUpWithError() throws { viewModel = HomeScreenViewModel(userSession: MockUserSession(clientProxy: MockClientProxy(userID: "@mock:client.com"), mediaProvider: MockMediaProvider()), attributedStringBuilder: AttributedStringBuilder()) context = viewModel.context } - @MainActor func testSelectRoom() async throws { + func testSelectRoom() async throws { let mockRoomId = "mock_room_id" var correctResult = false var selectedRoomId = "" @@ -49,7 +50,7 @@ class HomeScreenViewModelTests: XCTestCase { XCTAssertEqual(mockRoomId, selectedRoomId) } - @MainActor func testTapUserAvatar() async throws { + func testTapUserAvatar() async throws { var correctResult = false viewModel.callback = { result in switch result { diff --git a/UnitTests/Sources/ReportContentViewModelTests.swift b/UnitTests/Sources/ReportContentViewModelTests.swift index 9aa0c8d23..193d40b6c 100644 --- a/UnitTests/Sources/ReportContentViewModelTests.swift +++ b/UnitTests/Sources/ReportContentViewModelTests.swift @@ -36,7 +36,8 @@ class ReportContentScreenViewModelTests: XCTestCase { viewModel.state.bindings.reasonText = reportReason viewModel.state.bindings.ignoreUser = false viewModel.context.send(viewAction: .submit) - await Task.yield() + + _ = await viewModel.actions.values.first() // Then the content should be reported, but the user should not be included. XCTAssertEqual(roomProxy.reportContentReasonCallsCount, 1, "The content should always be reported.") @@ -59,7 +60,8 @@ class ReportContentScreenViewModelTests: XCTestCase { viewModel.state.bindings.reasonText = reportReason viewModel.state.bindings.ignoreUser = true viewModel.context.send(viewAction: .submit) - await Task.yield() + + _ = await viewModel.actions.values.first() // Then the content should be reported, and the user should be ignored. XCTAssertEqual(roomProxy.reportContentReasonCallsCount, 1, "The content should always be reported.") diff --git a/UnitTests/Sources/RoomDetailsViewModelTests.swift b/UnitTests/Sources/RoomDetailsViewModelTests.swift index f4be17f83..d7dd6e0bf 100644 --- a/UnitTests/Sources/RoomDetailsViewModelTests.swift +++ b/UnitTests/Sources/RoomDetailsViewModelTests.swift @@ -34,24 +34,21 @@ class RoomDetailsScreenViewModelTests: XCTestCase { roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isPublic: true, members: mockedMembers)) viewModel = RoomDetailsViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) context.send(viewAction: .processTapLeave) - await Task.yield() XCTAssertEqual(context.leaveRoomAlertItem?.state, .public) XCTAssertEqual(context.leaveRoomAlertItem?.subtitle, L10n.leaveRoomAlertSubtitle) } - func testLeavRoomTappedWhenRoomNotPublic() async { + func testLeaveRoomTappedWhenRoomNotPublic() async { let mockedMembers: [RoomMemberProxyMock] = [.mockBob, .mockAlice] roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isPublic: false, members: mockedMembers)) viewModel = RoomDetailsViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) context.send(viewAction: .processTapLeave) - await Task.yield() XCTAssertEqual(context.leaveRoomAlertItem?.state, .private) XCTAssertEqual(context.leaveRoomAlertItem?.subtitle, L10n.leaveRoomAlertPrivateSubtitle) } func testLeaveRoomTappedWithLessThanTwoMembers() async { context.send(viewAction: .processTapLeave) - await Task.yield() XCTAssertEqual(context.leaveRoomAlertItem?.state, .empty) XCTAssertEqual(context.leaveRoomAlertItem?.subtitle, L10n.leaveRoomAlertEmptySubtitle) } @@ -78,7 +75,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { .failure(.failedLeavingRoom) } context.send(viewAction: .confirmLeave) - await Task.yield() + await context.nextViewState() XCTAssertEqual(roomProxyMock.leaveRoomCallsCount, 1) XCTAssertNotNil(context.alertInfo) } @@ -103,10 +100,10 @@ class RoomDetailsScreenViewModelTests: XCTestCase { XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient)) context.send(viewAction: .ignoreConfirmed) - await Task.yield() + await context.nextViewState() XCTAssertTrue(context.viewState.isProcessingIgnoreRequest) - try await Task.sleep(for: .milliseconds(10)) + await context.nextViewState() XCTAssertFalse(context.viewState.isProcessingIgnoreRequest) XCTAssert(context.viewState.dmRecipient?.isIgnored == true) } @@ -123,10 +120,10 @@ class RoomDetailsScreenViewModelTests: XCTestCase { XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient)) context.send(viewAction: .ignoreConfirmed) - await Task.yield() + await context.nextViewState() XCTAssertTrue(context.viewState.isProcessingIgnoreRequest) - try await Task.sleep(for: .milliseconds(10)) + await context.nextViewState() XCTAssertFalse(context.viewState.isProcessingIgnoreRequest) XCTAssert(context.viewState.dmRecipient?.isIgnored == false) XCTAssertNotNil(context.alertInfo) @@ -144,10 +141,10 @@ class RoomDetailsScreenViewModelTests: XCTestCase { XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient)) context.send(viewAction: .unignoreConfirmed) - await Task.yield() + await context.nextViewState() XCTAssertTrue(context.viewState.isProcessingIgnoreRequest) - try await Task.sleep(for: .milliseconds(10)) + await context.nextViewState() XCTAssertFalse(context.viewState.isProcessingIgnoreRequest) XCTAssert(context.viewState.dmRecipient?.isIgnored == false) } @@ -164,10 +161,10 @@ class RoomDetailsScreenViewModelTests: XCTestCase { XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient)) context.send(viewAction: .unignoreConfirmed) - await Task.yield() + await context.nextViewState() XCTAssertTrue(context.viewState.isProcessingIgnoreRequest) - try await Task.sleep(for: .milliseconds(10)) + await context.nextViewState() XCTAssertFalse(context.viewState.isProcessingIgnoreRequest) XCTAssert(context.viewState.dmRecipient?.isIgnored == true) XCTAssertNotNil(context.alertInfo) diff --git a/UnitTests/Sources/RoomMemberDetailsViewModelTests.swift b/UnitTests/Sources/RoomMemberDetailsViewModelTests.swift index c31ff660d..f88569175 100644 --- a/UnitTests/Sources/RoomMemberDetailsViewModelTests.swift +++ b/UnitTests/Sources/RoomMemberDetailsViewModelTests.swift @@ -42,14 +42,13 @@ class RoomMemberDetailsViewModelTests: XCTestCase { viewModel = RoomMemberDetailsViewModel(roomMemberProxy: roomMemberProxyMock, mediaProvider: MockMediaProvider()) context.send(viewAction: .showIgnoreAlert) - await Task.yield() XCTAssertEqual(context.ignoreUserAlert, .init(action: .ignore)) context.send(viewAction: .ignoreConfirmed) - await Task.yield() + await context.nextViewState() XCTAssertTrue(context.viewState.isProcessingIgnoreRequest) XCTAssertFalse(context.viewState.details.isIgnored) - try await Task.sleep(for: .milliseconds(10)) + await context.nextViewState() XCTAssertFalse(context.viewState.isProcessingIgnoreRequest) XCTAssertTrue(context.viewState.details.isIgnored) } @@ -61,16 +60,14 @@ class RoomMemberDetailsViewModelTests: XCTestCase { return .failure(.ignoreUserFailed) } viewModel = RoomMemberDetailsViewModel(roomMemberProxy: roomMemberProxyMock, mediaProvider: MockMediaProvider()) - context.send(viewAction: .showIgnoreAlert) - await Task.yield() XCTAssertEqual(context.ignoreUserAlert, .init(action: .ignore)) context.send(viewAction: .ignoreConfirmed) - await Task.yield() + await context.nextViewState() XCTAssertTrue(context.viewState.isProcessingIgnoreRequest) XCTAssertFalse(context.viewState.details.isIgnored) - try await Task.sleep(for: .milliseconds(10)) + await context.nextViewState() XCTAssertFalse(context.viewState.isProcessingIgnoreRequest) XCTAssertNotNil(context.errorAlert) XCTAssertFalse(context.viewState.details.isIgnored) @@ -85,14 +82,13 @@ class RoomMemberDetailsViewModelTests: XCTestCase { viewModel = RoomMemberDetailsViewModel(roomMemberProxy: roomMemberProxyMock, mediaProvider: MockMediaProvider()) context.send(viewAction: .showUnignoreAlert) - await Task.yield() XCTAssertEqual(context.ignoreUserAlert, .init(action: .unignore)) context.send(viewAction: .unignoreConfirmed) - await Task.yield() + await context.nextViewState() XCTAssertTrue(context.viewState.isProcessingIgnoreRequest) XCTAssertTrue(context.viewState.details.isIgnored) - try await Task.sleep(for: .milliseconds(10)) + await context.nextViewState() XCTAssertFalse(context.viewState.isProcessingIgnoreRequest) XCTAssertFalse(context.viewState.details.isIgnored) } @@ -106,14 +102,13 @@ class RoomMemberDetailsViewModelTests: XCTestCase { viewModel = RoomMemberDetailsViewModel(roomMemberProxy: roomMemberProxyMock, mediaProvider: MockMediaProvider()) context.send(viewAction: .showUnignoreAlert) - await Task.yield() XCTAssertEqual(context.ignoreUserAlert, .init(action: .unignore)) context.send(viewAction: .unignoreConfirmed) - await Task.yield() + await context.nextViewState() XCTAssertTrue(context.viewState.isProcessingIgnoreRequest) XCTAssertTrue(context.viewState.details.isIgnored) - try await Task.sleep(for: .milliseconds(10)) + await context.nextViewState() XCTAssertFalse(context.viewState.isProcessingIgnoreRequest) XCTAssertTrue(context.viewState.details.isIgnored) XCTAssertNotNil(context.errorAlert) diff --git a/UnitTests/Sources/SessionVerificationViewModelTests.swift b/UnitTests/Sources/SessionVerificationViewModelTests.swift index 4dabc43e2..a20f82eff 100644 --- a/UnitTests/Sources/SessionVerificationViewModelTests.swift +++ b/UnitTests/Sources/SessionVerificationViewModelTests.swift @@ -25,7 +25,6 @@ class SessionVerificationViewModelTests: XCTestCase { var context: SessionVerificationViewModelType.Context! var sessionVerificationController: SessionVerificationControllerProxyMock! - @MainActor override func setUpWithError() throws { sessionVerificationController = SessionVerificationControllerProxyMock.configureMock() viewModel = SessionVerificationViewModel(sessionVerificationControllerProxy: sessionVerificationController) @@ -49,18 +48,14 @@ class SessionVerificationViewModelTests: XCTestCase { context.send(viewAction: .close) - await Task.yield() - XCTAssertEqual(context.viewState.verificationState, .cancelling) - try await Task.sleep(for: .milliseconds(100)) + await context.nextViewState() XCTAssertEqual(context.viewState.verificationState, .cancelled) context.send(viewAction: .restart) - await Task.yield() - XCTAssertEqual(context.viewState.verificationState, .initial) XCTAssert(sessionVerificationController.requestVerificationCallsCount == 1) diff --git a/UnitTests/SupportingFiles/target.yml b/UnitTests/SupportingFiles/target.yml index 378229bea..db013a09d 100644 --- a/UnitTests/SupportingFiles/target.yml +++ b/UnitTests/SupportingFiles/target.yml @@ -50,4 +50,3 @@ targets: - path: ../../Tools/Scripts/Templates/SimpleScreenExample/Tests/Unit - path: ../Resources - path: ../../ElementX/Sources/Other/InfoPlistReader.swift - - path: ../../ElementX/Sources/Other/Extensions/Publisher.swift