From ce1f213f42d088ec78e48d951ac3e791dd77e278 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 14 Sep 2023 12:53:33 +0300 Subject: [PATCH] Switch callbacks to combine (#1710) * #750 - Convert the SoftLogoutScreen to combine * #750 - Convert the UserSessionFlowCoordinator to Combine * #750 - Convert the AnalyticsPromptScreen to Combine * #750 - Convert the LoginScreen to Combine * #750 - Convert the ServerSelectionScreen to Combine * #750 - Convert the EmojiPickerScreen to Combine * #750 - Convert the HomeScreen to Combine * #750 - Convert the MediaUploadPreviewScreen to Combine * #750 - Convert the OnboardingScreen to Combine * Rename `Onboarding` to `OnboardingScreen` * #750 - Convert the ReportContentScreen to Combine * #750 - Convert the RoomDetailsSscreen to Combine * #750 - Convert the RoomMemberDetailsScreen to Combine * #750 - Convert the RoomMembersListScreen to Combine * #750 - Convert the SessionVerificationScreen to Combine * #750 - Convert the SettingsScreen to Combine * #750 - Convert the AdvancedSettingsScreen to Combine * #750 - Convert the DeveloperOptionsScreen to Combine * Fix the unit tests * Use .sink action and the same cancellables constructor everywhere * Cleanup cancellables when setting up tests --- ElementX.xcodeproj/project.pbxproj | 56 +++++----- .../xcshareddata/swiftpm/Package.resolved | 2 +- .../Sources/Application/AppCoordinator.swift | 44 +++++--- .../RoomFlowCoordinator.swift | 54 ++++++---- .../UserSessionFlowCoordinator.swift | 100 +++++++++-------- .../AnalyticsPromptScreenCoordinator.swift | 43 +++++--- .../AnalyticsPromptScreenViewModel.swift | 10 +- ...alyticsPromptScreenViewModelProtocol.swift | 4 +- .../AuthenticationCoordinator.swift | 101 ++++++++++-------- .../LoginScreen/LoginScreenCoordinator.swift | 45 ++++---- .../LoginScreen/LoginScreenViewModel.swift | 13 ++- .../LoginScreenViewModelProtocol.swift | 4 +- .../ServerConfirmationScreenCoordinator.swift | 2 +- .../ServerSelectionScreenCoordinator.swift | 30 ++++-- .../ServerSelectionScreenViewModel.swift | 11 +- ...rverSelectionScreenViewModelProtocol.swift | 4 +- .../SoftLogoutScreenCoordinator.swift | 39 ++++--- .../SoftLogoutScreenViewModel.swift | 15 ++- .../SoftLogoutScreenViewModelProtocol.swift | 4 +- .../WaitlistScreenCoordinator.swift | 2 +- .../BugReportScreenCoordinator.swift | 8 +- .../CreatePollScreenCoordinator.swift | 2 +- .../CreateRoom/CreateRoomCoordinator.swift | 2 +- .../EmojiPickerScreenCoordinator.swift | 28 +++-- .../EmojiPickerScreenViewModel.swift | 11 +- .../EmojiPickerScreenViewModelProtocol.swift | 4 +- .../HomeScreen/HomeScreenCoordinator.swift | 53 ++++----- .../HomeScreen/HomeScreenViewModel.swift | 26 +++-- .../HomeScreenViewModelProtocol.swift | 5 +- .../InviteUsersScreenCoordinator.swift | 2 +- .../InvitesScreenCoordinator.swift | 2 +- .../StaticLocationScreenCoordinator.swift | 2 +- .../MediaUploadPreviewScreenCoordinator.swift | 28 +++-- .../MediaUploadPreviewScreenViewModel.swift | 10 +- ...UploadPreviewScreenViewModelProtocol.swift | 4 +- .../MessageForwardingScreenCoordinator.swift | 2 +- .../MigrationScreenCoordinator.swift | 2 +- ...wift => OnboardingScreenCoordinator.swift} | 31 +++--- ...els.swift => OnboardingScreenModels.swift} | 10 +- ....swift => OnboardingScreenViewModel.swift} | 20 ++-- ...> OnboardingScreenViewModelProtocol.swift} | 8 +- .../View/OnboardingScreen.swift | 6 +- ... => OnboardingScreenBackgroundImage.swift} | 2 +- .../ReportContentScreenCoordinator.swift | 20 ++-- .../RoomDetailsEditScreenCoordinator.swift | 2 +- .../RoomDetailsScreenCoordinator.swift | 52 +++++---- .../RoomDetailsScreenViewModel.swift | 16 +-- .../RoomDetailsScreenViewModelProtocol.swift | 4 +- .../RoomMemberDetailsScreenCoordinator.swift | 8 +- .../RoomMemberDetailsScreenViewModel.swift | 7 +- ...MemberDetailsScreenViewModelProtocol.swift | 4 +- .../RoomMembersListScreenCoordinator.swift | 28 +++-- .../RoomMembersListScreenViewModel.swift | 11 +- ...omMembersListScreenViewModelProtocol.swift | 4 +- ...otificationSettingsScreenCoordinator.swift | 2 +- .../RoomScreen/RoomScreenCoordinator.swift | 4 +- .../TimelineTableViewController.swift | 2 +- ...SessionVerificationScreenCoordinator.swift | 28 +++-- .../SessionVerificationScreenViewModel.swift | 23 ++-- ...nVerificationScreenViewModelProtocol.swift | 4 +- .../AdvancedSettingsScreenCoordinator.swift | 8 +- .../AdvancedSettingsScreenViewModel.swift | 7 +- ...ancedSettingsScreenViewModelProtocol.swift | 4 +- .../DeveloperOptionsScreenCoordinator.swift | 25 +++-- .../DeveloperOptionsScreenViewModel.swift | 9 +- ...eloperOptionsScreenViewModelProtocol.swift | 4 +- ...icationSettingsEditScreenCoordinator.swift | 2 +- ...otificationSettingsScreenCoordinator.swift | 2 +- .../SettingsScreenCoordinator.swift | 96 ++++++++++------- .../SettingsScreenViewModel.swift | 28 ++--- .../SettingsScreenViewModelProtocol.swift | 4 +- .../StartChatScreenCoordinator.swift | 10 +- .../View/WelcomeScreen.swift | 2 +- .../WelcomeScreenScreenCoordinator.swift | 2 +- .../UITests/UITestsAppCoordinator.swift | 4 +- .../ElementX/TemplateScreenCoordinator.swift | 2 +- ...ts.swift => OnboardingScreenUITests.swift} | 2 +- .../Sources/CreateRoomViewModelTests.swift | 3 +- .../Sources/HomeScreenViewModelTests.swift | 81 +++++++------- .../Sources/InviteUsersViewModelTests.swift | 6 +- ...essageForwardingScreenViewModelTests.swift | 1 + ...t => OnboardingScreenViewModelTests.swift} | 2 +- .../Sources/RoomDetailsViewModelTests.swift | 37 ++++--- .../Sources/RoomFlowCoordinatorTests.swift | 3 +- ...ficationSettingsScreenViewModelTests.swift | 11 +- .../Sources/RoomScreenViewModelTests.swift | 1 + .../Sources/SettingsViewModelTests.swift | 40 ++++--- .../StaticLocationScreenViewModelTests.swift | 3 +- .../UserSession/UserSessionTests.swift | 4 +- 89 files changed, 884 insertions(+), 589 deletions(-) rename ElementX/Sources/Screens/OnboardingScreen/{OnboardingCoordinator.swift => OnboardingScreenCoordinator.swift} (51%) rename ElementX/Sources/Screens/OnboardingScreen/{OnboardingModels.swift => OnboardingScreenModels.swift} (80%) rename ElementX/Sources/Screens/OnboardingScreen/{OnboardingViewModel.swift => OnboardingScreenViewModel.swift} (53%) rename ElementX/Sources/Screens/OnboardingScreen/{OnboardingViewModelProtocol.swift => OnboardingScreenViewModelProtocol.swift} (75%) rename ElementX/Sources/Screens/OnboardingScreen/View/{OnboardingBackgroundImage.swift => OnboardingScreenBackgroundImage.swift} (95%) rename UITests/Sources/{OnboardingUITests.swift => OnboardingScreenUITests.swift} (94%) rename UnitTests/Sources/{OnboardingViewModelTests.swift => OnboardingScreenViewModelTests.swift} (93%) diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index afc2b0fdf..4d8b7dadf 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -39,6 +39,7 @@ 0BDA19079FD6E17C5AC62E22 /* RoomDetailsEditScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB06F22CFA34885B40976061 /* RoomDetailsEditScreen.swift */; }; 0BE4D5CBF86956410F071F91 /* CreateRoomViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15A657D96779D1DEB8EF1327 /* CreateRoomViewModel.swift */; }; 0BFA67AFD757EE2BA569836A /* ScrollViewAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53482ECA4B6633961EC224F5 /* ScrollViewAdapter.swift */; }; + 0C26A1588B17DCDE5F490FE3 /* OnboardingScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D53D6BB7E8E5EC031281872C /* OnboardingScreenViewModelTests.swift */; }; 0C47AE2CA7929CB3B0E2D793 /* ServerSelectionScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0685156EB62D7E243F097CFC /* ServerSelectionScreenViewModelProtocol.swift */; }; 0C58A846F61949B1D545D661 /* NoticeRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 421E716C521F96D24ECE69B3 /* NoticeRoomTimelineItem.swift */; }; 0C797CD650DFD2876BEC5173 /* CollapsibleReactionLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F7C6DDBB5D12F6EF6A3D6E1 /* CollapsibleReactionLayout.swift */; }; @@ -138,7 +139,6 @@ 2C4C750D0039AFABDF24236C /* TemplateScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 342BEBC3C5FC3F9943C41C4C /* TemplateScreenViewModelProtocol.swift */; }; 2C5E832434EE94E21AB3B238 /* EmojiPickerScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3EAE3E9D5EF4A6D5D9C6CFD /* EmojiPickerScreenViewModel.swift */; }; 2CA6ABBC9A88EB89EA52FCCB /* ConfettiScene.scn in Resources */ = {isa = PBXBuildFile; fileRef = B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */; }; - 2CB6787E25B11711518E9588 /* OnboardingCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6281B199D8A8F0892490C2E /* OnboardingCoordinator.swift */; }; 2DA90E38FF4E696825810C1A /* WaitlistScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECB08484CD5D77C9BF97AA78 /* WaitlistScreenUITests.swift */; }; 2E43A3D221BE9587BC19C3F1 /* MatrixEntityRegexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */; }; 2E8C6672D0EE7D5B1BEDB8E2 /* ServerConfirmationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7478623CECC9438014244BA /* ServerConfirmationScreen.swift */; }; @@ -152,7 +152,6 @@ 30CC4F796B27BE8B1DFDBF5A /* NSEUserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAA2832D93EC7D2608703FB /* NSEUserSession.swift */; }; 3113065AABBC14CEAE6843FA /* UserSessionFlowCoordinatorStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8774CF614849664B5B3C2A1 /* UserSessionFlowCoordinatorStateMachine.swift */; }; 3116693C5EB476E028990416 /* XCTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74611A4182DCF5F4D42696EC /* XCTestCase.swift */; }; - 329571083B132E4941131835 /* OnboardingBackgroundImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 686BCFA37AC6C67FF973CE67 /* OnboardingBackgroundImage.swift */; }; 32B7891D937377A59606EDFC /* UserFlowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21DD8599815136EFF5B73F38 /* UserFlowTests.swift */; }; 339BC18777912E1989F2F17D /* Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584A61D9C459FAFEF038A7C0 /* Section.swift */; }; 339D847497C51F2B36E3666B /* FixedIconSizeLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3518637393394901BF5BFAC3 /* FixedIconSizeLabelStyle.swift */; }; @@ -178,6 +177,7 @@ 388D39ED9FE1122EA6D76BF2 /* Common.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1BC84BA0AF11C2128D58ABD /* Common.swift */; }; 39929D29B265C3F6606047DE /* AlignedScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8872E9C5E91E9F2BFC4EBCCA /* AlignedScrollView.swift */; }; 3A08584ECDD4A4541DBF21F8 /* EmojiLoaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 201305507D7DFD16E544563A /* EmojiLoaderProtocol.swift */; }; + 3A5BD701D1AC916AC534F52C /* OnboardingScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB26F24164E9461B2054D0B3 /* OnboardingScreenModels.swift */; }; 3A64A93A651A3CB8774ADE8E /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = BA93CD75CCE486660C9040BD /* Collections */; }; 3A7DD0D13B0FB8876D69D829 /* TextBasedRoomTimelineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AB2C848BB9A7A9B618B7B89 /* TextBasedRoomTimelineTests.swift */; }; 3B0F9B57D25B07E66F15762A /* MediaUploadPreviewScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2E7C987AE5DC9087BB19F7D /* MediaUploadPreviewScreenModels.swift */; }; @@ -214,6 +214,7 @@ 46A261AA898344A1F3C406B1 /* ReportContentScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCE3636E3D01477C8B2E9D0 /* ReportContentScreenModels.swift */; }; 46BA7F4B4D3A7164DED44B88 /* FullscreenDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 565F1B2B300597C616B37888 /* FullscreenDialog.swift */; }; 46D1E2940ED8CCBF62FE8854 /* CreatePollScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EA0F71A3A400A202E15318 /* CreatePollScreen.swift */; }; + 4714991754A08B58B4D7ED85 /* OnboardingScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F27BAB69EB568369F1F6B3 /* OnboardingScreenViewModelProtocol.swift */; }; 47305C0911C9E1AA774A4000 /* TemplateScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA90BD288E5AE6BC643AFDDF /* TemplateScreenCoordinator.swift */; }; 4799A852132F1744E2825994 /* CreateRoomViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340179A0FC1AD4AEDA7FC134 /* CreateRoomViewModelProtocol.swift */; }; 484202C5D50983442D24D061 /* AttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BD6ED18E2EB61E28C340AD /* AttributedString.swift */; }; @@ -278,7 +279,6 @@ 5D2AF8C0DF872E7985F8FE54 /* TimelineDeliveryStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AC06FC11B6638F7BF1670E /* TimelineDeliveryStatusView.swift */; }; 5D53AE9342A4C06B704247ED /* MediaLoaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A02406480C351B8C6E0682C /* MediaLoaderProtocol.swift */; }; 5D70FAE4D2BF4553AFFFFE41 /* NotificationItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */; }; - 5D7960B32C350FA93F48D02B /* OnboardingModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB33A751BFDA223BDD106EC0 /* OnboardingModels.swift */; }; 5DD85A0FE3D85AEC3C7EFE36 /* DeveloperOptionsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7C7CFA6B2A62A685FF6CE3 /* DeveloperOptionsScreenCoordinator.swift */; }; 5E0F2E612718BB4397A6D40A /* TextRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */; }; 5E415EF9A5D31B1690CE27F5 /* CreatePollScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5DDF245FA51CF75F89E58A4 /* CreatePollScreenUITests.swift */; }; @@ -321,7 +321,6 @@ 69C7B956B74BEC3DB88224EA /* NavigationSplitCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78913D6E120D46138E97C107 /* NavigationSplitCoordinatorTests.swift */; }; 6A0E7551E0D1793245F34CDD /* ClientError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09A267106B9585D3D0CFC0D /* ClientError.swift */; }; 6AD722DD92E465E56D2885AB /* BugReportScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA919F521E9F0EE3638AFC85 /* BugReportScreen.swift */; }; - 6B15FF984906AAFCF9DC4F58 /* OnboardingUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C88046D6A070D9827181C4D /* OnboardingUITests.swift */; }; 6B31508C6334C617360C2EAB /* RoomMemberDetailsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC589E641AE46EFB2962534D /* RoomMemberDetailsViewModelTests.swift */; }; 6B4BF4A6450F55939B49FAEF /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67779D9A1B797285A09B7720 /* PollOptionView.swift */; }; 6BB6944443C421C722ED1E7D /* portrait_test_video.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = F2D513D2477B57F90E98EEC0 /* portrait_test_video.mp4 */; }; @@ -377,6 +376,7 @@ 7C384A8E54A4B60A14CDE8E5 /* WaitlistScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12F1E7F9C2BE8BB751037826 /* WaitlistScreenCoordinator.swift */; }; 7C6376192F578E0BA801BFEC /* AnalyticsSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C64A14EE89928207E3B42B /* AnalyticsSettingsScreenModels.swift */; }; 7CD16990BA843BE9ED639129 /* ImageRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DFE4453AB0B34C203447162 /* ImageRoomTimelineItem.swift */; }; + 7CFCC177F0ED083867FAD9C9 /* OnboardingScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E727F7E0BCE8A0BBFD33FF /* OnboardingScreenCoordinator.swift */; }; 7E2BB42805C59DB57E95610F /* PillView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7773CBFDBD458E0B7E270507 /* PillView.swift */; }; 7E91BAC17963ED41208F489B /* UserSessionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8BDC092D817B68CD9040C5 /* UserSessionStore.swift */; }; 7ECF12D5DCD69F67BD3E3842 /* RoomTimelineControllerFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FE0CDF1FFA92EA7EE17B0B /* RoomTimelineControllerFactoryProtocol.swift */; }; @@ -445,6 +445,7 @@ 90DF83A6A347F7EE7EDE89EE /* AttributedStringBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */; }; 90EB25D13AE6EEF034BDE9D2 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D52BAA5BADB06E5E8C295D /* Assets.swift */; }; 91ABC91758A6E4A5FAA2E9C4 /* ReadReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 314F1C79850BE46E8ABEAFCB /* ReadReceipt.swift */; }; + 92133B170A1F917685E9FF78 /* OnboardingScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D168471461717AF5689F64B /* OnboardingScreenUITests.swift */; }; 9219640F4D980CFC5FE855AD /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 536E72DCBEEC4A1FE66CFDCE /* target.yml */; }; 92D9088B901CEBB1A99ECA4E /* RoomMemberProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36FD673E24FBFCFDF398716A /* RoomMemberProxyMock.swift */; }; 93875ADD456142D20823ED24 /* ServerSelectionViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */; }; @@ -466,7 +467,6 @@ 981853650217B6C8ECDD998C /* NavigationRootCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F875D71347DC81EAE7687446 /* NavigationRootCoordinatorTests.swift */; }; 983896D611ABF52A5C37498D /* RoomSummaryProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */; }; 988BA75A182738150894A23F /* UserIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8AE4B3273BA189FDCD4055C /* UserIndicator.swift */; }; - 992477AB8E3F3C36D627D32E /* OnboardingViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BC4437C107D52ED19357DFC /* OnboardingViewModelProtocol.swift */; }; 992F5E750F5030C4BA2D0D03 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 01C4C7DB37597D7D8379511A /* Assets.xcassets */; }; 9965CB800CE6BC74ACA969FC /* EncryptedHistoryRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75697AB5E64A12F1F069F511 /* EncryptedHistoryRoomTimelineView.swift */; }; 99ED42B8F8D6BFB1DBCF4C45 /* AnalyticsEvents in Frameworks */ = {isa = PBXBuildFile; productRef = D661CAB418C075A94306A792 /* AnalyticsEvents */; }; @@ -518,6 +518,7 @@ A494741843F087881299ACF0 /* RestorationToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3558A15CFB934F9229301527 /* RestorationToken.swift */; }; A4E885358D7DD5A072A06824 /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = CCE5BF78B125320CBF3BB834 /* PostHog */; }; A5B9EF45C7B8ACEB4954AE36 /* LoginScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9780389F8A53E4D26E23DD03 /* LoginScreenViewModelProtocol.swift */; }; + A5C5C18671EDD2747AC16D2D /* OnboardingScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49C1CEBA9BCF5D2AD1884FA /* OnboardingScreenViewModel.swift */; }; A5D551E5691749066E0E0C44 /* RoomDetailsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837B440C4705E4B899BCB899 /* RoomDetailsScreenViewModel.swift */; }; A680F54935A6ADEA4ED6C38F /* TimelineItemStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A4C9547BBFEEF30AA11329B /* TimelineItemStatusView.swift */; }; A6D4C5EEA85A6A0ABA1559D6 /* RoomDetailsEditScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16D09C79746BDCD9173EB3A7 /* RoomDetailsEditScreenModels.swift */; }; @@ -612,6 +613,7 @@ C0090506A52A1991BAF4BA68 /* NotificationSettingsChatType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07579F9C29001E40715F3014 /* NotificationSettingsChatType.swift */; }; C051475DFF4C8EBDDF4DC8E4 /* StartChatScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = B99E13633862847D8B7E2815 /* StartChatScreenModels.swift */; }; C08AAE7563E0722C9383F51C /* RoomMembersListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8E176484A89BAC389D4076 /* RoomMembersListScreen.swift */; }; + C0DC02E2B91DC76A4D1A0E7F /* OnboardingScreenBackgroundImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3450F4C32D73532DBBC1A2 /* OnboardingScreenBackgroundImage.swift */; }; C11939FDC40716C4387275A4 /* NotificationSettingsEditScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8544F7058D31DBEB8DBFF0F5 /* NotificationSettingsEditScreenViewModelTests.swift */; }; C13128AAA787A4C2CBE4EE82 /* MessageForwardingScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC10CCC8D68B863E20660DBC /* MessageForwardingScreenViewModelProtocol.swift */; }; C1910A16BDF131FECA77BE22 /* EmojiPickerScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEA38B9851CFCC4D67F5587D /* EmojiPickerScreenCoordinator.swift */; }; @@ -662,7 +664,6 @@ CDCA8A559E098503DDE29477 /* AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5C6FBF97B6EED3D4FA5EFF /* AttributedStringBuilder.swift */; }; CE1694C7BB93C3311524EF28 /* Untranslated.strings in Resources */ = {isa = PBXBuildFile; fileRef = D2F7194F440375338F8E2487 /* Untranslated.strings */; }; CE6F237360875D3D573FD0B2 /* RoomNotificationSettingsProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD6B522BD637845AB9570B10 /* RoomNotificationSettingsProxy.swift */; }; - CE7148E80F09B7305E026AC6 /* OnboardingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1198B925F4A88DA74083662 /* OnboardingViewModel.swift */; }; CE9530A4CA661E090635C2F2 /* NotificationItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */; }; CEB8FB1269DE20536608B957 /* LoginMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B41FABA2B0AEF4389986495 /* LoginMode.swift */; }; CF3827071B0BC9638BD44F5D /* WaitlistScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AB58EF0176D4CFB1040DA22 /* WaitlistScreenViewModel.swift */; }; @@ -797,7 +798,6 @@ F94000E3D91B11C527DA8807 /* UserProfileCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 923485F85E1D765EF9D20E88 /* UserProfileCell.swift */; }; F9842667B68DC6FA1F9ECCBB /* NSItemProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72EFC8C634469F9262659C7 /* NSItemProvider.swift */; }; F99FB21EFC6D99D247FE7CBE /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DE8DC9B3FBA402117DC4C49F /* Kingfisher */; }; - F9F6D2883BBEBB9A3789A137 /* OnboardingViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00A941F289F6AB876BA3361A /* OnboardingViewModelTests.swift */; }; FA2BBAE9FC5E2E9F960C0980 /* NavigationCoordinators.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F28602AC7AC881AED37EBA /* NavigationCoordinators.swift */; }; FA4296218444C48BC890F46B /* RoomMemberDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B35311C7FED04B0E1B80C2 /* RoomMemberDetails.swift */; }; FA5A7E32B1920FCB4EEDC1BA /* RoomDetailsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6493AC9979CEB1410302BFE3 /* RoomDetailsScreenCoordinator.swift */; }; @@ -864,7 +864,6 @@ /* Begin PBXFileReference section */ 00245D40CD90FD71D6A05239 /* EmojiPickerScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreen.swift; sourceTree = ""; }; - 00A941F289F6AB876BA3361A /* OnboardingViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModelTests.swift; sourceTree = ""; }; 00E5B2CBEF8F96424F095508 /* RoomDetailsEditScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenViewModelTests.swift; sourceTree = ""; }; 01C4C7DB37597D7D8379511A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 024F7398C5FC12586FB10E9D /* EffectsScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EffectsScene.swift; sourceTree = ""; }; @@ -900,7 +899,6 @@ 0BCE3FAF40932AC7C7639AC4 /* AnalyticsSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenViewModel.swift; sourceTree = ""; }; 0C34667458773B02AB5FB0B2 /* LegalInformationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenViewModel.swift; sourceTree = ""; }; 0C671107BDFC6CD1778C0B4C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; - 0C88046D6A070D9827181C4D /* OnboardingUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingUITests.swift; sourceTree = ""; }; 0D0B159AFFBBD8ECFD0E37FA /* LoginScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenModels.swift; sourceTree = ""; }; 0D8F620C8B314840D8602E3F /* NSE.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = NSE.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 0E8BDC092D817B68CD9040C5 /* UserSessionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionStore.swift; sourceTree = ""; }; @@ -958,7 +956,6 @@ 1B6E30BB748F3F480F077969 /* RoomMemberDetailsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreenModels.swift; sourceTree = ""; }; 1B8E176484A89BAC389D4076 /* RoomMembersListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreen.swift; sourceTree = ""; }; 1B927CF5EF7FCCDA5EDC474B /* NotificationItemProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItemProxyProtocol.swift; sourceTree = ""; }; - 1BC4437C107D52ED19357DFC /* OnboardingViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModelProtocol.swift; sourceTree = ""; }; 1CC575D1895FA62591451A93 /* RoomMemberDetailsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreen.swift; sourceTree = ""; }; 1D56469A9EE0CFA2B7BA9760 /* SessionVerificationControllerProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationControllerProxyProtocol.swift; sourceTree = ""; }; 1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenUITests.swift; sourceTree = ""; }; @@ -1028,6 +1025,7 @@ 376D941BF8BB294389C0DE24 /* MapTilerURLBuildersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerURLBuildersTests.swift; sourceTree = ""; }; 37A243E04B58DC6E41FDCD82 /* EmojiItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiItem.swift; sourceTree = ""; }; 37CA26F55123E36B50DB0B3A /* AttributedStringTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringTests.swift; sourceTree = ""; }; + 37E727F7E0BCE8A0BBFD33FF /* OnboardingScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScreenCoordinator.swift; sourceTree = ""; }; 382B50F7E379B3DBBD174364 /* NotificationSettingsProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsProxyMock.swift; sourceTree = ""; }; 38E521D6C2BF8DF0DFB35146 /* DeveloperOptionsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreen.swift; sourceTree = ""; }; 3948D16F021DFDB2CD26EAA8 /* MockBackgroundTaskService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBackgroundTaskService.swift; sourceTree = ""; }; @@ -1167,7 +1165,6 @@ 66F2402D738694F98729A441 /* RoomTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineProvider.swift; sourceTree = ""; }; 67779D9A1B797285A09B7720 /* PollOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionView.swift; sourceTree = ""; }; 6861FE915C7B5466E6962BBA /* StartChatScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreen.swift; sourceTree = ""; }; - 686BCFA37AC6C67FF973CE67 /* OnboardingBackgroundImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingBackgroundImage.swift; sourceTree = ""; }; 693E16574C6F7F9FA1015A8C /* Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Search.swift; sourceTree = ""; }; 69B63F817FE305548DB4B512 /* RoomMembersListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListViewModelTests.swift; sourceTree = ""; }; 69CB8242D69B7E4D0B32E18D /* AggregatedReactionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AggregatedReactionMock.swift; sourceTree = ""; }; @@ -1265,6 +1262,7 @@ 8AFCE895ECFFA53FEE64D62B /* MediaLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoader.swift; sourceTree = ""; }; 8BEBF0E59F25E842EDB6FD11 /* LocationSharingScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSharingScreenModels.swift; sourceTree = ""; }; 8C8616254EE40CA8BA5E9BC2 /* VideoRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItemContent.swift; sourceTree = ""; }; + 8D168471461717AF5689F64B /* OnboardingScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScreenUITests.swift; sourceTree = ""; }; 8D55702474F279D910D2D162 /* RoomStateEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateEventStringBuilder.swift; sourceTree = ""; }; 8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = ""; }; @@ -1312,6 +1310,7 @@ 9CE3C90E487B255B735D73C8 /* RoomScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModel.swift; sourceTree = ""; }; 9CF1EE0AA78470C674554262 /* PillTextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillTextAttachment.swift; sourceTree = ""; }; 9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIConstants.swift; sourceTree = ""; }; + 9F3450F4C32D73532DBBC1A2 /* OnboardingScreenBackgroundImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScreenBackgroundImage.swift; sourceTree = ""; }; 9F85164F9475FF2867F71AAA /* RoomTimelineController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineController.swift; sourceTree = ""; }; A00C7A331B72C0F05C00392F /* RoomScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModelProtocol.swift; sourceTree = ""; }; A05707BF550D770168A406DB /* LoginViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModelTests.swift; sourceTree = ""; }; @@ -1402,7 +1401,6 @@ BA919F521E9F0EE3638AFC85 /* BugReportScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreen.swift; sourceTree = ""; }; BB23BEAF8831DC6A57E39F52 /* CreatePollScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePollScreenCoordinator.swift; sourceTree = ""; }; BB3073CCD77D906B330BC1D6 /* Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests.swift; sourceTree = ""; }; - BB33A751BFDA223BDD106EC0 /* OnboardingModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingModels.swift; sourceTree = ""; }; BB8BC4C791D0E88CFCF4E5DF /* ServerSelectionScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenCoordinator.swift; sourceTree = ""; }; BC8AA23D4F37CC26564F63C5 /* LayoutMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutMocks.swift; sourceTree = ""; }; BE148A4FFEE853C5A281500C /* UNNotificationContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNNotificationContent.swift; sourceTree = ""; }; @@ -1417,7 +1415,6 @@ C08E9043618AE5B0BF7B07E1 /* TemplateScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenViewModelTests.swift; sourceTree = ""; }; C0900BBF0A5D5D775E917C70 /* EventBasedMessageTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedMessageTimelineItemProtocol.swift; sourceTree = ""; }; C0FEA560929DD73FFEF8C3DF /* HomeScreenEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenEmptyStateView.swift; sourceTree = ""; }; - C1198B925F4A88DA74083662 /* OnboardingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModel.swift; sourceTree = ""; }; C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutDirection.swift; sourceTree = ""; }; C1511766C534367700C8DD75 /* RoomNotificationModeProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationModeProxy.swift; sourceTree = ""; }; C15E0017717EAE3A1D02D005 /* StaticLocationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticLocationScreenCoordinator.swift; sourceTree = ""; }; @@ -1428,6 +1425,7 @@ C2E9B841EE4878283ECDB554 /* InviteUsersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreen.swift; sourceTree = ""; }; C2F079B5DBD0D85FEA687AAE /* SDKGeneratedMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKGeneratedMocks.swift; sourceTree = ""; }; C352359663A0E52BA20761EE /* LoadableImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableImage.swift; sourceTree = ""; }; + C49C1CEBA9BCF5D2AD1884FA /* OnboardingScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScreenViewModel.swift; sourceTree = ""; }; C4C89820BB2B88D4EA28131C /* BugReportScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenViewModelProtocol.swift; sourceTree = ""; }; C54464351F170D570110AFCA /* WelcomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeScreen.swift; sourceTree = ""; }; C55D7E514F9DE4E3D72FDCAD /* SessionVerificationControllerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationControllerProxy.swift; sourceTree = ""; }; @@ -1456,6 +1454,7 @@ CA89A2DD51B6BBE1DA55E263 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; CA90BD288E5AE6BC643AFDDF /* TemplateScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenCoordinator.swift; sourceTree = ""; }; CACA846B3E3E9A521D98B178 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + CB26F24164E9461B2054D0B3 /* OnboardingScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScreenModels.swift; sourceTree = ""; }; CBBCC6E74774E79B599625D0 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; CBF9AEA706926DD0DA2B954C /* JoinedRoomSize+MemberCount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JoinedRoomSize+MemberCount.swift"; sourceTree = ""; }; CC03209FDE8CE0810617BFFF /* RoomMembersListScreenMemberCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenMemberCell.swift; sourceTree = ""; }; @@ -1488,6 +1487,7 @@ D3D455BC2423D911A62ACFB2 /* NSELogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSELogger.swift; sourceTree = ""; }; D49B9785E3AD7D1C15A29F2F /* MediaSourceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaSourceProxy.swift; sourceTree = ""; }; D4DA544B2520BFA65D6DB4BB /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; + D53D6BB7E8E5EC031281872C /* OnboardingScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScreenViewModelTests.swift; sourceTree = ""; }; D54E12B98252F6C527E31FEE /* MediaUploadPreviewScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenViewModelProtocol.swift; sourceTree = ""; }; D5685139D0B72BED3503EFCC /* MigrationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationScreen.swift; sourceTree = ""; }; D5AC06FC11B6638F7BF1670E /* TimelineDeliveryStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineDeliveryStatusView.swift; sourceTree = ""; }; @@ -1520,6 +1520,7 @@ E26747B3154A5DBC3A7E24A5 /* Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = ""; }; E2B1CC9AA154F4D5435BF60A /* Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Comparable.swift; sourceTree = ""; }; E2DCA495ED42D2463DDAA94D /* TimelineBubbleLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBubbleLayout.swift; sourceTree = ""; }; + E2F27BAB69EB568369F1F6B3 /* OnboardingScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScreenViewModelProtocol.swift; sourceTree = ""; }; E3059CFA00C67D8787273B20 /* ServerSelectionScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenViewModel.swift; sourceTree = ""; }; E36CB905A2B9EC2C92A2DA7C /* KeychainController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainController.swift; sourceTree = ""; }; E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsClientProtocol.swift; sourceTree = ""; }; @@ -1532,7 +1533,6 @@ E55B5EA766E89FF1F87C3ACB /* RoomNotificationSettingsProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsProxyProtocol.swift; sourceTree = ""; }; E5E94DCFEE803E5ABAE8ACCE /* KeychainControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerProtocol.swift; sourceTree = ""; }; E5F2B6443D1ED8602F328539 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Localizable.stringsdict; sourceTree = ""; }; - E6281B199D8A8F0892490C2E /* OnboardingCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingCoordinator.swift; sourceTree = ""; }; E65DA46BD5CA83747AE144F3 /* secrets.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = secrets.xcconfig; sourceTree = ""; }; E6E6BDF9D26DB05C88901416 /* RedactedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedactedRoomTimelineItem.swift; sourceTree = ""; }; E6F5D66F158A6662F953733E /* NotificationSettingsProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsProxy.swift; sourceTree = ""; }; @@ -2161,10 +2161,10 @@ 3F38EAC92E2281990E65DAF2 /* OnboardingScreen */ = { isa = PBXGroup; children = ( - E6281B199D8A8F0892490C2E /* OnboardingCoordinator.swift */, - BB33A751BFDA223BDD106EC0 /* OnboardingModels.swift */, - C1198B925F4A88DA74083662 /* OnboardingViewModel.swift */, - 1BC4437C107D52ED19357DFC /* OnboardingViewModelProtocol.swift */, + 37E727F7E0BCE8A0BBFD33FF /* OnboardingScreenCoordinator.swift */, + CB26F24164E9461B2054D0B3 /* OnboardingScreenModels.swift */, + C49C1CEBA9BCF5D2AD1884FA /* OnboardingScreenViewModel.swift */, + E2F27BAB69EB568369F1F6B3 /* OnboardingScreenViewModelProtocol.swift */, 7B14834450AE76EEFDDBCBB8 /* View */, ); path = OnboardingScreen; @@ -2646,7 +2646,7 @@ 9C698E30698EC59302A8EEBD /* NavigationStackCoordinatorTests.swift */, 8544F7058D31DBEB8DBFF0F5 /* NotificationSettingsEditScreenViewModelTests.swift */, 514363244AE7D68080D44C6F /* NotificationSettingsScreenViewModelTests.swift */, - 00A941F289F6AB876BA3361A /* OnboardingViewModelTests.swift */, + D53D6BB7E8E5EC031281872C /* OnboardingScreenViewModelTests.swift */, 6FB31A32C93D94930B253FBF /* PermalinkBuilderTests.swift */, 086C19086DD16E9B38E25954 /* ReportContentViewModelTests.swift */, 00E5B2CBEF8F96424F095508 /* RoomDetailsEditScreenViewModelTests.swift */, @@ -2798,8 +2798,8 @@ 7B14834450AE76EEFDDBCBB8 /* View */ = { isa = PBXGroup; children = ( - 686BCFA37AC6C67FF973CE67 /* OnboardingBackgroundImage.swift */, AB8E75B9CB6C78BE8D09B1AF /* OnboardingScreen.swift */, + 9F3450F4C32D73532DBBC1A2 /* OnboardingScreenBackgroundImage.swift */, ); path = View; sourceTree = ""; @@ -3064,7 +3064,7 @@ 75910F5A36EA8FF9BAD08D18 /* MigrationScreenUITests.swift */, 46F52419AEEDA2C006CB7181 /* NotificationSettingsEditScreenUITests.swift */, B83BC0DC9A2DF2DD60F9B6E9 /* NotificationSettingsScreenUITests.swift */, - 0C88046D6A070D9827181C4D /* OnboardingUITests.swift */, + 8D168471461717AF5689F64B /* OnboardingScreenUITests.swift */, 4132F882A984ED971338EE9D /* ReportContentScreenUITests.swift */, 122186B7CD1BC46A9C629DD9 /* RoomDetailsEditScreenUITests.swift */, 3BFDAF6918BB096C44788FC9 /* RoomDetailsScreenUITests.swift */, @@ -4399,7 +4399,7 @@ 1B2DADC008EE211AF1DA5292 /* NotificationManagerTests.swift in Sources */, C11939FDC40716C4387275A4 /* NotificationSettingsEditScreenViewModelTests.swift in Sources */, E3AC72E3E58F364EF15C1CC7 /* NotificationSettingsScreenViewModelTests.swift in Sources */, - F9F6D2883BBEBB9A3789A137 /* OnboardingViewModelTests.swift in Sources */, + 0C26A1588B17DCDE5F490FE3 /* OnboardingScreenViewModelTests.swift in Sources */, 27E9263DA75E266690A37EB1 /* PermalinkBuilderTests.swift in Sources */, D415764645491F10344FC6AC /* Publisher.swift in Sources */, D53B80EF02C1062E68659EDD /* ReportContentViewModelTests.swift in Sources */, @@ -4740,12 +4740,12 @@ CBD2ABE4C1A47ECD99E1488E /* NotificationSettingsScreenViewModelProtocol.swift in Sources */, 523C6800ED85D5810CF18C19 /* OIDCAccountSettingsPresenter.swift in Sources */, 9A4E3D5AA44B041DAC3A0D81 /* OIDCAuthenticationPresenter.swift in Sources */, - 329571083B132E4941131835 /* OnboardingBackgroundImage.swift in Sources */, - 2CB6787E25B11711518E9588 /* OnboardingCoordinator.swift in Sources */, - 5D7960B32C350FA93F48D02B /* OnboardingModels.swift in Sources */, 7F64FA937B95924B3A44EC12 /* OnboardingScreen.swift in Sources */, - CE7148E80F09B7305E026AC6 /* OnboardingViewModel.swift in Sources */, - 992477AB8E3F3C36D627D32E /* OnboardingViewModelProtocol.swift in Sources */, + C0DC02E2B91DC76A4D1A0E7F /* OnboardingScreenBackgroundImage.swift in Sources */, + 7CFCC177F0ED083867FAD9C9 /* OnboardingScreenCoordinator.swift in Sources */, + 3A5BD701D1AC916AC534F52C /* OnboardingScreenModels.swift in Sources */, + A5C5C18671EDD2747AC16D2D /* OnboardingScreenViewModel.swift in Sources */, + 4714991754A08B58B4D7ED85 /* OnboardingScreenViewModelProtocol.swift in Sources */, 804C15D8ADE0EA7A5268F58A /* OverridableAvatarImage.swift in Sources */, CD6A72B65D3B6076F4045C30 /* PHGPostHogConfiguration.swift in Sources */, 7501442D52A65F73DF79FFD4 /* PaginationIndicatorRoomTimelineItem.swift in Sources */, @@ -5019,7 +5019,7 @@ 51C240F4660F7269203A9B3A /* MigrationScreenUITests.swift in Sources */, 1830E5431DB426E2F3660D58 /* NotificationSettingsEditScreenUITests.swift in Sources */, AF4232E6F08C3DB86FFA9BBD /* NotificationSettingsScreenUITests.swift in Sources */, - 6B15FF984906AAFCF9DC4F58 /* OnboardingUITests.swift in Sources */, + 92133B170A1F917685E9FF78 /* OnboardingScreenUITests.swift in Sources */, BA0D3DDCEDD97502DAC4B6E9 /* ReportContentScreenUITests.swift in Sources */, F16109A6F6DF03DA26D59233 /* RoomDetailsEditScreenUITests.swift in Sources */, 829062DD3C3F7016FE1A6476 /* RoomDetailsScreenUITests.swift in Sources */, diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 219f12f85..ad897da5a 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -217,7 +217,7 @@ { "identity" : "swiftui-introspect", "kind" : "remoteSourceControl", - "location" : "https://github.com/siteline/SwiftUI-Introspect", + "location" : "https://github.com/siteline/SwiftUI-Introspect.git", "state" : { "revision" : "b94da693e57eaf79d16464b8b7c90d09cba4e290", "version" : "0.9.2" diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index 462abd031..9eb0aca63 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -51,7 +51,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate, private var appDelegateObserver: AnyCancellable? private var userSessionObserver: AnyCancellable? private var clientProxyObserver: AnyCancellable? - private var networkMonitorObserver: AnyCancellable? + private var cancellables = Set() let notificationManager: NotificationManagerProtocol @@ -374,15 +374,20 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate, keyBackupNeeded: false, userIndicatorController: ServiceLocator.shared.userIndicatorController) let coordinator = SoftLogoutScreenCoordinator(parameters: parameters) - coordinator.callback = { result in - switch result { - case .signedIn(let session): - self.userSession = session - self.stateMachine.processEvent(.createdUserSession) - case .clearAllData: - self.stateMachine.processEvent(.signOut(isSoft: false)) + + coordinator.actions + .sink { [weak self] action in + guard let self else { return } + + switch action { + case .signedIn(let session): + self.userSession = session + stateMachine.processEvent(.createdUserSession) + case .clearAllData: + stateMachine.processEvent(.signOut(isSoft: false)) + } } - } + .store(in: &cancellables) navigationRootCoordinator.setRootCoordinator(coordinator) } @@ -401,14 +406,18 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate, appSettings: appSettings, analytics: ServiceLocator.shared.analytics) - userSessionFlowCoordinator.callback = { [weak self] action in - switch action { - case .signOut: - self?.stateMachine.processEvent(.signOut(isSoft: false)) - case .clearCache: - self?.stateMachine.processEvent(.clearCache) + userSessionFlowCoordinator.actions + .sink { [weak self] action in + guard let self else { return } + + switch action { + case .signOut: + stateMachine.processEvent(.signOut(isSoft: false)) + case .clearCache: + stateMachine.processEvent(.clearCache) + } } - } + .store(in: &cancellables) userSessionFlowCoordinator.start() @@ -528,7 +537,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate, private func observeNetworkState() { let reachabilityNotificationIdentifier = "io.element.elementx.reachability.notification" - networkMonitorObserver = ServiceLocator.shared.networkMonitor + ServiceLocator.shared.networkMonitor .reachabilityPublisher .removeDuplicates() .sink { reachability in @@ -542,6 +551,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate, persistent: true)) } } + .store(in: &cancellables) } private func handleAppRoute(_ appRoute: AppRoute) { diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index b330271ca..15ca828cf 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -36,7 +36,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { private let stateMachine: StateMachine = .init(state: .initial) - private var cancellables: Set = .init() + private var cancellables = Set() private let actionsSubject: PassthroughSubject = .init() var actions: AnyPublisher { @@ -456,16 +456,22 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { roomProxy: roomProxy, userIndicatorController: userIndicatorController) let coordinator = ReportContentScreenCoordinator(parameters: parameters) - coordinator.callback = { [weak self] completion in - self?.navigationStackCoordinator.setSheetCoordinator(nil) - - switch completion { - case .cancel: - break - case .finish: - userIndicatorController.submitIndicator(UserIndicator(title: L10n.commonReportSubmitted, iconName: "checkmark")) + + coordinator.actions + .sink { [weak self] action in + guard let self else { return } + + navigationStackCoordinator.setSheetCoordinator(nil) + + switch action { + case .cancel: + break + case .finish: + userIndicatorController.submitIndicator(UserIndicator(title: L10n.commonReportSubmitted, iconName: "checkmark")) + } } - } + .store(in: &cancellables) + navigationCoordinator.setRootCoordinator(coordinator) navigationStackCoordinator.setSheetCoordinator(userIndicatorController) { [weak self] in self?.stateMachine.tryEvent(.dismissReportContent) @@ -508,12 +514,18 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { title: url.lastPathComponent, url: url) - let mediaUploadPreviewScreenCoordinator = MediaUploadPreviewScreenCoordinator(parameters: parameters) { [weak self] action in - switch action { - case .dismiss: - self?.navigationStackCoordinator.setSheetCoordinator(nil) + let mediaUploadPreviewScreenCoordinator = MediaUploadPreviewScreenCoordinator(parameters: parameters) + + mediaUploadPreviewScreenCoordinator.actions + .sink { [weak self] action in + guard let self else { return } + + switch action { + case .dismiss: + navigationStackCoordinator.setSheetCoordinator(nil) + } } - } + .store(in: &cancellables) stackCoordinator.setRootCoordinator(mediaUploadPreviewScreenCoordinator) @@ -526,18 +538,22 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { let params = EmojiPickerScreenCoordinatorParameters(emojiProvider: emojiProvider, itemID: itemID, selectedEmojis: selectedEmoji) let coordinator = EmojiPickerScreenCoordinator(parameters: params) - coordinator.callback = { [weak self] action in + + coordinator.actions.sink { [weak self] action in + guard let self else { return } + switch action { case let .emojiSelected(emoji: emoji, itemID: itemID): MXLog.debug("Selected \(emoji) for \(itemID)") - self?.navigationStackCoordinator.setSheetCoordinator(nil) + navigationStackCoordinator.setSheetCoordinator(nil) Task { - await self?.timelineController?.toggleReaction(emoji, to: itemID) + await self.timelineController?.toggleReaction(emoji, to: itemID) } case .dismiss: - self?.navigationStackCoordinator.setSheetCoordinator(nil) + navigationStackCoordinator.setSheetCoordinator(nil) } } + .store(in: &cancellables) navigationStackCoordinator.setSheetCoordinator(coordinator) { [weak self] in self?.stateMachine.tryEvent(.dismissEmojiPicker) diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift index 46a635a00..15b8bba4a 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift @@ -29,11 +29,12 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { private let roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol private let appSettings: AppSettings private let analytics: AnalyticsService + private let actionsSubject: PassthroughSubject = .init() private let stateMachine: UserSessionFlowCoordinatorStateMachine private let roomFlowCoordinator: RoomFlowCoordinator - private var cancellables: Set = .init() + private var cancellables = Set() private var migrationCancellable: AnyCancellable? private let sidebarNavigationStackCoordinator: NavigationStackCoordinator @@ -41,7 +42,9 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { private let selectedRoomSubject = CurrentValueSubject(nil) - var callback: ((UserSessionFlowCoordinatorAction) -> Void)? + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } init(userSession: UserSessionProtocol, navigationSplitCoordinator: NavigationSplitCoordinator, @@ -263,34 +266,36 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { navigationStackCoordinator: detailNavigationStackCoordinator, selectedRoomPublisher: selectedRoomSubject.asCurrentValuePublisher()) let coordinator = HomeScreenCoordinator(parameters: parameters) - - coordinator.callback = { [weak self] action in - guard let self else { return } - - switch action { - case .presentRoom(let roomID): - self.roomFlowCoordinator.handleAppRoute(.room(roomID: roomID), animated: true) - case .presentRoomDetails(let roomID): - self.roomFlowCoordinator.handleAppRoute(.roomDetails(roomID: roomID), animated: true) - case .roomLeft(let roomID): - if case .roomList(selectedRoomID: let selectedRoomID) = stateMachine.state, - selectedRoomID == roomID { - self.roomFlowCoordinator.handleAppRoute(.roomList, animated: true) + + coordinator.actions + .sink { [weak self] action in + guard let self else { return } + + switch action { + case .presentRoom(let roomID): + roomFlowCoordinator.handleAppRoute(.room(roomID: roomID), animated: true) + case .presentRoomDetails(let roomID): + roomFlowCoordinator.handleAppRoute(.roomDetails(roomID: roomID), animated: true) + case .roomLeft(let roomID): + if case .roomList(selectedRoomID: let selectedRoomID) = stateMachine.state, + selectedRoomID == roomID { + roomFlowCoordinator.handleAppRoute(.roomList, animated: true) + } + case .presentSettingsScreen: + stateMachine.processEvent(.showSettingsScreen) + case .presentFeedbackScreen: + stateMachine.processEvent(.feedbackScreen) + case .presentSessionVerificationScreen: + stateMachine.processEvent(.showSessionVerificationScreen) + case .presentStartChatScreen: + stateMachine.processEvent(.showStartChatScreen) + case .signOut: + actionsSubject.send(.signOut) + case .presentInvitesScreen: + stateMachine.processEvent(.showInvitesScreen) } - case .presentSettingsScreen: - self.stateMachine.processEvent(.showSettingsScreen) - case .presentFeedbackScreen: - self.stateMachine.processEvent(.feedbackScreen) - case .presentSessionVerificationScreen: - self.stateMachine.processEvent(.showSessionVerificationScreen) - case .presentStartChatScreen: - self.stateMachine.processEvent(.showStartChatScreen) - case .signOut: - self.callback?(.signOut) - case .presentInvitesScreen: - self.stateMachine.processEvent(.showInvitesScreen) } - } + .store(in: &cancellables) sidebarNavigationStackCoordinator.setRootCoordinator(coordinator) } @@ -324,18 +329,22 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { notificationSettings: userSession.clientProxy.notificationSettings, appSettings: appSettings) let settingsScreenCoordinator = SettingsScreenCoordinator(parameters: parameters) - settingsScreenCoordinator.callback = { [weak self] action in - guard let self else { return } - switch action { - case .dismiss: - self.navigationSplitCoordinator.setSheetCoordinator(nil) - case .logout: - self.navigationSplitCoordinator.setSheetCoordinator(nil) - self.callback?(.signOut) - case .clearCache: - self.callback?(.clearCache) + + settingsScreenCoordinator.actions + .sink { [weak self] action in + guard let self else { return } + + switch action { + case .dismiss: + navigationSplitCoordinator.setSheetCoordinator(nil) + case .logout: + navigationSplitCoordinator.setSheetCoordinator(nil) + actionsSubject.send(.signOut) + case .clearCache: + actionsSubject.send(.clearCache) + } } - } + .store(in: &cancellables) settingsNavigationStackCoordinator.setRootCoordinator(settingsScreenCoordinator, animated: animated) @@ -355,9 +364,16 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { let coordinator = SessionVerificationScreenCoordinator(parameters: parameters) - coordinator.callback = { [weak self] in - self?.navigationSplitCoordinator.setSheetCoordinator(nil) - } + coordinator.actions + .sink { [weak self] action in + guard let self else { return } + + switch action { + case .done: + navigationSplitCoordinator.setSheetCoordinator(nil) + } + } + .store(in: &cancellables) navigationSplitCoordinator.setSheetCoordinator(coordinator, animated: animated) { [weak self] in self?.stateMachine.processEvent(.dismissedSessionVerificationScreen) diff --git a/ElementX/Sources/Screens/AnalyticsPromptScreen/AnalyticsPromptScreenCoordinator.swift b/ElementX/Sources/Screens/AnalyticsPromptScreen/AnalyticsPromptScreenCoordinator.swift index d9f619f93..a18b21365 100644 --- a/ElementX/Sources/Screens/AnalyticsPromptScreen/AnalyticsPromptScreenCoordinator.swift +++ b/ElementX/Sources/Screens/AnalyticsPromptScreen/AnalyticsPromptScreenCoordinator.swift @@ -14,13 +14,22 @@ // limitations under the License. // +import Combine import SwiftUI +enum AnalyticsPromptScreenCoordinatorAction { + case done +} + final class AnalyticsPromptScreenCoordinator: CoordinatorProtocol { private let analytics: AnalyticsService - private var viewModel: AnalyticsPromptScreenViewModel - - var callback: (@MainActor () -> Void)? + private var viewModel: AnalyticsPromptScreenViewModelProtocol + private let actionsSubject: PassthroughSubject = .init() + private var cancellables = Set() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } init(analytics: AnalyticsService, termsURL: URL) { self.analytics = analytics @@ -30,20 +39,22 @@ final class AnalyticsPromptScreenCoordinator: CoordinatorProtocol { // MARK: - Public func start() { - viewModel.callback = { [weak self] result in - guard let self else { return } - - switch result { - case .enable: - MXLog.info("Enable Analytics") - analytics.optIn() - self.callback?() - case .disable: - MXLog.info("Disable Analytics") - analytics.optOut() - self.callback?() + viewModel.actions + .sink { [weak self] action in + guard let self else { return } + + switch action { + case .enable: + MXLog.info("Enable Analytics") + analytics.optIn() + actionsSubject.send(.done) + case .disable: + MXLog.info("Disable Analytics") + analytics.optOut() + actionsSubject.send(.done) + } } - } + .store(in: &cancellables) } func toPresentable() -> AnyView { diff --git a/ElementX/Sources/Screens/AnalyticsPromptScreen/AnalyticsPromptScreenViewModel.swift b/ElementX/Sources/Screens/AnalyticsPromptScreen/AnalyticsPromptScreenViewModel.swift index e2ece39fb..689f294aa 100644 --- a/ElementX/Sources/Screens/AnalyticsPromptScreen/AnalyticsPromptScreenViewModel.swift +++ b/ElementX/Sources/Screens/AnalyticsPromptScreen/AnalyticsPromptScreenViewModel.swift @@ -20,7 +20,11 @@ import SwiftUI typealias AnalyticsPromptScreenViewModelType = StateStoreViewModel class AnalyticsPromptScreenViewModel: AnalyticsPromptScreenViewModelType, AnalyticsPromptScreenViewModelProtocol { - var callback: (@MainActor (AnalyticsPromptScreenViewModelAction) -> Void)? + private var actionsSubject: PassthroughSubject = .init() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } /// Initialize a view model with the specified prompt type and app display name. init(termsURL: URL) { @@ -33,9 +37,9 @@ class AnalyticsPromptScreenViewModel: AnalyticsPromptScreenViewModelType, Analyt override func process(viewAction: AnalyticsPromptScreenViewAction) { switch viewAction { case .enable: - callback?(.enable) + actionsSubject.send(.enable) case .disable: - callback?(.disable) + actionsSubject.send(.disable) } } } diff --git a/ElementX/Sources/Screens/AnalyticsPromptScreen/AnalyticsPromptScreenViewModelProtocol.swift b/ElementX/Sources/Screens/AnalyticsPromptScreen/AnalyticsPromptScreenViewModelProtocol.swift index 39c044ce9..ba4a11bc8 100644 --- a/ElementX/Sources/Screens/AnalyticsPromptScreen/AnalyticsPromptScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/AnalyticsPromptScreen/AnalyticsPromptScreenViewModelProtocol.swift @@ -14,10 +14,10 @@ // limitations under the License. // -import Foundation +import Combine @MainActor protocol AnalyticsPromptScreenViewModelProtocol { - var callback: (@MainActor (AnalyticsPromptScreenViewModelAction) -> Void)? { get set } + var actions: AnyPublisher { get } var context: AnalyticsPromptScreenViewModelType.Context { get } } diff --git a/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift b/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift index 9a3f73504..87817f244 100644 --- a/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/AuthenticationCoordinator.swift @@ -30,7 +30,7 @@ class AuthenticationCoordinator: CoordinatorProtocol { private let analytics: AnalyticsService private let userIndicatorController: UserIndicatorControllerProtocol - private var cancellables: Set = [] + private var cancellables = Set() weak var delegate: AuthenticationCoordinatorDelegate? @@ -57,15 +57,18 @@ class AuthenticationCoordinator: CoordinatorProtocol { // MARK: - Private private func showOnboarding() { - let coordinator = OnboardingCoordinator() - - coordinator.callback = { [weak self] action in - guard let self else { return } - switch action { - case .login: - Task { await self.startAuthentication() } + let coordinator = OnboardingScreenCoordinator() + + coordinator.actions + .sink { [weak self] action in + guard let self else { return } + + switch action { + case .login: + Task { await self.startAuthentication() } + } } - } + .store(in: &cancellables) navigationStackCoordinator.setRootCoordinator(coordinator) } @@ -92,29 +95,31 @@ class AuthenticationCoordinator: CoordinatorProtocol { isModallyPresented: isModallyPresented) let coordinator = ServerSelectionScreenCoordinator(parameters: parameters) - coordinator.callback = { [weak self] action in - guard let self else { return } - - switch action { - case .updated: - if isModallyPresented { - navigationStackCoordinator.setSheetCoordinator(nil) - } else { - // We are here because the default server failed to respond. - if authenticationService.homeserver.value.loginMode == .password { - // Add the password login screen directly to the flow, its fine. - showLoginScreen() + coordinator.actions + .sink { [weak self] action in + guard let self else { return } + + switch action { + case .updated: + if isModallyPresented { + navigationStackCoordinator.setSheetCoordinator(nil) } else { - // OIDC is presented from the confirmation screen so replace the - // server selection screen which was inserted to handle the failure. - navigationStackCoordinator.pop() - showServerConfirmationScreen() + // We are here because the default server failed to respond. + if authenticationService.homeserver.value.loginMode == .password { + // Add the password login screen directly to the flow, its fine. + showLoginScreen() + } else { + // OIDC is presented from the confirmation screen so replace the + // server selection screen which was inserted to handle the failure. + navigationStackCoordinator.pop() + showServerConfirmationScreen() + } } + case .dismiss: + navigationStackCoordinator.setSheetCoordinator(nil) } - case .dismiss: - navigationStackCoordinator.setSheetCoordinator(nil) } - } + .store(in: &cancellables) if isModallyPresented { navigationCoordinator.setRootCoordinator(coordinator) @@ -178,20 +183,22 @@ class AuthenticationCoordinator: CoordinatorProtocol { userIndicatorController: userIndicatorController) let coordinator = LoginScreenCoordinator(parameters: parameters) - coordinator.callback = { [weak self] action in - guard let self else { return } + coordinator.actions + .sink { [weak self] action in + guard let self else { return } - switch action { - case .signedIn(let userSession): - userHasSignedIn(userSession: userSession) - case .configuredForOIDC: - // Pop back to the confirmation screen for OIDC login to continue. - navigationStackCoordinator.pop(animated: false) - case .isOnWaitlist(let credentials): - showWaitlistScreen(for: credentials) + switch action { + case .signedIn(let userSession): + userHasSignedIn(userSession: userSession) + case .configuredForOIDC: + // Pop back to the confirmation screen for OIDC login to continue. + navigationStackCoordinator.pop(animated: false) + case .isOnWaitlist(let credentials): + showWaitlistScreen(for: credentials) + } } - } - + .store(in: &cancellables) + navigationStackCoordinator.push(coordinator) } @@ -228,10 +235,18 @@ class AuthenticationCoordinator: CoordinatorProtocol { completion() return } + let coordinator = AnalyticsPromptScreenCoordinator(analytics: analytics, termsURL: appSettings.analyticsConfiguration.termsURL) - coordinator.callback = { - completion() - } + + coordinator.actions + .sink { action in + switch action { + case .done: + completion() + } + } + .store(in: &cancellables) + navigationStackCoordinator.push(coordinator) } diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenCoordinator.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenCoordinator.swift index 8acad732f..e34d39564 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenCoordinator.swift @@ -42,7 +42,12 @@ final class LoginScreenCoordinator: CoordinatorProtocol { private var authenticationService: AuthenticationServiceProxyProtocol { parameters.authenticationService } - var callback: (@MainActor (LoginScreenCoordinatorAction) -> Void)? + private let actionsSubject: PassthroughSubject = .init() + private var cancellables = Set() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } // MARK: - Setup @@ -56,18 +61,20 @@ final class LoginScreenCoordinator: CoordinatorProtocol { // MARK: - Public func start() { - viewModel.callback = { [weak self] action in - guard let self else { return } - - switch action { - case .parseUsername(let username): - parseUsername(username) - case .forgotPassword: - showForgotPasswordScreen() - case .login(let username, let password): - login(username: username, password: password) + viewModel.actions + .sink { [weak self] action in + guard let self else { return } + + switch action { + case .parseUsername(let username): + parseUsername(username) + case .forgotPassword: + showForgotPasswordScreen() + case .login(let username, let password): + login(username: username, password: password) + } } - } + .store(in: &cancellables) } func stop() { @@ -126,7 +133,7 @@ final class LoginScreenCoordinator: CoordinatorProtocol { initialDeviceName: UIDevice.current.initialDeviceName, deviceID: nil) { case .success(let userSession): - callback?(.signedIn(userSession)) + actionsSubject.send(.signedIn(userSession)) parameters.analytics.signpost.endLogin() stopLoading() case .failure(let error): @@ -134,11 +141,11 @@ final class LoginScreenCoordinator: CoordinatorProtocol { parameters.analytics.signpost.endLogin() switch error { case .isOnWaitlist: - callback?(.isOnWaitlist(.init(username: username, - password: password, - initialDeviceName: UIDevice.current.initialDeviceName, - deviceID: nil, - homeserver: authenticationService.homeserver.value))) + actionsSubject.send(.isOnWaitlist(.init(username: username, + password: password, + initialDeviceName: UIDevice.current.initialDeviceName, + deviceID: nil, + homeserver: authenticationService.homeserver.value))) default: handleError(error) } @@ -159,7 +166,7 @@ final class LoginScreenCoordinator: CoordinatorProtocol { case .success: stopLoading() if authenticationService.homeserver.value.loginMode == .oidc { - callback?(.configuredForOIDC) + actionsSubject.send(.configuredForOIDC) } else { updateViewModel() } diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenViewModel.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenViewModel.swift index e29dba42f..990bc2119 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenViewModel.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenViewModel.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import Combine import SwiftUI typealias LoginScreenViewModelType = StateStoreViewModel @@ -21,7 +22,11 @@ typealias LoginScreenViewModelType = StateStoreViewModel Void)? + private var actionsSubject: PassthroughSubject = .init() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } init(homeserver: LoginHomeserver, slidingSyncLearnMoreURL: URL) { self.slidingSyncLearnMoreURL = slidingSyncLearnMoreURL @@ -34,11 +39,11 @@ class LoginScreenViewModel: LoginScreenViewModelType, LoginScreenViewModelProtoc override func process(viewAction: LoginScreenViewAction) { switch viewAction { case .parseUsername: - callback?(.parseUsername(state.bindings.username)) + actionsSubject.send(.parseUsername(state.bindings.username)) case .forgotPassword: - callback?(.forgotPassword) + actionsSubject.send(.forgotPassword) case .next: - callback?(.login(username: state.bindings.username, password: state.bindings.password)) + actionsSubject.send(.login(username: state.bindings.username, password: state.bindings.password)) } } diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenViewModelProtocol.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenViewModelProtocol.swift index de61f098f..b2473586c 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenViewModelProtocol.swift @@ -14,11 +14,11 @@ // limitations under the License. // -import Foundation +import Combine @MainActor protocol LoginScreenViewModelProtocol { - var callback: (@MainActor (LoginScreenViewModelAction) -> Void)? { get set } + var actions: AnyPublisher { get } var context: LoginScreenViewModelType.Context { get } /// Update the view to reflect that a new homeserver is being loaded. diff --git a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenCoordinator.swift b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenCoordinator.swift index a4bfa9ff2..753f86ed5 100644 --- a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenCoordinator.swift @@ -31,7 +31,7 @@ final class ServerConfirmationScreenCoordinator: CoordinatorProtocol { private let parameters: ServerConfirmationScreenCoordinatorParameters private var viewModel: ServerConfirmationScreenViewModelProtocol private let actionsSubject: PassthroughSubject = .init() - private var cancellables: Set = .init() + private var cancellables = Set() var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() diff --git a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenCoordinator.swift b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenCoordinator.swift index 0ddeb9adf..79807d9bc 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenCoordinator.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import Combine import SwiftUI struct ServerSelectionScreenCoordinatorParameters { @@ -35,7 +36,12 @@ final class ServerSelectionScreenCoordinator: CoordinatorProtocol { private var viewModel: ServerSelectionScreenViewModelProtocol private var authenticationService: AuthenticationServiceProxyProtocol { parameters.authenticationService } - var callback: (@MainActor (ServerSelectionScreenCoordinatorAction) -> Void)? + private let actionsSubject: PassthroughSubject = .init() + private var cancellables = Set() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } init(parameters: ServerSelectionScreenCoordinatorParameters) { self.parameters = parameters @@ -48,16 +54,18 @@ final class ServerSelectionScreenCoordinator: CoordinatorProtocol { // MARK: - Public func start() { - viewModel.callback = { [weak self] action in - guard let self else { return } - - switch action { - case .confirm(let homeserverAddress): - self.useHomeserver(homeserverAddress) - case .dismiss: - self.callback?(.dismiss) + viewModel.actions + .sink { [weak self] action in + guard let self else { return } + + switch action { + case .confirm(let homeserverAddress): + self.useHomeserver(homeserverAddress) + case .dismiss: + actionsSubject.send(.dismiss) + } } - } + .store(in: &cancellables) } func stop() { @@ -88,7 +96,7 @@ final class ServerSelectionScreenCoordinator: CoordinatorProtocol { switch await authenticationService.configure(for: homeserverAddress) { case .success: MXLog.info("Selected homeserver: \(homeserverAddress)") - callback?(.updated) + actionsSubject.send(.updated) stopLoading() case .failure(let error): MXLog.info("Invalid homeserver: \(homeserverAddress)") diff --git a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModel.swift b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModel.swift index 1936998f0..31f1da2af 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModel.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModel.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import Combine import SwiftUI typealias ServerSelectionScreenViewModelType = StateStoreViewModel @@ -21,7 +22,11 @@ typealias ServerSelectionScreenViewModelType = StateStoreViewModel Void)? + private var actionsSubject: PassthroughSubject = .init() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } init(homeserverAddress: String, slidingSyncLearnMoreURL: URL, isModallyPresented: Bool) { self.slidingSyncLearnMoreURL = slidingSyncLearnMoreURL @@ -35,9 +40,9 @@ class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, Server override func process(viewAction: ServerSelectionScreenViewAction) { switch viewAction { case .confirm: - callback?(.confirm(homeserverAddress: state.bindings.homeserverAddress)) + actionsSubject.send(.confirm(homeserverAddress: state.bindings.homeserverAddress)) case .dismiss: - callback?(.dismiss) + actionsSubject.send(.dismiss) case .clearFooterError: clearFooterError() } diff --git a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModelProtocol.swift b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModelProtocol.swift index 320b6f3da..d7fbdc6de 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModelProtocol.swift @@ -14,11 +14,11 @@ // limitations under the License. // -import Foundation +import Combine @MainActor protocol ServerSelectionScreenViewModelProtocol { - var callback: (@MainActor (ServerSelectionScreenViewModelAction) -> Void)? { get set } + var actions: AnyPublisher { get } var context: ServerSelectionScreenViewModelType.Context { get } /// Displays an error to the user. diff --git a/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenCoordinator.swift b/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenCoordinator.swift index e5040e927..c2fae31ab 100644 --- a/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenCoordinator.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import Combine import SwiftUI struct SoftLogoutScreenCoordinatorParameters { @@ -43,10 +44,14 @@ enum SoftLogoutScreenCoordinatorResult: CustomStringConvertible { final class SoftLogoutScreenCoordinator: CoordinatorProtocol { private let parameters: SoftLogoutScreenCoordinatorParameters private var viewModel: SoftLogoutScreenViewModelProtocol + private let actionsSubject: PassthroughSubject = .init() + private var cancellables = Set() private var authenticationService: AuthenticationServiceProxyProtocol { parameters.authenticationService } - var callback: (@MainActor (SoftLogoutScreenCoordinatorResult) -> Void)? + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } @MainActor init(parameters: SoftLogoutScreenCoordinatorParameters) { self.parameters = parameters @@ -60,21 +65,23 @@ final class SoftLogoutScreenCoordinator: CoordinatorProtocol { // MARK: - Public func start() { - viewModel.callback = { [weak self] result in - guard let self else { return } - MXLog.info("[SoftLogoutCoordinator] SoftLogoutViewModel did complete with result: \(result).") + viewModel.actions + .sink { [weak self] action in + guard let self else { return } + MXLog.info("[SoftLogoutCoordinator] SoftLogoutViewModel did complete with result: \(action).") - switch result { - case .login(let password): - self.login(withPassword: password) - case .forgotPassword: - self.showForgotPasswordScreen() - case .clearAllData: - self.callback?(.clearAllData) - case .continueWithOIDC: - self.continueWithOIDC(presentationAnchor: viewModel.context.viewState.window) + switch action { + case .login(let password): + login(withPassword: password) + case .forgotPassword: + showForgotPasswordScreen() + case .clearAllData: + actionsSubject.send(.clearAllData) + case .continueWithOIDC: + continueWithOIDC(presentationAnchor: viewModel.context.viewState.window) + } } - } + .store(in: &cancellables) } func stop() { @@ -119,7 +126,7 @@ final class SoftLogoutScreenCoordinator: CoordinatorProtocol { initialDeviceName: UIDevice.current.initialDeviceName, deviceID: parameters.credentials.deviceID) { case .success(let userSession): - callback?(.signedIn(userSession)) + actionsSubject.send(.signedIn(userSession)) stopLoading() case .failure(let error): stopLoading() @@ -146,7 +153,7 @@ final class SoftLogoutScreenCoordinator: CoordinatorProtocol { presentationAnchor: presentationAnchor) switch await presenter.authenticate(using: oidcData) { case .success(let userSession): - callback?(.signedIn(userSession)) + actionsSubject.send(.signedIn(userSession)) case .failure(let error): handleError(error) } diff --git a/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenViewModel.swift b/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenViewModel.swift index afe970a0d..8558f9580 100644 --- a/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenViewModel.swift +++ b/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenViewModel.swift @@ -14,12 +14,17 @@ // limitations under the License. // +import Combine import SwiftUI typealias SoftLogoutScreenViewModelType = StateStoreViewModel class SoftLogoutScreenViewModel: SoftLogoutScreenViewModelType, SoftLogoutScreenViewModelProtocol { - var callback: (@MainActor (SoftLogoutScreenViewModelAction) -> Void)? + private var actionsSubject: PassthroughSubject = .init() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } init(credentials: SoftLogoutScreenCredentials, homeserver: LoginHomeserver, @@ -36,13 +41,13 @@ class SoftLogoutScreenViewModel: SoftLogoutScreenViewModelType, SoftLogoutScreen override func process(viewAction: SoftLogoutScreenViewAction) { switch viewAction { case .login: - callback?(.login(state.bindings.password)) + actionsSubject.send(.login(state.bindings.password)) case .forgotPassword: - callback?(.forgotPassword) + actionsSubject.send(.forgotPassword) case .clearAllData: - callback?(.clearAllData) + actionsSubject.send(.clearAllData) case .continueWithOIDC: - callback?(.continueWithOIDC) + actionsSubject.send(.continueWithOIDC) case .updateWindow(let window): guard state.window != window else { return } Task { state.window = window } diff --git a/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenViewModelProtocol.swift b/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenViewModelProtocol.swift index ed1a442c0..df224a850 100644 --- a/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenViewModelProtocol.swift @@ -14,10 +14,10 @@ // limitations under the License. // -import Foundation +import Combine protocol SoftLogoutScreenViewModelProtocol { - var callback: (@MainActor (SoftLogoutScreenViewModelAction) -> Void)? { get set } + var actions: AnyPublisher { get } var context: SoftLogoutScreenViewModelType.Context { get } /// Display an error to the user. diff --git a/ElementX/Sources/Screens/Authentication/WaitlistScreen/WaitlistScreenCoordinator.swift b/ElementX/Sources/Screens/Authentication/WaitlistScreen/WaitlistScreenCoordinator.swift index 0b01dafcf..1d71fdf48 100644 --- a/ElementX/Sources/Screens/Authentication/WaitlistScreen/WaitlistScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/WaitlistScreen/WaitlistScreenCoordinator.swift @@ -37,7 +37,7 @@ final class WaitlistScreenCoordinator: CoordinatorProtocol { private let parameters: WaitlistScreenCoordinatorParameters private var viewModel: WaitlistScreenViewModelProtocol private let actionsSubject: PassthroughSubject = .init() - private var cancellables: Set = .init() + private var cancellables = Set() private var refreshCancellable: AnyCancellable? var actions: AnyPublisher { diff --git a/ElementX/Sources/Screens/BugReportScreen/BugReportScreenCoordinator.swift b/ElementX/Sources/Screens/BugReportScreen/BugReportScreenCoordinator.swift index 443a19ee6..61041af6f 100644 --- a/ElementX/Sources/Screens/BugReportScreen/BugReportScreenCoordinator.swift +++ b/ElementX/Sources/Screens/BugReportScreen/BugReportScreenCoordinator.swift @@ -35,7 +35,7 @@ struct BugReportScreenCoordinatorParameters { final class BugReportScreenCoordinator: CoordinatorProtocol { private let parameters: BugReportScreenCoordinatorParameters private var viewModel: BugReportScreenViewModelProtocol - private var cancellables: Set = .init() + private var cancellables = Set() var completion: ((BugReportScreenCoordinatorResult) -> Void)? @@ -54,10 +54,10 @@ final class BugReportScreenCoordinator: CoordinatorProtocol { func start() { viewModel .actions - .sink { [weak self] result in + .sink { [weak self] action in guard let self else { return } - MXLog.info("BugReportViewModel did complete with result: \(result).") - switch result { + MXLog.info("BugReportViewModel did complete with result: \(action).") + switch action { case .cancel: self.completion?(.cancel) case let .submitStarted(progressPublisher): diff --git a/ElementX/Sources/Screens/CreatePollScreen/CreatePollScreenCoordinator.swift b/ElementX/Sources/Screens/CreatePollScreen/CreatePollScreenCoordinator.swift index 8d6dbac25..7f2a853ab 100644 --- a/ElementX/Sources/Screens/CreatePollScreen/CreatePollScreenCoordinator.swift +++ b/ElementX/Sources/Screens/CreatePollScreen/CreatePollScreenCoordinator.swift @@ -28,7 +28,7 @@ final class CreatePollScreenCoordinator: CoordinatorProtocol { private let parameters: CreatePollScreenCoordinatorParameters private var viewModel: CreatePollScreenViewModelProtocol private let actionsSubject: PassthroughSubject = .init() - private var cancellables: Set = .init() + private var cancellables = Set() var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() diff --git a/ElementX/Sources/Screens/CreateRoom/CreateRoomCoordinator.swift b/ElementX/Sources/Screens/CreateRoom/CreateRoomCoordinator.swift index b2a10e443..7bdca1c60 100644 --- a/ElementX/Sources/Screens/CreateRoom/CreateRoomCoordinator.swift +++ b/ElementX/Sources/Screens/CreateRoom/CreateRoomCoordinator.swift @@ -36,7 +36,7 @@ final class CreateRoomCoordinator: CoordinatorProtocol { private let parameters: CreateRoomCoordinatorParameters private var viewModel: CreateRoomViewModelProtocol private let actionsSubject: PassthroughSubject = .init() - private var cancellables: Set = .init() + private var cancellables = Set() var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() diff --git a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenCoordinator.swift b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenCoordinator.swift index 25601d2ae..9e5b55a58 100644 --- a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenCoordinator.swift +++ b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenCoordinator.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import Combine import SwiftUI struct EmojiPickerScreenCoordinatorParameters { @@ -31,7 +32,12 @@ final class EmojiPickerScreenCoordinator: CoordinatorProtocol { private let parameters: EmojiPickerScreenCoordinatorParameters private var viewModel: EmojiPickerScreenViewModelProtocol - var callback: ((EmojiPickerScreenCoordinatorAction) -> Void)? + private let actionsSubject: PassthroughSubject = .init() + private var cancellables = Set() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } init(parameters: EmojiPickerScreenCoordinatorParameters) { self.parameters = parameters @@ -40,16 +46,18 @@ final class EmojiPickerScreenCoordinator: CoordinatorProtocol { } func start() { - viewModel.callback = { [weak self] action in - guard let self else { return } - - switch action { - case let .emojiSelected(emoji: emoji): - self.callback?(.emojiSelected(emoji: emoji, itemID: self.parameters.itemID)) - case .dismiss: - self.callback?(.dismiss) + viewModel.actions + .sink { [weak self] action in + guard let self else { return } + + switch action { + case let .emojiSelected(emoji: emoji): + actionsSubject.send(.emojiSelected(emoji: emoji, itemID: self.parameters.itemID)) + case .dismiss: + actionsSubject.send(.dismiss) + } } - } + .store(in: &cancellables) } func toPresentable() -> AnyView { diff --git a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift index ee027604d..07ab521ce 100644 --- a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift +++ b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift @@ -14,12 +14,17 @@ // limitations under the License. // +import Combine import SwiftUI typealias EmojiPickerScreenViewModelType = StateStoreViewModel class EmojiPickerScreenViewModel: EmojiPickerScreenViewModelType, EmojiPickerScreenViewModelProtocol { - var callback: ((EmojiPickerScreenViewModelAction) -> Void)? + private var actionsSubject: PassthroughSubject = .init() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } private let emojiProvider: EmojiProviderProtocol @@ -40,9 +45,9 @@ class EmojiPickerScreenViewModel: EmojiPickerScreenViewModelType, EmojiPickerScr state.categories = convert(emojiCategories: categories) } case let .emojiTapped(emoji: emoji): - callback?(.emojiSelected(emoji: emoji.value)) + actionsSubject.send(.emojiSelected(emoji: emoji.value)) case .dismiss: - callback?(.dismiss) + actionsSubject.send(.dismiss) } } diff --git a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModelProtocol.swift b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModelProtocol.swift index 1084995e3..a3e07bdc3 100644 --- a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModelProtocol.swift @@ -14,10 +14,10 @@ // limitations under the License. // -import Foundation +import Combine @MainActor protocol EmojiPickerScreenViewModelProtocol { - var callback: ((EmojiPickerScreenViewModelAction) -> Void)? { get set } + var actions: AnyPublisher { get } var context: EmojiPickerScreenViewModelType.Context { get } } diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift index 454a3ac73..5da492345 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift @@ -41,9 +41,12 @@ final class HomeScreenCoordinator: CoordinatorProtocol { private let parameters: HomeScreenCoordinatorParameters private var viewModel: HomeScreenViewModelProtocol + private let actionsSubject: PassthroughSubject = .init() private var cancellables = Set() - var callback: ((HomeScreenCoordinatorAction) -> Void)? + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } init(parameters: HomeScreenCoordinatorParameters) { self.parameters = parameters @@ -55,30 +58,32 @@ final class HomeScreenCoordinator: CoordinatorProtocol { analytics: ServiceLocator.shared.analytics, userIndicatorController: ServiceLocator.shared.userIndicatorController) - viewModel.callback = { [weak self] action in - guard let self else { return } - - switch action { - case .presentRoom(let roomIdentifier): - self.callback?(.presentRoom(roomIdentifier: roomIdentifier)) - case .presentRoomDetails(roomIdentifier: let roomIdentifier): - self.callback?(.presentRoomDetails(roomIdentifier: roomIdentifier)) - case .roomLeft(roomIdentifier: let roomIdentifier): - self.callback?(.roomLeft(roomIdentifier: roomIdentifier)) - case .presentFeedbackScreen: - self.callback?(.presentFeedbackScreen) - case .presentSettingsScreen: - self.callback?(.presentSettingsScreen) - case .presentSessionVerificationScreen: - self.callback?(.presentSessionVerificationScreen) - case .signOut: - self.callback?(.signOut) - case .presentStartChatScreen: - self.callback?(.presentStartChatScreen) - case .presentInvitesScreen: - self.callback?(.presentInvitesScreen) + viewModel.actions + .sink { [weak self] action in + guard let self else { return } + + switch action { + case .presentRoom(let roomIdentifier): + actionsSubject.send(.presentRoom(roomIdentifier: roomIdentifier)) + case .presentRoomDetails(roomIdentifier: let roomIdentifier): + actionsSubject.send(.presentRoomDetails(roomIdentifier: roomIdentifier)) + case .roomLeft(roomIdentifier: let roomIdentifier): + actionsSubject.send(.roomLeft(roomIdentifier: roomIdentifier)) + case .presentFeedbackScreen: + actionsSubject.send(.presentFeedbackScreen) + case .presentSettingsScreen: + actionsSubject.send(.presentSettingsScreen) + case .presentSessionVerificationScreen: + actionsSubject.send(.presentSessionVerificationScreen) + case .signOut: + actionsSubject.send(.signOut) + case .presentStartChatScreen: + actionsSubject.send(.presentStartChatScreen) + case .presentInvitesScreen: + actionsSubject.send(.presentInvitesScreen) + } } - } + .store(in: &cancellables) } // MARK: - Public diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index fe53bf8b9..e352f9b78 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -32,7 +32,11 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol private var visibleItemRangeObservationToken: AnyCancellable? private let visibleItemRangePublisher = CurrentValueSubject<(range: Range, isScrolling: Bool), Never>((0..<0, false)) - var callback: ((HomeScreenViewModelAction) -> Void)? + private var actionsSubject: PassthroughSubject = .init() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } init(userSession: UserSessionProtocol, attributedStringBuilder: AttributedStringBuilderProtocol, @@ -92,9 +96,9 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol override func process(viewAction: HomeScreenViewAction) { switch viewAction { case .selectRoom(let roomIdentifier): - callback?(.presentRoom(roomIdentifier: roomIdentifier)) + actionsSubject.send(.presentRoom(roomIdentifier: roomIdentifier)) case .showRoomDetails(roomIdentifier: let roomIdentifier): - callback?(.presentRoomDetails(roomIdentifier: roomIdentifier)) + actionsSubject.send(.presentRoomDetails(roomIdentifier: roomIdentifier)) case .leaveRoom(roomIdentifier: let roomIdentifier): startLeaveRoomProcess(roomId: roomIdentifier) case .confirmLeaveRoom(roomIdentifier: let roomIdentifier): @@ -102,22 +106,22 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol case .userMenu(let action): switch action { case .feedback: - callback?(.presentFeedbackScreen) + actionsSubject.send(.presentFeedbackScreen) case .settings: - callback?(.presentSettingsScreen) + actionsSubject.send(.presentSettingsScreen) case .signOut: - callback?(.signOut) + actionsSubject.send(.signOut) } case .verifySession: - callback?(.presentSessionVerificationScreen) + actionsSubject.send(.presentSessionVerificationScreen) case .skipSessionVerification: state.showSessionVerificationBanner = false case .updateVisibleItemRange(let range, let isScrolling): visibleItemRangePublisher.send((range, isScrolling)) case .startChat: - callback?(.presentStartChatScreen) + actionsSubject.send(.presentStartChatScreen) case .selectInvites: - callback?(.presentInvitesScreen) + actionsSubject.send(.presentInvitesScreen) } } @@ -126,7 +130,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol title: L10n.crashDetectionDialogContent(InfoPlistReader.main.bundleDisplayName), primaryButton: .init(title: L10n.actionNo, action: nil), secondaryButton: .init(title: L10n.actionYes) { [weak self] in - self?.callback?(.presentFeedbackScreen) + self?.actionsSubject.send(.presentFeedbackScreen) }) } @@ -335,7 +339,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol type: .modal(progress: .none, interactiveDismissDisabled: false, allowsInteraction: false), title: L10n.commonCurrentUserLeftRoom, iconName: "checkmark")) - callback?(.roomLeft(roomIdentifier: roomId)) + actionsSubject.send(.roomLeft(roomIdentifier: roomId)) } } } diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModelProtocol.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModelProtocol.swift index dfa6b035d..02c6d170d 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModelProtocol.swift @@ -14,12 +14,11 @@ // limitations under the License. // -import Foundation -import UIKit +import Combine @MainActor protocol HomeScreenViewModelProtocol { - var callback: ((HomeScreenViewModelAction) -> Void)? { get set } + var actions: AnyPublisher { get } var context: HomeScreenViewModelType.Context { get } diff --git a/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenCoordinator.swift b/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenCoordinator.swift index 8ae2967db..a70aea4c4 100644 --- a/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenCoordinator.swift +++ b/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenCoordinator.swift @@ -36,7 +36,7 @@ final class InviteUsersScreenCoordinator: CoordinatorProtocol { private let parameters: InviteUsersScreenCoordinatorParameters private let viewModel: InviteUsersScreenViewModelProtocol private let actionsSubject: PassthroughSubject = .init() - private var cancellables: Set = .init() + private var cancellables = Set() var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() diff --git a/ElementX/Sources/Screens/InvitesScreen/InvitesScreenCoordinator.swift b/ElementX/Sources/Screens/InvitesScreen/InvitesScreenCoordinator.swift index 411e93f96..57caa0826 100644 --- a/ElementX/Sources/Screens/InvitesScreen/InvitesScreenCoordinator.swift +++ b/ElementX/Sources/Screens/InvitesScreen/InvitesScreenCoordinator.swift @@ -29,7 +29,7 @@ final class InvitesScreenCoordinator: CoordinatorProtocol { private let parameters: InvitesScreenCoordinatorParameters private var viewModel: InvitesScreenViewModelProtocol private let actionsSubject: PassthroughSubject = .init() - private var cancellables: Set = .init() + private var cancellables = Set() var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() diff --git a/ElementX/Sources/Screens/LocationSharing/StaticLocationScreenCoordinator.swift b/ElementX/Sources/Screens/LocationSharing/StaticLocationScreenCoordinator.swift index 59b6e4b73..49689a8dd 100644 --- a/ElementX/Sources/Screens/LocationSharing/StaticLocationScreenCoordinator.swift +++ b/ElementX/Sources/Screens/LocationSharing/StaticLocationScreenCoordinator.swift @@ -31,7 +31,7 @@ final class StaticLocationScreenCoordinator: CoordinatorProtocol { let viewModel: StaticLocationScreenViewModelProtocol private let actionsSubject: PassthroughSubject = .init() - private var cancellables: Set = .init() + private var cancellables = Set() var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() diff --git a/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenCoordinator.swift b/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenCoordinator.swift index dbef11d8a..ec3ffd866 100644 --- a/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenCoordinator.swift +++ b/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenCoordinator.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import Combine import SwiftUI struct MediaUploadPreviewScreenCoordinatorParameters { @@ -30,11 +31,14 @@ enum MediaUploadPreviewScreenCoordinatorAction { final class MediaUploadPreviewScreenCoordinator: CoordinatorProtocol { private var viewModel: MediaUploadPreviewScreenViewModelProtocol - private let callback: (MediaUploadPreviewScreenCoordinatorAction) -> Void + private let actionsSubject: PassthroughSubject = .init() + private var cancellables = Set() - init(parameters: MediaUploadPreviewScreenCoordinatorParameters, callback: @escaping (MediaUploadPreviewScreenCoordinatorAction) -> Void) { - self.callback = callback - + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(parameters: MediaUploadPreviewScreenCoordinatorParameters) { viewModel = MediaUploadPreviewScreenViewModel(userIndicatorController: parameters.userIndicatorController, roomProxy: parameters.roomProxy, mediaUploadingPreprocessor: parameters.mediaUploadingPreprocessor, @@ -43,14 +47,18 @@ final class MediaUploadPreviewScreenCoordinator: CoordinatorProtocol { } func start() { - viewModel.callback = { [weak self] action in - switch action { - case .dismiss: - self?.callback(.dismiss) + viewModel.actions + .sink { [weak self] action in + guard let self else { return } + + switch action { + case .dismiss: + actionsSubject.send(.dismiss) + } } - } + .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(MediaUploadPreviewScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModel.swift b/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModel.swift index f91c5dda1..9bc2c308e 100644 --- a/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModel.swift +++ b/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModel.swift @@ -31,7 +31,11 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType, } } - var callback: ((MediaUploadPreviewScreenViewModelAction) -> Void)? + private var actionsSubject: PassthroughSubject = .init() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } init(userIndicatorController: UserIndicatorControllerProtocol?, roomProxy: RoomProxyProtocol, @@ -58,7 +62,7 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType, case .success(let mediaInfo): switch await sendAttachment(mediaInfo: mediaInfo, progressSubject: progressSubject) { case .success: - callback?(.dismiss) + actionsSubject.send(.dismiss) case .failure(let error): MXLog.error("Failed sending attachment with error: \(error)") showError(label: L10n.screenMediaUploadPreviewErrorFailedSending) @@ -74,7 +78,7 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType, case .cancel: requestHandle?.cancel() - callback?(.dismiss) + actionsSubject.send(.dismiss) } } diff --git a/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModelProtocol.swift b/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModelProtocol.swift index a4f034420..1cb414d86 100644 --- a/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModelProtocol.swift @@ -14,10 +14,10 @@ // limitations under the License. // -import Foundation +import Combine @MainActor protocol MediaUploadPreviewScreenViewModelProtocol { - var callback: ((MediaUploadPreviewScreenViewModelAction) -> Void)? { get set } + var actions: AnyPublisher { get } var context: MediaUploadPreviewScreenViewModelType.Context { get } } diff --git a/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenCoordinator.swift b/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenCoordinator.swift index b485b2665..675e688d4 100644 --- a/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenCoordinator.swift +++ b/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenCoordinator.swift @@ -31,7 +31,7 @@ final class MessageForwardingScreenCoordinator: CoordinatorProtocol { private let parameters: MessageForwardingScreenCoordinatorParameters private var viewModel: MessageForwardingScreenViewModelProtocol private let actionsSubject: PassthroughSubject = .init() - private var cancellables: Set = .init() + private var cancellables = Set() var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() diff --git a/ElementX/Sources/Screens/MigrationScreen/MigrationScreenCoordinator.swift b/ElementX/Sources/Screens/MigrationScreen/MigrationScreenCoordinator.swift index 4a0ba77fb..afd71adb5 100644 --- a/ElementX/Sources/Screens/MigrationScreen/MigrationScreenCoordinator.swift +++ b/ElementX/Sources/Screens/MigrationScreen/MigrationScreenCoordinator.swift @@ -19,7 +19,7 @@ import SwiftUI final class MigrationScreenCoordinator: CoordinatorProtocol { private var viewModel: MigrationScreenViewModelProtocol - private var cancellables: Set = .init() + private var cancellables = Set() init() { viewModel = MigrationScreenViewModel() diff --git a/ElementX/Sources/Screens/OnboardingScreen/OnboardingCoordinator.swift b/ElementX/Sources/Screens/OnboardingScreen/OnboardingScreenCoordinator.swift similarity index 51% rename from ElementX/Sources/Screens/OnboardingScreen/OnboardingCoordinator.swift rename to ElementX/Sources/Screens/OnboardingScreen/OnboardingScreenCoordinator.swift index 64f0f8d71..ea008ccc8 100644 --- a/ElementX/Sources/Screens/OnboardingScreen/OnboardingCoordinator.swift +++ b/ElementX/Sources/Screens/OnboardingScreen/OnboardingScreenCoordinator.swift @@ -14,28 +14,35 @@ // limitations under the License. // +import Combine import SwiftUI -final class OnboardingCoordinator: CoordinatorProtocol { - private var viewModel: OnboardingViewModelProtocol - - var callback: ((OnboardingCoordinatorAction) -> Void)? +final class OnboardingScreenCoordinator: CoordinatorProtocol { + private var viewModel: OnboardingScreenViewModelProtocol + private let actionsSubject: PassthroughSubject = .init() + private var cancellables = Set() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } init() { - viewModel = OnboardingViewModel() + viewModel = OnboardingScreenViewModel() } // MARK: - Public func start() { - viewModel.callback = { [weak self] action in - guard let self else { return } - - switch action { - case .login: - self.callback?(.login) + viewModel.actions + .sink { [weak self] action in + guard let self else { return } + + switch action { + case .login: + actionsSubject.send(.login) + } } - } + .store(in: &cancellables) } func toPresentable() -> AnyView { diff --git a/ElementX/Sources/Screens/OnboardingScreen/OnboardingModels.swift b/ElementX/Sources/Screens/OnboardingScreen/OnboardingScreenModels.swift similarity index 80% rename from ElementX/Sources/Screens/OnboardingScreen/OnboardingModels.swift rename to ElementX/Sources/Screens/OnboardingScreen/OnboardingScreenModels.swift index 8aa8f8770..d6b16f355 100644 --- a/ElementX/Sources/Screens/OnboardingScreen/OnboardingModels.swift +++ b/ElementX/Sources/Screens/OnboardingScreen/OnboardingScreenModels.swift @@ -18,23 +18,23 @@ import SwiftUI // MARK: - Coordinator -enum OnboardingCoordinatorAction { +enum OnboardingScreenCoordinatorAction { case login } /// The content displayed in a single screen page. -struct OnboardingPageContent { +struct OnboardingScreenPageContent { let title: AttributedString let message: String let image: ImageAsset } -enum OnboardingViewModelAction { +enum OnboardingScreenViewModelAction { case login } -struct OnboardingViewState: BindableState { } +struct OnboardingScreenViewState: BindableState { } -enum OnboardingViewAction { +enum OnboardingScreenViewAction { case login } diff --git a/ElementX/Sources/Screens/OnboardingScreen/OnboardingViewModel.swift b/ElementX/Sources/Screens/OnboardingScreen/OnboardingScreenViewModel.swift similarity index 53% rename from ElementX/Sources/Screens/OnboardingScreen/OnboardingViewModel.swift rename to ElementX/Sources/Screens/OnboardingScreen/OnboardingScreenViewModel.swift index e29b890ca..c34acf7df 100644 --- a/ElementX/Sources/Screens/OnboardingScreen/OnboardingViewModel.swift +++ b/ElementX/Sources/Screens/OnboardingScreen/OnboardingScreenViewModel.swift @@ -17,19 +17,23 @@ import Combine import SwiftUI -typealias OnboardingViewModelType = StateStoreViewModel +typealias OnboardingScreenViewModelType = StateStoreViewModel -class OnboardingViewModel: OnboardingViewModelType, OnboardingViewModelProtocol { - var callback: ((OnboardingViewModelAction) -> Void)? - - init() { - super.init(initialViewState: OnboardingViewState()) +class OnboardingScreenViewModel: OnboardingScreenViewModelType, OnboardingScreenViewModelProtocol { + private var actionsSubject: PassthroughSubject = .init() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() } - override func process(viewAction: OnboardingViewAction) { + init() { + super.init(initialViewState: OnboardingScreenViewState()) + } + + override func process(viewAction: OnboardingScreenViewAction) { switch viewAction { case .login: - callback?(.login) + actionsSubject.send(.login) } } } diff --git a/ElementX/Sources/Screens/OnboardingScreen/OnboardingViewModelProtocol.swift b/ElementX/Sources/Screens/OnboardingScreen/OnboardingScreenViewModelProtocol.swift similarity index 75% rename from ElementX/Sources/Screens/OnboardingScreen/OnboardingViewModelProtocol.swift rename to ElementX/Sources/Screens/OnboardingScreen/OnboardingScreenViewModelProtocol.swift index a876a2156..e0ad01013 100644 --- a/ElementX/Sources/Screens/OnboardingScreen/OnboardingViewModelProtocol.swift +++ b/ElementX/Sources/Screens/OnboardingScreen/OnboardingScreenViewModelProtocol.swift @@ -14,10 +14,10 @@ // limitations under the License. // -import Foundation +import Combine @MainActor -protocol OnboardingViewModelProtocol { - var callback: ((OnboardingViewModelAction) -> Void)? { get set } - var context: OnboardingViewModelType.Context { get } +protocol OnboardingScreenViewModelProtocol { + var actions: AnyPublisher { get } + var context: OnboardingScreenViewModelType.Context { get } } diff --git a/ElementX/Sources/Screens/OnboardingScreen/View/OnboardingScreen.swift b/ElementX/Sources/Screens/OnboardingScreen/View/OnboardingScreen.swift index 51c9e8477..7a44061c0 100644 --- a/ElementX/Sources/Screens/OnboardingScreen/View/OnboardingScreen.swift +++ b/ElementX/Sources/Screens/OnboardingScreen/View/OnboardingScreen.swift @@ -21,7 +21,7 @@ import SwiftUI struct OnboardingScreen: View { @Environment(\.verticalSizeClass) private var verticalSizeClass - @ObservedObject var context: OnboardingViewModel.Context + @ObservedObject var context: OnboardingScreenViewModel.Context var body: some View { GeometryReader { geometry in @@ -45,7 +45,7 @@ struct OnboardingScreen: View { } .navigationBarHidden(true) .background { - OnboardingBackgroundImage() + OnboardingScreenBackgroundImage() } } @@ -99,7 +99,7 @@ struct OnboardingScreen: View { // MARK: - Previews struct OnboardingScreen_Previews: PreviewProvider { - static let viewModel = OnboardingViewModel() + static let viewModel = OnboardingScreenViewModel() static var previews: some View { OnboardingScreen(context: viewModel.context) diff --git a/ElementX/Sources/Screens/OnboardingScreen/View/OnboardingBackgroundImage.swift b/ElementX/Sources/Screens/OnboardingScreen/View/OnboardingScreenBackgroundImage.swift similarity index 95% rename from ElementX/Sources/Screens/OnboardingScreen/View/OnboardingBackgroundImage.swift rename to ElementX/Sources/Screens/OnboardingScreen/View/OnboardingScreenBackgroundImage.swift index 0e25364f1..721b17415 100644 --- a/ElementX/Sources/Screens/OnboardingScreen/View/OnboardingBackgroundImage.swift +++ b/ElementX/Sources/Screens/OnboardingScreen/View/OnboardingScreenBackgroundImage.swift @@ -17,7 +17,7 @@ import SwiftUI /// The background gradient shown on the launch, splash and onboarding screens. -struct OnboardingBackgroundImage: View { +struct OnboardingScreenBackgroundImage: View { var body: some View { Image(asset: Asset.Images.launchBackground) .resizable() diff --git a/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenCoordinator.swift b/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenCoordinator.swift index 9efc705d8..ad3327554 100644 --- a/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenCoordinator.swift +++ b/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenCoordinator.swift @@ -32,9 +32,12 @@ enum ReportContentScreenCoordinatorAction { final class ReportContentScreenCoordinator: CoordinatorProtocol { private let parameters: ReportContentScreenCoordinatorParameters private var viewModel: ReportContentScreenViewModelProtocol - private var cancellables: Set = .init() + private let actionsSubject: PassthroughSubject = .init() + private var cancellables = Set() - var callback: ((ReportContentScreenCoordinatorAction) -> Void)? + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } init(parameters: ReportContentScreenCoordinatorParameters) { self.parameters = parameters @@ -48,17 +51,18 @@ final class ReportContentScreenCoordinator: CoordinatorProtocol { viewModel.actions .sink { [weak self] action in guard let self else { return } + switch action { case .submitStarted: - self.startLoading() + startLoading() case let .submitFailed(error): - self.stopLoading() - self.showError(description: error.localizedDescription) + stopLoading() + showError(description: error.localizedDescription) case .submitFinished: - self.stopLoading() - self.callback?(.finish) + stopLoading() + actionsSubject.send(.finish) case .cancel: - self.callback?(.cancel) + actionsSubject.send(.cancel) } } .store(in: &cancellables) diff --git a/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenCoordinator.swift b/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenCoordinator.swift index f94f9edcf..6b420d087 100644 --- a/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenCoordinator.swift @@ -33,7 +33,7 @@ final class RoomDetailsEditScreenCoordinator: CoordinatorProtocol { private let parameters: RoomDetailsEditScreenCoordinatorParameters private var viewModel: RoomDetailsEditScreenViewModelProtocol private let actionsSubject: PassthroughSubject = .init() - private var cancellables: Set = .init() + private var cancellables = Set() var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift index 9a8a98f06..6522c4c7f 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift @@ -41,7 +41,7 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol { } private let actionsSubject: PassthroughSubject = .init() - private var cancellables: Set = .init() + private var cancellables = Set() var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() @@ -60,22 +60,24 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol { // MARK: - Public func start() { - viewModel.callback = { [weak self] action in - guard let self else { return } - - switch action { - case .requestMemberDetailsPresentation: - self.presentRoomMembersList() - case .requestInvitePeoplePresentation: - self.presentInviteUsersScreen() - case .leftRoom: - self.actionsSubject.send(.leftRoom) - case .requestEditDetailsPresentation(let accountOwner): - self.presentRoomDetailsEditScreen(accountOwner: accountOwner) - case .requestNotificationSettingsPresentation: - self.presentNotificationSettings() + viewModel.actions + .sink { [weak self] action in + guard let self else { return } + + switch action { + case .requestMemberDetailsPresentation: + presentRoomMembersList() + case .requestInvitePeoplePresentation: + presentInviteUsersScreen() + case .leftRoom: + actionsSubject.send(.leftRoom) + case .requestEditDetailsPresentation(let accountOwner): + presentRoomDetailsEditScreen(accountOwner: accountOwner) + case .requestNotificationSettingsPresentation: + presentNotificationSettings() + } } - } + .store(in: &cancellables) } func stop() { @@ -94,12 +96,16 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol { roomProxy: parameters.roomProxy) let coordinator = RoomMembersListScreenCoordinator(parameters: params) - coordinator.callback = { [weak self] action in - switch action { - case .invite: - self?.presentInviteUsersScreen() + coordinator.actions + .sink { [weak self] action in + guard let self else { return } + + switch action { + case .invite: + presentInviteUsersScreen() + } } - } + .store(in: &cancellables) navigationStackCoordinator?.push(coordinator) } @@ -115,10 +121,10 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol { let coordinator = InviteUsersScreenCoordinator(parameters: inviteParameters) inviteUsersStackCoordinator.setRootCoordinator(coordinator) - coordinator.actions.sink { [weak self] result in + coordinator.actions.sink { [weak self] action in guard let self else { return } - switch result { + switch action { case .cancel: navigationStackCoordinator?.setSheetCoordinator(nil) case .proceed: diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift index 33107e62a..9f0d123d1 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift @@ -32,7 +32,11 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr private var dmRecipient: RoomMemberProxyProtocol? - var callback: ((RoomDetailsScreenViewModelAction) -> Void)? + private var actionsSubject: PassthroughSubject = .init() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } init(accountUserID: String, roomProxy: RoomProxyProtocol, @@ -75,9 +79,9 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr override func process(viewAction: RoomDetailsScreenViewAction) { switch viewAction { case .processTapPeople: - callback?(.requestMemberDetailsPresentation) + actionsSubject.send(.requestMemberDetailsPresentation) case .processTapInvite: - callback?(.requestInvitePeoplePresentation) + actionsSubject.send(.requestInvitePeoplePresentation) case .processTapLeave: guard state.joinedMembersCount > 1 else { state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(roomId: roomProxy.id, state: .empty) @@ -95,7 +99,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr MXLog.error("Missing account owner when presenting the room's edit details screen") return } - callback?(.requestEditDetailsPresentation(accountOwner)) + actionsSubject.send(.requestEditDetailsPresentation(accountOwner)) case .ignoreConfirmed: Task { await ignore() } case .unignoreConfirmed: @@ -104,7 +108,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr if state.notificationSettingsState.isError { fetchNotificationSettings() } else { - callback?(.requestNotificationSettingsPresentation) + actionsSubject.send(.requestNotificationSettingsPresentation) } case .processToogleMuteNotifications: Task { await toggleMuteNotifications() } @@ -239,7 +243,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr case .failure: state.bindings.alertInfo = AlertInfo(id: .unknown) case .success: - callback?(.leftRoom) + actionsSubject.send(.leftRoom) } } diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModelProtocol.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModelProtocol.swift index 3030758e2..bafd1543e 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModelProtocol.swift @@ -14,11 +14,11 @@ // limitations under the License. // -import Foundation +import Combine @MainActor protocol RoomDetailsScreenViewModelProtocol { - var callback: ((RoomDetailsScreenViewModelAction) -> Void)? { get set } + var actions: AnyPublisher { get } var context: RoomDetailsScreenViewModelType.Context { get } func stop() } diff --git a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenCoordinator.swift b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenCoordinator.swift index 1e8625f09..0a1d7727e 100644 --- a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenCoordinator.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import Combine import SwiftUI struct RoomMemberDetailsScreenCoordinatorParameters { @@ -29,7 +30,12 @@ final class RoomMemberDetailsScreenCoordinator: CoordinatorProtocol { private let parameters: RoomMemberDetailsScreenCoordinatorParameters private var viewModel: RoomMemberDetailsScreenViewModelProtocol - var callback: ((RoomMemberDetailsScreenCoordinatorAction) -> Void)? + private let actionsSubject: PassthroughSubject = .init() + private var cancellables = Set() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } init(parameters: RoomMemberDetailsScreenCoordinatorParameters) { self.parameters = parameters diff --git a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift index 877966d04..786e947fe 100644 --- a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import Combine import SwiftUI typealias RoomMemberDetailsScreenViewModelType = StateStoreViewModel @@ -24,7 +25,11 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro private let mediaProvider: MediaProviderProtocol private let userIndicatorController: UserIndicatorControllerProtocol - var callback: ((RoomMemberDetailsScreenViewModelAction) -> Void)? + private var actionsSubject: PassthroughSubject = .init() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } init(roomProxy: RoomProxyProtocol, roomMemberProxy: RoomMemberProxyProtocol, diff --git a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModelProtocol.swift b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModelProtocol.swift index 96f42d983..56a58c900 100644 --- a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModelProtocol.swift @@ -14,11 +14,11 @@ // limitations under the License. // -import Foundation +import Combine @MainActor protocol RoomMemberDetailsScreenViewModelProtocol { - var callback: ((RoomMemberDetailsScreenViewModelAction) -> Void)? { get set } + var actions: AnyPublisher { get } var context: RoomMemberDetailsScreenViewModelType.Context { get } func stop() } diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenCoordinator.swift b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenCoordinator.swift index 1be8dfdc0..30e196e67 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenCoordinator.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import Combine import SwiftUI struct RoomMembersListScreenCoordinatorParameters { @@ -33,7 +34,12 @@ final class RoomMembersListScreenCoordinator: CoordinatorProtocol { parameters.navigationStackCoordinator } - var callback: ((RoomMembersListScreenCoordinatorAction) -> Void)? + private let actionsSubject: PassthroughSubject = .init() + private var cancellables = Set() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } init(parameters: RoomMembersListScreenCoordinatorParameters) { self.parameters = parameters @@ -44,16 +50,18 @@ final class RoomMembersListScreenCoordinator: CoordinatorProtocol { } func start() { - viewModel.callback = { [weak self] action in - guard let self else { return } - - switch action { - case let .selectMember(member): - self.selectMember(member) - case .invite: - callback?(.invite) + viewModel.actions + .sink { [weak self] action in + guard let self else { return } + + switch action { + case let .selectMember(member): + selectMember(member) + case .invite: + actionsSubject.send(.invite) + } } - } + .store(in: &cancellables) } func toPresentable() -> AnyView { diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift index c67b10b57..cf71a150d 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import Combine import SwiftUI typealias RoomMembersListScreenViewModelType = StateStoreViewModel @@ -24,7 +25,11 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe private var members: [RoomMemberProxyProtocol] = [] - var callback: ((RoomMembersListScreenViewModelAction) -> Void)? + private var actionsSubject: PassthroughSubject = .init() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } init(roomProxy: RoomProxyProtocol, mediaProvider: MediaProviderProtocol, @@ -47,9 +52,9 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe MXLog.error("Selected member \(id) not found") return } - callback?(.selectMember(member)) + actionsSubject.send(.selectMember(member)) case .invite: - callback?(.invite) + actionsSubject.send(.invite) } } diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModelProtocol.swift b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModelProtocol.swift index 5c66a7850..27eb47a76 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModelProtocol.swift @@ -14,10 +14,10 @@ // limitations under the License. // -import Foundation +import Combine @MainActor protocol RoomMembersListScreenViewModelProtocol { - var callback: ((RoomMembersListScreenViewModelAction) -> Void)? { get set } + var actions: AnyPublisher { get } var context: RoomMembersListScreenViewModelType.Context { get } } diff --git a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenCoordinator.swift b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenCoordinator.swift index d12878f23..9a65a3b35 100644 --- a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenCoordinator.swift @@ -33,7 +33,7 @@ final class RoomNotificationSettingsScreenCoordinator: CoordinatorProtocol { private var viewModel: RoomNotificationSettingsScreenViewModelProtocol private let actionsSubject: PassthroughSubject = .init() - private var cancellables: Set = .init() + private var cancellables = Set() var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index 74b03c17a..8321ec7d1 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -106,10 +106,10 @@ final class RoomScreenCoordinator: CoordinatorProtocol { .store(in: &cancellables) composerViewModel.actions - .sink { [weak self] composerAction in + .sink { [weak self] action in guard let self else { return } - viewModel.process(composerAction: composerAction) + viewModel.process(composerAction: action) } .store(in: &cancellables) } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineTableViewController.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineTableViewController.swift index bc0981310..a32e25647 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineTableViewController.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineTableViewController.swift @@ -74,7 +74,7 @@ class TimelineTableViewController: UIViewController { /// The table's diffable data source. private var dataSource: UITableViewDiffableDataSource? - private var cancellables: Set = [] + private var cancellables = Set() /// A publisher used to throttle back pagination requests. /// diff --git a/ElementX/Sources/Screens/SessionVerificationScreen/SessionVerificationScreenCoordinator.swift b/ElementX/Sources/Screens/SessionVerificationScreen/SessionVerificationScreenCoordinator.swift index 33b9cd20f..31dd8f679 100644 --- a/ElementX/Sources/Screens/SessionVerificationScreen/SessionVerificationScreenCoordinator.swift +++ b/ElementX/Sources/Screens/SessionVerificationScreen/SessionVerificationScreenCoordinator.swift @@ -14,8 +14,13 @@ // limitations under the License. // +import Combine import SwiftUI +enum SessionVerificationScreenCoordinatorAction { + case done +} + struct SessionVerificationScreenCoordinatorParameters { let sessionVerificationControllerProxy: SessionVerificationControllerProxyProtocol } @@ -24,7 +29,12 @@ final class SessionVerificationScreenCoordinator: CoordinatorProtocol { private let parameters: SessionVerificationScreenCoordinatorParameters private var viewModel: SessionVerificationScreenViewModelProtocol - var callback: (() -> Void)? + private let actionsSubject: PassthroughSubject = .init() + private var cancellables = Set() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } init(parameters: SessionVerificationScreenCoordinatorParameters) { self.parameters = parameters @@ -35,14 +45,16 @@ final class SessionVerificationScreenCoordinator: CoordinatorProtocol { // MARK: - Public func start() { - viewModel.callback = { [weak self] action in - guard let self else { return } - - switch action { - case .finished: - self.callback?() + viewModel.actions + .sink { [weak self] action in + guard let self else { return } + + switch action { + case .finished: + actionsSubject.send(.done) + } } - } + .store(in: &cancellables) } func toPresentable() -> AnyView { diff --git a/ElementX/Sources/Screens/SessionVerificationScreen/SessionVerificationScreenViewModel.swift b/ElementX/Sources/Screens/SessionVerificationScreen/SessionVerificationScreenViewModel.swift index b54d2b730..48f3ed89d 100644 --- a/ElementX/Sources/Screens/SessionVerificationScreen/SessionVerificationScreenViewModel.swift +++ b/ElementX/Sources/Screens/SessionVerificationScreen/SessionVerificationScreenViewModel.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import Combine import SwiftUI typealias SessionVerificationViewModelType = StateStoreViewModel @@ -23,7 +24,11 @@ class SessionVerificationScreenViewModel: SessionVerificationViewModelType, Sess private var stateMachine: SessionVerificationScreenStateMachine - var callback: ((SessionVerificationScreenViewModelAction) -> Void)? + private var actionsSubject: PassthroughSubject = .init() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } init(sessionVerificationControllerProxy: SessionVerificationControllerProxyProtocol, initialState: SessionVerificationScreenViewState = SessionVerificationScreenViewState()) { @@ -79,7 +84,7 @@ class SessionVerificationScreenViewModel: SessionVerificationViewModelType, Sess return } - callback?(.finished) + actionsSubject.send(.finished) case .accept: stateMachine.processEvent(.acceptChallenge) case .decline: @@ -93,24 +98,24 @@ class SessionVerificationScreenViewModel: SessionVerificationViewModelType, Sess stateMachine.addTransitionHandler { [weak self] context in guard let self else { return } - self.state.verificationState = context.toState + state.verificationState = context.toState switch (context.fromState, context.event, context.toState) { case (.initial, .requestVerification, .requestingVerification): - self.requestVerification() + requestVerification() case (.verificationRequestAccepted, .startSasVerification, .startingSasVerification): - self.startSasVerification() + startSasVerification() case (.showingChallenge, .acceptChallenge, .acceptingChallenge): - self.acceptChallenge() + acceptChallenge() case (.showingChallenge, .declineChallenge, .decliningChallenge): - self.declineChallenge() + declineChallenge() case (_, .cancel, .cancelling): - self.cancelVerification() + cancelVerification() case (_, _, .verified): // Dismiss the success screen automatically. Task { try? await Task.sleep(for: .seconds(2)) - self.callback?(.finished) + self.actionsSubject.send(.finished) } default: break diff --git a/ElementX/Sources/Screens/SessionVerificationScreen/SessionVerificationScreenViewModelProtocol.swift b/ElementX/Sources/Screens/SessionVerificationScreen/SessionVerificationScreenViewModelProtocol.swift index 9c1ebb74f..c145f0a7d 100644 --- a/ElementX/Sources/Screens/SessionVerificationScreen/SessionVerificationScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/SessionVerificationScreen/SessionVerificationScreenViewModelProtocol.swift @@ -14,10 +14,10 @@ // limitations under the License. // -import Foundation +import Combine @MainActor protocol SessionVerificationScreenViewModelProtocol { - var callback: ((SessionVerificationScreenViewModelAction) -> Void)? { get set } + var actions: AnyPublisher { get } var context: SessionVerificationViewModelType.Context { get } } diff --git a/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenCoordinator.swift index dd3492412..c0ef9cdec 100644 --- a/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenCoordinator.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import Combine import SwiftUI enum AdvancedSettingsScreenCoordinatorAction { @@ -23,7 +24,12 @@ enum AdvancedSettingsScreenCoordinatorAction { final class AdvancedSettingsScreenCoordinator: CoordinatorProtocol { private var viewModel: AdvancedSettingsScreenViewModelProtocol - var callback: ((AdvancedSettingsScreenCoordinatorAction) -> Void)? + private let actionsSubject: PassthroughSubject = .init() + private var cancellables = Set() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } init() { viewModel = AdvancedSettingsScreenViewModel(advancedSettings: ServiceLocator.shared.settings) diff --git a/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenViewModel.swift index 979db8714..5500a2769 100644 --- a/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenViewModel.swift @@ -14,12 +14,17 @@ // limitations under the License. // +import Combine import SwiftUI typealias AdvancedSettingsScreenViewModelType = StateStoreViewModel class AdvancedSettingsScreenViewModel: AdvancedSettingsScreenViewModelType, AdvancedSettingsScreenViewModelProtocol { - var callback: ((AdvancedSettingsScreenViewModelAction) -> Void)? + private var actionsSubject: PassthroughSubject = .init() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } init(advancedSettings: AdvancedSettingsProtocol) { let bindings = AdvancedSettingsScreenViewStateBindings(advancedSettings: advancedSettings) diff --git a/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenViewModelProtocol.swift b/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenViewModelProtocol.swift index 5f48e8748..34b4b7cfa 100644 --- a/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenViewModelProtocol.swift @@ -14,10 +14,10 @@ // limitations under the License. // -import Foundation +import Combine @MainActor protocol AdvancedSettingsScreenViewModelProtocol { - var callback: ((AdvancedSettingsScreenViewModelAction) -> Void)? { get set } + var actions: AnyPublisher { get } var context: AdvancedSettingsScreenViewModelType.Context { get } } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenCoordinator.swift index 49fd7bd54..28d11c608 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenCoordinator.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import Combine import SwiftUI enum DeveloperOptionsScreenCoordinatorAction { @@ -23,18 +24,28 @@ enum DeveloperOptionsScreenCoordinatorAction { final class DeveloperOptionsScreenCoordinator: CoordinatorProtocol { private var viewModel: DeveloperOptionsScreenViewModelProtocol - var callback: ((DeveloperOptionsScreenCoordinatorAction) -> Void)? + private let actionsSubject: PassthroughSubject = .init() + private var cancellables = Set() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } init() { viewModel = DeveloperOptionsScreenViewModel(developerOptions: ServiceLocator.shared.settings) - viewModel.callback = { [weak self] action in - switch action { - case .clearCache: - self?.callback?(.clearCache) + + viewModel.actions + .sink { [weak self] action in + guard let self else { return } + + switch action { + case .clearCache: + actionsSubject.send(.clearCache) + } } - } + .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(DeveloperOptionsScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift index 2923a4ea3..57292aee4 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift @@ -14,12 +14,17 @@ // limitations under the License. // +import Combine import SwiftUI typealias DeveloperOptionsScreenViewModelType = StateStoreViewModel class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, DeveloperOptionsScreenViewModelProtocol { - var callback: ((DeveloperOptionsScreenViewModelAction) -> Void)? + private var actionsSubject: PassthroughSubject = .init() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } init(developerOptions: DeveloperOptionsProtocol) { let bindings = DeveloperOptionsScreenViewStateBindings(developerOptions: developerOptions) @@ -31,7 +36,7 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve override func process(viewAction: DeveloperOptionsScreenViewAction) { switch viewAction { case .clearCache: - callback?(.clearCache) + actionsSubject.send(.clearCache) } } } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModelProtocol.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModelProtocol.swift index 257b5754d..baedd5e3d 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModelProtocol.swift @@ -14,10 +14,10 @@ // limitations under the License. // -import Foundation +import Combine @MainActor protocol DeveloperOptionsScreenViewModelProtocol { - var callback: ((DeveloperOptionsScreenViewModelAction) -> Void)? { get set } + var actions: AnyPublisher { get } var context: DeveloperOptionsScreenViewModelType.Context { get } } diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenCoordinator.swift index bc07c5682..3af88616e 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenCoordinator.swift @@ -30,7 +30,7 @@ final class NotificationSettingsEditScreenCoordinator: CoordinatorProtocol { private let parameters: NotificationSettingsEditScreenCoordinatorParameters private var viewModel: NotificationSettingsEditScreenViewModelProtocol private let actionsSubject: PassthroughSubject = .init() - private var cancellables: Set = .init() + private var cancellables = Set() var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenCoordinator.swift index 8141c8389..6107a5bc3 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenCoordinator.swift @@ -33,7 +33,7 @@ final class NotificationSettingsScreenCoordinator: CoordinatorProtocol { private let parameters: NotificationSettingsScreenCoordinatorParameters private var viewModel: NotificationSettingsScreenViewModelProtocol private let actionsSubject: PassthroughSubject = .init() - private var cancellables: Set = .init() + private var cancellables = Set() private var navigationStackCoordinator: NavigationStackCoordinator? { parameters.navigationStackCoordinator diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift index 70edfc4d8..08f97940f 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import Combine import SwiftUI struct SettingsScreenCoordinatorParameters { @@ -34,8 +35,13 @@ enum SettingsScreenCoordinatorAction { final class SettingsScreenCoordinator: CoordinatorProtocol { private let parameters: SettingsScreenCoordinatorParameters private var viewModel: SettingsScreenViewModelProtocol - - var callback: ((SettingsScreenCoordinatorAction) -> Void)? + + private let actionsSubject: PassthroughSubject = .init() + private var cancellables = Set() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } // MARK: - Setup @@ -43,34 +49,37 @@ final class SettingsScreenCoordinator: CoordinatorProtocol { self.parameters = parameters viewModel = SettingsScreenViewModel(userSession: parameters.userSession, appSettings: ServiceLocator.shared.settings) - viewModel.callback = { [weak self] action in - guard let self else { return } - - switch action { - case .close: - callback?(.dismiss) - case .accountProfile: - presentAccountProfileURL() - case .analytics: - presentAnalyticsScreen() - case .reportBug: - presentBugReportScreen() - case .about: - presentLegalInformationScreen() - case .sessionVerification: - verifySession() - case .accountSessionsList: - presentAccountSessionsListURL() - case .notifications: - presentNotificationSettings() - case .advancedSettings: - self.presentAdvancedSettings() - case .developerOptions: - presentDeveloperOptions() - case .logout: - callback?(.logout) + + viewModel.actions + .sink { [weak self] action in + guard let self else { return } + + switch action { + case .close: + actionsSubject.send(.dismiss) + case .accountProfile: + presentAccountProfileURL() + case .analytics: + presentAnalyticsScreen() + case .reportBug: + presentBugReportScreen() + case .about: + presentLegalInformationScreen() + case .sessionVerification: + verifySession() + case .accountSessionsList: + presentAccountSessionsListURL() + case .notifications: + presentNotificationSettings() + case .advancedSettings: + self.presentAdvancedSettings() + case .developerOptions: + presentDeveloperOptions() + case .logout: + actionsSubject.send(.logout) + } } - } + .store(in: &cancellables) } // MARK: - Public @@ -152,10 +161,17 @@ final class SettingsScreenCoordinator: CoordinatorProtocol { let verificationParameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationController) let coordinator = SessionVerificationScreenCoordinator(parameters: verificationParameters) - coordinator.callback = { [weak self] in - self?.parameters.navigationStackCoordinator?.setSheetCoordinator(nil) - } - + coordinator.actions + .sink { [weak self] action in + guard let self else { return } + + switch action { + case .done: + parameters.navigationStackCoordinator?.setSheetCoordinator(nil) + } + } + .store(in: &cancellables) + parameters.navigationStackCoordinator?.setSheetCoordinator(coordinator) { [weak self] in self?.parameters.navigationStackCoordinator?.setSheetCoordinator(nil) } @@ -179,12 +195,16 @@ final class SettingsScreenCoordinator: CoordinatorProtocol { private func presentDeveloperOptions() { let coordinator = DeveloperOptionsScreenCoordinator() - coordinator.callback = { [weak self] action in - switch action { - case .clearCache: - self?.callback?(.clearCache) + coordinator.actions + .sink { [weak self] action in + guard let self else { return } + + switch action { + case .clearCache: + actionsSubject.send(.clearCache) + } } - } + .store(in: &cancellables) parameters.navigationStackCoordinator?.push(coordinator) } diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift index bb96c14f7..b7fe2986e 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift @@ -23,7 +23,11 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo private let userSession: UserSessionProtocol private let appSettings: AppSettings - var callback: ((SettingsScreenViewModelAction) -> Void)? + private var actionsSubject: PassthroughSubject = .init() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } init(userSession: UserSessionProtocol, appSettings: AppSettings) { self.userSession = userSession @@ -74,27 +78,27 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo override func process(viewAction: SettingsScreenViewAction) { switch viewAction { case .close: - callback?(.close) + actionsSubject.send(.close) case .accountProfile: - callback?(.accountProfile) + actionsSubject.send(.accountProfile) case .analytics: - callback?(.analytics) + actionsSubject.send(.analytics) case .reportBug: - callback?(.reportBug) + actionsSubject.send(.reportBug) case .about: - callback?(.about) + actionsSubject.send(.about) case .logout: - callback?(.logout) + actionsSubject.send(.logout) case .sessionVerification: - callback?(.sessionVerification) + actionsSubject.send(.sessionVerification) case .notifications: - callback?(.notifications) + actionsSubject.send(.notifications) case .accountSessionsList: - callback?(.accountSessionsList) + actionsSubject.send(.accountSessionsList) case .advancedSettings: - callback?(.advancedSettings) + actionsSubject.send(.advancedSettings) case .developerOptions: - callback?(.developerOptions) + actionsSubject.send(.developerOptions) case .updateWindow(let window): Task { diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModelProtocol.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModelProtocol.swift index 149523328..d934c43e6 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModelProtocol.swift @@ -14,10 +14,10 @@ // limitations under the License. // -import Foundation +import Combine @MainActor protocol SettingsScreenViewModelProtocol { - var callback: ((SettingsScreenViewModelAction) -> Void)? { get set } + var actions: AnyPublisher { get } var context: SettingsScreenViewModelType.Context { get } } diff --git a/ElementX/Sources/Screens/StartChatScreen/StartChatScreenCoordinator.swift b/ElementX/Sources/Screens/StartChatScreen/StartChatScreenCoordinator.swift index cb0973fdb..0d790b992 100644 --- a/ElementX/Sources/Screens/StartChatScreen/StartChatScreenCoordinator.swift +++ b/ElementX/Sources/Screens/StartChatScreen/StartChatScreenCoordinator.swift @@ -33,7 +33,7 @@ final class StartChatScreenCoordinator: CoordinatorProtocol { private let parameters: StartChatScreenCoordinatorParameters private var viewModel: StartChatScreenViewModelProtocol private let actionsSubject: PassthroughSubject = .init() - private var cancellables: Set = .init() + private var cancellables = Set() private var createRoomParameters = CurrentValueSubject(.init()) private var createRoomParametersPublisher: CurrentValuePublisher { @@ -97,10 +97,10 @@ final class StartChatScreenCoordinator: CoordinatorProtocol { mediaProvider: parameters.userSession.mediaProvider, userDiscoveryService: parameters.userDiscoveryService) let coordinator = InviteUsersScreenCoordinator(parameters: inviteParameters) - coordinator.actions.sink { [weak self] result in + coordinator.actions.sink { [weak self] action in guard let self else { return } - switch result { + switch action { case .cancel: break // Not shown in this flow. case .proceed: @@ -125,9 +125,9 @@ final class StartChatScreenCoordinator: CoordinatorProtocol { createRoomParameters: createRoomParametersPublisher, selectedUsers: selectedUsersPublisher) let coordinator = CreateRoomCoordinator(parameters: createParameters) - coordinator.actions.sink { [weak self] result in + coordinator.actions.sink { [weak self] action in guard let self else { return } - switch result { + switch action { case .deselectUser(let user): self.toggleUser(user) case .updateDetails(let details): diff --git a/ElementX/Sources/Screens/WelcomeScreenScreen/View/WelcomeScreen.swift b/ElementX/Sources/Screens/WelcomeScreenScreen/View/WelcomeScreen.swift index 1bfb88ab8..a65cf28f0 100644 --- a/ElementX/Sources/Screens/WelcomeScreenScreen/View/WelcomeScreen.swift +++ b/ElementX/Sources/Screens/WelcomeScreenScreen/View/WelcomeScreen.swift @@ -26,7 +26,7 @@ struct WelcomeScreen: View { } bottomContent: { button } - .background(OnboardingBackgroundImage()) + .background(OnboardingScreenBackgroundImage()) .environment(\.backgroundStyle, AnyShapeStyle(Color.clear)) .onAppear { context.send(viewAction: .appeared) diff --git a/ElementX/Sources/Screens/WelcomeScreenScreen/WelcomeScreenScreenCoordinator.swift b/ElementX/Sources/Screens/WelcomeScreenScreen/WelcomeScreenScreenCoordinator.swift index ee79df7fa..eb9ab2730 100644 --- a/ElementX/Sources/Screens/WelcomeScreenScreen/WelcomeScreenScreenCoordinator.swift +++ b/ElementX/Sources/Screens/WelcomeScreenScreen/WelcomeScreenScreenCoordinator.swift @@ -24,7 +24,7 @@ enum WelcomeScreenScreenCoordinatorAction { final class WelcomeScreenScreenCoordinator: CoordinatorProtocol { private var viewModel: WelcomeScreenScreenViewModelProtocol private let actionsSubject: PassthroughSubject = .init() - private var cancellables: Set = .init() + private var cancellables = Set() var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index ecb372955..070f49023 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -71,7 +71,7 @@ class MockScreen: Identifiable { let id: UITestsScreenIdentifier private var retainedState = [Any]() - private var cancellables: Set = [] + private var cancellables = Set() init(id: UITestsScreenIdentifier) { self.id = id @@ -229,7 +229,7 @@ class MockScreen: Identifiable { isModallyPresented: false) return NotificationSettingsScreenCoordinator(parameters: parameters) case .onboarding: - return OnboardingCoordinator() + return OnboardingScreenCoordinator() case .roomPlainNoAvatar: let navigationStackCoordinator = NavigationStackCoordinator() let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(with: .init(displayName: "Some room name", avatarURL: nil)), diff --git a/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateScreenCoordinator.swift b/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateScreenCoordinator.swift index 9ff71f1d9..a39aad8eb 100644 --- a/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateScreenCoordinator.swift +++ b/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateScreenCoordinator.swift @@ -29,7 +29,7 @@ final class TemplateScreenCoordinator: CoordinatorProtocol { private let parameters: TemplateScreenCoordinatorParameters private var viewModel: TemplateScreenViewModelProtocol private let actionsSubject: PassthroughSubject = .init() - private var cancellables: Set = .init() + private var cancellables = Set() var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() diff --git a/UITests/Sources/OnboardingUITests.swift b/UITests/Sources/OnboardingScreenUITests.swift similarity index 94% rename from UITests/Sources/OnboardingUITests.swift rename to UITests/Sources/OnboardingScreenUITests.swift index fd3eff634..0ac5f324b 100644 --- a/UITests/Sources/OnboardingUITests.swift +++ b/UITests/Sources/OnboardingScreenUITests.swift @@ -17,7 +17,7 @@ import XCTest @MainActor -class OnboardingUITests: XCTestCase { +class OnboardingScreenUITests: XCTestCase { func testInitialStateComponents() async throws { let app = Application.launch(.onboarding) try await app.assertScreenshot(.onboarding) diff --git a/UnitTests/Sources/CreateRoomViewModelTests.swift b/UnitTests/Sources/CreateRoomViewModelTests.swift index c01631d79..5316f2018 100644 --- a/UnitTests/Sources/CreateRoomViewModelTests.swift +++ b/UnitTests/Sources/CreateRoomViewModelTests.swift @@ -26,13 +26,14 @@ class CreateRoomScreenViewModelTests: XCTestCase { var userSession: MockUserSession! private let usersSubject = CurrentValueSubject<[UserProfileProxy], Never>([]) - private var cancellables: Set = [] + private var cancellables = Set() var context: CreateRoomViewModel.Context { viewModel.context } override func setUpWithError() throws { + cancellables.removeAll() clientProxy = MockClientProxy(userID: "@a:b.com") userSession = MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider()) let parameters = CreateRoomFlowParameters() diff --git a/UnitTests/Sources/HomeScreenViewModelTests.swift b/UnitTests/Sources/HomeScreenViewModelTests.swift index f8950e4c5..e3c80aeb0 100644 --- a/UnitTests/Sources/HomeScreenViewModelTests.swift +++ b/UnitTests/Sources/HomeScreenViewModelTests.swift @@ -23,12 +23,11 @@ import XCTest class HomeScreenViewModelTests: XCTestCase { var viewModel: HomeScreenViewModelProtocol! var clientProxy: MockClientProxy! - - var context: HomeScreenViewModelType.Context! { - viewModel.context - } + var context: HomeScreenViewModelType.Context! { viewModel.context } + var cancellables = Set() override func setUpWithError() throws { + cancellables.removeAll() clientProxy = MockClientProxy(userID: "@mock:client.com") viewModel = HomeScreenViewModel(userSession: MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider()), @@ -43,16 +42,19 @@ class HomeScreenViewModelTests: XCTestCase { let mockRoomId = "mock_room_id" var correctResult = false var selectedRoomId = "" - viewModel.callback = { result in - switch result { - case .presentRoom(let roomId): - correctResult = true - selectedRoomId = roomId - default: - break + + viewModel.actions + .sink { action in + switch action { + case .presentRoom(let roomId): + correctResult = true + selectedRoomId = roomId + default: + break + } } - } - + .store(in: &cancellables) + context.send(viewAction: .selectRoom(roomIdentifier: mockRoomId)) await Task.yield() XCTAssert(correctResult) @@ -61,15 +63,18 @@ class HomeScreenViewModelTests: XCTestCase { func testTapUserAvatar() async throws { var correctResult = false - viewModel.callback = { result in - switch result { - case .presentSettingsScreen: - correctResult = true - default: - break + + viewModel.actions + .sink { action in + switch action { + case .presentSettingsScreen: + correctResult = true + default: + break + } } - } - + .store(in: &cancellables) + context.send(viewAction: .userMenu(action: .settings)) await Task.yield() XCTAssert(correctResult) @@ -97,15 +102,17 @@ class HomeScreenViewModelTests: XCTestCase { let mockRoomId = "1" var correctResult = false let expectation = expectation(description: #function) - viewModel.callback = { result in - switch result { - case .roomLeft(let roomIdentifier): - correctResult = roomIdentifier == mockRoomId - default: - break + viewModel.actions + .sink { action in + switch action { + case .roomLeft(let roomIdentifier): + correctResult = roomIdentifier == mockRoomId + default: + break + } + expectation.fulfill() } - expectation.fulfill() - } + .store(in: &cancellables) let room: RoomProxyMock = .init(with: .init(id: mockRoomId, displayName: "Some room")) room.leaveRoomClosure = { .success(()) } clientProxy.roomForIdentifierMocks[mockRoomId] = room @@ -118,14 +125,16 @@ class HomeScreenViewModelTests: XCTestCase { func testShowRoomDetails() async throws { let mockRoomId = "1" var correctResult = false - viewModel.callback = { result in - switch result { - case .presentRoomDetails(let roomIdentifier): - correctResult = roomIdentifier == mockRoomId - default: - break + viewModel.actions + .sink { action in + switch action { + case .presentRoomDetails(let roomIdentifier): + correctResult = roomIdentifier == mockRoomId + default: + break + } } - } + .store(in: &cancellables) context.send(viewAction: .showRoomDetails(roomIdentifier: mockRoomId)) await Task.yield() XCTAssertNil(context.alertInfo) diff --git a/UnitTests/Sources/InviteUsersViewModelTests.swift b/UnitTests/Sources/InviteUsersViewModelTests.swift index 574d7a5c2..87253bdf3 100644 --- a/UnitTests/Sources/InviteUsersViewModelTests.swift +++ b/UnitTests/Sources/InviteUsersViewModelTests.swift @@ -25,12 +25,16 @@ class InviteUsersScreenViewModelTests: XCTestCase { var clientProxy: MockClientProxy! var userDiscoveryService: UserDiscoveryServiceMock! - private var cancellables: Set = [] + private var cancellables = Set() var context: InviteUsersScreenViewModel.Context { viewModel.context } + override func setUp() { + cancellables.removeAll() + } + func testSelectUser() { setupWithRoomType(roomType: .draft) XCTAssertTrue(context.viewState.selectedUsers.isEmpty) diff --git a/UnitTests/Sources/MessageForwardingScreenViewModelTests.swift b/UnitTests/Sources/MessageForwardingScreenViewModelTests.swift index 00222917a..715763310 100644 --- a/UnitTests/Sources/MessageForwardingScreenViewModelTests.swift +++ b/UnitTests/Sources/MessageForwardingScreenViewModelTests.swift @@ -26,6 +26,7 @@ class MessageForwardingScreenViewModelTests: XCTestCase { var cancellables = Set() override func setUpWithError() throws { + cancellables.removeAll() viewModel = MessageForwardingScreenViewModel(roomSummaryProvider: MockRoomSummaryProvider(state: .loaded(.mockRooms)), sourceRoomID: "1") context = viewModel.context } diff --git a/UnitTests/Sources/OnboardingViewModelTests.swift b/UnitTests/Sources/OnboardingScreenViewModelTests.swift similarity index 93% rename from UnitTests/Sources/OnboardingViewModelTests.swift rename to UnitTests/Sources/OnboardingScreenViewModelTests.swift index b4f5ebb34..2d0ba3aa2 100644 --- a/UnitTests/Sources/OnboardingViewModelTests.swift +++ b/UnitTests/Sources/OnboardingScreenViewModelTests.swift @@ -18,6 +18,6 @@ import XCTest @testable import ElementX -class OnboardingViewModelTests: XCTestCase { +class OnboardingScreenViewModelTests: XCTestCase { // Nothing to test, the view model has no mutable state. } diff --git a/UnitTests/Sources/RoomDetailsViewModelTests.swift b/UnitTests/Sources/RoomDetailsViewModelTests.swift index face36c41..bd0bc1093 100644 --- a/UnitTests/Sources/RoomDetailsViewModelTests.swift +++ b/UnitTests/Sources/RoomDetailsViewModelTests.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import Combine import MatrixRustSDK import SwiftUI import XCTest @@ -26,8 +27,10 @@ class RoomDetailsScreenViewModelTests: XCTestCase { var roomProxyMock: RoomProxyMock! var notificationSettingsProxyMock: NotificationSettingsProxyMock! var context: RoomDetailsScreenViewModelType.Context { viewModel.context } + var cancellables = Set() override func setUp() { + cancellables.removeAll() roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", joinedMembersCount: 0)) notificationSettingsProxyMock = NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()) viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", @@ -84,15 +87,17 @@ class RoomDetailsScreenViewModelTests: XCTestCase { roomProxyMock.leaveRoomClosure = { .success(()) } - viewModel.callback = { action in - switch action { - case .leftRoom: - break - default: - XCTFail("leftRoom expected") + viewModel.actions + .sink { action in + switch action { + case .leftRoom: + break + default: + XCTFail("leftRoom expected") + } + expectation.fulfill() } - expectation.fulfill() - } + .store(in: &cancellables) context.send(viewAction: .confirmLeave) await fulfillment(of: [expectation]) XCTAssertEqual(roomProxyMock.leaveRoomCallsCount, 1) @@ -263,14 +268,16 @@ class RoomDetailsScreenViewModelTests: XCTestCase { XCTAssertTrue(context.viewState.canInviteUsers) var callbackCorrectlyCalled = false - viewModel.callback = { action in - switch action { - case .requestInvitePeoplePresentation: - callbackCorrectlyCalled = true - default: - callbackCorrectlyCalled = false + viewModel.actions + .sink { action in + switch action { + case .requestInvitePeoplePresentation: + callbackCorrectlyCalled = true + default: + callbackCorrectlyCalled = false + } } - } + .store(in: &cancellables) context.send(viewAction: .processTapInvite) await Task.yield() diff --git a/UnitTests/Sources/RoomFlowCoordinatorTests.swift b/UnitTests/Sources/RoomFlowCoordinatorTests.swift index deabbcd14..991201e3c 100644 --- a/UnitTests/Sources/RoomFlowCoordinatorTests.swift +++ b/UnitTests/Sources/RoomFlowCoordinatorTests.swift @@ -23,9 +23,10 @@ import Combine class RoomFlowCoordinatorTests: XCTestCase { var roomFlowCoordinator: RoomFlowCoordinator! var navigationStackCoordinator: NavigationStackCoordinator! - private var cancellables: Set = .init() + var cancellables = Set() override func setUp() async throws { + cancellables.removeAll() let clientProxy = MockClientProxy(userID: "hi@bob", roomSummaryProvider: MockRoomSummaryProvider(state: .loaded(.mockRooms))) let mediaProvider = MockMediaProvider() let userSession = MockUserSession(clientProxy: clientProxy, mediaProvider: mediaProvider) diff --git a/UnitTests/Sources/RoomNotificationSettingsScreenViewModelTests.swift b/UnitTests/Sources/RoomNotificationSettingsScreenViewModelTests.swift index 56b7c759f..a7053cd55 100644 --- a/UnitTests/Sources/RoomNotificationSettingsScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomNotificationSettingsScreenViewModelTests.swift @@ -26,9 +26,10 @@ class RoomNotificationSettingsScreenViewModelTests: XCTestCase { var roomProxyMock: RoomProxyMock! var notificationSettingsProxyMock: NotificationSettingsProxyMock! var context: RoomNotificationSettingsScreenViewModelType.Context { viewModel.context } - var cancellables: Set = [] + var cancellables = Set() override func setUpWithError() throws { + cancellables.removeAll() roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", joinedMembersCount: 0)) notificationSettingsProxyMock = NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()) viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock, @@ -172,8 +173,8 @@ class RoomNotificationSettingsScreenViewModelTests: XCTestCase { var actionSent: RoomNotificationSettingsScreenViewModelAction? viewModel.actions - .sink { value in - actionSent = value + .sink { action in + actionSent = action } .store(in: &cancellables) @@ -207,8 +208,8 @@ class RoomNotificationSettingsScreenViewModelTests: XCTestCase { var actionSent: RoomNotificationSettingsScreenViewModelAction? viewModel.actions - .sink { value in - actionSent = value + .sink { action in + actionSent = action } .store(in: &cancellables) diff --git a/UnitTests/Sources/RoomScreenViewModelTests.swift b/UnitTests/Sources/RoomScreenViewModelTests.swift index 78936a417..71d0636ca 100644 --- a/UnitTests/Sources/RoomScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomScreenViewModelTests.swift @@ -25,6 +25,7 @@ class RoomScreenViewModelTests: XCTestCase { var cancellables = Set() override func setUp() async throws { + cancellables.removeAll() userIndicatorControllerMock = UserIndicatorControllerMock.default } diff --git a/UnitTests/Sources/SettingsViewModelTests.swift b/UnitTests/Sources/SettingsViewModelTests.swift index f26c32c7a..5707f40f0 100644 --- a/UnitTests/Sources/SettingsViewModelTests.swift +++ b/UnitTests/Sources/SettingsViewModelTests.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import Combine import XCTest @testable import ElementX @@ -22,8 +23,10 @@ import XCTest class SettingsScreenViewModelTests: XCTestCase { var viewModel: SettingsScreenViewModelProtocol! var context: SettingsScreenViewModelType.Context! + var cancellables = Set() @MainActor override func setUpWithError() throws { + cancellables.removeAll() let userSession = MockUserSession(clientProxy: MockClientProxy(userID: ""), mediaProvider: MockMediaProvider()) viewModel = SettingsScreenViewModel(userSession: userSession, appSettings: ServiceLocator.shared.settings) @@ -32,14 +35,17 @@ class SettingsScreenViewModelTests: XCTestCase { @MainActor func testLogout() async throws { var correctResult = false - viewModel.callback = { result in - switch result { - case .logout: - correctResult = true - default: - break + + viewModel.actions + .sink { action in + switch action { + case .logout: + correctResult = true + default: + break + } } - } + .store(in: &cancellables) context.send(viewAction: .logout) await Task.yield() @@ -48,10 +54,12 @@ class SettingsScreenViewModelTests: XCTestCase { func testReportBug() async throws { var correctResult = false - viewModel.callback = { result in - correctResult = result == .reportBug - } - + viewModel.actions + .sink { action in + correctResult = action == .reportBug + } + .store(in: &cancellables) + context.send(viewAction: .reportBug) await Task.yield() XCTAssert(correctResult) @@ -59,10 +67,12 @@ class SettingsScreenViewModelTests: XCTestCase { func testAnalytics() async throws { var correctResult = false - viewModel.callback = { result in - correctResult = result == .analytics - } - + viewModel.actions + .sink { action in + correctResult = action == .analytics + } + .store(in: &cancellables) + context.send(viewAction: .analytics) await Task.yield() XCTAssert(correctResult) diff --git a/UnitTests/Sources/StaticLocationScreenViewModelTests.swift b/UnitTests/Sources/StaticLocationScreenViewModelTests.swift index 4966361c6..609ef3621 100644 --- a/UnitTests/Sources/StaticLocationScreenViewModelTests.swift +++ b/UnitTests/Sources/StaticLocationScreenViewModelTests.swift @@ -24,13 +24,14 @@ class StaticLocationScreenViewModelTests: XCTestCase { var viewModel: StaticLocationScreenViewModelProtocol! private let usersSubject = CurrentValueSubject<[UserProfileProxy], Never>([]) - private var cancellables: Set = [] + private var cancellables = Set() var context: StaticLocationScreenViewModel.Context { viewModel.context } override func setUpWithError() throws { + cancellables.removeAll() let viewModel = StaticLocationScreenViewModel(interactionMode: .picker) viewModel.state.bindings.isLocationAuthorized = true self.viewModel = viewModel diff --git a/UnitTests/Sources/UserSession/UserSessionTests.swift b/UnitTests/Sources/UserSession/UserSessionTests.swift index f41e4152b..74afe3a33 100644 --- a/UnitTests/Sources/UserSession/UserSessionTests.swift +++ b/UnitTests/Sources/UserSession/UserSessionTests.swift @@ -21,10 +21,10 @@ final class UserSessionTests: XCTestCase { var userSession: UserSession! let clientProxy = MockClientProxy(userID: "@test:user.net") - private var cancellables: Set = [] + private var cancellables = Set() override func setUpWithError() throws { - cancellables = [] + cancellables.removeAll() userSession = UserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider()) }