Fixes #317 - Adopt a split layout for iPad and Mac apps
Rename navigation components: SplitScreenCoordinator -> NavigationSplitCoordinator, StackScreenCoordinator -> NavigationStackCoordinator and SingleScreenCoordinator -> NavigationRootCoordinator [0c161039] Tweak navigation logging [826c19cf] Move the navigation dismissal callbacks to the NavigationModule, add SingleScreenCoordinator tests [b8830d9c] Add tests [252ad119] Merge the StackScreenCoordinator and SplitScreenCoordinators into a single file and stop publicly exposing their internal workings. Add more documentation. [37671699] Cleanup navigation logging [51406184] Use the parent SplitScreenCoordinator to present embedded StackScreenCoordinator sheets [b94b04c9] Retract the room "syncing" indicator when dismissing a room [1467b0ac] Correctly move to the no room selected state when popping in compact layouts [10bf2ad8] Allow nilling root coordinators, replace present/dismiss sheet with setSheetCoordinator(?) [33716784] Add single screen coordinator fade transition animation [3cbe65e7] Prevent the timeline table view from being reused between different rooms [9c94c50b] Move files around [c10b6bc5] Adapt the user session state machine to the split layout [7115a319] Fix unit and UI tests [1ece59e8] Fix login flows [6884dc3b] Use modules everywhere the underlying object is a NavigationModule [ab08d44c] Rename navigation components to: SingleScreenCoordinator, SplitScreenCoordinator and StackScreenCoordinator [ada2be57] Add SplitNavigationController * Remove the navigationRootCoordinator from the UserSessionFlowCoordinator
This commit is contained in:
@@ -131,7 +131,6 @@
|
||||
407DCE030E0F9B7C9861D38A /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 997C7385E1A07E061D7E2100 /* GZIP */; };
|
||||
414F50CFCFEEE2611127DCFB /* RestorationToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3558A15CFB934F9229301527 /* RestorationToken.swift */; };
|
||||
41DFDD212D1BE57CA50D783B /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = FD43A50D9B75C9D6D30F006B /* SwiftyBeaver */; };
|
||||
41E16904B30C529373B4E1A4 /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 495D3EC4972639C1A87DDF8E /* NavigationController.swift */; };
|
||||
438FB9BC535BC95948AA5F34 /* SettingsViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B2F9D5C39A4494D19F33E38 /* SettingsViewModelProtocol.swift */; };
|
||||
43BD17BC8794BB9B04F2A26B /* MediaSourceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179423E34EE846E048E49CBF /* MediaSourceProxy.swift */; };
|
||||
43FD77998F33C32718C51450 /* TemplateCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD460ED7ED1E03B85DEA25C /* TemplateCoordinator.swift */; };
|
||||
@@ -145,6 +144,7 @@
|
||||
49E9B99CB6A275C7744351F0 /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D58333B377888012740101 /* LoginViewModel.swift */; };
|
||||
49F2E7DD8CAACE09CEECE3E6 /* SeparatorRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6390A6DC140CA3D6865A66FF /* SeparatorRoomTimelineView.swift */; };
|
||||
4A2E0DBB63919AC8309B6D40 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A191D3FDB995309C7E2DE7D /* SettingsViewModel.swift */; };
|
||||
4BB282209EA82015D0DF8F89 /* NavigationStackCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C698E30698EC59302A8EEBD /* NavigationStackCoordinatorTests.swift */; };
|
||||
4C3365818DE1CEAEDF590FD3 /* MediaProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C888BCD78E2A55DCE364F160 /* MediaProviderProtocol.swift */; };
|
||||
4E945AD6862C403F74E57755 /* RoomTimelineItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 105B2A8426404EF66F00CFDB /* RoomTimelineItemFactory.swift */; };
|
||||
4FC1EFE4968A259CBBACFAFB /* RoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */; };
|
||||
@@ -210,6 +210,7 @@
|
||||
706F79A39BDB32F592B8C2C7 /* UIKitBackgroundTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92FCD9116ADDE820E4E30F92 /* UIKitBackgroundTask.swift */; };
|
||||
7096FA3AC218D914E88BFB70 /* AggregratedReaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15BE37BE2FB86E00C8D150A /* AggregratedReaction.swift */; };
|
||||
719E7AAD1F8E68F68F30FECD /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40C19719687984FD9478FBE /* Task.swift */; };
|
||||
71C1347F23868324A4F43940 /* NavigationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A22A05E472533ED3C5A31B3 /* NavigationModule.swift */; };
|
||||
7354D094A4C59B555F407FA1 /* RustTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542D4F49FABA056DEEEB3400 /* RustTracing.swift */; };
|
||||
7361B011A79BF723D8C9782B /* EmojiCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C1A3D524D63815B28FA4D62 /* EmojiCategory.swift */; };
|
||||
7405B4824D45BA7C3D943E76 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D0CBC76C80E04345E11F2DB /* Application.swift */; };
|
||||
@@ -242,7 +243,6 @@
|
||||
8024BE37156FF0A95A7A3465 /* AnalyticsPromptUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF11DD57D9FACF2A757AB024 /* AnalyticsPromptUITests.swift */; };
|
||||
80D00A7C62AAB44F54725C43 /* PermalinkBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F754E66A8970963B15B2A41E /* PermalinkBuilder.swift */; };
|
||||
8196A2E71ACC902DD69F24EE /* UserNotificationControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DE6C5C756E1393202BA95CD /* UserNotificationControllerTests.swift */; };
|
||||
834DD9E41FC42A509BAD52E3 /* NavigationControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 109361C96BFFBE2FD89BF15C /* NavigationControllerTests.swift */; };
|
||||
83E5054739949181CA981193 /* LoginCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD667C4BB98CF4F3FE2CE3B0 /* LoginCoordinator.swift */; };
|
||||
841172E1576A863F4450132D /* WeakKeyDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ADFDC712027931F2216668 /* WeakKeyDictionary.swift */; };
|
||||
85AFBB433AD56704A880F8A0 /* FramePreferenceKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4798B3B7A1E8AE3901CEE8C6 /* FramePreferenceKey.swift */; };
|
||||
@@ -278,6 +278,7 @@
|
||||
9738F894DB1BD383BE05767A /* ElementSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1027BB9A852F445B7623897F /* ElementSettings.swift */; };
|
||||
978BB24F2A5D31EE59EEC249 /* UserSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */; };
|
||||
97CECF91D68235F1D13598D7 /* AnalyticsConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77B3D4950F1707E66E4A45A /* AnalyticsConfiguration.swift */; };
|
||||
981853650217B6C8ECDD998C /* NavigationRootCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F875D71347DC81EAE7687446 /* NavigationRootCoordinatorTests.swift */; };
|
||||
983896D611ABF52A5C37498D /* RoomSummaryProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */; };
|
||||
989029A28C9E2F828AD6658A /* AppIcon.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 16DC8C5B2991724903F1FA6A /* AppIcon.pdf */; };
|
||||
992477AB8E3F3C36D627D32E /* OnboardingViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BC4437C107D52ED19357DFC /* OnboardingViewModelProtocol.swift */; };
|
||||
@@ -338,6 +339,7 @@
|
||||
B4AAB3257A83B73F53FB2689 /* StateStoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3DFE5B444F131648066F05 /* StateStoreViewModel.swift */; };
|
||||
B5111BAF5F601C139EBBD8BB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 01C4C7DB37597D7D8379511A /* Assets.xcassets */; };
|
||||
B5903E48CF43259836BF2DBF /* EncryptedRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56C1BCB9E83B09A45387FCA2 /* EncryptedRoomTimelineView.swift */; };
|
||||
B5E455C9689EA600EDB3E9E0 /* NavigationRootCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA28F29C9F93E93CC3C2C715 /* NavigationRootCoordinator.swift */; };
|
||||
B6048166B4AA4CEFEA9B77A6 /* InfoPlistReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */; };
|
||||
B66757D0254843162595B25D /* MXLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A34A814CBD56230BC74FFCF4 /* MXLogger.swift */; };
|
||||
B6DA66EFC13A90846B625836 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 91DE43B8815918E590912DDA /* InfoPlist.strings */; };
|
||||
@@ -438,9 +440,11 @@
|
||||
F9981191DC408AED537C1749 /* MediaProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12C9E0B61A77C7F0EE7918C /* MediaProxy.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 */; };
|
||||
FA9C427FFB11B1AA2DCC5602 /* RoomProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */; };
|
||||
FBCD77D557AACBE9B445133A /* MediaProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12C9E0B61A77C7F0EE7918C /* MediaProxy.swift */; };
|
||||
FBF09B6C900415800DDF2A21 /* EmojiProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C113E0CB7E15E9765B1817A /* EmojiProvider.swift */; };
|
||||
FCB9B475F908765531335859 /* NavigationSplitCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A63A32A0627A03F00A2900FE /* NavigationSplitCoordinator.swift */; };
|
||||
FCD3F2B82CAB29A07887A127 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 2B43F2AF7456567FE37270A7 /* KeychainAccess */; };
|
||||
FE4593FC2A02AAF92E089565 /* ElementAnimations.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF1593DD87F974F8509BB619 /* ElementAnimations.swift */; };
|
||||
FE79E2BCCF69E8BF4D21E15A /* RoomMessageFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA154570F693D93513E584C1 /* RoomMessageFactory.swift */; };
|
||||
@@ -503,7 +507,6 @@
|
||||
04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = "<group>"; };
|
||||
054F469E433864CC6FE6EE8E /* ServerSelectionUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionUITests.swift; sourceTree = "<group>"; };
|
||||
057B747CF045D3C6C30EAB2C /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fi; path = fi.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
05C4897C156B01C5775F3A5D /* dev-element-x-ios */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "dev-element-x-ios"; path = .; sourceTree = SOURCE_ROOT; };
|
||||
077D7C3BE199B6E5DDEC07EC /* AppCoordinatorStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorStateMachine.swift; sourceTree = "<group>"; };
|
||||
086B997409328F091EBA43CE /* RoomScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenUITests.swift; sourceTree = "<group>"; };
|
||||
08F64963396A6A23538EFCEC /* is */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = is; path = is.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
@@ -533,7 +536,6 @@
|
||||
1059E2AE7878CF7820592637 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
105B2A8426404EF66F00CFDB /* RoomTimelineItemFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemFactory.swift; sourceTree = "<group>"; };
|
||||
105D16E7DB0CCE9526612BDD /* bn-IN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "bn-IN"; path = "bn-IN.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
109361C96BFFBE2FD89BF15C /* NavigationControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationControllerTests.swift; sourceTree = "<group>"; };
|
||||
10CC626F97AD70FF0420C115 /* RoomSummaryProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProviderProtocol.swift; sourceTree = "<group>"; };
|
||||
1113CA0A67B4AA227AAFB63B /* UserNotificationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationController.swift; sourceTree = "<group>"; };
|
||||
11151E78D6BB2B04A8FBD389 /* EmojiPickerScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||
@@ -652,7 +654,6 @@
|
||||
48CE6BF18E542B32FA52CE06 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fa; path = fa.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
49193CB0C248D621A96FB2AA /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
4959CECEC984B3995616F427 /* DataProtectionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataProtectionManager.swift; sourceTree = "<group>"; };
|
||||
495D3EC4972639C1A87DDF8E /* NavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationController.swift; sourceTree = "<group>"; };
|
||||
4990FDBDA96B88E214F92F48 /* SettingsModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModels.swift; sourceTree = "<group>"; };
|
||||
49D2C8E66E83EA578A7F318A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNNotificationRequest.swift; sourceTree = "<group>"; };
|
||||
@@ -789,12 +790,14 @@
|
||||
98A2932515EA11D3DD8A3506 /* TimelineItemBubbledStylerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemBubbledStylerView.swift; sourceTree = "<group>"; };
|
||||
997783054A2E95F9E624217E /* kaa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kaa; path = kaa.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
99DE232F24EAD72A3DF7EF1A /* kab */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = kab; path = kab.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
9A22A05E472533ED3C5A31B3 /* NavigationModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModule.swift; sourceTree = "<group>"; };
|
||||
9A68BCE6438873D2661D93D0 /* BugReportServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportServiceProtocol.swift; sourceTree = "<group>"; };
|
||||
9B1FBF8CA40199B8058B1F08 /* NotificationItemProxy+NSE.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationItemProxy+NSE.swift"; sourceTree = "<group>"; };
|
||||
9B577F829C693B8DFB7014FD /* RedactedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedactedRoomTimelineItem.swift; sourceTree = "<group>"; };
|
||||
9BF9E3E6A23180EC05F06460 /* EmojiMartJSONLoaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiMartJSONLoaderTests.swift; sourceTree = "<group>"; };
|
||||
9C4048041C1A6B20CB97FD18 /* TestMeasurementParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestMeasurementParser.swift; sourceTree = "<group>"; };
|
||||
9C5E81214D27A6B898FC397D /* ElementX.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ElementX.entitlements; sourceTree = "<group>"; };
|
||||
9C698E30698EC59302A8EEBD /* NavigationStackCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationStackCoordinatorTests.swift; sourceTree = "<group>"; };
|
||||
9C7F7DE62D33C6A26CBFCD72 /* IntegrationTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = IntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
9CE3C90E487B255B735D73C8 /* RoomScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModel.swift; sourceTree = "<group>"; };
|
||||
9D7D706FFF438CAF16F44D8C /* ServerSelectionCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionCoordinator.swift; sourceTree = "<group>"; };
|
||||
@@ -816,6 +819,7 @@
|
||||
A4756C5A8C8649AD6C10C615 /* MockUserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserSession.swift; sourceTree = "<group>"; };
|
||||
A4B5B19A10D3F7C2BC5315DF /* VideoRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItem.swift; sourceTree = "<group>"; };
|
||||
A5B0B1226DA8DB55918B34CD /* FileCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileCache.swift; sourceTree = "<group>"; };
|
||||
A63A32A0627A03F00A2900FE /* NavigationSplitCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSplitCoordinator.swift; sourceTree = "<group>"; };
|
||||
A64F0DB78E0AC23C91AD89EF /* mk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mk; path = mk.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxy.swift; sourceTree = "<group>"; };
|
||||
A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHGPostHogConfiguration.swift; sourceTree = "<group>"; };
|
||||
@@ -861,6 +865,7 @@
|
||||
B8347789959986B374DB25DD /* sq */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sq; path = sq.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
B83CB897B183BF3C33715F55 /* bn-IN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "bn-IN"; path = "bn-IN.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
B8A56EA2A5AE726F445CB2E3 /* eo */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = eo; path = eo.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
B8F28602AC7AC881AED37EBA /* NavigationCoordinators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationCoordinators.swift; sourceTree = "<group>"; };
|
||||
B902EA6CD3296B0E10EE432B /* HomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreen.swift; sourceTree = "<group>"; };
|
||||
BA7B2E9CC5DC3B76ADC35A43 /* AnalyticsPromptCheckmarkItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptCheckmarkItem.swift; sourceTree = "<group>"; };
|
||||
BB3073CCD77D906B330BC1D6 /* Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests.swift; sourceTree = "<group>"; };
|
||||
@@ -891,6 +896,7 @@
|
||||
C8F2A7A4E3F5060F52ACFFB0 /* RedactedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedactedRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
C95ADE8D9527523572532219 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = hu; path = hu.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
C9A86C95340248A8B7BA9A43 /* AnalyticsPromptViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||
CA28F29C9F93E93CC3C2C715 /* NavigationRootCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRootCoordinator.swift; sourceTree = "<group>"; };
|
||||
CA89A2DD51B6BBE1DA55E263 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
|
||||
CA9D14D6F914324865C7DB9F /* ActivityCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityCoordinator.swift; sourceTree = "<group>"; };
|
||||
CAAE4A709C0A2144C103AA0F /* ang */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ang; path = ang.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
@@ -915,6 +921,7 @@
|
||||
D1A9CCCF53495CF3D7B19FCE /* MockSessionVerificationControllerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSessionVerificationControllerProxy.swift; sourceTree = "<group>"; };
|
||||
D263254AFE5B7993FFBBF324 /* NSE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NSE.entitlements; sourceTree = "<group>"; };
|
||||
D2D783758EAE6A88C93564EB /* VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModel.swift; sourceTree = "<group>"; };
|
||||
D31DC8105C6233E5FFD9B84C /* element-x-ios */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "element-x-ios"; path = .; sourceTree = SOURCE_ROOT; };
|
||||
D33116993D54FADC0C721C1F /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
|
||||
D3D455BC2423D911A62ACFB2 /* NSELogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSELogger.swift; sourceTree = "<group>"; };
|
||||
D4DA544B2520BFA65D6DB4BB /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = "<group>"; };
|
||||
@@ -987,6 +994,7 @@
|
||||
F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormattedBodyText.swift; sourceTree = "<group>"; };
|
||||
F754E66A8970963B15B2A41E /* PermalinkBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermalinkBuilder.swift; sourceTree = "<group>"; };
|
||||
F77C060C2ACC4CB7336A29E7 /* EmoteRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItem.swift; sourceTree = "<group>"; };
|
||||
F875D71347DC81EAE7687446 /* NavigationRootCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRootCoordinatorTests.swift; sourceTree = "<group>"; };
|
||||
F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
FA154570F693D93513E584C1 /* RoomMessageFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageFactory.swift; sourceTree = "<group>"; };
|
||||
FAB10E673916D2B8D21FD197 /* TemplateModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateModels.swift; sourceTree = "<group>"; };
|
||||
@@ -1632,7 +1640,9 @@
|
||||
3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */,
|
||||
A05707BF550D770168A406DB /* LoginViewModelTests.swift */,
|
||||
F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */,
|
||||
109361C96BFFBE2FD89BF15C /* NavigationControllerTests.swift */,
|
||||
F875D71347DC81EAE7687446 /* NavigationRootCoordinatorTests.swift */,
|
||||
A63A32A0627A03F00A2900FE /* NavigationSplitCoordinator.swift */,
|
||||
9C698E30698EC59302A8EEBD /* NavigationStackCoordinatorTests.swift */,
|
||||
00A941F289F6AB876BA3361A /* OnboardingViewModelTests.swift */,
|
||||
6FB31A32C93D94930B253FBF /* PermalinkBuilderTests.swift */,
|
||||
93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */,
|
||||
@@ -1668,6 +1678,16 @@
|
||||
path = Items;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
780F74C73E826685A9DB289B /* Navigation */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B8F28602AC7AC881AED37EBA /* NavigationCoordinators.swift */,
|
||||
9A22A05E472533ED3C5A31B3 /* NavigationModule.swift */,
|
||||
CA28F29C9F93E93CC3C2C715 /* NavigationRootCoordinator.swift */,
|
||||
);
|
||||
path = Navigation;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
78915D878159D302395D57BF /* SupportingFiles */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1828,7 +1848,7 @@
|
||||
9413F680ECDFB2B0DDB0DEF2 /* Packages */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
05C4897C156B01C5775F3A5D /* dev-element-x-ios */,
|
||||
D31DC8105C6233E5FFD9B84C /* element-x-ios */,
|
||||
);
|
||||
name = Packages;
|
||||
sourceTree = SOURCE_ROOT;
|
||||
@@ -1948,8 +1968,8 @@
|
||||
CA89A2DD51B6BBE1DA55E263 /* Application.swift */,
|
||||
263B3B811C2B900F12C6F695 /* BuildSettings.swift */,
|
||||
B251F5B4511D1CA0BA8361FE /* CoordinatorProtocol.swift */,
|
||||
495D3EC4972639C1A87DDF8E /* NavigationController.swift */,
|
||||
57F95CADD0A5DBD76B990FCB /* ServiceLocator.swift */,
|
||||
780F74C73E826685A9DB289B /* Navigation */,
|
||||
);
|
||||
path = Application;
|
||||
sourceTree = "<group>";
|
||||
@@ -2743,7 +2763,9 @@
|
||||
149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */,
|
||||
1E59B77A0B2CE83DCC1B203C /* LoginViewModelTests.swift in Sources */,
|
||||
2E43A3D221BE9587BC19C3F1 /* MatrixEntityRegexTests.swift in Sources */,
|
||||
834DD9E41FC42A509BAD52E3 /* NavigationControllerTests.swift in Sources */,
|
||||
981853650217B6C8ECDD998C /* NavigationRootCoordinatorTests.swift in Sources */,
|
||||
FCB9B475F908765531335859 /* NavigationSplitCoordinator.swift in Sources */,
|
||||
4BB282209EA82015D0DF8F89 /* NavigationStackCoordinatorTests.swift in Sources */,
|
||||
F9F6D2883BBEBB9A3789A137 /* OnboardingViewModelTests.swift in Sources */,
|
||||
27E9263DA75E266690A37EB1 /* PermalinkBuilderTests.swift in Sources */,
|
||||
46562110EE202E580A5FFD9C /* RoomScreenViewModelTests.swift in Sources */,
|
||||
@@ -2903,7 +2925,9 @@
|
||||
C74EE50257ED925C2B8EFCE6 /* MockSoftLogoutScreenState.swift in Sources */,
|
||||
FE8D76708280968F7A670852 /* MockUserNotificationController.swift in Sources */,
|
||||
D8359F67AF3A83516E9083C1 /* MockUserSession.swift in Sources */,
|
||||
41E16904B30C529373B4E1A4 /* NavigationController.swift in Sources */,
|
||||
FA2BBAE9FC5E2E9F960C0980 /* NavigationCoordinators.swift in Sources */,
|
||||
71C1347F23868324A4F43940 /* NavigationModule.swift in Sources */,
|
||||
B5E455C9689EA600EDB3E9E0 /* NavigationRootCoordinator.swift in Sources */,
|
||||
8BBD3AA589DEE02A1B0923B2 /* NoticeRoomTimelineItem.swift in Sources */,
|
||||
368C8758FCD079E6AAA18C2C /* NoticeRoomTimelineView.swift in Sources */,
|
||||
3F70E237CE4C3FAB02FC227F /* NotificationConstants.swift in Sources */,
|
||||
|
||||
@@ -20,7 +20,7 @@ import SwiftUI
|
||||
|
||||
class AppCoordinator: AppCoordinatorProtocol {
|
||||
private let stateMachine: AppCoordinatorStateMachine
|
||||
private let navigationController: NavigationController
|
||||
private let navigationRootCoordinator: NavigationRootCoordinator
|
||||
private let userSessionStore: UserSessionStoreProtocol
|
||||
/// Common background task to resume long-running tasks in the background.
|
||||
/// When this task expiring, we'll try to suspend the state machine by `suspend` event.
|
||||
@@ -51,14 +51,14 @@ class AppCoordinator: AppCoordinatorProtocol {
|
||||
private(set) var notificationManager: NotificationManagerProtocol?
|
||||
|
||||
init() {
|
||||
navigationController = NavigationController()
|
||||
navigationRootCoordinator = NavigationRootCoordinator()
|
||||
stateMachine = AppCoordinatorStateMachine()
|
||||
|
||||
bugReportService = BugReportService(withBaseURL: BuildSettings.bugReportServiceBaseURL, sentryURL: BuildSettings.bugReportSentryURL)
|
||||
|
||||
navigationController.setRootCoordinator(SplashScreenCoordinator())
|
||||
navigationRootCoordinator.setRootCoordinator(SplashScreenCoordinator())
|
||||
|
||||
ServiceLocator.shared.register(userNotificationController: UserNotificationController(rootCoordinator: navigationController))
|
||||
ServiceLocator.shared.register(userNotificationController: UserNotificationController(rootCoordinator: navigationRootCoordinator))
|
||||
|
||||
backgroundTaskService = UIKitBackgroundTaskService {
|
||||
UIApplication.shared
|
||||
@@ -168,12 +168,15 @@ class AppCoordinator: AppCoordinatorProtocol {
|
||||
}
|
||||
|
||||
private func startAuthentication() {
|
||||
let authenticationNavigationStackCoordinator = NavigationStackCoordinator()
|
||||
let authenticationService = AuthenticationServiceProxy(userSessionStore: userSessionStore)
|
||||
authenticationCoordinator = AuthenticationCoordinator(authenticationService: authenticationService,
|
||||
navigationController: navigationController)
|
||||
navigationStackCoordinator: authenticationNavigationStackCoordinator)
|
||||
authenticationCoordinator?.delegate = self
|
||||
|
||||
authenticationCoordinator?.start()
|
||||
|
||||
navigationRootCoordinator.setRootCoordinator(authenticationNavigationStackCoordinator)
|
||||
}
|
||||
|
||||
private func startAuthenticationSoftLogout() {
|
||||
@@ -208,13 +211,14 @@ class AppCoordinator: AppCoordinatorProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
navigationController.setRootCoordinator(coordinator)
|
||||
navigationRootCoordinator.setRootCoordinator(coordinator)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupUserSession() {
|
||||
let navigationSplitCoordinator = NavigationSplitCoordinator(placeholderCoordinator: SplashScreenCoordinator())
|
||||
let userSessionFlowCoordinator = UserSessionFlowCoordinator(userSession: userSession,
|
||||
navigationController: navigationController,
|
||||
navigationSplitCoordinator: navigationSplitCoordinator,
|
||||
bugReportService: bugReportService)
|
||||
|
||||
userSessionFlowCoordinator.callback = { [weak self] action in
|
||||
@@ -227,6 +231,8 @@ class AppCoordinator: AppCoordinatorProtocol {
|
||||
userSessionFlowCoordinator.start()
|
||||
|
||||
self.userSessionFlowCoordinator = userSessionFlowCoordinator
|
||||
|
||||
navigationRootCoordinator.setRootCoordinator(navigationSplitCoordinator)
|
||||
}
|
||||
|
||||
private func tearDownUserSession(isSoftLogout: Bool = false) {
|
||||
@@ -253,7 +259,7 @@ class AppCoordinator: AppCoordinatorProtocol {
|
||||
}
|
||||
|
||||
private func presentSplashScreen(isSoftLogout: Bool = false) {
|
||||
navigationController.setRootCoordinator(SplashScreenCoordinator())
|
||||
navigationRootCoordinator.setRootCoordinator(SplashScreenCoordinator())
|
||||
|
||||
if isSoftLogout {
|
||||
startAuthenticationSoftLogout()
|
||||
|
||||
@@ -0,0 +1,526 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
/// Class responsible for displaying 2 coordinators side by side and collapsing them
|
||||
/// into a single navigation stack on compact layouts
|
||||
class NavigationSplitCoordinator: CoordinatorProtocol, ObservableObject, CustomStringConvertible {
|
||||
fileprivate let placeholderModule: NavigationModule
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
@Published fileprivate var sidebarModule: NavigationModule? {
|
||||
didSet {
|
||||
if let oldValue {
|
||||
logPresentationChange("Remove sidebar", oldValue)
|
||||
oldValue.coordinator.stop()
|
||||
oldValue.dismissalCallback?()
|
||||
}
|
||||
|
||||
if let sidebarModule {
|
||||
logPresentationChange("Set sidebar", sidebarModule)
|
||||
sidebarModule.coordinator.start()
|
||||
}
|
||||
|
||||
updateCompactLayoutComponents()
|
||||
}
|
||||
}
|
||||
|
||||
/// The currently displayed sidebar coordinator
|
||||
var sidebarCoordinator: (any CoordinatorProtocol)? {
|
||||
sidebarModule?.coordinator
|
||||
}
|
||||
|
||||
@Published fileprivate var detailModule: NavigationModule? {
|
||||
didSet {
|
||||
if let oldValue {
|
||||
logPresentationChange("Remove detail", oldValue)
|
||||
oldValue.coordinator.stop()
|
||||
oldValue.dismissalCallback?()
|
||||
}
|
||||
|
||||
if let detailModule {
|
||||
logPresentationChange("Set detail", detailModule)
|
||||
detailModule.coordinator.start()
|
||||
}
|
||||
|
||||
updateCompactLayoutComponents()
|
||||
}
|
||||
}
|
||||
|
||||
/// The currently displayed detail coordinator
|
||||
var detailCoordinator: (any CoordinatorProtocol)? {
|
||||
detailModule?.coordinator
|
||||
}
|
||||
|
||||
@Published fileprivate var sheetModule: NavigationModule? {
|
||||
didSet {
|
||||
if let oldValue {
|
||||
logPresentationChange("Remove sheet", oldValue)
|
||||
oldValue.coordinator.stop()
|
||||
oldValue.dismissalCallback?()
|
||||
}
|
||||
|
||||
if let sheetModule {
|
||||
logPresentationChange("Set sheet", sheetModule)
|
||||
sheetModule.coordinator.start()
|
||||
}
|
||||
|
||||
updateCompactLayoutComponents()
|
||||
}
|
||||
}
|
||||
|
||||
/// The currently displayed sheet coordinator
|
||||
var sheetCoordinator: (any CoordinatorProtocol)? {
|
||||
sheetModule?.coordinator
|
||||
}
|
||||
|
||||
@Published fileprivate var compactLayoutRootModule: NavigationModule?
|
||||
|
||||
var compactLayoutRootCoordinator: (any CoordinatorProtocol)? {
|
||||
compactLayoutRootModule?.coordinator
|
||||
}
|
||||
|
||||
/// This is set as internal so that we can manipulate the compact layout stack from the unit tests.
|
||||
/// Shouldn't be used otherwise.
|
||||
@Published internal var compactLayoutStackModules: [NavigationModule] = []
|
||||
|
||||
var compactLayoutStackCoordinators: [any CoordinatorProtocol] {
|
||||
compactLayoutStackModules.map(\.coordinator)
|
||||
}
|
||||
|
||||
/// Default NavigationSplitCoordinator initialiser
|
||||
/// - Parameter placeholderCoordinator: coordinator to use if no siderbar or detail is set
|
||||
init(placeholderCoordinator: CoordinatorProtocol) {
|
||||
placeholderModule = NavigationModule(placeholderCoordinator)
|
||||
}
|
||||
|
||||
/// Set the coordinator to be used on the split's left pannel
|
||||
/// - Parameters:
|
||||
/// - coordinator: the sidebar coordinator
|
||||
/// - dismissalCallback: called when this particular sidebar coordinator has removed/replaced
|
||||
func setSidebarCoordinator(_ coordinator: (any CoordinatorProtocol)?, dismissalCallback: (() -> Void)? = nil) {
|
||||
guard let coordinator else {
|
||||
sidebarModule = nil
|
||||
return
|
||||
}
|
||||
|
||||
sidebarModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback)
|
||||
}
|
||||
|
||||
/// Set the coordinator to be used on the split's right pannel
|
||||
/// - Parameters:
|
||||
/// - coordinator: the detail coordinator
|
||||
/// - dismissalCallback: called when this particular detail coordinator has removed/replaced
|
||||
func setDetailCoordinator(_ coordinator: (any CoordinatorProtocol)?, dismissalCallback: (() -> Void)? = nil) {
|
||||
guard let coordinator else {
|
||||
detailModule = nil
|
||||
return
|
||||
}
|
||||
|
||||
detailModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback)
|
||||
}
|
||||
|
||||
/// Present a sheet on top of the split view
|
||||
/// - Parameters:
|
||||
/// - coordinator: the coordinator to display
|
||||
/// - dismissalCallback: called when the sheet has been dismissed, programatically or otherwise
|
||||
func setSheetCoordinator(_ coordinator: (any CoordinatorProtocol)?, dismissalCallback: (() -> Void)? = nil) {
|
||||
guard let coordinator else {
|
||||
sheetModule = nil
|
||||
return
|
||||
}
|
||||
|
||||
sheetModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback)
|
||||
}
|
||||
|
||||
// MARK: - CoordinatorProtocol
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(NavigationSplitCoordinatorView(navigationSplitCoordinator: self))
|
||||
}
|
||||
|
||||
// MARK: - CustomStringConvertible
|
||||
|
||||
var description: String {
|
||||
switch (sidebarModule?.coordinator, detailModule?.coordinator) {
|
||||
case (.some(let sidebarCoordinator), .some(let detailCoordinator)):
|
||||
return "NavigationSplitCoordinator(\(sidebarCoordinator) | \(detailCoordinator))"
|
||||
case (.some(let sidebarCoordinator), .none):
|
||||
return "NavigationSplitCoordinator(\(sidebarCoordinator) | Empty)"
|
||||
case (.none, .some(let detailCoordinator)):
|
||||
return "NavigationSplitCoordinator(Empty | \(detailCoordinator))"
|
||||
case (.none, .none):
|
||||
return "NavigationSplitCoordinator(Empty | Empty)"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func logPresentationChange(_ change: String, _ module: NavigationModule) {
|
||||
MXLog.info("\(self) \(change): \(module.coordinator)")
|
||||
}
|
||||
|
||||
/// We need to update the compact layout whenever anything changes within the split coordinator or
|
||||
/// the navigation coordinators embedded into it
|
||||
private func updateCompactLayoutComponents() {
|
||||
// First remove all observers
|
||||
cancellables.removeAll()
|
||||
|
||||
// Start building the new compact layout navigation stack
|
||||
var stackModules: [NavigationModule] = []
|
||||
// If the sidebar is a stackCoordinator then use it's root as the compact layout root
|
||||
// and push its children to the compact layout stack
|
||||
if let sidebarNavigationStackCoordinator = sidebarModule?.coordinator as? NavigationStackCoordinator {
|
||||
// Observe changes on embedded stackCoordinators and reflect them in the compact layout components
|
||||
observe(navigationStackCoordinator: sidebarNavigationStackCoordinator)
|
||||
|
||||
if let sidebarRootModule = sidebarNavigationStackCoordinator.rootModule {
|
||||
compactLayoutRootModule = sidebarRootModule
|
||||
}
|
||||
|
||||
stackModules.append(contentsOf: sidebarNavigationStackCoordinator.stackModules)
|
||||
} else if let sidebarModule { // Otherwise just use it as a root directly
|
||||
compactLayoutRootModule = sidebarModule
|
||||
}
|
||||
|
||||
// If the detail is a stackCoordinator then push its root and children to the compact layout stack
|
||||
if let detailNavigationStackCoordinator = detailModule?.coordinator as? NavigationStackCoordinator {
|
||||
// Observe changes on embedded stackCoordinators and reflect them in the compact layout components
|
||||
observe(navigationStackCoordinator: detailNavigationStackCoordinator)
|
||||
|
||||
if let detailRootCoordinator = detailNavigationStackCoordinator.rootModule {
|
||||
stackModules.append(detailRootCoordinator)
|
||||
}
|
||||
|
||||
stackModules.append(contentsOf: detailNavigationStackCoordinator.stackModules)
|
||||
} else if let detailModule { // Otherwise just push it entirely
|
||||
stackModules.append(detailModule)
|
||||
}
|
||||
compactLayoutStackModules = stackModules
|
||||
|
||||
// Observe and process compact layout changes
|
||||
observeCompactLayoutStackChanges()
|
||||
}
|
||||
|
||||
/// Changes to the navigation stack while in a compact layout should be
|
||||
/// reflected back onto the embedded components e.g. stackCoordinator pops
|
||||
private func observeCompactLayoutStackChanges() {
|
||||
$compactLayoutStackModules.sink { [weak self] stackModules in
|
||||
guard let self, self.compactLayoutStackModules != stackModules else { return }
|
||||
|
||||
let diffs = stackModules.difference(from: self.compactLayoutStackModules)
|
||||
diffs.forEach { change in
|
||||
switch change {
|
||||
case .insert:
|
||||
break
|
||||
case .remove(_, let module, _):
|
||||
self.processCompactLayoutStackModuleRemoval(module)
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
/// Manually process changes to the compact layout navigation stack and update embedded components
|
||||
/// We need to either: pop from the detail, nil the detail or pop from the sidebar
|
||||
private func processCompactLayoutStackModuleRemoval(_ module: NavigationModule) {
|
||||
if let sidebarNavigationStackCoordinator = sidebarModule?.coordinator as? NavigationStackCoordinator {
|
||||
if sidebarNavigationStackCoordinator.stackModules.contains(module) {
|
||||
sidebarNavigationStackCoordinator.stackModules.removeAll { $0 == module }
|
||||
}
|
||||
}
|
||||
|
||||
if module == detailModule {
|
||||
detailModule = nil
|
||||
}
|
||||
|
||||
if let detailNavigationStackCoordinator = detailModule?.coordinator as? NavigationStackCoordinator {
|
||||
if detailNavigationStackCoordinator.stackModules.contains(module) {
|
||||
detailNavigationStackCoordinator.stackModules.removeAll { $0 == module }
|
||||
} else if module == detailNavigationStackCoordinator.rootModule {
|
||||
detailModule = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Any change to a NavigationStackCoordinator's internal state should be observed and reflected in the
|
||||
/// compact layout components
|
||||
private func observe(navigationStackCoordinator: NavigationStackCoordinator) {
|
||||
navigationStackCoordinator.$rootModule.sink { [weak self] rootModule in
|
||||
guard navigationStackCoordinator.rootModule != rootModule else { return }
|
||||
DispatchQueue.main.async { self?.updateCompactLayoutComponents() }
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
navigationStackCoordinator.$stackModules.sink { [weak self] stackModules in
|
||||
guard navigationStackCoordinator.stackModules != stackModules else { return }
|
||||
DispatchQueue.main.async { self?.updateCompactLayoutComponents() }
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
navigationStackCoordinator.$sheetModule.sink { [weak self] sheetModule in
|
||||
guard navigationStackCoordinator.sheetModule != sheetModule else { return }
|
||||
DispatchQueue.main.async { self?.updateCompactLayoutComponents() }
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
private struct NavigationSplitCoordinatorView: View {
|
||||
@State private var columnVisibility = NavigationSplitViewVisibility.all
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
@ObservedObject var navigationSplitCoordinator: NavigationSplitCoordinator
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if horizontalSizeClass == .compact {
|
||||
navigationStack
|
||||
} else {
|
||||
navigationSplitView
|
||||
}
|
||||
}
|
||||
// This needs to be handled on the top level otherwise sheets
|
||||
// will be automatically dismissed on hierarchy changes.
|
||||
// Embedded NavigationStackCoordinators will present their sheets
|
||||
// through the NavigationSplitCoordinator as well.
|
||||
.sheet(item: $navigationSplitCoordinator.sheetModule) { module in
|
||||
module.coordinator.toPresentable()
|
||||
}
|
||||
}
|
||||
|
||||
/// The NavigationStack that will be used in compact layouts
|
||||
var navigationStack: some View {
|
||||
NavigationStack(path: $navigationSplitCoordinator.compactLayoutStackModules) {
|
||||
navigationSplitCoordinator.compactLayoutRootModule?.coordinator.toPresentable()
|
||||
.navigationDestination(for: NavigationModule.self) { module in
|
||||
module.coordinator.toPresentable()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The NavigationSplitView that will be used in non-compact layouts
|
||||
var navigationSplitView: some View {
|
||||
NavigationSplitView(columnVisibility: $columnVisibility) {
|
||||
if let sidebarModule = navigationSplitCoordinator.sidebarModule {
|
||||
sidebarModule.coordinator.toPresentable()
|
||||
} else {
|
||||
navigationSplitCoordinator.placeholderModule.coordinator.toPresentable()
|
||||
}
|
||||
} detail: {
|
||||
if let detailModule = navigationSplitCoordinator.detailModule {
|
||||
detailModule.coordinator.toPresentable()
|
||||
} else {
|
||||
navigationSplitCoordinator.placeholderModule.coordinator.toPresentable()
|
||||
}
|
||||
}
|
||||
.navigationSplitViewStyle(.balanced)
|
||||
.navigationDestination(for: NavigationModule.self) { module in
|
||||
module.coordinator.toPresentable()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - NavigationStackCoordinator
|
||||
|
||||
/// Class responsible for displaying a normal "NavigationController" style hierarchy
|
||||
class NavigationStackCoordinator: ObservableObject, CoordinatorProtocol, CustomStringConvertible {
|
||||
private(set) weak var navigationSplitCoordinator: NavigationSplitCoordinator?
|
||||
|
||||
@Published fileprivate var rootModule: NavigationModule? {
|
||||
didSet {
|
||||
if let oldValue {
|
||||
logPresentationChange("Remove root", oldValue)
|
||||
oldValue.coordinator.stop()
|
||||
oldValue.dismissalCallback?()
|
||||
}
|
||||
|
||||
if let rootModule {
|
||||
logPresentationChange("Set root", rootModule)
|
||||
rootModule.coordinator.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The stack's current root coordinator
|
||||
var rootCoordinator: (any CoordinatorProtocol)? {
|
||||
rootModule?.coordinator
|
||||
}
|
||||
|
||||
@Published fileprivate var sheetModule: NavigationModule? {
|
||||
didSet {
|
||||
if let oldValue {
|
||||
logPresentationChange("Remove sheet", oldValue)
|
||||
oldValue.coordinator.stop()
|
||||
oldValue.dismissalCallback?()
|
||||
}
|
||||
|
||||
if let sheetModule {
|
||||
logPresentationChange("Set sheet", sheetModule)
|
||||
sheetModule.coordinator.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The currently presented sheet coordinator
|
||||
// Sheets will be presented through the NavigationSplitCoordinator if provided
|
||||
var sheetCoordinator: (any CoordinatorProtocol)? {
|
||||
if let navigationSplitCoordinator {
|
||||
return navigationSplitCoordinator.sheetCoordinator
|
||||
}
|
||||
|
||||
return sheetModule?.coordinator
|
||||
}
|
||||
|
||||
@Published fileprivate var stackModules = [NavigationModule]() {
|
||||
didSet {
|
||||
let diffs = stackModules.difference(from: oldValue)
|
||||
diffs.forEach { change in
|
||||
switch change {
|
||||
case .insert(_, let module, _):
|
||||
logPresentationChange("Push", module)
|
||||
module.coordinator.start()
|
||||
case .remove(_, let module, _):
|
||||
logPresentationChange("Pop", module)
|
||||
module.coordinator.stop()
|
||||
module.dismissalCallback?()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The current navigation stack. Excludes the rootCoordinator
|
||||
var stackCoordinators: [any CoordinatorProtocol] {
|
||||
stackModules.map(\.coordinator)
|
||||
}
|
||||
|
||||
/// If this NavigationStackCoordinator will be embedded into a NavigationSplitCoordinator pass it here
|
||||
/// so that sheet presentations are done through it. Otherwise sheets will not be presented properly
|
||||
/// and dismissed automatically in compact layouts
|
||||
/// - Parameter navigationSplitCoordinator: The expected parent NavigationSplitCoordinator
|
||||
init(navigationSplitCoordinator: NavigationSplitCoordinator? = nil) {
|
||||
self.navigationSplitCoordinator = navigationSplitCoordinator
|
||||
}
|
||||
|
||||
/// Set the coordinator to be used on the stack's root
|
||||
/// - Parameters:
|
||||
/// - coordinator: the root coordinator
|
||||
/// - dismissalCallback: called when this root coordinator has removed/replaced
|
||||
func setRootCoordinator(_ coordinator: (any CoordinatorProtocol)?, dismissalCallback: (() -> Void)? = nil) {
|
||||
guard let coordinator else {
|
||||
rootModule = nil
|
||||
return
|
||||
}
|
||||
|
||||
popToRoot(animated: false)
|
||||
|
||||
rootModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback)
|
||||
}
|
||||
|
||||
/// Pushes a new coordinator on the navigation stack
|
||||
/// - Parameters:
|
||||
/// - coordinator: the coordinator to be displayed
|
||||
/// - dismissalCallback: called when the coordinator has been popped, programatically or otherwise
|
||||
func push(_ coordinator: any CoordinatorProtocol, dismissalCallback: (() -> Void)? = nil) {
|
||||
stackModules.append(NavigationModule(coordinator, dismissalCallback: dismissalCallback))
|
||||
}
|
||||
|
||||
/// Pop all the coordinators from the stack, returning to the root coordinator
|
||||
/// - Parameter animated: whether to animate the transition or not. Default is true
|
||||
func popToRoot(animated: Bool = true) {
|
||||
guard !stackModules.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
if !animated {
|
||||
// Disabling animations doesn't work through normal Transactions
|
||||
// https://stackoverflow.com/questions/72832243
|
||||
UIView.setAnimationsEnabled(false)
|
||||
}
|
||||
|
||||
stackModules.removeAll()
|
||||
|
||||
if !animated {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
|
||||
UIView.setAnimationsEnabled(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the last coordinator from the navigation stack
|
||||
func pop() {
|
||||
stackModules.removeLast()
|
||||
}
|
||||
|
||||
/// Present a sheet on top of the stack. If this NavigationStackCoordinator is embedded within a NavigationSplitCoordinator
|
||||
/// then the presentation will be proxied to the split
|
||||
/// - Parameters:
|
||||
/// - coordinator: the coordinator to display
|
||||
/// - dismissalCallback: called when the sheet has been dismissed, programatically or otherwise
|
||||
func setSheetCoordinator(_ coordinator: (any CoordinatorProtocol)?, dismissalCallback: (() -> Void)? = nil) {
|
||||
if let navigationSplitCoordinator {
|
||||
navigationSplitCoordinator.setSheetCoordinator(coordinator, dismissalCallback: dismissalCallback)
|
||||
return
|
||||
}
|
||||
|
||||
guard let coordinator else {
|
||||
sheetModule = nil
|
||||
return
|
||||
}
|
||||
|
||||
sheetModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback)
|
||||
}
|
||||
|
||||
// MARK: - CoordinatorProtocol
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(NavigationStackCoordinatorView(navigationStackCoordinator: self))
|
||||
}
|
||||
|
||||
// MARK: - CustomStringConvertible
|
||||
|
||||
var description: String {
|
||||
if let rootCoordinator = rootModule?.coordinator {
|
||||
return "NavigationStackCoordinator(\(rootCoordinator))"
|
||||
} else {
|
||||
return "NavigationStackCoordinator(Empty)"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func logPresentationChange(_ change: String, _ module: NavigationModule) {
|
||||
MXLog.info("\(self) \(change): \(module.coordinator)")
|
||||
}
|
||||
}
|
||||
|
||||
private struct NavigationStackCoordinatorView: View {
|
||||
@ObservedObject var navigationStackCoordinator: NavigationStackCoordinator
|
||||
|
||||
var body: some View {
|
||||
NavigationStack(path: $navigationStackCoordinator.stackModules) {
|
||||
navigationStackCoordinator.rootModule?.coordinator.toPresentable()
|
||||
.navigationDestination(for: NavigationModule.self) { module in
|
||||
module.coordinator.toPresentable()
|
||||
}
|
||||
}
|
||||
.sheet(item: $navigationStackCoordinator.sheetModule) { module in
|
||||
module.coordinator.toPresentable()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// A CoordinatorProtocol wrapper and type erasing component that allows
|
||||
/// dynamically presenting arbitrary screens
|
||||
struct NavigationModule: Identifiable, Hashable {
|
||||
let id = UUID()
|
||||
let coordinator: any CoordinatorProtocol
|
||||
let dismissalCallback: (() -> Void)?
|
||||
|
||||
init(_ coordinator: any CoordinatorProtocol, dismissalCallback: (() -> Void)? = nil) {
|
||||
self.coordinator = coordinator
|
||||
self.dismissalCallback = dismissalCallback
|
||||
}
|
||||
|
||||
static func == (lhs: NavigationModule, rhs: NavigationModule) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
class NavigationRootCoordinator: ObservableObject, CoordinatorProtocol, CustomStringConvertible {
|
||||
@Published fileprivate var rootModule: NavigationModule? {
|
||||
didSet {
|
||||
if let oldValue {
|
||||
oldValue.coordinator.stop()
|
||||
oldValue.dismissalCallback?()
|
||||
}
|
||||
|
||||
if let rootModule {
|
||||
logPresentationChange("Set root", rootModule)
|
||||
rootModule.coordinator.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The currently displayed coordinator
|
||||
var rootCoordinator: (any CoordinatorProtocol)? {
|
||||
rootModule?.coordinator
|
||||
}
|
||||
|
||||
/// Sets or replaces the presented coordinator
|
||||
/// - Parameter coordinator: the coordinator to display
|
||||
func setRootCoordinator(_ coordinator: (any CoordinatorProtocol)?, dismissalCallback: (() -> Void)? = nil) {
|
||||
guard let coordinator else {
|
||||
rootModule = nil
|
||||
return
|
||||
}
|
||||
|
||||
rootModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback)
|
||||
}
|
||||
|
||||
// MARK: - CoordinatorProtocol
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(NavigationRootCoordinatorView(rootCoordinator: self))
|
||||
}
|
||||
|
||||
// MARK: - CustomStringConvertible
|
||||
|
||||
var description: String {
|
||||
if let rootCoordinator = rootModule?.coordinator {
|
||||
return "SingleScreenCoordinator(\(rootCoordinator)"
|
||||
} else {
|
||||
return "SingleScreenCoordinator(Empty)"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func logPresentationChange(_ change: String, _ module: NavigationModule) {
|
||||
MXLog.info("\(self) \(change): \(module.coordinator)")
|
||||
}
|
||||
}
|
||||
|
||||
private struct NavigationRootCoordinatorView: View {
|
||||
@ObservedObject var rootCoordinator: NavigationRootCoordinator
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
rootCoordinator.rootModule?.coordinator.toPresentable()
|
||||
}
|
||||
.animation(.elementDefault, value: rootCoordinator.rootModule)
|
||||
}
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
class NavigationController: ObservableObject, CoordinatorProtocol {
|
||||
private var dismissalCallbacks = [UUID: () -> Void]()
|
||||
|
||||
@Published fileprivate var internalRootCoordinator: AnyCoordinator? {
|
||||
didSet {
|
||||
if let oldValue {
|
||||
oldValue.coordinator.stop()
|
||||
}
|
||||
|
||||
if let internalRootCoordinator {
|
||||
logPresentationChange("Set root", internalRootCoordinator)
|
||||
internalRootCoordinator.coordinator.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Published fileprivate var internalSheetCoordinator: AnyCoordinator? {
|
||||
didSet {
|
||||
if let oldValue {
|
||||
logPresentationChange("Dismiss", oldValue)
|
||||
oldValue.coordinator.stop()
|
||||
dismissalCallbacks[oldValue.id]?()
|
||||
dismissalCallbacks.removeValue(forKey: oldValue.id)
|
||||
}
|
||||
|
||||
if let internalSheetCoordinator {
|
||||
logPresentationChange("Present", internalSheetCoordinator)
|
||||
internalSheetCoordinator.coordinator.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Published fileprivate var internalNavigationStack = [AnyCoordinator]() {
|
||||
didSet {
|
||||
let diffs = internalNavigationStack.difference(from: oldValue)
|
||||
diffs.forEach { change in
|
||||
switch change {
|
||||
case .insert(_, let anyCoordinator, _):
|
||||
logPresentationChange("Push", anyCoordinator)
|
||||
anyCoordinator.coordinator.start()
|
||||
case .remove(_, let anyCoordinator, _):
|
||||
logPresentationChange("Pop", anyCoordinator)
|
||||
anyCoordinator.coordinator.stop()
|
||||
|
||||
dismissalCallbacks[anyCoordinator.id]?()
|
||||
dismissalCallbacks.removeValue(forKey: anyCoordinator.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var rootCoordinator: CoordinatorProtocol? {
|
||||
internalRootCoordinator?.coordinator
|
||||
}
|
||||
|
||||
var coordinators: [CoordinatorProtocol] {
|
||||
internalNavigationStack.map(\.coordinator)
|
||||
}
|
||||
|
||||
var sheetCoordinator: CoordinatorProtocol? {
|
||||
internalSheetCoordinator?.coordinator
|
||||
}
|
||||
|
||||
func setRootCoordinator(_ coordinator: any CoordinatorProtocol) {
|
||||
popToRoot(animated: false)
|
||||
internalRootCoordinator = AnyCoordinator(coordinator)
|
||||
}
|
||||
|
||||
func push(_ coordinator: any CoordinatorProtocol, dismissalCallback: (() -> Void)? = nil) {
|
||||
let anyCoordinator = AnyCoordinator(coordinator)
|
||||
|
||||
if let dismissalCallback {
|
||||
dismissalCallbacks[anyCoordinator.id] = dismissalCallback
|
||||
}
|
||||
|
||||
internalNavigationStack.append(anyCoordinator)
|
||||
}
|
||||
|
||||
func popToRoot(animated: Bool = true) {
|
||||
dismissSheet()
|
||||
|
||||
guard !internalNavigationStack.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
if !animated {
|
||||
// Disabling animations doesn't work through normal Transactions
|
||||
// https://stackoverflow.com/questions/72832243
|
||||
UIView.setAnimationsEnabled(false)
|
||||
}
|
||||
|
||||
internalNavigationStack.removeAll()
|
||||
|
||||
if !animated {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
|
||||
UIView.setAnimationsEnabled(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func pop() {
|
||||
dismissSheet()
|
||||
internalNavigationStack.removeLast()
|
||||
}
|
||||
|
||||
func presentSheet(_ coordinator: any CoordinatorProtocol, dismissalCallback: (() -> Void)? = nil) {
|
||||
let anyCoordinator = AnyCoordinator(coordinator)
|
||||
|
||||
if let dismissalCallback {
|
||||
dismissalCallbacks[anyCoordinator.id] = dismissalCallback
|
||||
}
|
||||
|
||||
internalSheetCoordinator = anyCoordinator
|
||||
}
|
||||
|
||||
func dismissSheet() {
|
||||
internalSheetCoordinator = nil
|
||||
}
|
||||
|
||||
// MARK: - CoordinatorProtocol
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(NavigationControllerView(navigationController: self))
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func logPresentationChange(_ change: String, _ anyCoordinator: AnyCoordinator) {
|
||||
if let navigationCoordinator = anyCoordinator.coordinator as? NavigationController, let rootCoordinator = navigationCoordinator.rootCoordinator {
|
||||
MXLog.info("\(change): NavigationController(\(anyCoordinator.id)) - \(rootCoordinator)")
|
||||
} else {
|
||||
MXLog.info("\(change): \(anyCoordinator.coordinator)(\(anyCoordinator.id))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct NavigationControllerView: View {
|
||||
@ObservedObject var navigationController: NavigationController
|
||||
|
||||
var body: some View {
|
||||
NavigationStack(path: $navigationController.internalNavigationStack) {
|
||||
navigationController.internalRootCoordinator?.coordinator.toPresentable()
|
||||
.navigationDestination(for: AnyCoordinator.self) { anyCoordinator in
|
||||
anyCoordinator.coordinator.toPresentable()
|
||||
}
|
||||
}
|
||||
.sheet(item: $navigationController.internalSheetCoordinator) { anyCoordinator in
|
||||
anyCoordinator.coordinator.toPresentable()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct AnyCoordinator: Identifiable, Hashable {
|
||||
let id = UUID()
|
||||
let coordinator: any CoordinatorProtocol
|
||||
|
||||
init(_ coordinator: any CoordinatorProtocol) {
|
||||
self.coordinator = coordinator
|
||||
}
|
||||
|
||||
static func == (lhs: AnyCoordinator, rhs: AnyCoordinator) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
class UserNotificationController: ObservableObject, UserNotificationControllerProtocol {
|
||||
class UserNotificationController: ObservableObject, UserNotificationControllerProtocol, CustomStringConvertible {
|
||||
private let rootCoordinator: CoordinatorProtocol
|
||||
|
||||
private var dismisalTimer: Timer?
|
||||
@@ -77,4 +77,10 @@ class UserNotificationController: ObservableObject, UserNotificationControllerPr
|
||||
self?.displayTimes[id] = nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CustomStringConvertible
|
||||
|
||||
var description: String {
|
||||
"UserNotificationController(\(String(describing: rootCoordinator)))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,14 +24,14 @@ protocol AuthenticationCoordinatorDelegate: AnyObject {
|
||||
|
||||
class AuthenticationCoordinator: CoordinatorProtocol {
|
||||
private let authenticationService: AuthenticationServiceProxyProtocol
|
||||
private let navigationController: NavigationController
|
||||
private let navigationStackCoordinator: NavigationStackCoordinator
|
||||
|
||||
weak var delegate: AuthenticationCoordinatorDelegate?
|
||||
|
||||
init(authenticationService: AuthenticationServiceProxyProtocol,
|
||||
navigationController: NavigationController) {
|
||||
navigationStackCoordinator: NavigationStackCoordinator) {
|
||||
self.authenticationService = authenticationService
|
||||
self.navigationController = navigationController
|
||||
self.navigationStackCoordinator = navigationStackCoordinator
|
||||
}
|
||||
|
||||
func start() {
|
||||
@@ -55,7 +55,7 @@ class AuthenticationCoordinator: CoordinatorProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
navigationController.setRootCoordinator(coordinator)
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
}
|
||||
|
||||
private func startAuthentication() async {
|
||||
@@ -88,12 +88,12 @@ class AuthenticationCoordinator: CoordinatorProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
navigationController.push(coordinator)
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
|
||||
private func showLoginScreen() {
|
||||
let parameters = LoginCoordinatorParameters(authenticationService: authenticationService,
|
||||
navigationController: navigationController)
|
||||
navigationStackCoordinator: navigationStackCoordinator)
|
||||
let coordinator = LoginCoordinator(parameters: parameters)
|
||||
|
||||
coordinator.callback = { [weak self] action in
|
||||
@@ -105,7 +105,7 @@ class AuthenticationCoordinator: CoordinatorProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
navigationController.push(coordinator)
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
|
||||
private func showAnalyticsPrompt(with userSession: UserSessionProtocol) {
|
||||
@@ -117,7 +117,7 @@ class AuthenticationCoordinator: CoordinatorProtocol {
|
||||
self.delegate?.authenticationCoordinator(self, didLoginWithSession: userSession)
|
||||
}
|
||||
|
||||
navigationController.setRootCoordinator(coordinator)
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
}
|
||||
|
||||
static let loadingIndicatorIdentifier = "AuthenticationCoordinatorLoading"
|
||||
|
||||
@@ -21,7 +21,7 @@ struct LoginCoordinatorParameters {
|
||||
/// The service used to authenticate the user.
|
||||
let authenticationService: AuthenticationServiceProxyProtocol
|
||||
/// The navigation router used to present the server selection screen.
|
||||
let navigationController: NavigationController
|
||||
let navigationStackCoordinator: NavigationStackCoordinator
|
||||
}
|
||||
|
||||
enum LoginCoordinatorAction {
|
||||
@@ -43,7 +43,7 @@ final class LoginCoordinator: CoordinatorProtocol {
|
||||
}
|
||||
|
||||
private var authenticationService: AuthenticationServiceProxyProtocol { parameters.authenticationService }
|
||||
private var navigationController: NavigationController { parameters.navigationController }
|
||||
private var navigationStackCoordinator: NavigationStackCoordinator { parameters.navigationStackCoordinator }
|
||||
|
||||
var callback: (@MainActor (LoginCoordinatorAction) -> Void)?
|
||||
|
||||
@@ -195,9 +195,9 @@ final class LoginCoordinator: CoordinatorProtocol {
|
||||
|
||||
/// Presents the server selection screen as a modal.
|
||||
private func presentServerSelectionScreen() {
|
||||
let serverSelectionNavigationController = NavigationController()
|
||||
let serverSelectionNavigationStackCoordinator = NavigationStackCoordinator()
|
||||
|
||||
let userNotificationController = UserNotificationController(rootCoordinator: serverSelectionNavigationController)
|
||||
let userNotificationController = UserNotificationController(rootCoordinator: serverSelectionNavigationStackCoordinator)
|
||||
|
||||
let parameters = ServerSelectionCoordinatorParameters(authenticationService: authenticationService,
|
||||
userNotificationController: userNotificationController,
|
||||
@@ -209,9 +209,9 @@ final class LoginCoordinator: CoordinatorProtocol {
|
||||
self.serverSelectionCoordinator(coordinator, didCompleteWith: action)
|
||||
}
|
||||
|
||||
serverSelectionNavigationController.setRootCoordinator(coordinator)
|
||||
serverSelectionNavigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
|
||||
navigationController.presentSheet(userNotificationController)
|
||||
navigationStackCoordinator.setSheetCoordinator(userNotificationController)
|
||||
}
|
||||
|
||||
/// Handles the result from the server selection modal, dismissing it after updating the view.
|
||||
@@ -221,7 +221,7 @@ final class LoginCoordinator: CoordinatorProtocol {
|
||||
updateViewModel()
|
||||
}
|
||||
|
||||
navigationController.dismissSheet()
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
|
||||
/// Shows the forgot password screen.
|
||||
|
||||
@@ -21,7 +21,7 @@ struct HomeScreenCoordinatorParameters {
|
||||
let userSession: UserSessionProtocol
|
||||
let attributedStringBuilder: AttributedStringBuilderProtocol
|
||||
let bugReportService: BugReportServiceProtocol
|
||||
let navigationController: NavigationController
|
||||
let navigationStackCoordinator: NavigationStackCoordinator
|
||||
}
|
||||
|
||||
enum HomeScreenCoordinatorAction {
|
||||
@@ -94,6 +94,6 @@ final class HomeScreenCoordinator: CoordinatorProtocol {
|
||||
}
|
||||
|
||||
private func presentInviteFriends() {
|
||||
parameters.navigationController.presentSheet(InviteFriendsCoordinator(userId: parameters.userSession.userID))
|
||||
parameters.navigationStackCoordinator.setSheetCoordinator(InviteFriendsCoordinator(userId: parameters.userSession.userID))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import SwiftUI
|
||||
|
||||
struct RoomScreenCoordinatorParameters {
|
||||
let navigationController: NavigationController
|
||||
let navigationStackCoordinator: NavigationStackCoordinator
|
||||
let timelineController: RoomTimelineControllerProtocol
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
let roomName: String?
|
||||
@@ -29,12 +29,12 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
private var parameters: RoomScreenCoordinatorParameters?
|
||||
|
||||
private var viewModel: RoomScreenViewModelProtocol?
|
||||
private var navigationController: NavigationController {
|
||||
private var navigationStackCoordinator: NavigationStackCoordinator {
|
||||
guard let parameters else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
return parameters.navigationController
|
||||
return parameters.navigationStackCoordinator
|
||||
}
|
||||
|
||||
init(parameters: RoomScreenCoordinatorParameters) {
|
||||
@@ -86,19 +86,19 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
|
||||
if params.isModallyPresented {
|
||||
coordinator.callback = { [weak self] _ in
|
||||
self?.navigationController.dismissSheet()
|
||||
self?.navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
|
||||
let controller = NavigationController()
|
||||
let controller = NavigationStackCoordinator()
|
||||
controller.setRootCoordinator(coordinator)
|
||||
|
||||
navigationController.presentSheet(controller)
|
||||
navigationStackCoordinator.setSheetCoordinator(controller)
|
||||
} else {
|
||||
coordinator.callback = { [weak self] _ in
|
||||
self?.navigationController.pop()
|
||||
self?.navigationStackCoordinator.pop()
|
||||
}
|
||||
|
||||
navigationController.push(coordinator)
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,10 +106,10 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
let params = FilePreviewCoordinatorParameters(fileURL: fileURL, title: title)
|
||||
let coordinator = FilePreviewCoordinator(parameters: params)
|
||||
coordinator.callback = { [weak self] _ in
|
||||
self?.navigationController.pop()
|
||||
self?.navigationStackCoordinator.pop()
|
||||
}
|
||||
|
||||
navigationController.push(coordinator)
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
|
||||
private func displayEmojiPickerScreen(for itemId: String) {
|
||||
@@ -123,13 +123,14 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
coordinator.callback = { [weak self] action in
|
||||
switch action {
|
||||
case let .emojiSelected(emoji: emoji, itemId: itemId):
|
||||
self?.navigationController.dismissSheet()
|
||||
self?.navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
MXLog.debug("Save \(emoji) for \(itemId)")
|
||||
Task {
|
||||
await timelineController.sendReaction(emoji, for: itemId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
navigationController.presentSheet(coordinator)
|
||||
navigationStackCoordinator.setSheetCoordinator(coordinator)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
deinit {
|
||||
ServiceLocator.shared.userNotificationController.retractNotificationWithId(Constants.backPaginationIndicatorID)
|
||||
}
|
||||
|
||||
init(timelineController: RoomTimelineControllerProtocol,
|
||||
timelineViewFactory: RoomTimelineViewFactoryProtocol,
|
||||
mediaProvider: MediaProviderProtocol,
|
||||
|
||||
@@ -33,6 +33,7 @@ struct RoomScreen: View {
|
||||
|
||||
var timeline: some View {
|
||||
TimelineTableView()
|
||||
.id(context.viewState.roomId)
|
||||
.environmentObject(context)
|
||||
.timelineStyle(settings.timelineStyle)
|
||||
.overlay(alignment: .bottomTrailing) { scrollToBottomButton }
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsCoordinatorParameters {
|
||||
let navigationController: NavigationController
|
||||
let navigationStackCoordinator: NavigationStackCoordinator
|
||||
let userNotificationController: UserNotificationControllerProtocol
|
||||
let userSession: UserSessionProtocol
|
||||
let bugReportService: BugReportServiceProtocol
|
||||
@@ -88,10 +88,10 @@ final class SettingsCoordinator: CoordinatorProtocol {
|
||||
break
|
||||
}
|
||||
|
||||
self?.parameters.navigationController.pop()
|
||||
self?.parameters.navigationStackCoordinator.pop()
|
||||
}
|
||||
|
||||
parameters.navigationController.push(coordinator)
|
||||
parameters.navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
|
||||
private func showSuccess(label: String) {
|
||||
|
||||
@@ -24,18 +24,26 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
||||
private let stateMachine: UserSessionFlowCoordinatorStateMachine
|
||||
|
||||
private let userSession: UserSessionProtocol
|
||||
private let navigationController: NavigationController
|
||||
private let navigationSplitCoordinator: NavigationSplitCoordinator
|
||||
private let bugReportService: BugReportServiceProtocol
|
||||
private let emojiProvider: EmojiProviderProtocol = EmojiProvider()
|
||||
|
||||
private let sidebarNavigationStackCoordinator: NavigationStackCoordinator
|
||||
private let detailNavigationStackCoordinator: NavigationStackCoordinator
|
||||
|
||||
var callback: ((UserSessionFlowCoordinatorAction) -> Void)?
|
||||
|
||||
init(userSession: UserSessionProtocol, navigationController: NavigationController, bugReportService: BugReportServiceProtocol) {
|
||||
init(userSession: UserSessionProtocol, navigationSplitCoordinator: NavigationSplitCoordinator, bugReportService: BugReportServiceProtocol) {
|
||||
stateMachine = UserSessionFlowCoordinatorStateMachine()
|
||||
self.userSession = userSession
|
||||
self.navigationController = navigationController
|
||||
self.navigationSplitCoordinator = navigationSplitCoordinator
|
||||
self.bugReportService = bugReportService
|
||||
|
||||
sidebarNavigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator)
|
||||
detailNavigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator)
|
||||
|
||||
navigationSplitCoordinator.setSidebarCoordinator(sidebarNavigationStackCoordinator)
|
||||
|
||||
setupStateMachine()
|
||||
}
|
||||
|
||||
@@ -50,7 +58,7 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
||||
}
|
||||
|
||||
func tryDisplayingRoomScreen(roomId: String) {
|
||||
stateMachine.processEvent(.showRoomScreen(roomId: roomId))
|
||||
stateMachine.processEvent(.selectRoom(roomId: roomId))
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
@@ -61,27 +69,32 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
||||
guard let self else { return }
|
||||
|
||||
switch (context.fromState, context.event, context.toState) {
|
||||
case (.initial, .start, .homeScreen):
|
||||
case (.initial, .start, .roomList):
|
||||
self.presentHomeScreen()
|
||||
|
||||
case(.homeScreen, .showRoomScreen, .roomScreen(let roomId)):
|
||||
self.presentRoomWithIdentifier(roomId)
|
||||
case(.roomScreen, .dismissedRoomScreen, .homeScreen):
|
||||
break
|
||||
case(.roomList(let currentRoomId), .selectRoom, .roomList(let selectedRoomId)):
|
||||
guard let selectedRoomId,
|
||||
selectedRoomId != currentRoomId else {
|
||||
return
|
||||
}
|
||||
|
||||
case (.homeScreen, .showSessionVerificationScreen, .sessionVerificationScreen):
|
||||
self.presentRoomWithIdentifier(selectedRoomId)
|
||||
case(.roomList, .deselectRoom, .roomList):
|
||||
break
|
||||
|
||||
case (.roomList, .showSessionVerificationScreen, .sessionVerificationScreen):
|
||||
self.presentSessionVerification()
|
||||
case (.sessionVerificationScreen, .dismissedSessionVerificationScreen, .homeScreen):
|
||||
case (.sessionVerificationScreen, .dismissedSessionVerificationScreen, .roomList):
|
||||
break
|
||||
|
||||
case (.homeScreen, .showSettingsScreen, .settingsScreen):
|
||||
|
||||
case (.roomList, .showSettingsScreen, .settingsScreen):
|
||||
self.presentSettingsScreen()
|
||||
case (.settingsScreen, .dismissedSettingsScreen, .homeScreen):
|
||||
case (.settingsScreen, .dismissedSettingsScreen, .roomList):
|
||||
break
|
||||
|
||||
case (.homeScreen, .feedbackScreen, .feedbackScreen):
|
||||
case (.roomList, .feedbackScreen, .feedbackScreen):
|
||||
self.presentFeedbackScreen()
|
||||
case (.feedbackScreen, .dismissedFeedbackScreen, .homeScreen):
|
||||
case (.feedbackScreen, .dismissedFeedbackScreen, .roomList):
|
||||
break
|
||||
|
||||
default:
|
||||
@@ -100,7 +113,7 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
||||
let parameters = HomeScreenCoordinatorParameters(userSession: userSession,
|
||||
attributedStringBuilder: AttributedStringBuilder(),
|
||||
bugReportService: bugReportService,
|
||||
navigationController: navigationController)
|
||||
navigationStackCoordinator: detailNavigationStackCoordinator)
|
||||
let coordinator = HomeScreenCoordinator(parameters: parameters)
|
||||
|
||||
coordinator.callback = { [weak self] action in
|
||||
@@ -108,7 +121,7 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
||||
|
||||
switch action {
|
||||
case .presentRoomScreen(let roomIdentifier):
|
||||
self.stateMachine.processEvent(.showRoomScreen(roomId: roomIdentifier))
|
||||
self.stateMachine.processEvent(.selectRoom(roomId: roomIdentifier))
|
||||
case .presentSettingsScreen:
|
||||
self.stateMachine.processEvent(.showSettingsScreen)
|
||||
case .presentFeedbackScreen:
|
||||
@@ -119,8 +132,8 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
||||
self.callback?(.signOut)
|
||||
}
|
||||
}
|
||||
|
||||
navigationController.setRootCoordinator(coordinator)
|
||||
|
||||
sidebarNavigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
}
|
||||
|
||||
// MARK: Rooms
|
||||
@@ -145,7 +158,7 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
||||
mediaProvider: userSession.mediaProvider,
|
||||
roomProxy: roomProxy)
|
||||
|
||||
let parameters = RoomScreenCoordinatorParameters(navigationController: navigationController,
|
||||
let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: detailNavigationStackCoordinator,
|
||||
timelineController: timelineController,
|
||||
mediaProvider: userSession.mediaProvider,
|
||||
roomName: roomProxy.displayName ?? roomProxy.name,
|
||||
@@ -153,9 +166,17 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
||||
emojiProvider: emojiProvider)
|
||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||
|
||||
navigationController.push(coordinator) { [weak self] in
|
||||
detailNavigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
navigationSplitCoordinator.setDetailCoordinator(detailNavigationStackCoordinator) { [weak self, roomIdentifier] in
|
||||
guard let self else { return }
|
||||
self.stateMachine.processEvent(.dismissedRoomScreen)
|
||||
|
||||
// Move the state machine to no room selected if the room currently being dimissed
|
||||
// is the same as the one selected in the state machine.
|
||||
// This generally happens when popping the room screen while in a compact layout
|
||||
if case let .roomList(selectedRoomId) = self.stateMachine.state, selectedRoomId == roomIdentifier {
|
||||
self.stateMachine.processEvent(.deselectRoom)
|
||||
self.detailNavigationStackCoordinator.setRootCoordinator(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,11 +184,11 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
||||
// MARK: Settings
|
||||
|
||||
private func presentSettingsScreen() {
|
||||
let settingsNavigationController = NavigationController()
|
||||
let settingsNavigationStackCoordinator = NavigationStackCoordinator()
|
||||
|
||||
let userNotificationController = UserNotificationController(rootCoordinator: settingsNavigationController)
|
||||
let userNotificationController = UserNotificationController(rootCoordinator: settingsNavigationStackCoordinator)
|
||||
|
||||
let parameters = SettingsCoordinatorParameters(navigationController: settingsNavigationController,
|
||||
let parameters = SettingsCoordinatorParameters(navigationStackCoordinator: settingsNavigationStackCoordinator,
|
||||
userNotificationController: userNotificationController,
|
||||
userSession: userSession,
|
||||
bugReportService: bugReportService)
|
||||
@@ -176,16 +197,16 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .dismiss:
|
||||
self.navigationController.dismissSheet()
|
||||
self.navigationSplitCoordinator.setSheetCoordinator(nil)
|
||||
case .logout:
|
||||
self.navigationController.dismissSheet()
|
||||
self.navigationSplitCoordinator.setSheetCoordinator(nil)
|
||||
self.callback?(.signOut)
|
||||
}
|
||||
}
|
||||
|
||||
settingsNavigationController.setRootCoordinator(settingsCoordinator)
|
||||
settingsNavigationStackCoordinator.setRootCoordinator(settingsCoordinator)
|
||||
|
||||
navigationController.presentSheet(userNotificationController) { [weak self] in
|
||||
navigationSplitCoordinator.setSheetCoordinator(userNotificationController) { [weak self] in
|
||||
self?.stateMachine.processEvent(.dismissedSettingsScreen)
|
||||
}
|
||||
}
|
||||
@@ -202,10 +223,10 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
||||
let coordinator = SessionVerificationCoordinator(parameters: parameters)
|
||||
|
||||
coordinator.callback = { [weak self] in
|
||||
self?.navigationController.dismissSheet()
|
||||
self?.navigationSplitCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
|
||||
navigationController.presentSheet(coordinator) { [weak self] in
|
||||
navigationSplitCoordinator.setSheetCoordinator(coordinator) { [weak self] in
|
||||
self?.stateMachine.processEvent(.dismissedSessionVerificationScreen)
|
||||
}
|
||||
}
|
||||
@@ -213,9 +234,9 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
||||
// MARK: Bug reporting
|
||||
|
||||
private func presentFeedbackScreen(for image: UIImage? = nil) {
|
||||
let feedbackNavigationController = NavigationController()
|
||||
let feedbackNavigationStackCoordinator = NavigationStackCoordinator()
|
||||
|
||||
let userNotificationController = UserNotificationController(rootCoordinator: feedbackNavigationController)
|
||||
let userNotificationController = UserNotificationController(rootCoordinator: feedbackNavigationStackCoordinator)
|
||||
|
||||
let parameters = BugReportCoordinatorParameters(bugReportService: bugReportService,
|
||||
userNotificationController: userNotificationController,
|
||||
@@ -223,12 +244,12 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
||||
isModallyPresented: true)
|
||||
let coordinator = BugReportCoordinator(parameters: parameters)
|
||||
coordinator.completion = { [weak self] _ in
|
||||
self?.navigationController.dismissSheet()
|
||||
self?.navigationSplitCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
|
||||
feedbackNavigationController.setRootCoordinator(coordinator)
|
||||
feedbackNavigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
|
||||
navigationController.presentSheet(userNotificationController) { [weak self] in
|
||||
navigationSplitCoordinator.setSheetCoordinator(userNotificationController) { [weak self] in
|
||||
self?.stateMachine.processEvent(.dismissedFeedbackScreen)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,20 +24,16 @@ class UserSessionFlowCoordinatorStateMachine {
|
||||
case initial
|
||||
|
||||
/// Showing the home screen
|
||||
case homeScreen
|
||||
|
||||
/// Showing a particular room's timeline
|
||||
/// - Parameter roomId: that room's identifier
|
||||
case roomScreen(roomId: String)
|
||||
case roomList(selectedRoomId: String?)
|
||||
|
||||
/// Showing the session verification flows
|
||||
case sessionVerificationScreen(selectedRoomId: String?)
|
||||
|
||||
/// Showing the session verification flows
|
||||
case sessionVerificationScreen
|
||||
|
||||
/// Showing the session verification flows
|
||||
case feedbackScreen
|
||||
case feedbackScreen(selectedRoomId: String?)
|
||||
|
||||
/// Showing the settings screen
|
||||
case settingsScreen
|
||||
case settingsScreen(selectedRoomId: String?)
|
||||
}
|
||||
|
||||
/// Events that can be triggered on the AppCoordinator state machine
|
||||
@@ -47,9 +43,9 @@ class UserSessionFlowCoordinatorStateMachine {
|
||||
|
||||
/// Request presentation for a particular room
|
||||
/// - Parameter roomId:the room identifier
|
||||
case showRoomScreen(roomId: String)
|
||||
case selectRoom(roomId: String)
|
||||
/// The room screen has been dismissed
|
||||
case dismissedRoomScreen
|
||||
case deselectRoom
|
||||
|
||||
/// Request presentation of the settings screen
|
||||
case showSettingsScreen
|
||||
@@ -69,35 +65,39 @@ class UserSessionFlowCoordinatorStateMachine {
|
||||
|
||||
private let stateMachine: StateMachine<State, Event>
|
||||
|
||||
var state: UserSessionFlowCoordinatorStateMachine.State {
|
||||
stateMachine.state
|
||||
}
|
||||
|
||||
init() {
|
||||
stateMachine = StateMachine(state: .initial)
|
||||
configure()
|
||||
}
|
||||
|
||||
private func configure() {
|
||||
stateMachine.addRoutes(event: .start, transitions: [.initial => .homeScreen])
|
||||
stateMachine.addRoutes(event: .start, transitions: [.initial => .roomList(selectedRoomId: nil)])
|
||||
|
||||
stateMachine.addRouteMapping { event, fromState, _ in
|
||||
switch (event, fromState) {
|
||||
case (.showRoomScreen(let roomId), .homeScreen):
|
||||
return .roomScreen(roomId: roomId)
|
||||
case (.dismissedRoomScreen, .roomScreen):
|
||||
return .homeScreen
|
||||
case (.selectRoom(let roomId), .roomList):
|
||||
return .roomList(selectedRoomId: roomId)
|
||||
case (.deselectRoom, .roomList):
|
||||
return .roomList(selectedRoomId: nil)
|
||||
|
||||
case (.showSettingsScreen, .homeScreen):
|
||||
return .settingsScreen
|
||||
case (.dismissedSettingsScreen, .settingsScreen):
|
||||
return .homeScreen
|
||||
case (.showSettingsScreen, .roomList(let selectedRoomId)):
|
||||
return .settingsScreen(selectedRoomId: selectedRoomId)
|
||||
case (.dismissedSettingsScreen, .settingsScreen(let selectedRoomId)):
|
||||
return .roomList(selectedRoomId: selectedRoomId)
|
||||
|
||||
case (.feedbackScreen, .homeScreen):
|
||||
return .feedbackScreen
|
||||
case (.dismissedFeedbackScreen, .feedbackScreen):
|
||||
return .homeScreen
|
||||
case (.feedbackScreen, .roomList(let selectedRoomId)):
|
||||
return .feedbackScreen(selectedRoomId: selectedRoomId)
|
||||
case (.dismissedFeedbackScreen, .feedbackScreen(let selectedRoomId)):
|
||||
return .roomList(selectedRoomId: selectedRoomId)
|
||||
|
||||
case (.showSessionVerificationScreen, .homeScreen):
|
||||
return .sessionVerificationScreen
|
||||
case (.dismissedSessionVerificationScreen, .sessionVerificationScreen):
|
||||
return .homeScreen
|
||||
case (.showSessionVerificationScreen, .roomList(let selectedRoomId)):
|
||||
return .sessionVerificationScreen(selectedRoomId: selectedRoomId)
|
||||
case (.dismissedSessionVerificationScreen, .sessionVerificationScreen(let selectedRoomId)):
|
||||
return .roomList(selectedRoomId: selectedRoomId)
|
||||
|
||||
default:
|
||||
return nil
|
||||
@@ -132,8 +132,8 @@ class UserSessionFlowCoordinatorStateMachine {
|
||||
/// Flag indicating the machine is displaying room screen with given room identifier
|
||||
func isDisplayingRoomScreen(withRoomId roomId: String) -> Bool {
|
||||
switch stateMachine.state {
|
||||
case .roomScreen(let displayedRoomId):
|
||||
return roomId == displayedRoomId
|
||||
case .roomList(let selectedRoomId):
|
||||
return roomId == selectedRoomId
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -19,11 +19,11 @@ import UIKit
|
||||
|
||||
class UITestsAppCoordinator: AppCoordinatorProtocol {
|
||||
private var currentRootCoordinator: CoordinatorProtocol?
|
||||
private let navigationController: NavigationController
|
||||
private let navigationStackCoordinator: NavigationStackCoordinator
|
||||
let notificationManager: NotificationManagerProtocol? = nil
|
||||
|
||||
init() {
|
||||
navigationController = NavigationController()
|
||||
navigationStackCoordinator = NavigationStackCoordinator()
|
||||
|
||||
ServiceLocator.shared.register(userNotificationController: MockUserNotificationController())
|
||||
}
|
||||
@@ -39,32 +39,32 @@ class UITestsAppCoordinator: AppCoordinatorProtocol {
|
||||
// For example when replacing the root in the authentication flows
|
||||
self.currentRootCoordinator = screen.coordinator
|
||||
|
||||
self.navigationController.setRootCoordinator(screen.coordinator)
|
||||
self.navigationStackCoordinator.setRootCoordinator(screen.coordinator)
|
||||
}
|
||||
|
||||
navigationController.setRootCoordinator(rootCoordinator)
|
||||
navigationStackCoordinator.setRootCoordinator(rootCoordinator)
|
||||
|
||||
Bundle.elementFallbackLanguage = "en"
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
navigationController.toPresentable()
|
||||
navigationStackCoordinator.toPresentable()
|
||||
}
|
||||
|
||||
private func mockScreens() -> [MockScreen] {
|
||||
UITestScreenIdentifier.allCases.map { MockScreen(id: $0, navigationController: navigationController) }
|
||||
UITestScreenIdentifier.allCases.map { MockScreen(id: $0, navigationStackCoordinator: navigationStackCoordinator) }
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
class MockScreen: Identifiable {
|
||||
let id: UITestScreenIdentifier
|
||||
let navigationController: NavigationController
|
||||
let navigationStackCoordinator: NavigationStackCoordinator
|
||||
lazy var coordinator: CoordinatorProtocol = {
|
||||
switch id {
|
||||
case .login:
|
||||
return LoginCoordinator(parameters: .init(authenticationService: MockAuthenticationServiceProxy(),
|
||||
navigationController: navigationController))
|
||||
navigationStackCoordinator: navigationStackCoordinator))
|
||||
case .serverSelection:
|
||||
return ServerSelectionCoordinator(parameters: .init(authenticationService: MockAuthenticationServiceProxy(),
|
||||
userNotificationController: MockUserNotificationController(),
|
||||
@@ -78,7 +78,7 @@ class MockScreen: Identifiable {
|
||||
mediaProvider: MockMediaProvider())))
|
||||
case .authenticationFlow:
|
||||
return AuthenticationCoordinator(authenticationService: MockAuthenticationServiceProxy(),
|
||||
navigationController: navigationController)
|
||||
navigationStackCoordinator: navigationStackCoordinator)
|
||||
case .softLogout:
|
||||
let credentials = SoftLogoutCredentials(userId: "@mock:matrix.org",
|
||||
homeserverName: "matrix.org",
|
||||
@@ -97,9 +97,9 @@ class MockScreen: Identifiable {
|
||||
return HomeScreenCoordinator(parameters: .init(userSession: session,
|
||||
attributedStringBuilder: AttributedStringBuilder(),
|
||||
bugReportService: MockBugReportService(),
|
||||
navigationController: navigationController))
|
||||
navigationStackCoordinator: navigationStackCoordinator))
|
||||
case .settings:
|
||||
return SettingsCoordinator(parameters: .init(navigationController: navigationController,
|
||||
return SettingsCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
|
||||
userNotificationController: MockUserNotificationController(),
|
||||
userSession: MockUserSession(clientProxy: MockClientProxy(userIdentifier: "@mock:client.com"),
|
||||
mediaProvider: MockMediaProvider()),
|
||||
@@ -117,7 +117,7 @@ class MockScreen: Identifiable {
|
||||
case .onboarding:
|
||||
return OnboardingCoordinator()
|
||||
case .roomPlainNoAvatar:
|
||||
let parameters = RoomScreenCoordinatorParameters(navigationController: navigationController,
|
||||
let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
||||
timelineController: MockRoomTimelineController(),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
roomName: "Some room name",
|
||||
@@ -125,7 +125,7 @@ class MockScreen: Identifiable {
|
||||
emojiProvider: EmojiProvider())
|
||||
return RoomScreenCoordinator(parameters: parameters)
|
||||
case .roomEncryptedWithAvatar:
|
||||
let parameters = RoomScreenCoordinatorParameters(navigationController: navigationController,
|
||||
let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
||||
timelineController: MockRoomTimelineController(),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
roomName: "Some room name",
|
||||
@@ -138,8 +138,8 @@ class MockScreen: Identifiable {
|
||||
}
|
||||
}()
|
||||
|
||||
init(id: UITestScreenIdentifier, navigationController: NavigationController) {
|
||||
init(id: UITestScreenIdentifier, navigationStackCoordinator: NavigationStackCoordinator) {
|
||||
self.id = id
|
||||
self.navigationController = navigationController
|
||||
self.navigationStackCoordinator = navigationStackCoordinator
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,221 +0,0 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import ElementX
|
||||
|
||||
@MainActor
|
||||
class NavigationControllerTests: XCTestCase {
|
||||
private var navigationController: NavigationController!
|
||||
|
||||
override func setUp() {
|
||||
navigationController = NavigationController()
|
||||
}
|
||||
|
||||
func testRoot() {
|
||||
let rootCoordinator = SomeTestCoordinator()
|
||||
navigationController.setRootCoordinator(rootCoordinator)
|
||||
|
||||
assertCoordinatorsEqual(rootCoordinator, navigationController.rootCoordinator)
|
||||
}
|
||||
|
||||
func testSingleSheet() {
|
||||
let rootCoordinator = SomeTestCoordinator()
|
||||
navigationController.setRootCoordinator(rootCoordinator)
|
||||
|
||||
let coordinator = SomeTestCoordinator()
|
||||
navigationController.presentSheet(coordinator)
|
||||
|
||||
assertCoordinatorsEqual(rootCoordinator, navigationController.rootCoordinator)
|
||||
assertCoordinatorsEqual(coordinator, navigationController.sheetCoordinator)
|
||||
|
||||
navigationController.dismissSheet()
|
||||
|
||||
assertCoordinatorsEqual(rootCoordinator, navigationController.rootCoordinator)
|
||||
XCTAssertNil(navigationController.sheetCoordinator)
|
||||
}
|
||||
|
||||
func testMultipleSheets() {
|
||||
let rootCoordinator = SomeTestCoordinator()
|
||||
navigationController.setRootCoordinator(rootCoordinator)
|
||||
|
||||
let sheetCoordinator = SomeTestCoordinator()
|
||||
navigationController.presentSheet(sheetCoordinator)
|
||||
|
||||
assertCoordinatorsEqual(rootCoordinator, navigationController.rootCoordinator)
|
||||
XCTAssert(navigationController.coordinators.isEmpty)
|
||||
assertCoordinatorsEqual(sheetCoordinator, navigationController.sheetCoordinator)
|
||||
|
||||
let someOtherSheetCoordinator = SomeTestCoordinator()
|
||||
navigationController.presentSheet(someOtherSheetCoordinator)
|
||||
|
||||
assertCoordinatorsEqual(rootCoordinator, navigationController.rootCoordinator)
|
||||
XCTAssert(navigationController.coordinators.isEmpty)
|
||||
assertCoordinatorsEqual(someOtherSheetCoordinator, navigationController.sheetCoordinator)
|
||||
}
|
||||
|
||||
func testSinglePush() {
|
||||
let rootCoordinator = SomeTestCoordinator()
|
||||
navigationController.setRootCoordinator(rootCoordinator)
|
||||
|
||||
let coordinator = SomeTestCoordinator()
|
||||
navigationController.push(coordinator)
|
||||
|
||||
assertCoordinatorsEqual(rootCoordinator, navigationController.rootCoordinator)
|
||||
assertCoordinatorsEqual(coordinator, navigationController.coordinators.first)
|
||||
|
||||
navigationController.pop()
|
||||
|
||||
assertCoordinatorsEqual(rootCoordinator, navigationController.rootCoordinator)
|
||||
XCTAssert(navigationController.coordinators.isEmpty)
|
||||
}
|
||||
|
||||
func testMultiplePushes() {
|
||||
let rootCoordinator = SomeTestCoordinator()
|
||||
navigationController.setRootCoordinator(rootCoordinator)
|
||||
|
||||
var coordinators = [CoordinatorProtocol]()
|
||||
for _ in 0...10 {
|
||||
let coordinator = SomeTestCoordinator()
|
||||
coordinators.append(coordinator)
|
||||
navigationController.push(coordinator)
|
||||
}
|
||||
|
||||
assertCoordinatorsEqual(rootCoordinator, navigationController.rootCoordinator)
|
||||
XCTAssertEqual(navigationController.coordinators.count, coordinators.count)
|
||||
|
||||
for index in coordinators.indices {
|
||||
assertCoordinatorsEqual(coordinators[index], navigationController.coordinators[index])
|
||||
}
|
||||
|
||||
navigationController.popToRoot()
|
||||
|
||||
assertCoordinatorsEqual(rootCoordinator, navigationController.rootCoordinator)
|
||||
XCTAssert(navigationController.coordinators.isEmpty)
|
||||
}
|
||||
|
||||
func testRootReplacementDimissesTheRest() {
|
||||
let rootCoordinator = SomeTestCoordinator()
|
||||
navigationController.setRootCoordinator(rootCoordinator)
|
||||
|
||||
let sheetCoordinator = SomeTestCoordinator()
|
||||
navigationController.presentSheet(sheetCoordinator)
|
||||
|
||||
let pushedCoordinator = SomeTestCoordinator()
|
||||
navigationController.push(pushedCoordinator)
|
||||
|
||||
assertCoordinatorsEqual(rootCoordinator, navigationController.rootCoordinator)
|
||||
assertCoordinatorsEqual(pushedCoordinator, navigationController.coordinators.first)
|
||||
assertCoordinatorsEqual(sheetCoordinator, navigationController.sheetCoordinator)
|
||||
|
||||
let newRootCoordinator = SomeTestCoordinator()
|
||||
navigationController.setRootCoordinator(newRootCoordinator)
|
||||
|
||||
assertCoordinatorsEqual(newRootCoordinator, navigationController.rootCoordinator)
|
||||
XCTAssert(navigationController.coordinators.isEmpty)
|
||||
XCTAssertNil(navigationController.sheetCoordinator)
|
||||
}
|
||||
|
||||
func testPushesDontReplaceSheet() {
|
||||
let sheetCoordinator = SomeTestCoordinator()
|
||||
navigationController.presentSheet(sheetCoordinator)
|
||||
|
||||
let pushedCoordinator = SomeTestCoordinator()
|
||||
navigationController.push(pushedCoordinator)
|
||||
|
||||
assertCoordinatorsEqual(pushedCoordinator, navigationController.coordinators.first)
|
||||
assertCoordinatorsEqual(sheetCoordinator, navigationController.sheetCoordinator)
|
||||
|
||||
let newlyPushedCoordinator = SomeTestCoordinator()
|
||||
navigationController.push(newlyPushedCoordinator)
|
||||
|
||||
assertCoordinatorsEqual(pushedCoordinator, navigationController.coordinators.first)
|
||||
assertCoordinatorsEqual(newlyPushedCoordinator, navigationController.coordinators.last)
|
||||
assertCoordinatorsEqual(sheetCoordinator, navigationController.sheetCoordinator)
|
||||
}
|
||||
|
||||
func testPopDismissalCallbacks() {
|
||||
let pushedCoordinator = SomeTestCoordinator()
|
||||
|
||||
let expectation = expectation(description: "Wait for callback")
|
||||
navigationController.push(pushedCoordinator) {
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
navigationController.pop()
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
func testPopToRootDismissalCallbacks() {
|
||||
navigationController.push(SomeTestCoordinator())
|
||||
navigationController.push(SomeTestCoordinator())
|
||||
|
||||
let coordinator = SomeTestCoordinator()
|
||||
let expectation = expectation(description: "Wait for callback")
|
||||
navigationController.push(coordinator) {
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
navigationController.popToRoot()
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
func testSheetDismissalCallbac() {
|
||||
let coordinator = SomeTestCoordinator()
|
||||
let expectation = expectation(description: "Wait for callback")
|
||||
navigationController.presentSheet(coordinator) {
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
navigationController.dismissSheet()
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
func testRootReplacmeentCallbacks() {
|
||||
navigationController.setRootCoordinator(SomeTestCoordinator())
|
||||
|
||||
let popExpectation = expectation(description: "Waiting for callback")
|
||||
navigationController.push(SomeTestCoordinator()) {
|
||||
popExpectation.fulfill()
|
||||
}
|
||||
|
||||
let sheetExpectation = expectation(description: "Waiting for callback")
|
||||
navigationController.presentSheet(SomeTestCoordinator()) {
|
||||
sheetExpectation.fulfill()
|
||||
}
|
||||
|
||||
navigationController.setRootCoordinator(SomeTestCoordinator())
|
||||
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func assertCoordinatorsEqual(_ lhs: CoordinatorProtocol?, _ rhs: CoordinatorProtocol?) {
|
||||
guard let lhs = lhs as? SomeTestCoordinator,
|
||||
let rhs = rhs as? SomeTestCoordinator else {
|
||||
XCTFail("Coordinators are not the same")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(lhs.id, rhs.id)
|
||||
}
|
||||
}
|
||||
|
||||
private struct SomeTestCoordinator: CoordinatorProtocol {
|
||||
let id = UUID()
|
||||
}
|
||||
72
UnitTests/Sources/NavigationRootCoordinatorTests.swift
Normal file
72
UnitTests/Sources/NavigationRootCoordinatorTests.swift
Normal file
@@ -0,0 +1,72 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import ElementX
|
||||
|
||||
@MainActor
|
||||
class NavigationRootCoordinatorTests: XCTestCase {
|
||||
private var navigationRootCoordinator: NavigationRootCoordinator!
|
||||
|
||||
override func setUp() {
|
||||
navigationRootCoordinator = NavigationRootCoordinator()
|
||||
}
|
||||
|
||||
func testRootChanges() {
|
||||
XCTAssertNil(navigationRootCoordinator.rootCoordinator)
|
||||
|
||||
let firstRootCoordinator = SomeTestCoordinator()
|
||||
navigationRootCoordinator.setRootCoordinator(firstRootCoordinator)
|
||||
|
||||
assertCoordinatorsEqual(firstRootCoordinator, navigationRootCoordinator.rootCoordinator)
|
||||
|
||||
let secondRootCoordinator = SomeTestCoordinator()
|
||||
navigationRootCoordinator.setRootCoordinator(secondRootCoordinator)
|
||||
|
||||
assertCoordinatorsEqual(secondRootCoordinator, navigationRootCoordinator.rootCoordinator)
|
||||
}
|
||||
|
||||
func testReplacementDismissalCallbacks() {
|
||||
XCTAssertNil(navigationRootCoordinator.rootCoordinator)
|
||||
|
||||
let rootCoordinator = SomeTestCoordinator()
|
||||
|
||||
let expectation = expectation(description: "Wait for callback")
|
||||
navigationRootCoordinator.setRootCoordinator(rootCoordinator) {
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
navigationRootCoordinator.setRootCoordinator(nil)
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func assertCoordinatorsEqual(_ lhs: CoordinatorProtocol?, _ rhs: CoordinatorProtocol?) {
|
||||
guard let lhs = lhs as? SomeTestCoordinator,
|
||||
let rhs = rhs as? SomeTestCoordinator else {
|
||||
XCTFail("Coordinators are not the same")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(lhs.id, rhs.id)
|
||||
}
|
||||
}
|
||||
|
||||
private struct SomeTestCoordinator: CoordinatorProtocol {
|
||||
let id = UUID()
|
||||
}
|
||||
274
UnitTests/Sources/NavigationSplitCoordinator.swift
Normal file
274
UnitTests/Sources/NavigationSplitCoordinator.swift
Normal file
@@ -0,0 +1,274 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import ElementX
|
||||
|
||||
@MainActor
|
||||
class NavigationSplitCoordinatorTests: XCTestCase {
|
||||
private var navigationSplitCoordinator: NavigationSplitCoordinator!
|
||||
|
||||
override func setUp() {
|
||||
navigationSplitCoordinator = NavigationSplitCoordinator(placeholderCoordinator: SomeTestCoordinator())
|
||||
}
|
||||
|
||||
func testSidebar() {
|
||||
XCTAssertNil(navigationSplitCoordinator.sidebarCoordinator)
|
||||
XCTAssertNil(navigationSplitCoordinator.detailCoordinator)
|
||||
|
||||
let sidebarCoordinator = SomeTestCoordinator()
|
||||
navigationSplitCoordinator.setSidebarCoordinator(sidebarCoordinator)
|
||||
assertCoordinatorsEqual(sidebarCoordinator, navigationSplitCoordinator.sidebarCoordinator)
|
||||
}
|
||||
|
||||
func testDetail() {
|
||||
XCTAssertNil(navigationSplitCoordinator.sidebarCoordinator)
|
||||
XCTAssertNil(navigationSplitCoordinator.detailCoordinator)
|
||||
|
||||
let detailCoordinator = SomeTestCoordinator()
|
||||
navigationSplitCoordinator.setDetailCoordinator(detailCoordinator)
|
||||
assertCoordinatorsEqual(detailCoordinator, navigationSplitCoordinator.detailCoordinator)
|
||||
}
|
||||
|
||||
func testSidebarAndDetail() {
|
||||
XCTAssertNil(navigationSplitCoordinator.sidebarCoordinator)
|
||||
XCTAssertNil(navigationSplitCoordinator.detailCoordinator)
|
||||
|
||||
let sidebarCoordinator = SomeTestCoordinator()
|
||||
navigationSplitCoordinator.setSidebarCoordinator(sidebarCoordinator)
|
||||
|
||||
let detailCoordinator = SomeTestCoordinator()
|
||||
navigationSplitCoordinator.setDetailCoordinator(detailCoordinator)
|
||||
|
||||
assertCoordinatorsEqual(sidebarCoordinator, navigationSplitCoordinator.sidebarCoordinator)
|
||||
assertCoordinatorsEqual(detailCoordinator, navigationSplitCoordinator.detailCoordinator)
|
||||
}
|
||||
|
||||
func testSingleSheet() {
|
||||
let sidebarCoordinator = SomeTestCoordinator()
|
||||
navigationSplitCoordinator.setSidebarCoordinator(sidebarCoordinator)
|
||||
let detailCoordinator = SomeTestCoordinator()
|
||||
navigationSplitCoordinator.setDetailCoordinator(detailCoordinator)
|
||||
|
||||
let sheetCoordinator = SomeTestCoordinator()
|
||||
navigationSplitCoordinator.setSheetCoordinator(sheetCoordinator)
|
||||
|
||||
assertCoordinatorsEqual(sidebarCoordinator, navigationSplitCoordinator.sidebarCoordinator)
|
||||
assertCoordinatorsEqual(detailCoordinator, navigationSplitCoordinator.detailCoordinator)
|
||||
assertCoordinatorsEqual(sheetCoordinator, navigationSplitCoordinator.sheetCoordinator)
|
||||
|
||||
navigationSplitCoordinator.setSheetCoordinator(nil)
|
||||
|
||||
assertCoordinatorsEqual(sidebarCoordinator, navigationSplitCoordinator.sidebarCoordinator)
|
||||
assertCoordinatorsEqual(detailCoordinator, navigationSplitCoordinator.detailCoordinator)
|
||||
XCTAssertNil(navigationSplitCoordinator.sheetCoordinator)
|
||||
}
|
||||
|
||||
func testMultipleSheets() {
|
||||
let sidebarCoordinator = SomeTestCoordinator()
|
||||
navigationSplitCoordinator.setSidebarCoordinator(sidebarCoordinator)
|
||||
let detailCoordinator = SomeTestCoordinator()
|
||||
navigationSplitCoordinator.setDetailCoordinator(detailCoordinator)
|
||||
|
||||
let sheetCoordinator = SomeTestCoordinator()
|
||||
navigationSplitCoordinator.setSheetCoordinator(sheetCoordinator)
|
||||
|
||||
assertCoordinatorsEqual(sidebarCoordinator, navigationSplitCoordinator.sidebarCoordinator)
|
||||
assertCoordinatorsEqual(detailCoordinator, navigationSplitCoordinator.detailCoordinator)
|
||||
assertCoordinatorsEqual(sheetCoordinator, navigationSplitCoordinator.sheetCoordinator)
|
||||
|
||||
let someOtherSheetCoordinator = SomeTestCoordinator()
|
||||
navigationSplitCoordinator.setSheetCoordinator(someOtherSheetCoordinator)
|
||||
|
||||
assertCoordinatorsEqual(sidebarCoordinator, navigationSplitCoordinator.sidebarCoordinator)
|
||||
assertCoordinatorsEqual(detailCoordinator, navigationSplitCoordinator.detailCoordinator)
|
||||
assertCoordinatorsEqual(someOtherSheetCoordinator, navigationSplitCoordinator.sheetCoordinator)
|
||||
}
|
||||
|
||||
func testSidebarReplacementCallbacks() {
|
||||
let sidebarCoordinator = SomeTestCoordinator()
|
||||
|
||||
let expectation = expectation(description: "Wait for callback")
|
||||
navigationSplitCoordinator.setSidebarCoordinator(sidebarCoordinator) {
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
navigationSplitCoordinator.setSidebarCoordinator(nil)
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
func testDetailReplacementCallbacks() {
|
||||
let detailCoordinator = SomeTestCoordinator()
|
||||
|
||||
let expectation = expectation(description: "Wait for callback")
|
||||
navigationSplitCoordinator.setDetailCoordinator(detailCoordinator) {
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
navigationSplitCoordinator.setDetailCoordinator(nil)
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
func testSheetDismissalCallback() {
|
||||
let sheetCoordinator = SomeTestCoordinator()
|
||||
|
||||
let expectation = expectation(description: "Wait for callback")
|
||||
navigationSplitCoordinator.setSheetCoordinator(sheetCoordinator) {
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
navigationSplitCoordinator.setSheetCoordinator(nil)
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
func testEmbeddedStackPresentsSheetThroughSplit() {
|
||||
let sidebarNavigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator)
|
||||
sidebarNavigationStackCoordinator.setRootCoordinator(SomeTestCoordinator())
|
||||
|
||||
let sheetCoordinator = SomeTestCoordinator()
|
||||
sidebarNavigationStackCoordinator.setSheetCoordinator(sheetCoordinator)
|
||||
|
||||
navigationSplitCoordinator.setSidebarCoordinator(sidebarNavigationStackCoordinator)
|
||||
|
||||
assertCoordinatorsEqual(sheetCoordinator, sidebarNavigationStackCoordinator.sheetCoordinator)
|
||||
assertCoordinatorsEqual(sheetCoordinator, navigationSplitCoordinator.sheetCoordinator)
|
||||
}
|
||||
|
||||
func testSplitTracksEmbeddedStackRootChanges() {
|
||||
let sidebarNavigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator)
|
||||
sidebarNavigationStackCoordinator.setRootCoordinator(SomeTestCoordinator())
|
||||
|
||||
navigationSplitCoordinator.setSidebarCoordinator(sidebarNavigationStackCoordinator)
|
||||
|
||||
assertCoordinatorsEqual(sidebarNavigationStackCoordinator.rootCoordinator, navigationSplitCoordinator.compactLayoutRootCoordinator)
|
||||
|
||||
sidebarNavigationStackCoordinator.setRootCoordinator(SomeTestCoordinator())
|
||||
|
||||
let expectation = expectation(description: "Coordinators should match")
|
||||
DispatchQueue.main.async {
|
||||
self.assertCoordinatorsEqual(sidebarNavigationStackCoordinator.rootCoordinator, self.navigationSplitCoordinator.compactLayoutRootCoordinator)
|
||||
expectation.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
func testSplitTracksEmbeddedStackChanges() {
|
||||
let sidebarNavigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator)
|
||||
sidebarNavigationStackCoordinator.setRootCoordinator(SomeTestCoordinator())
|
||||
|
||||
navigationSplitCoordinator.setSidebarCoordinator(sidebarNavigationStackCoordinator)
|
||||
|
||||
assertCoordinatorsEqual(sidebarNavigationStackCoordinator.rootCoordinator, navigationSplitCoordinator.compactLayoutRootCoordinator)
|
||||
|
||||
sidebarNavigationStackCoordinator.push(SomeTestCoordinator())
|
||||
|
||||
let expectation = expectation(description: "Coordinators should match")
|
||||
DispatchQueue.main.async {
|
||||
XCTAssertEqual(sidebarNavigationStackCoordinator.stackCoordinators.count, self.navigationSplitCoordinator.compactLayoutStackCoordinators.count)
|
||||
for index in sidebarNavigationStackCoordinator.stackCoordinators.indices {
|
||||
self.assertCoordinatorsEqual(sidebarNavigationStackCoordinator.stackCoordinators[index], self.navigationSplitCoordinator.compactLayoutStackCoordinators[index])
|
||||
}
|
||||
expectation.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
func testSplitPropagatesCompactStackChanges() {
|
||||
let sidebarNavigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator)
|
||||
sidebarNavigationStackCoordinator.setRootCoordinator(SomeTestCoordinator())
|
||||
sidebarNavigationStackCoordinator.push(SomeTestCoordinator())
|
||||
|
||||
navigationSplitCoordinator.setSidebarCoordinator(sidebarNavigationStackCoordinator)
|
||||
|
||||
assertCoordinatorsEqual(sidebarNavigationStackCoordinator.rootCoordinator, navigationSplitCoordinator.compactLayoutRootCoordinator)
|
||||
XCTAssertEqual(sidebarNavigationStackCoordinator.stackCoordinators.count, navigationSplitCoordinator.compactLayoutStackCoordinators.count)
|
||||
|
||||
navigationSplitCoordinator.compactLayoutStackModules.removeAll()
|
||||
|
||||
XCTAssertTrue(sidebarNavigationStackCoordinator.stackCoordinators.isEmpty)
|
||||
}
|
||||
|
||||
func testCompactStackCreation() {
|
||||
let sidebarCoordinator = NavigationStackCoordinator()
|
||||
sidebarCoordinator.setRootCoordinator(SomeTestCoordinator())
|
||||
sidebarCoordinator.push(SomeTestCoordinator())
|
||||
|
||||
let detailCoordinator = NavigationStackCoordinator()
|
||||
detailCoordinator.setRootCoordinator(SomeTestCoordinator())
|
||||
detailCoordinator.push(SomeTestCoordinator())
|
||||
detailCoordinator.push(SomeTestCoordinator())
|
||||
|
||||
navigationSplitCoordinator.setSidebarCoordinator(sidebarCoordinator)
|
||||
navigationSplitCoordinator.setDetailCoordinator(detailCoordinator)
|
||||
|
||||
let expectation = expectation(description: "Coordinators should match")
|
||||
DispatchQueue.main.async {
|
||||
self.assertCoordinatorsEqual(self.navigationSplitCoordinator.compactLayoutRootCoordinator, sidebarCoordinator.rootCoordinator)
|
||||
self.assertCoordinatorsEqual(self.navigationSplitCoordinator.compactLayoutStackModules[0].coordinator, sidebarCoordinator.stackCoordinators.first)
|
||||
self.assertCoordinatorsEqual(self.navigationSplitCoordinator.compactLayoutStackModules[1].coordinator, detailCoordinator.rootCoordinator)
|
||||
self.assertCoordinatorsEqual(self.navigationSplitCoordinator.compactLayoutStackModules[2].coordinator, detailCoordinator.stackCoordinators.first)
|
||||
self.assertCoordinatorsEqual(self.navigationSplitCoordinator.compactLayoutStackModules[3].coordinator, detailCoordinator.stackCoordinators.last)
|
||||
expectation.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
func testRemovesDetailRootFromCompactStack() {
|
||||
let sidebarCoordinator = NavigationStackCoordinator()
|
||||
sidebarCoordinator.setRootCoordinator(SomeTestCoordinator())
|
||||
|
||||
let detailCoordinator = NavigationStackCoordinator()
|
||||
detailCoordinator.setRootCoordinator(SomeTestCoordinator())
|
||||
detailCoordinator.push(SomeTestCoordinator())
|
||||
|
||||
navigationSplitCoordinator.setSidebarCoordinator(sidebarCoordinator)
|
||||
navigationSplitCoordinator.setDetailCoordinator(detailCoordinator)
|
||||
|
||||
let expectation = expectation(description: "Coordinators should match")
|
||||
DispatchQueue.main.async {
|
||||
self.assertCoordinatorsEqual(self.navigationSplitCoordinator.compactLayoutRootCoordinator, sidebarCoordinator.rootCoordinator)
|
||||
self.assertCoordinatorsEqual(self.navigationSplitCoordinator.compactLayoutStackModules[0].coordinator, detailCoordinator.rootCoordinator)
|
||||
self.assertCoordinatorsEqual(self.navigationSplitCoordinator.compactLayoutStackModules[1].coordinator, detailCoordinator.stackCoordinators.first)
|
||||
|
||||
detailCoordinator.setRootCoordinator(nil)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.assertCoordinatorsEqual(self.navigationSplitCoordinator.compactLayoutRootCoordinator, sidebarCoordinator.rootCoordinator)
|
||||
self.assertCoordinatorsEqual(self.navigationSplitCoordinator.compactLayoutStackModules[0].coordinator, detailCoordinator.stackCoordinators.first)
|
||||
}
|
||||
|
||||
expectation.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func assertCoordinatorsEqual(_ lhs: CoordinatorProtocol?, _ rhs: CoordinatorProtocol?) {
|
||||
guard let lhs = lhs as? SomeTestCoordinator,
|
||||
let rhs = rhs as? SomeTestCoordinator else {
|
||||
XCTFail("Coordinators are not the same")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(lhs.id, rhs.id)
|
||||
}
|
||||
}
|
||||
|
||||
private struct SomeTestCoordinator: CoordinatorProtocol {
|
||||
let id = UUID()
|
||||
}
|
||||
217
UnitTests/Sources/NavigationStackCoordinatorTests.swift
Normal file
217
UnitTests/Sources/NavigationStackCoordinatorTests.swift
Normal file
@@ -0,0 +1,217 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import ElementX
|
||||
|
||||
@MainActor
|
||||
class NavigationStackCoordinatorTests: XCTestCase {
|
||||
private var navigationStackCoordinator: NavigationStackCoordinator!
|
||||
|
||||
override func setUp() {
|
||||
navigationStackCoordinator = NavigationStackCoordinator()
|
||||
}
|
||||
|
||||
func testRoot() {
|
||||
XCTAssertNil(navigationStackCoordinator.rootCoordinator)
|
||||
|
||||
let rootCoordinator = SomeTestCoordinator()
|
||||
navigationStackCoordinator.setRootCoordinator(rootCoordinator)
|
||||
|
||||
assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator)
|
||||
}
|
||||
|
||||
func testSingleSheet() {
|
||||
let rootCoordinator = SomeTestCoordinator()
|
||||
navigationStackCoordinator.setRootCoordinator(rootCoordinator)
|
||||
|
||||
let coordinator = SomeTestCoordinator()
|
||||
navigationStackCoordinator.setSheetCoordinator(coordinator)
|
||||
|
||||
assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator)
|
||||
assertCoordinatorsEqual(coordinator, navigationStackCoordinator.sheetCoordinator)
|
||||
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
|
||||
assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator)
|
||||
XCTAssertNil(navigationStackCoordinator.sheetCoordinator)
|
||||
}
|
||||
|
||||
func testMultipleSheets() {
|
||||
let rootCoordinator = SomeTestCoordinator()
|
||||
navigationStackCoordinator.setRootCoordinator(rootCoordinator)
|
||||
|
||||
let sheetCoordinator = SomeTestCoordinator()
|
||||
navigationStackCoordinator.setSheetCoordinator(sheetCoordinator)
|
||||
|
||||
assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators.isEmpty)
|
||||
assertCoordinatorsEqual(sheetCoordinator, navigationStackCoordinator.sheetCoordinator)
|
||||
|
||||
let someOtherSheetCoordinator = SomeTestCoordinator()
|
||||
navigationStackCoordinator.setSheetCoordinator(someOtherSheetCoordinator)
|
||||
|
||||
assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators.isEmpty)
|
||||
assertCoordinatorsEqual(someOtherSheetCoordinator, navigationStackCoordinator.sheetCoordinator)
|
||||
}
|
||||
|
||||
func testSinglePush() {
|
||||
let rootCoordinator = SomeTestCoordinator()
|
||||
navigationStackCoordinator.setRootCoordinator(rootCoordinator)
|
||||
|
||||
let coordinator = SomeTestCoordinator()
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
|
||||
assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator)
|
||||
assertCoordinatorsEqual(coordinator, navigationStackCoordinator.stackCoordinators.first)
|
||||
|
||||
navigationStackCoordinator.pop()
|
||||
|
||||
assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators.isEmpty)
|
||||
}
|
||||
|
||||
func testMultiplePushes() {
|
||||
let rootCoordinator = SomeTestCoordinator()
|
||||
navigationStackCoordinator.setRootCoordinator(rootCoordinator)
|
||||
|
||||
var coordinators = [CoordinatorProtocol]()
|
||||
for _ in 0...10 {
|
||||
let coordinator = SomeTestCoordinator()
|
||||
coordinators.append(coordinator)
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
|
||||
assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, coordinators.count)
|
||||
|
||||
for index in coordinators.indices {
|
||||
assertCoordinatorsEqual(coordinators[index], navigationStackCoordinator.stackCoordinators[index])
|
||||
}
|
||||
|
||||
navigationStackCoordinator.popToRoot()
|
||||
|
||||
assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators.isEmpty)
|
||||
}
|
||||
|
||||
func testRootReplacementDimissesTheRest() {
|
||||
let rootCoordinator = SomeTestCoordinator()
|
||||
navigationStackCoordinator.setRootCoordinator(rootCoordinator)
|
||||
|
||||
let sheetCoordinator = SomeTestCoordinator()
|
||||
navigationStackCoordinator.setSheetCoordinator(sheetCoordinator)
|
||||
|
||||
let pushedCoordinator = SomeTestCoordinator()
|
||||
navigationStackCoordinator.push(pushedCoordinator)
|
||||
|
||||
assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator)
|
||||
assertCoordinatorsEqual(pushedCoordinator, navigationStackCoordinator.stackCoordinators.first)
|
||||
assertCoordinatorsEqual(sheetCoordinator, navigationStackCoordinator.sheetCoordinator)
|
||||
|
||||
let newRootCoordinator = SomeTestCoordinator()
|
||||
navigationStackCoordinator.setRootCoordinator(newRootCoordinator)
|
||||
|
||||
assertCoordinatorsEqual(newRootCoordinator, navigationStackCoordinator.rootCoordinator)
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators.isEmpty)
|
||||
}
|
||||
|
||||
func testPushesDontReplaceSheet() {
|
||||
let sheetCoordinator = SomeTestCoordinator()
|
||||
navigationStackCoordinator.setSheetCoordinator(sheetCoordinator)
|
||||
|
||||
let pushedCoordinator = SomeTestCoordinator()
|
||||
navigationStackCoordinator.push(pushedCoordinator)
|
||||
|
||||
assertCoordinatorsEqual(pushedCoordinator, navigationStackCoordinator.stackCoordinators.first)
|
||||
assertCoordinatorsEqual(sheetCoordinator, navigationStackCoordinator.sheetCoordinator)
|
||||
|
||||
let newlyPushedCoordinator = SomeTestCoordinator()
|
||||
navigationStackCoordinator.push(newlyPushedCoordinator)
|
||||
|
||||
assertCoordinatorsEqual(pushedCoordinator, navigationStackCoordinator.stackCoordinators.first)
|
||||
assertCoordinatorsEqual(newlyPushedCoordinator, navigationStackCoordinator.stackCoordinators.last)
|
||||
assertCoordinatorsEqual(sheetCoordinator, navigationStackCoordinator.sheetCoordinator)
|
||||
}
|
||||
|
||||
func testPopDismissalCallbacks() {
|
||||
let pushedCoordinator = SomeTestCoordinator()
|
||||
|
||||
let expectation = expectation(description: "Wait for callback")
|
||||
navigationStackCoordinator.push(pushedCoordinator) {
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
navigationStackCoordinator.pop()
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
func testPopToRootDismissalCallbacks() {
|
||||
navigationStackCoordinator.push(SomeTestCoordinator())
|
||||
navigationStackCoordinator.push(SomeTestCoordinator())
|
||||
|
||||
let coordinator = SomeTestCoordinator()
|
||||
let expectation = expectation(description: "Wait for callback")
|
||||
navigationStackCoordinator.push(coordinator) {
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
navigationStackCoordinator.popToRoot()
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
func testSheetDismissalCallback() {
|
||||
let coordinator = SomeTestCoordinator()
|
||||
let expectation = expectation(description: "Wait for callback")
|
||||
navigationStackCoordinator.setSheetCoordinator(coordinator) {
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
func testRootReplacementCallbacks() {
|
||||
navigationStackCoordinator.setRootCoordinator(SomeTestCoordinator())
|
||||
|
||||
let popExpectation = expectation(description: "Waiting for callback")
|
||||
navigationStackCoordinator.push(SomeTestCoordinator()) {
|
||||
popExpectation.fulfill()
|
||||
}
|
||||
|
||||
navigationStackCoordinator.setRootCoordinator(SomeTestCoordinator())
|
||||
|
||||
waitForExpectations(timeout: 1.0)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func assertCoordinatorsEqual(_ lhs: CoordinatorProtocol?, _ rhs: CoordinatorProtocol?) {
|
||||
guard let lhs = lhs as? SomeTestCoordinator,
|
||||
let rhs = rhs as? SomeTestCoordinator else {
|
||||
XCTFail("Coordinators are not the same")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(lhs.id, rhs.id)
|
||||
}
|
||||
}
|
||||
|
||||
private struct SomeTestCoordinator: CoordinatorProtocol {
|
||||
let id = UUID()
|
||||
}
|
||||
1
changelog.d/317.feature
Normal file
1
changelog.d/317.feature
Normal file
@@ -0,0 +1 @@
|
||||
Implement a split screen layout for when running on iPad and MacOS
|
||||
Reference in New Issue
Block a user