From a3fcc0f6129d57a2401f3eb639f614df390e4291 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 14 Feb 2022 18:05:21 +0200 Subject: [PATCH] Initial project setup. --- .gitignore | 22 + .swiftlint.yml | 50 + ElementX.xcodeproj/project.pbxproj | 995 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/swiftpm/Package.resolved | 34 + ElementX/Sources/AppCoordinator.swift | 78 ++ ElementX/Sources/AppDelegate.swift | 20 + .../AuthenticationCoordinator.swift | 123 +++ .../Authentication/KeychainController.swift | 57 + .../KeychainControllerProtocol.swift | 15 + .../LoginScreen/LoginScreenCoordinator.swift | 66 ++ .../LoginScreen/LoginScreenModels.swift | 44 + .../LoginScreen/LoginScreenViewModel.swift | 49 + .../LoginScreenViewModelProtocol.swift | 24 + .../Test/UI/LoginScreenUITests.swift | 45 + .../Test/Unit/LoginScreenViewModelTests.swift | 31 + .../LoginScreen/View/LoginScreen.swift | 52 + .../Modules/Authentication/UserSession.swift | 44 + .../HomeScreen/HomeScreenCoordinator.swift | 64 ++ .../Modules/HomeScreen/HomeScreenModels.swift | 31 + .../HomeScreen/HomeScreenViewModel.swift | 45 + .../HomeScreenViewModelProtocol.swift | 24 + .../Test/UI/HomeScreenUITests.swift | 45 + .../Test/Unit/HomeScreenViewModelTests.swift | 30 + .../Modules/HomeScreen/View/HomeScreen.swift | 41 + .../Sources/Modules/Models/RoomModel.swift | 13 + .../Sources/Modules/Other/Coordinator.swift | 51 + ElementX/Sources/Modules/Other/MXLog.swift | 179 ++++ .../SwiftUI/ViewModel/BindableState.swift | 37 + .../ViewModel/StateStoreViewModel.swift | 140 +++ .../Other/WeakDictionary/WeakDictionary.swift | 104 ++ .../WeakDictionaryKeyReference.swift | 36 + .../WeakDictionaryReference.swift | 21 + .../WeakDictionary/WeakKeyDictionary.swift | 124 +++ .../Modules/Routers/NavigationModule.swift | 36 + .../Modules/Routers/NavigationRouter.swift | 405 +++++++ .../Routers/NavigationRouterStore.swift | 96 ++ .../NavigationRouterStoreProtocol.swift | 25 + .../Routers/NavigationRouterType.swift | 135 +++ .../Sources/Modules/Routers/Presentable.swift | 38 + .../Sources/Modules/Routers/RootRouter.swift | 87 ++ .../Modules/Routers/RootRouterType.swift | 49 + .../Modules/Splash/SplashViewController.swift | 12 + .../Modules/Splash/SplashViewController.xib | 45 + .../TemplateSimpleScreenCoordinator.swift | 64 ++ .../MockTemplateSimpleScreenScreenState.swift | 57 + .../TemplateSimpleScreenModels.swift | 67 ++ .../TemplateSimpleScreenViewModel.swift | 54 + ...emplateSimpleScreenViewModelProtocol.swift | 24 + .../Test/UI/TemplateSimpleScreenUITests.swift | 45 + .../TemplateSimpleScreenViewModelTests.swift | 49 + .../View/TemplateSimpleScreen.swift | 113 ++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 98 ++ .../Assets.xcassets/Contents.json | 6 + .../app-logo.imageset/Contents.json | 26 + .../app-logo.imageset/launch_screen_logo.png | Bin 0 -> 6163 bytes .../launch_screen_logo@2x.png | Bin 0 -> 11812 bytes .../launch_screen_logo@3x.png | Bin 0 -> 18100 bytes .../Base.lproj/LaunchScreen.storyboard | 32 + .../Supporting Files/ElementX.entitlements | 10 + ElementXTests/ElementXTests.swift | 36 + ElementXUITests/ElementXUITests.swift | 42 + .../ElementXUITestsLaunchTests.swift | 32 + Scripts/createSwiftUISimpleScreen.sh | 31 + 66 files changed, 4474 insertions(+) create mode 100644 .gitignore create mode 100755 .swiftlint.yml create mode 100644 ElementX.xcodeproj/project.pbxproj create mode 100644 ElementX.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 ElementX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 ElementX/Sources/AppCoordinator.swift create mode 100644 ElementX/Sources/AppDelegate.swift create mode 100644 ElementX/Sources/Modules/Authentication/AuthenticationCoordinator.swift create mode 100644 ElementX/Sources/Modules/Authentication/KeychainController.swift create mode 100644 ElementX/Sources/Modules/Authentication/KeychainControllerProtocol.swift create mode 100644 ElementX/Sources/Modules/Authentication/LoginScreen/LoginScreenCoordinator.swift create mode 100644 ElementX/Sources/Modules/Authentication/LoginScreen/LoginScreenModels.swift create mode 100644 ElementX/Sources/Modules/Authentication/LoginScreen/LoginScreenViewModel.swift create mode 100644 ElementX/Sources/Modules/Authentication/LoginScreen/LoginScreenViewModelProtocol.swift create mode 100644 ElementX/Sources/Modules/Authentication/LoginScreen/Test/UI/LoginScreenUITests.swift create mode 100644 ElementX/Sources/Modules/Authentication/LoginScreen/Test/Unit/LoginScreenViewModelTests.swift create mode 100644 ElementX/Sources/Modules/Authentication/LoginScreen/View/LoginScreen.swift create mode 100644 ElementX/Sources/Modules/Authentication/UserSession.swift create mode 100644 ElementX/Sources/Modules/HomeScreen/HomeScreenCoordinator.swift create mode 100644 ElementX/Sources/Modules/HomeScreen/HomeScreenModels.swift create mode 100644 ElementX/Sources/Modules/HomeScreen/HomeScreenViewModel.swift create mode 100644 ElementX/Sources/Modules/HomeScreen/HomeScreenViewModelProtocol.swift create mode 100644 ElementX/Sources/Modules/HomeScreen/Test/UI/HomeScreenUITests.swift create mode 100644 ElementX/Sources/Modules/HomeScreen/Test/Unit/HomeScreenViewModelTests.swift create mode 100644 ElementX/Sources/Modules/HomeScreen/View/HomeScreen.swift create mode 100644 ElementX/Sources/Modules/Models/RoomModel.swift create mode 100755 ElementX/Sources/Modules/Other/Coordinator.swift create mode 100644 ElementX/Sources/Modules/Other/MXLog.swift create mode 100644 ElementX/Sources/Modules/Other/SwiftUI/ViewModel/BindableState.swift create mode 100644 ElementX/Sources/Modules/Other/SwiftUI/ViewModel/StateStoreViewModel.swift create mode 100644 ElementX/Sources/Modules/Other/WeakDictionary/WeakDictionary.swift create mode 100644 ElementX/Sources/Modules/Other/WeakDictionary/WeakDictionaryKeyReference.swift create mode 100644 ElementX/Sources/Modules/Other/WeakDictionary/WeakDictionaryReference.swift create mode 100644 ElementX/Sources/Modules/Other/WeakDictionary/WeakKeyDictionary.swift create mode 100644 ElementX/Sources/Modules/Routers/NavigationModule.swift create mode 100755 ElementX/Sources/Modules/Routers/NavigationRouter.swift create mode 100644 ElementX/Sources/Modules/Routers/NavigationRouterStore.swift create mode 100644 ElementX/Sources/Modules/Routers/NavigationRouterStoreProtocol.swift create mode 100755 ElementX/Sources/Modules/Routers/NavigationRouterType.swift create mode 100755 ElementX/Sources/Modules/Routers/Presentable.swift create mode 100755 ElementX/Sources/Modules/Routers/RootRouter.swift create mode 100755 ElementX/Sources/Modules/Routers/RootRouterType.swift create mode 100644 ElementX/Sources/Modules/Splash/SplashViewController.swift create mode 100644 ElementX/Sources/Modules/Splash/SplashViewController.xib create mode 100644 ElementX/Sources/Modules/Templates/SimpleScreenExample/Coordinator/TemplateSimpleScreenCoordinator.swift create mode 100644 ElementX/Sources/Modules/Templates/SimpleScreenExample/MockTemplateSimpleScreenScreenState.swift create mode 100644 ElementX/Sources/Modules/Templates/SimpleScreenExample/TemplateSimpleScreenModels.swift create mode 100644 ElementX/Sources/Modules/Templates/SimpleScreenExample/TemplateSimpleScreenViewModel.swift create mode 100644 ElementX/Sources/Modules/Templates/SimpleScreenExample/TemplateSimpleScreenViewModelProtocol.swift create mode 100644 ElementX/Sources/Modules/Templates/SimpleScreenExample/Test/UI/TemplateSimpleScreenUITests.swift create mode 100644 ElementX/Sources/Modules/Templates/SimpleScreenExample/Test/Unit/TemplateSimpleScreenViewModelTests.swift create mode 100644 ElementX/Sources/Modules/Templates/SimpleScreenExample/View/TemplateSimpleScreen.swift create mode 100644 ElementX/Supporting Files/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 ElementX/Supporting Files/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 ElementX/Supporting Files/Assets.xcassets/Contents.json create mode 100644 ElementX/Supporting Files/Assets.xcassets/app-logo.imageset/Contents.json create mode 100644 ElementX/Supporting Files/Assets.xcassets/app-logo.imageset/launch_screen_logo.png create mode 100644 ElementX/Supporting Files/Assets.xcassets/app-logo.imageset/launch_screen_logo@2x.png create mode 100644 ElementX/Supporting Files/Assets.xcassets/app-logo.imageset/launch_screen_logo@3x.png create mode 100644 ElementX/Supporting Files/Base.lproj/LaunchScreen.storyboard create mode 100644 ElementX/Supporting Files/ElementX.entitlements create mode 100644 ElementXTests/ElementXTests.swift create mode 100644 ElementXUITests/ElementXUITests.swift create mode 100644 ElementXUITests/ElementXUITestsLaunchTests.swift create mode 100755 Scripts/createSwiftUISimpleScreen.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..cb464e706 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +## User settings +xcuserdata/ + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output \ No newline at end of file diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100755 index 000000000..4386e6bba --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,50 @@ +# # rule identifiers to exclude from running +disabled_rules: + - trailing_whitespace + - unused_setter_value + +# some rules are only opt-in +opt_in_rules: + - force_unwrapping + - private_action + - explicit_init + +# paths to include during linting. `--path` is ignored if present. +included: + - ElementX + +line_length: + warning: 250 + error: 1000 + +file_length: + warning: 800 + error: 1000 + +type_name: + min_length: 3 # only warning + max_length: # warning and error + warning: 150 + error: 1000 + +custom_rules: + print_deprecation: + regex: "\\b(print)\\b" + match_kinds: identifier + message: "MXLog should be used instead of print()" + severity: error + + print_ln_deprecation: + regex: "\\b(println)\\b" + match_kinds: identifier + message: "MXLog should be used instead of println()" + severity: error + + os_log_deprecation: + regex: "\\b(os_log)\\b" + match_kinds: identifier + message: "MXLog should be used instead of os_log()" + severity: error + + + diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj new file mode 100644 index 000000000..42269f9d5 --- /dev/null +++ b/ElementX.xcodeproj/project.pbxproj @@ -0,0 +1,995 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + 1850253F27B6918D002E6B18 /* ElementXTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1850253E27B6918D002E6B18 /* ElementXTests.swift */; }; + 1850254927B6918D002E6B18 /* ElementXUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1850254827B6918D002E6B18 /* ElementXUITests.swift */; }; + 1850254B27B6918D002E6B18 /* ElementXUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1850254A27B6918D002E6B18 /* ElementXUITestsLaunchTests.swift */; }; + 1850255927B69388002E6B18 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 1850255827B69388002E6B18 /* MatrixRustSDK */; }; + 1850256C27B6A135002E6B18 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1850256227B6A135002E6B18 /* AppCoordinator.swift */; }; + 1850256F27B6A135002E6B18 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1850256527B6A135002E6B18 /* AppDelegate.swift */; }; + 1850257027B6A135002E6B18 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1850256827B6A135002E6B18 /* Assets.xcassets */; }; + 1850257127B6A135002E6B18 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1850256927B6A135002E6B18 /* LaunchScreen.storyboard */; }; + 1863A3FC27BA5A9100B52E4D /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 1863A3FB27BA5A9100B52E4D /* KeychainAccess */; }; + 1863A40627BA6DFC00B52E4D /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = 1863A40527BA6DFC00B52E4D /* SwiftyBeaver */; }; + 1863A41427BA716A00B52E4D /* AuthenticationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A40E27BA716A00B52E4D /* AuthenticationCoordinator.swift */; }; + 1863A41527BA716A00B52E4D /* UserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A40F27BA716A00B52E4D /* UserSession.swift */; }; + 1863A41627BA716A00B52E4D /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A41027BA716A00B52E4D /* KeychainController.swift */; }; + 1863A41727BA716A00B52E4D /* KeychainControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A41127BA716A00B52E4D /* KeychainControllerProtocol.swift */; }; + 1863A41C27BA76B900B52E4D /* MXLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A41B27BA76B900B52E4D /* MXLog.swift */; }; + 1863A42C27BA784300B52E4D /* LoginScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A41F27BA784300B52E4D /* LoginScreenViewModel.swift */; }; + 1863A43027BA784300B52E4D /* LoginScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A42727BA784300B52E4D /* LoginScreenViewModelProtocol.swift */; }; + 1863A43127BA784300B52E4D /* LoginScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A42827BA784300B52E4D /* LoginScreenModels.swift */; }; + 1863A43227BA784300B52E4D /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A42A27BA784300B52E4D /* LoginScreen.swift */; }; + 1863A43427BA786400B52E4D /* LoginScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A42627BA784300B52E4D /* LoginScreenViewModelTests.swift */; }; + 1863A43527BA788500B52E4D /* LoginScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A42427BA784300B52E4D /* LoginScreenUITests.swift */; }; + 1863A43A27BA789800B52E4D /* StateStoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A43827BA789800B52E4D /* StateStoreViewModel.swift */; }; + 1863A43B27BA789800B52E4D /* BindableState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A43927BA789800B52E4D /* BindableState.swift */; }; + 1863A43F27BA790000B52E4D /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A43D27BA790000B52E4D /* Coordinator.swift */; }; + 1863A44927BA79FF00B52E4D /* NavigationRouterStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A44127BA79FF00B52E4D /* NavigationRouterStore.swift */; }; + 1863A44A27BA79FF00B52E4D /* NavigationRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A44227BA79FF00B52E4D /* NavigationRouter.swift */; }; + 1863A44B27BA79FF00B52E4D /* RootRouterType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A44327BA79FF00B52E4D /* RootRouterType.swift */; }; + 1863A44C27BA79FF00B52E4D /* NavigationRouterStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A44427BA79FF00B52E4D /* NavigationRouterStoreProtocol.swift */; }; + 1863A44D27BA79FF00B52E4D /* RootRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A44527BA79FF00B52E4D /* RootRouter.swift */; }; + 1863A44E27BA79FF00B52E4D /* Presentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A44627BA79FF00B52E4D /* Presentable.swift */; }; + 1863A44F27BA79FF00B52E4D /* NavigationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A44727BA79FF00B52E4D /* NavigationModule.swift */; }; + 1863A45027BA79FF00B52E4D /* NavigationRouterType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A44827BA79FF00B52E4D /* NavigationRouterType.swift */; }; + 1863A45627BA7A7800B52E4D /* WeakDictionaryKeyReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A45227BA7A7800B52E4D /* WeakDictionaryKeyReference.swift */; }; + 1863A45727BA7A7800B52E4D /* WeakDictionaryReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A45327BA7A7800B52E4D /* WeakDictionaryReference.swift */; }; + 1863A45827BA7A7800B52E4D /* WeakDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A45427BA7A7800B52E4D /* WeakDictionary.swift */; }; + 1863A45927BA7A7800B52E4D /* WeakKeyDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A45527BA7A7800B52E4D /* WeakKeyDictionary.swift */; }; + 1863A45B27BA7B4700B52E4D /* LoginScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A45A27BA7B4700B52E4D /* LoginScreenCoordinator.swift */; }; + 1863A45F27BAA60300B52E4D /* SplashViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A45D27BAA60300B52E4D /* SplashViewController.swift */; }; + 1863A46027BAA60300B52E4D /* SplashViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1863A45E27BAA60300B52E4D /* SplashViewController.xib */; }; + 1863A48527BAA8A900B52E4D /* HomeScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A47927BAA8A900B52E4D /* HomeScreenCoordinator.swift */; }; + 1863A48827BAA8A900B52E4D /* HomeScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A47F27BAA8A900B52E4D /* HomeScreenViewModelProtocol.swift */; }; + 1863A48927BAA8A900B52E4D /* HomeScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A48027BAA8A900B52E4D /* HomeScreenModels.swift */; }; + 1863A48A27BAA8A900B52E4D /* HomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A48227BAA8A900B52E4D /* HomeScreen.swift */; }; + 1863A48C27BAA8A900B52E4D /* HomeScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A48427BAA8A900B52E4D /* HomeScreenViewModel.swift */; }; + 1863A48E27BAA8C800B52E4D /* HomeScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A47C27BAA8A900B52E4D /* HomeScreenUITests.swift */; }; + 1863A48F27BAA8CC00B52E4D /* HomeScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A47E27BAA8A900B52E4D /* HomeScreenViewModelTests.swift */; }; + 1863A49427BAAA6700B52E4D /* RoomModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1863A49327BAAA6700B52E4D /* RoomModel.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 1850253B27B6918D002E6B18 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 1850251C27B6918C002E6B18 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 1850252327B6918C002E6B18; + remoteInfo = ElementX; + }; + 1850254527B6918D002E6B18 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 1850251C27B6918C002E6B18 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 1850252327B6918C002E6B18; + remoteInfo = ElementX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 1850252427B6918C002E6B18 /* ElementX.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ElementX.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 1850253A27B6918D002E6B18 /* ElementXTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ElementXTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 1850253E27B6918D002E6B18 /* ElementXTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementXTests.swift; sourceTree = ""; }; + 1850254427B6918D002E6B18 /* ElementXUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ElementXUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 1850254827B6918D002E6B18 /* ElementXUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementXUITests.swift; sourceTree = ""; }; + 1850254A27B6918D002E6B18 /* ElementXUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementXUITestsLaunchTests.swift; sourceTree = ""; }; + 1850256227B6A135002E6B18 /* AppCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; + 1850256527B6A135002E6B18 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 1850256727B6A135002E6B18 /* ElementX.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = ElementX.entitlements; sourceTree = ""; }; + 1850256827B6A135002E6B18 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 1850256A27B6A135002E6B18 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 1863A40E27BA716A00B52E4D /* AuthenticationCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationCoordinator.swift; sourceTree = ""; }; + 1863A40F27BA716A00B52E4D /* UserSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserSession.swift; sourceTree = ""; }; + 1863A41027BA716A00B52E4D /* KeychainController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainController.swift; sourceTree = ""; }; + 1863A41127BA716A00B52E4D /* KeychainControllerProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainControllerProtocol.swift; sourceTree = ""; }; + 1863A41B27BA76B900B52E4D /* MXLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MXLog.swift; sourceTree = ""; }; + 1863A41F27BA784300B52E4D /* LoginScreenViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModel.swift; sourceTree = ""; }; + 1863A42427BA784300B52E4D /* LoginScreenUITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginScreenUITests.swift; sourceTree = ""; }; + 1863A42627BA784300B52E4D /* LoginScreenViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModelTests.swift; sourceTree = ""; }; + 1863A42727BA784300B52E4D /* LoginScreenViewModelProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModelProtocol.swift; sourceTree = ""; }; + 1863A42827BA784300B52E4D /* LoginScreenModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginScreenModels.swift; sourceTree = ""; }; + 1863A42A27BA784300B52E4D /* LoginScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginScreen.swift; sourceTree = ""; }; + 1863A43827BA789800B52E4D /* StateStoreViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StateStoreViewModel.swift; sourceTree = ""; }; + 1863A43927BA789800B52E4D /* BindableState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BindableState.swift; sourceTree = ""; }; + 1863A43D27BA790000B52E4D /* Coordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = ""; }; + 1863A44127BA79FF00B52E4D /* NavigationRouterStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationRouterStore.swift; sourceTree = ""; }; + 1863A44227BA79FF00B52E4D /* NavigationRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationRouter.swift; sourceTree = ""; }; + 1863A44327BA79FF00B52E4D /* RootRouterType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootRouterType.swift; sourceTree = ""; }; + 1863A44427BA79FF00B52E4D /* NavigationRouterStoreProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationRouterStoreProtocol.swift; sourceTree = ""; }; + 1863A44527BA79FF00B52E4D /* RootRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootRouter.swift; sourceTree = ""; }; + 1863A44627BA79FF00B52E4D /* Presentable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Presentable.swift; sourceTree = ""; }; + 1863A44727BA79FF00B52E4D /* NavigationModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationModule.swift; sourceTree = ""; }; + 1863A44827BA79FF00B52E4D /* NavigationRouterType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationRouterType.swift; sourceTree = ""; }; + 1863A45227BA7A7800B52E4D /* WeakDictionaryKeyReference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeakDictionaryKeyReference.swift; sourceTree = ""; }; + 1863A45327BA7A7800B52E4D /* WeakDictionaryReference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeakDictionaryReference.swift; sourceTree = ""; }; + 1863A45427BA7A7800B52E4D /* WeakDictionary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeakDictionary.swift; sourceTree = ""; }; + 1863A45527BA7A7800B52E4D /* WeakKeyDictionary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeakKeyDictionary.swift; sourceTree = ""; }; + 1863A45A27BA7B4700B52E4D /* LoginScreenCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginScreenCoordinator.swift; sourceTree = ""; }; + 1863A45D27BAA60300B52E4D /* SplashViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashViewController.swift; sourceTree = ""; }; + 1863A45E27BAA60300B52E4D /* SplashViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SplashViewController.xib; sourceTree = ""; }; + 1863A47927BAA8A900B52E4D /* HomeScreenCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeScreenCoordinator.swift; sourceTree = ""; }; + 1863A47C27BAA8A900B52E4D /* HomeScreenUITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeScreenUITests.swift; sourceTree = ""; }; + 1863A47E27BAA8A900B52E4D /* HomeScreenViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModelTests.swift; sourceTree = ""; }; + 1863A47F27BAA8A900B52E4D /* HomeScreenViewModelProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModelProtocol.swift; sourceTree = ""; }; + 1863A48027BAA8A900B52E4D /* HomeScreenModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeScreenModels.swift; sourceTree = ""; }; + 1863A48227BAA8A900B52E4D /* HomeScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeScreen.swift; sourceTree = ""; }; + 1863A48427BAA8A900B52E4D /* HomeScreenViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModel.swift; sourceTree = ""; }; + 1863A49327BAAA6700B52E4D /* RoomModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomModel.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 1850252127B6918C002E6B18 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 1863A3FC27BA5A9100B52E4D /* KeychainAccess in Frameworks */, + 1850255927B69388002E6B18 /* MatrixRustSDK in Frameworks */, + 1863A40627BA6DFC00B52E4D /* SwiftyBeaver in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1850253727B6918D002E6B18 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1850254127B6918D002E6B18 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 1850251B27B6918C002E6B18 = { + isa = PBXGroup; + children = ( + 1850252627B6918C002E6B18 /* ElementX */, + 1850253D27B6918D002E6B18 /* ElementXTests */, + 1850254727B6918D002E6B18 /* ElementXUITests */, + 1850252527B6918C002E6B18 /* Products */, + ); + sourceTree = ""; + }; + 1850252527B6918C002E6B18 /* Products */ = { + isa = PBXGroup; + children = ( + 1850252427B6918C002E6B18 /* ElementX.app */, + 1850253A27B6918D002E6B18 /* ElementXTests.xctest */, + 1850254427B6918D002E6B18 /* ElementXUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 1850252627B6918C002E6B18 /* ElementX */ = { + isa = PBXGroup; + children = ( + 1850256127B6A135002E6B18 /* Sources */, + 1850256627B6A135002E6B18 /* Supporting Files */, + ); + path = ElementX; + sourceTree = ""; + }; + 1850253D27B6918D002E6B18 /* ElementXTests */ = { + isa = PBXGroup; + children = ( + 1850253E27B6918D002E6B18 /* ElementXTests.swift */, + ); + path = ElementXTests; + sourceTree = ""; + }; + 1850254727B6918D002E6B18 /* ElementXUITests */ = { + isa = PBXGroup; + children = ( + 1850254827B6918D002E6B18 /* ElementXUITests.swift */, + 1850254A27B6918D002E6B18 /* ElementXUITestsLaunchTests.swift */, + ); + path = ElementXUITests; + sourceTree = ""; + }; + 1850256127B6A135002E6B18 /* Sources */ = { + isa = PBXGroup; + children = ( + 1850256527B6A135002E6B18 /* AppDelegate.swift */, + 1850256227B6A135002E6B18 /* AppCoordinator.swift */, + 1863A40927BA716A00B52E4D /* Modules */, + ); + path = Sources; + sourceTree = ""; + }; + 1850256627B6A135002E6B18 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 1850256727B6A135002E6B18 /* ElementX.entitlements */, + 1850256827B6A135002E6B18 /* Assets.xcassets */, + 1850256927B6A135002E6B18 /* LaunchScreen.storyboard */, + ); + path = "Supporting Files"; + sourceTree = ""; + }; + 1863A40927BA716A00B52E4D /* Modules */ = { + isa = PBXGroup; + children = ( + 1863A45C27BAA5F100B52E4D /* Splash */, + 1863A40D27BA716A00B52E4D /* Authentication */, + 1863A47827BAA8A900B52E4D /* HomeScreen */, + 1863A49227BAAA6700B52E4D /* Models */, + 1863A44027BA79FF00B52E4D /* Routers */, + 1863A41A27BA76B900B52E4D /* Other */, + ); + path = Modules; + sourceTree = ""; + }; + 1863A40D27BA716A00B52E4D /* Authentication */ = { + isa = PBXGroup; + children = ( + 1863A40E27BA716A00B52E4D /* AuthenticationCoordinator.swift */, + 1863A40F27BA716A00B52E4D /* UserSession.swift */, + 1863A41027BA716A00B52E4D /* KeychainController.swift */, + 1863A41127BA716A00B52E4D /* KeychainControllerProtocol.swift */, + 1863A41D27BA784300B52E4D /* LoginScreen */, + ); + path = Authentication; + sourceTree = ""; + }; + 1863A41A27BA76B900B52E4D /* Other */ = { + isa = PBXGroup; + children = ( + 1863A43D27BA790000B52E4D /* Coordinator.swift */, + 1863A41B27BA76B900B52E4D /* MXLog.swift */, + 1863A43627BA789800B52E4D /* SwiftUI */, + 1863A45127BA7A7800B52E4D /* WeakDictionary */, + ); + path = Other; + sourceTree = ""; + }; + 1863A41D27BA784300B52E4D /* LoginScreen */ = { + isa = PBXGroup; + children = ( + 1863A45A27BA7B4700B52E4D /* LoginScreenCoordinator.swift */, + 1863A42827BA784300B52E4D /* LoginScreenModels.swift */, + 1863A41F27BA784300B52E4D /* LoginScreenViewModel.swift */, + 1863A42727BA784300B52E4D /* LoginScreenViewModelProtocol.swift */, + 1863A42227BA784300B52E4D /* Test */, + 1863A42927BA784300B52E4D /* View */, + ); + path = LoginScreen; + sourceTree = ""; + }; + 1863A42227BA784300B52E4D /* Test */ = { + isa = PBXGroup; + children = ( + 1863A42327BA784300B52E4D /* UI */, + 1863A42527BA784300B52E4D /* Unit */, + ); + path = Test; + sourceTree = ""; + }; + 1863A42327BA784300B52E4D /* UI */ = { + isa = PBXGroup; + children = ( + 1863A42427BA784300B52E4D /* LoginScreenUITests.swift */, + ); + path = UI; + sourceTree = ""; + }; + 1863A42527BA784300B52E4D /* Unit */ = { + isa = PBXGroup; + children = ( + 1863A42627BA784300B52E4D /* LoginScreenViewModelTests.swift */, + ); + path = Unit; + sourceTree = ""; + }; + 1863A42927BA784300B52E4D /* View */ = { + isa = PBXGroup; + children = ( + 1863A42A27BA784300B52E4D /* LoginScreen.swift */, + ); + path = View; + sourceTree = ""; + }; + 1863A43627BA789800B52E4D /* SwiftUI */ = { + isa = PBXGroup; + children = ( + 1863A43727BA789800B52E4D /* ViewModel */, + ); + path = SwiftUI; + sourceTree = ""; + }; + 1863A43727BA789800B52E4D /* ViewModel */ = { + isa = PBXGroup; + children = ( + 1863A43827BA789800B52E4D /* StateStoreViewModel.swift */, + 1863A43927BA789800B52E4D /* BindableState.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + 1863A44027BA79FF00B52E4D /* Routers */ = { + isa = PBXGroup; + children = ( + 1863A44127BA79FF00B52E4D /* NavigationRouterStore.swift */, + 1863A44227BA79FF00B52E4D /* NavigationRouter.swift */, + 1863A44327BA79FF00B52E4D /* RootRouterType.swift */, + 1863A44427BA79FF00B52E4D /* NavigationRouterStoreProtocol.swift */, + 1863A44527BA79FF00B52E4D /* RootRouter.swift */, + 1863A44627BA79FF00B52E4D /* Presentable.swift */, + 1863A44727BA79FF00B52E4D /* NavigationModule.swift */, + 1863A44827BA79FF00B52E4D /* NavigationRouterType.swift */, + ); + path = Routers; + sourceTree = ""; + }; + 1863A45127BA7A7800B52E4D /* WeakDictionary */ = { + isa = PBXGroup; + children = ( + 1863A45227BA7A7800B52E4D /* WeakDictionaryKeyReference.swift */, + 1863A45327BA7A7800B52E4D /* WeakDictionaryReference.swift */, + 1863A45427BA7A7800B52E4D /* WeakDictionary.swift */, + 1863A45527BA7A7800B52E4D /* WeakKeyDictionary.swift */, + ); + path = WeakDictionary; + sourceTree = ""; + }; + 1863A45C27BAA5F100B52E4D /* Splash */ = { + isa = PBXGroup; + children = ( + 1863A45D27BAA60300B52E4D /* SplashViewController.swift */, + 1863A45E27BAA60300B52E4D /* SplashViewController.xib */, + ); + path = Splash; + sourceTree = ""; + }; + 1863A47827BAA8A900B52E4D /* HomeScreen */ = { + isa = PBXGroup; + children = ( + 1863A47927BAA8A900B52E4D /* HomeScreenCoordinator.swift */, + 1863A48027BAA8A900B52E4D /* HomeScreenModels.swift */, + 1863A48427BAA8A900B52E4D /* HomeScreenViewModel.swift */, + 1863A47F27BAA8A900B52E4D /* HomeScreenViewModelProtocol.swift */, + 1863A47A27BAA8A900B52E4D /* Test */, + 1863A48127BAA8A900B52E4D /* View */, + ); + path = HomeScreen; + sourceTree = ""; + }; + 1863A47A27BAA8A900B52E4D /* Test */ = { + isa = PBXGroup; + children = ( + 1863A47B27BAA8A900B52E4D /* UI */, + 1863A47D27BAA8A900B52E4D /* Unit */, + ); + path = Test; + sourceTree = ""; + }; + 1863A47B27BAA8A900B52E4D /* UI */ = { + isa = PBXGroup; + children = ( + 1863A47C27BAA8A900B52E4D /* HomeScreenUITests.swift */, + ); + path = UI; + sourceTree = ""; + }; + 1863A47D27BAA8A900B52E4D /* Unit */ = { + isa = PBXGroup; + children = ( + 1863A47E27BAA8A900B52E4D /* HomeScreenViewModelTests.swift */, + ); + path = Unit; + sourceTree = ""; + }; + 1863A48127BAA8A900B52E4D /* View */ = { + isa = PBXGroup; + children = ( + 1863A48227BAA8A900B52E4D /* HomeScreen.swift */, + ); + path = View; + sourceTree = ""; + }; + 1863A49227BAAA6700B52E4D /* Models */ = { + isa = PBXGroup; + children = ( + 1863A49327BAAA6700B52E4D /* RoomModel.swift */, + ); + path = Models; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 1850252327B6918C002E6B18 /* ElementX */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1850254E27B6918D002E6B18 /* Build configuration list for PBXNativeTarget "ElementX" */; + buildPhases = ( + 1850252027B6918C002E6B18 /* Sources */, + 1850252127B6918C002E6B18 /* Frameworks */, + 1850252227B6918C002E6B18 /* Resources */, + 184230FC27BAAC5800033771 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ElementX; + packageProductDependencies = ( + 1850255827B69388002E6B18 /* MatrixRustSDK */, + 1863A3FB27BA5A9100B52E4D /* KeychainAccess */, + 1863A40527BA6DFC00B52E4D /* SwiftyBeaver */, + ); + productName = ElementX; + productReference = 1850252427B6918C002E6B18 /* ElementX.app */; + productType = "com.apple.product-type.application"; + }; + 1850253927B6918D002E6B18 /* ElementXTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1850255127B6918D002E6B18 /* Build configuration list for PBXNativeTarget "ElementXTests" */; + buildPhases = ( + 1850253627B6918D002E6B18 /* Sources */, + 1850253727B6918D002E6B18 /* Frameworks */, + 1850253827B6918D002E6B18 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 1850253C27B6918D002E6B18 /* PBXTargetDependency */, + ); + name = ElementXTests; + productName = ElementXTests; + productReference = 1850253A27B6918D002E6B18 /* ElementXTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 1850254327B6918D002E6B18 /* ElementXUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1850255427B6918D002E6B18 /* Build configuration list for PBXNativeTarget "ElementXUITests" */; + buildPhases = ( + 1850254027B6918D002E6B18 /* Sources */, + 1850254127B6918D002E6B18 /* Frameworks */, + 1850254227B6918D002E6B18 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 1850254627B6918D002E6B18 /* PBXTargetDependency */, + ); + name = ElementXUITests; + productName = ElementXUITests; + productReference = 1850254427B6918D002E6B18 /* ElementXUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 1850251C27B6918C002E6B18 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1320; + LastUpgradeCheck = 1320; + TargetAttributes = { + 1850252327B6918C002E6B18 = { + CreatedOnToolsVersion = 13.2.1; + }; + 1850253927B6918D002E6B18 = { + CreatedOnToolsVersion = 13.2.1; + TestTargetID = 1850252327B6918C002E6B18; + }; + 1850254327B6918D002E6B18 = { + CreatedOnToolsVersion = 13.2.1; + TestTargetID = 1850252327B6918C002E6B18; + }; + }; + }; + buildConfigurationList = 1850251F27B6918C002E6B18 /* Build configuration list for PBXProject "ElementX" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 1850251B27B6918C002E6B18; + packageReferences = ( + 1850255727B69388002E6B18 /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */, + 1863A3FA27BA5A9100B52E4D /* XCRemoteSwiftPackageReference "KeychainAccess" */, + 1863A40427BA6DFC00B52E4D /* XCRemoteSwiftPackageReference "SwiftyBeaver" */, + ); + productRefGroup = 1850252527B6918C002E6B18 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 1850252327B6918C002E6B18 /* ElementX */, + 1850253927B6918D002E6B18 /* ElementXTests */, + 1850254327B6918D002E6B18 /* ElementXUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 1850252227B6918C002E6B18 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1850257127B6A135002E6B18 /* LaunchScreen.storyboard in Resources */, + 1863A46027BAA60300B52E4D /* SplashViewController.xib in Resources */, + 1850257027B6A135002E6B18 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1850253827B6918D002E6B18 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1850254227B6918D002E6B18 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 184230FC27BAAC5800033771 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "export PATH=\"$PATH:/opt/homebrew/bin\"\n\nif which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 1850252027B6918C002E6B18 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1863A44B27BA79FF00B52E4D /* RootRouterType.swift in Sources */, + 1863A48C27BAA8A900B52E4D /* HomeScreenViewModel.swift in Sources */, + 1863A41727BA716A00B52E4D /* KeychainControllerProtocol.swift in Sources */, + 1863A45B27BA7B4700B52E4D /* LoginScreenCoordinator.swift in Sources */, + 1863A41427BA716A00B52E4D /* AuthenticationCoordinator.swift in Sources */, + 1863A43B27BA789800B52E4D /* BindableState.swift in Sources */, + 1863A42C27BA784300B52E4D /* LoginScreenViewModel.swift in Sources */, + 1863A44F27BA79FF00B52E4D /* NavigationModule.swift in Sources */, + 1863A43027BA784300B52E4D /* LoginScreenViewModelProtocol.swift in Sources */, + 1863A48827BAA8A900B52E4D /* HomeScreenViewModelProtocol.swift in Sources */, + 1863A44D27BA79FF00B52E4D /* RootRouter.swift in Sources */, + 1863A45F27BAA60300B52E4D /* SplashViewController.swift in Sources */, + 1863A48A27BAA8A900B52E4D /* HomeScreen.swift in Sources */, + 1863A44927BA79FF00B52E4D /* NavigationRouterStore.swift in Sources */, + 1863A45727BA7A7800B52E4D /* WeakDictionaryReference.swift in Sources */, + 1863A48927BAA8A900B52E4D /* HomeScreenModels.swift in Sources */, + 1863A45027BA79FF00B52E4D /* NavigationRouterType.swift in Sources */, + 1863A45627BA7A7800B52E4D /* WeakDictionaryKeyReference.swift in Sources */, + 1863A49427BAAA6700B52E4D /* RoomModel.swift in Sources */, + 1850256F27B6A135002E6B18 /* AppDelegate.swift in Sources */, + 1863A41627BA716A00B52E4D /* KeychainController.swift in Sources */, + 1863A41527BA716A00B52E4D /* UserSession.swift in Sources */, + 1863A43A27BA789800B52E4D /* StateStoreViewModel.swift in Sources */, + 1863A45827BA7A7800B52E4D /* WeakDictionary.swift in Sources */, + 1863A43227BA784300B52E4D /* LoginScreen.swift in Sources */, + 1863A48527BAA8A900B52E4D /* HomeScreenCoordinator.swift in Sources */, + 1863A43F27BA790000B52E4D /* Coordinator.swift in Sources */, + 1863A41C27BA76B900B52E4D /* MXLog.swift in Sources */, + 1863A44E27BA79FF00B52E4D /* Presentable.swift in Sources */, + 1850256C27B6A135002E6B18 /* AppCoordinator.swift in Sources */, + 1863A43127BA784300B52E4D /* LoginScreenModels.swift in Sources */, + 1863A44A27BA79FF00B52E4D /* NavigationRouter.swift in Sources */, + 1863A45927BA7A7800B52E4D /* WeakKeyDictionary.swift in Sources */, + 1863A44C27BA79FF00B52E4D /* NavigationRouterStoreProtocol.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1850253627B6918D002E6B18 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1863A48F27BAA8CC00B52E4D /* HomeScreenViewModelTests.swift in Sources */, + 1863A43427BA786400B52E4D /* LoginScreenViewModelTests.swift in Sources */, + 1850253F27B6918D002E6B18 /* ElementXTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1850254027B6918D002E6B18 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1850254B27B6918D002E6B18 /* ElementXUITestsLaunchTests.swift in Sources */, + 1863A48E27BAA8C800B52E4D /* HomeScreenUITests.swift in Sources */, + 1863A43527BA788500B52E4D /* LoginScreenUITests.swift in Sources */, + 1850254927B6918D002E6B18 /* ElementXUITests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 1850253C27B6918D002E6B18 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 1850252327B6918C002E6B18 /* ElementX */; + targetProxy = 1850253B27B6918D002E6B18 /* PBXContainerItemProxy */; + }; + 1850254627B6918D002E6B18 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 1850252327B6918C002E6B18 /* ElementX */; + targetProxy = 1850254527B6918D002E6B18 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 1850256927B6A135002E6B18 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 1850256A27B6A135002E6B18 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 1850254C27B6918D002E6B18 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = ""; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 1850254D27B6918D002E6B18 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = ""; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 1850254F27B6918D002E6B18 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = ElementX/ElementX.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 7J4U792NQT; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ""; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.element.ElementX; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTS_MACCATALYST = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 1850255027B6918D002E6B18 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = ElementX/ElementX.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 7J4U792NQT; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ""; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.element.ElementX; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTS_MACCATALYST = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 1850255227B6918D002E6B18 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 7J4U792NQT; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.element.ElementXTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ElementX.app/ElementX"; + }; + name = Debug; + }; + 1850255327B6918D002E6B18 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 7J4U792NQT; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.element.ElementXTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ElementX.app/ElementX"; + }; + name = Release; + }; + 1850255527B6918D002E6B18 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 7J4U792NQT; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.element.ElementXUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = ElementX; + }; + name = Debug; + }; + 1850255627B6918D002E6B18 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 7J4U792NQT; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.element.ElementXUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = ElementX; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 1850251F27B6918C002E6B18 /* Build configuration list for PBXProject "ElementX" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1850254C27B6918D002E6B18 /* Debug */, + 1850254D27B6918D002E6B18 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1850254E27B6918D002E6B18 /* Build configuration list for PBXNativeTarget "ElementX" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1850254F27B6918D002E6B18 /* Debug */, + 1850255027B6918D002E6B18 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1850255127B6918D002E6B18 /* Build configuration list for PBXNativeTarget "ElementXTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1850255227B6918D002E6B18 /* Debug */, + 1850255327B6918D002E6B18 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1850255427B6918D002E6B18 /* Build configuration list for PBXNativeTarget "ElementXUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1850255527B6918D002E6B18 /* Debug */, + 1850255627B6918D002E6B18 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 1850255727B69388002E6B18 /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/matrix-org/matrix-rust-components-swift.git"; + requirement = { + branch = main; + kind = branch; + }; + }; + 1863A3FA27BA5A9100B52E4D /* XCRemoteSwiftPackageReference "KeychainAccess" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess"; + requirement = { + branch = master; + kind = branch; + }; + }; + 1863A40427BA6DFC00B52E4D /* XCRemoteSwiftPackageReference "SwiftyBeaver" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/SwiftyBeaver/SwiftyBeaver"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 1850255827B69388002E6B18 /* MatrixRustSDK */ = { + isa = XCSwiftPackageProductDependency; + package = 1850255727B69388002E6B18 /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */; + productName = MatrixRustSDK; + }; + 1863A3FB27BA5A9100B52E4D /* KeychainAccess */ = { + isa = XCSwiftPackageProductDependency; + package = 1863A3FA27BA5A9100B52E4D /* XCRemoteSwiftPackageReference "KeychainAccess" */; + productName = KeychainAccess; + }; + 1863A40527BA6DFC00B52E4D /* SwiftyBeaver */ = { + isa = XCSwiftPackageProductDependency; + package = 1863A40427BA6DFC00B52E4D /* XCRemoteSwiftPackageReference "SwiftyBeaver" */; + productName = SwiftyBeaver; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 1850251C27B6918C002E6B18 /* Project object */; +} diff --git a/ElementX.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ElementX.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/ElementX.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 000000000..e35f95b49 --- /dev/null +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,34 @@ +{ + "object": { + "pins": [ + { + "package": "KeychainAccess", + "repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess", + "state": { + "branch": "master", + "revision": "6299daec1d74be12164fec090faf9ed14d0da9d6", + "version": null + } + }, + { + "package": "MatrixRustSDK", + "repositoryURL": "https://github.com/matrix-org/matrix-rust-components-swift.git", + "state": { + "branch": "main", + "revision": "cb680b1783849ecabd0bdf61f65faff767ce32c8", + "version": null + } + }, + { + "package": "SwiftyBeaver", + "repositoryURL": "https://github.com/SwiftyBeaver/SwiftyBeaver", + "state": { + "branch": null, + "revision": "2c039501d6eeb4d4cd4aec4a8d884ad28862e044", + "version": "1.9.5" + } + } + ] + }, + "version": 1 +} diff --git a/ElementX/Sources/AppCoordinator.swift b/ElementX/Sources/AppCoordinator.swift new file mode 100644 index 000000000..e59f8768f --- /dev/null +++ b/ElementX/Sources/AppCoordinator.swift @@ -0,0 +1,78 @@ +// +// AppCoordinator.swift +// ElementX +// +// Created by Stefan Ceriu on 11.02.2022. +// + +import UIKit + +class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { + private let window: UIWindow + + private let mainNavigationController: UINavigationController + private let splashViewController: UIViewController + + private let navigationRouter: NavigationRouter + + private let keychainController: KeychainControllerProtocol + private let authenticationCoordinator: AuthenticationCoordinator! + + var childCoordinators: [Coordinator] = [] + + init() { + splashViewController = SplashViewController() + mainNavigationController = UINavigationController(rootViewController: splashViewController) + mainNavigationController.setNavigationBarHidden(true, animated: false) + window = UIWindow(frame: UIScreen.main.bounds) + window.rootViewController = mainNavigationController + + navigationRouter = NavigationRouter(navigationController: mainNavigationController) + + guard let bundleIdentifier = Bundle.main.bundleIdentifier else { + fatalError("Should have a valid bundle identifier at this point") + } + + keychainController = KeychainController(identifier: bundleIdentifier) + authenticationCoordinator = AuthenticationCoordinator(keychainController: keychainController, + navigationRouter: navigationRouter) + authenticationCoordinator.delegate = self + } + + func start() { + window.makeKeyAndVisible() + authenticationCoordinator.start() + } + + // MARK: - AuthenticationCoordinatorDelegate + + func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, didFailWithError error: AuthenticationCoordinatorError) { + + } + + func authenticationCoordinatorDidSetupUserSession(_ authenticationCoordinator: AuthenticationCoordinator) { + presentHomeScreen() + } + + func authenticationCoordinatorDidTearDownUserSession(_ authenticationCoordinator: AuthenticationCoordinator) { + + } + + // MARK: - Private + + private func presentHomeScreen() { + guard let userSession = authenticationCoordinator.userSession else { + fatalError("User session should be already setup at this point") + } + + let parameters = HomeScreenCoordinatorParameters(userSession: userSession) + let coordinator = HomeScreenCoordinator(parameters: parameters) + + add(childCoordinator: coordinator) + navigationRouter.setRootModule(coordinator) + } + + private func restart() { + + } +} diff --git a/ElementX/Sources/AppDelegate.swift b/ElementX/Sources/AppDelegate.swift new file mode 100644 index 000000000..314bd0891 --- /dev/null +++ b/ElementX/Sources/AppDelegate.swift @@ -0,0 +1,20 @@ +// +// AppDelegate.swift +// ElementX +// +// Created by Stefan Ceriu on 11.02.2022. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + private var appCoordinator: AppCoordinator! + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { + appCoordinator = AppCoordinator() + appCoordinator.start() + return true + } +} diff --git a/ElementX/Sources/Modules/Authentication/AuthenticationCoordinator.swift b/ElementX/Sources/Modules/Authentication/AuthenticationCoordinator.swift new file mode 100644 index 000000000..d50ae82a9 --- /dev/null +++ b/ElementX/Sources/Modules/Authentication/AuthenticationCoordinator.swift @@ -0,0 +1,123 @@ +// +// AuthenticationCoordinator.swift +// ElementX +// +// Created by Stefan Ceriu on 11.02.2022. +// + +import Foundation +import MatrixRustSDK + +enum AuthenticationCoordinatorError: Error { + case failedLoggingIn + case failedRestoringLogin + case failedSettingUpSession +} + +protocol AuthenticationCoordinatorDelegate: AnyObject { + func authenticationCoordinatorDidSetupUserSession(_ authenticationCoordinator: AuthenticationCoordinator) + + func authenticationCoordinatorDidTearDownUserSession(_ authenticationCoordinator: AuthenticationCoordinator) + + func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, + didFailWithError error: AuthenticationCoordinatorError) +} + +class AuthenticationCoordinator: Coordinator { + + private let keychainController: KeychainControllerProtocol + private let navigationRouter: NavigationRouter + + private(set) var userSession: UserSession? + var childCoordinators: [Coordinator] = [] + + weak var delegate: AuthenticationCoordinatorDelegate? + + init(keychainController: KeychainControllerProtocol, + navigationRouter: NavigationRouter) { + self.keychainController = keychainController + self.navigationRouter = navigationRouter + } + + func start() { + let availableRestoreTokens = keychainController.restoreTokens() + + guard let usernameTokenTuple = availableRestoreTokens.first else { + startNewLoginFlow() + return + } + + restorePreviousLogin(usernameTokenTuple) + } + + // MARK: - Private + + private func startNewLoginFlow() { + let parameters = LoginScreenCoordinatorParameters() + let coordinator = LoginScreenCoordinator(parameters: parameters) + + coordinator.completion = { [weak self, weak coordinator] result in + guard let self = self, let coordinator = coordinator else { + return + } + + switch result { + case .login(let result): + do { + self.setupUserSessionForClient(try loginNewClient(basePath: self.baseDirectoryPathForUsername(result.username), + username: result.username, + password: result.password)) + + self.remove(childCoordinator: coordinator) + self.navigationRouter.dismissModule() + } catch { + self.delegate?.authenticationCoordinator(self, didFailWithError: .failedLoggingIn) + MXLog.error("Failed logging in user with error: \(error)") + } + } + } + + add(childCoordinator: coordinator) + navigationRouter.present(coordinator) + + coordinator.start() + } + + private func restorePreviousLogin(_ usernameTokenTuple: (username: String, token: String)) { + do { + setupUserSessionForClient(try loginWithToken(basePath: baseDirectoryPathForUsername(usernameTokenTuple.username), + restoreToken: usernameTokenTuple.token)) + } catch { + delegate?.authenticationCoordinator(self, didFailWithError: .failedRestoringLogin) + MXLog.error("Failed restoring login with error: \(error)") + } + } + + private func setupUserSessionForClient(_ client: Client) { + + do { + let restoreToken = try client.restoreToken() + let userId = try client.userId() + + keychainController.setRestoreToken(restoreToken, forUsername: userId) + } catch { + delegate?.authenticationCoordinator(self, didFailWithError: .failedSettingUpSession) + MXLog.error("Failed setting up user session with error: \(error)") + } + + userSession = UserSession(client: client) + delegate?.authenticationCoordinatorDidSetupUserSession(self) + } + + private func baseDirectoryPathForUsername(_ username: String) -> String { + guard var url = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else { + fatalError("Should always be able to retrieve the caches directory") + } + + url = url.appendingPathComponent(username) + + try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: false, attributes: nil) + + return url.path + } +} diff --git a/ElementX/Sources/Modules/Authentication/KeychainController.swift b/ElementX/Sources/Modules/Authentication/KeychainController.swift new file mode 100644 index 000000000..d0a2d34ad --- /dev/null +++ b/ElementX/Sources/Modules/Authentication/KeychainController.swift @@ -0,0 +1,57 @@ +// +// KeychainController.swift +// ElementX +// +// Created by Stefan Ceriu on 14.02.2022. +// + +import Foundation +import KeychainAccess + +class KeychainController: KeychainControllerProtocol { + + struct Constants { + static let restoreTokenGroupKey = "restoreTokens" + } + + private let keychain: Keychain + + init(identifier: String) { + keychain = Keychain(service: identifier) + } + + func setRestoreToken(_ token: String, forUsername username: String) { + do { + try keychain.set(token, key: username) + } catch { + MXLog.error("Failed storing user restore token") + } + } + + func restoreTokenForUsername(_ username: String) -> String? { + do { + return try keychain.get(username) + } catch { + MXLog.error("Failed retrieving user restore token") + return nil + } + } + + func restoreTokens() -> [(username: String, token: String)] { + keychain.allKeys().compactMap { username in + guard let token = restoreTokenForUsername(username) else { + return nil + } + + return (username, token) + } + } + + func removeAllTokens() { + do { + try keychain.removeAll() + } catch { + MXLog.error("Failed removing all tokens") + } + } +} diff --git a/ElementX/Sources/Modules/Authentication/KeychainControllerProtocol.swift b/ElementX/Sources/Modules/Authentication/KeychainControllerProtocol.swift new file mode 100644 index 000000000..21693394a --- /dev/null +++ b/ElementX/Sources/Modules/Authentication/KeychainControllerProtocol.swift @@ -0,0 +1,15 @@ +// +// KeychainControllerProtocol.swift +// ElementX +// +// Created by Stefan Ceriu on 14.02.2022. +// + +import Foundation + +protocol KeychainControllerProtocol { + func setRestoreToken(_ token: String, forUsername username: String) + func restoreTokenForUsername(_ username: String) -> String? + func restoreTokens() -> [(username: String, token: String)] + func removeAllTokens() +} diff --git a/ElementX/Sources/Modules/Authentication/LoginScreen/LoginScreenCoordinator.swift b/ElementX/Sources/Modules/Authentication/LoginScreen/LoginScreenCoordinator.swift new file mode 100644 index 000000000..d20888a80 --- /dev/null +++ b/ElementX/Sources/Modules/Authentication/LoginScreen/LoginScreenCoordinator.swift @@ -0,0 +1,66 @@ +// +// Copyright 2021 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 + +struct LoginScreenCoordinatorParameters { + +} + +final class LoginScreenCoordinator: Coordinator, Presentable { + + // MARK: - Properties + + // MARK: Private + + private let parameters: LoginScreenCoordinatorParameters + private let loginScreenHostingController: UIViewController + private var loginScreenViewModel: LoginScreenViewModelProtocol + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + var completion: ((LoginScreenViewModelResult) -> Void)? + + // MARK: - Setup + + @available(iOS 14.0, *) + init(parameters: LoginScreenCoordinatorParameters) { + self.parameters = parameters + + loginScreenViewModel = LoginScreenViewModel() + let view = LoginScreen(context: loginScreenViewModel.context) + + loginScreenHostingController = UIHostingController(rootView: view) + loginScreenHostingController.isModalInPresentation = true + + loginScreenViewModel.completion = { [weak self] result in + MXLog.debug("[LoginScreenCoordinator] LoginScreenViewModel did complete with result: \(result).") + guard let self = self else { return } + self.completion?(result) + } + } + + // MARK: - Public + func start() { + + } + + func toPresentable() -> UIViewController { + return self.loginScreenHostingController + } +} diff --git a/ElementX/Sources/Modules/Authentication/LoginScreen/LoginScreenModels.swift b/ElementX/Sources/Modules/Authentication/LoginScreen/LoginScreenModels.swift new file mode 100644 index 000000000..46ef0e2c3 --- /dev/null +++ b/ElementX/Sources/Modules/Authentication/LoginScreen/LoginScreenModels.swift @@ -0,0 +1,44 @@ +// +// Copyright 2021 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 + +enum LoginScreenViewModelResult { + case login((username: String, password: String)) +} + +enum LoginScreenViewAction { + case login +} + +struct LoginScreenViewState: BindableState { + var bindings: LoginScreenViewStateBindings +} + +struct LoginScreenViewStateBindings { + var username: String + var password: String +} + +struct LoginScreenErrorAlertInfo: Identifiable { + enum AlertType { + case genericFailure + } + + let id: AlertType + let title: String + let subtitle: String +} diff --git a/ElementX/Sources/Modules/Authentication/LoginScreen/LoginScreenViewModel.swift b/ElementX/Sources/Modules/Authentication/LoginScreen/LoginScreenViewModel.swift new file mode 100644 index 000000000..0e9f99631 --- /dev/null +++ b/ElementX/Sources/Modules/Authentication/LoginScreen/LoginScreenViewModel.swift @@ -0,0 +1,49 @@ +// +// Copyright 2021 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 + +@available(iOS 14, *) +typealias LoginScreenViewModelType = StateStoreViewModel +@available(iOS 14, *) +class LoginScreenViewModel: LoginScreenViewModelType, LoginScreenViewModelProtocol { + + // MARK: - Properties + + // MARK: Private + + // MARK: Public + + var completion: ((LoginScreenViewModelResult) -> Void)? + + // MARK: - Setup + + init() { + super.init(initialViewState: LoginScreenViewState(bindings: LoginScreenViewStateBindings(username: "@stefan.ceriu-element01:matrix.org", + password: "radeon"))) + } + + // MARK: - Public + + override func process(viewAction: LoginScreenViewAction) { + switch viewAction { + case .login: + completion?(.login((username: context.username, password: context.password))) + } + } +} diff --git a/ElementX/Sources/Modules/Authentication/LoginScreen/LoginScreenViewModelProtocol.swift b/ElementX/Sources/Modules/Authentication/LoginScreen/LoginScreenViewModelProtocol.swift new file mode 100644 index 000000000..5f7c4f418 --- /dev/null +++ b/ElementX/Sources/Modules/Authentication/LoginScreen/LoginScreenViewModelProtocol.swift @@ -0,0 +1,24 @@ +// +// Copyright 2021 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 + +protocol LoginScreenViewModelProtocol { + + var completion: ((LoginScreenViewModelResult) -> Void)? { get set } + @available(iOS 14, *) + var context: LoginScreenViewModelType.Context { get } +} diff --git a/ElementX/Sources/Modules/Authentication/LoginScreen/Test/UI/LoginScreenUITests.swift b/ElementX/Sources/Modules/Authentication/LoginScreen/Test/UI/LoginScreenUITests.swift new file mode 100644 index 000000000..ee547b6a1 --- /dev/null +++ b/ElementX/Sources/Modules/Authentication/LoginScreen/Test/UI/LoginScreenUITests.swift @@ -0,0 +1,45 @@ +// +// Copyright 2021 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 +import RiotSwiftUI + +@available(iOS 14.0, *) +class LoginScreenUITests: MockScreenTest { + + override class var screenType: MockScreenState.Type { + return MockLoginScreenScreenState.self + } + + override class func createTest() -> MockScreenTest { + return LoginScreenUITests(selector: #selector(verifyLoginScreenScreen)) + } + + func verifyLoginScreenScreen() throws { + guard let screenState = screenState as? MockLoginScreenScreenState else { fatalError("no screen") } + switch screenState { + case .promptType(let promptType): + verifyLoginScreenPromptType(promptType: promptType) + } + } + + func verifyLoginScreenPromptType(promptType: LoginScreenPromptType) { + let title = app.staticTexts["title"] + XCTAssert(title.exists) + XCTAssertEqual(title.label, promptType.title) + } + +} diff --git a/ElementX/Sources/Modules/Authentication/LoginScreen/Test/Unit/LoginScreenViewModelTests.swift b/ElementX/Sources/Modules/Authentication/LoginScreen/Test/Unit/LoginScreenViewModelTests.swift new file mode 100644 index 000000000..4ec5e7773 --- /dev/null +++ b/ElementX/Sources/Modules/Authentication/LoginScreen/Test/Unit/LoginScreenViewModelTests.swift @@ -0,0 +1,31 @@ +// +// Copyright 2021 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 + +@available(iOS 14.0, *) +class LoginScreenViewModelTests: XCTestCase { + + override func setUpWithError() throws { + + } + + func testInitialState() { + + } +} diff --git a/ElementX/Sources/Modules/Authentication/LoginScreen/View/LoginScreen.swift b/ElementX/Sources/Modules/Authentication/LoginScreen/View/LoginScreen.swift new file mode 100644 index 000000000..3ead27259 --- /dev/null +++ b/ElementX/Sources/Modules/Authentication/LoginScreen/View/LoginScreen.swift @@ -0,0 +1,52 @@ +// +// Copyright 2021 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 + +@available(iOS 14.0, *) +struct LoginScreen: View { + + @ObservedObject var context: LoginScreenViewModel.Context + + var body: some View { + NavigationView { + VStack { + TextField("Username", text: $context.username) + .textFieldStyle(.roundedBorder) + SecureField("Enter a password", text: $context.password) + .textFieldStyle(.roundedBorder) + + Button { context.send(viewAction: .login) } label: { + Text("Login") + } + .buttonStyle(.borderedProminent) + .padding(.horizontal, 50) + } + .padding(.horizontal, 8.0) + .navigationTitle("Login") + .navigationBarTitleDisplayMode(.inline) + } + } +} + +// MARK: - Previews + +struct LoginScreen_Previews: PreviewProvider { + static var previews: some View { + let viewModel = LoginScreenViewModel() + LoginScreen(context: viewModel.context) + } +} diff --git a/ElementX/Sources/Modules/Authentication/UserSession.swift b/ElementX/Sources/Modules/Authentication/UserSession.swift new file mode 100644 index 000000000..9619201e1 --- /dev/null +++ b/ElementX/Sources/Modules/Authentication/UserSession.swift @@ -0,0 +1,44 @@ +// +// UserSession.swift +// ElementX +// +// Created by Stefan Ceriu on 14.02.2022. +// + +import Foundation +import MatrixRustSDK + +class UserSession { + + private let client: Client + + init(client: Client) { + self.client = client + + if !client.hasFirstSynced() { + MXLog.info("Started initial sync") + client.startSync() + MXLog.info("Finished intial sync") + } + } + + func roomList() -> [RoomModel] { + client.conversations().compactMap { room in + do { + return RoomModel(displayName: try room.displayName()) + } catch { + MXLog.error("Failed retrieving room info with error: \(error)") + return nil + } + } + } + + var displayName: String? { + do { + return try client.displayName() + } catch { + MXLog.error("Failed retrieving room info with error: \(error)") + return nil + } + } +} diff --git a/ElementX/Sources/Modules/HomeScreen/HomeScreenCoordinator.swift b/ElementX/Sources/Modules/HomeScreen/HomeScreenCoordinator.swift new file mode 100644 index 000000000..6bfec026c --- /dev/null +++ b/ElementX/Sources/Modules/HomeScreen/HomeScreenCoordinator.swift @@ -0,0 +1,64 @@ +// +// Copyright 2021 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 + +struct HomeScreenCoordinatorParameters { + let userSession: UserSession +} + +final class HomeScreenCoordinator: Coordinator, Presentable { + + // MARK: - Properties + + // MARK: Private + + private let parameters: HomeScreenCoordinatorParameters + private let homeScreenHostingController: UIViewController + private var homeScreenViewModel: HomeScreenViewModelProtocol + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + var completion: ((HomeScreenViewModelResult) -> Void)? + + // MARK: - Setup + + @available(iOS 14.0, *) + init(parameters: HomeScreenCoordinatorParameters) { + self.parameters = parameters + + let viewModel = HomeScreenViewModel(username: self.parameters.userSession.displayName ?? "💥") + let view = HomeScreen(context: viewModel.context) + homeScreenViewModel = viewModel + homeScreenHostingController = UIHostingController(rootView: view) + + homeScreenViewModel.completion = { [weak self] result in + guard let self = self else { return } + self.completion?(result) + } + } + + // MARK: - Public + func start() { + + } + + func toPresentable() -> UIViewController { + return self.homeScreenHostingController + } +} diff --git a/ElementX/Sources/Modules/HomeScreen/HomeScreenModels.swift b/ElementX/Sources/Modules/HomeScreen/HomeScreenModels.swift new file mode 100644 index 000000000..0522ada4d --- /dev/null +++ b/ElementX/Sources/Modules/HomeScreen/HomeScreenModels.swift @@ -0,0 +1,31 @@ +// +// Copyright 2021 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 + +enum HomeScreenViewModelResult { + case logout +} + +// MARK: View + +struct HomeScreenViewState: BindableState { + let username: String +} + +enum HomeScreenViewAction { + case logout +} diff --git a/ElementX/Sources/Modules/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Modules/HomeScreen/HomeScreenViewModel.swift new file mode 100644 index 000000000..fda288ab6 --- /dev/null +++ b/ElementX/Sources/Modules/HomeScreen/HomeScreenViewModel.swift @@ -0,0 +1,45 @@ +// +// Copyright 2021 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 + +@available(iOS 14, *) +typealias HomeScreenViewModelType = StateStoreViewModel +@available(iOS 14, *) +class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol { + + // MARK: - Properties + + // MARK: Private + + // MARK: Public + + var completion: ((HomeScreenViewModelResult) -> Void)? + + // MARK: - Setup + + init(username: String) { + super.init(initialViewState: HomeScreenViewState(username: username)) + } + + // MARK: - Public + + override func process(viewAction: HomeScreenViewAction) { + + } +} diff --git a/ElementX/Sources/Modules/HomeScreen/HomeScreenViewModelProtocol.swift b/ElementX/Sources/Modules/HomeScreen/HomeScreenViewModelProtocol.swift new file mode 100644 index 000000000..45d1bd509 --- /dev/null +++ b/ElementX/Sources/Modules/HomeScreen/HomeScreenViewModelProtocol.swift @@ -0,0 +1,24 @@ +// +// Copyright 2021 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 + +protocol HomeScreenViewModelProtocol { + + var completion: ((HomeScreenViewModelResult) -> Void)? { get set } + @available(iOS 14, *) + var context: HomeScreenViewModelType.Context { get } +} diff --git a/ElementX/Sources/Modules/HomeScreen/Test/UI/HomeScreenUITests.swift b/ElementX/Sources/Modules/HomeScreen/Test/UI/HomeScreenUITests.swift new file mode 100644 index 000000000..9eab6c1fa --- /dev/null +++ b/ElementX/Sources/Modules/HomeScreen/Test/UI/HomeScreenUITests.swift @@ -0,0 +1,45 @@ +// +// Copyright 2021 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 +import ElementX + +@available(iOS 14.0, *) +class HomeScreenUITests: MockScreenTest { + + override class var screenType: MockScreenState.Type { + return MockHomeScreenScreenState.self + } + + override class func createTest() -> MockScreenTest { + return HomeScreenUITests(selector: #selector(verifyHomeScreenScreen)) + } + + func verifyHomeScreenScreen() throws { + guard let screenState = screenState as? MockHomeScreenScreenState else { fatalError("no screen") } + switch screenState { + case .promptType(let promptType): + verifyHomeScreenPromptType(promptType: promptType) + } + } + + func verifyHomeScreenPromptType(promptType: HomeScreenPromptType) { + let title = app.staticTexts["title"] + XCTAssert(title.exists) + XCTAssertEqual(title.label, promptType.title) + } + +} diff --git a/ElementX/Sources/Modules/HomeScreen/Test/Unit/HomeScreenViewModelTests.swift b/ElementX/Sources/Modules/HomeScreen/Test/Unit/HomeScreenViewModelTests.swift new file mode 100644 index 000000000..2a8aa1f11 --- /dev/null +++ b/ElementX/Sources/Modules/HomeScreen/Test/Unit/HomeScreenViewModelTests.swift @@ -0,0 +1,30 @@ +// +// Copyright 2021 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 + +@available(iOS 14.0, *) +class HomeScreenViewModelTests: XCTestCase { + override func setUpWithError() throws { + + } + + func testInitialState() { + + } +} diff --git a/ElementX/Sources/Modules/HomeScreen/View/HomeScreen.swift b/ElementX/Sources/Modules/HomeScreen/View/HomeScreen.swift new file mode 100644 index 000000000..87fae221c --- /dev/null +++ b/ElementX/Sources/Modules/HomeScreen/View/HomeScreen.swift @@ -0,0 +1,41 @@ +// +// Copyright 2021 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 + +@available(iOS 14.0, *) +struct HomeScreen: View { + + @ObservedObject var context: HomeScreenViewModel.Context + + // MARK: Views + + var body: some View { + VStack { + Text("Hello, \(context.viewState.username)!") + } + } +} + +// MARK: - Previews + +@available(iOS 14.0, *) +struct HomeScreen_Previews: PreviewProvider { + static var previews: some View { + let viewModel = HomeScreenViewModel(username: "Johnny Appleseed") + HomeScreen(context: viewModel.context) + } +} diff --git a/ElementX/Sources/Modules/Models/RoomModel.swift b/ElementX/Sources/Modules/Models/RoomModel.swift new file mode 100644 index 000000000..f90f79bba --- /dev/null +++ b/ElementX/Sources/Modules/Models/RoomModel.swift @@ -0,0 +1,13 @@ +// +// RoomModel.swift +// ElementX +// +// Created by Stefan Ceriu on 14.02.2022. +// + +import Foundation +import UIKit + +struct RoomModel { + let displayName: String +} diff --git a/ElementX/Sources/Modules/Other/Coordinator.swift b/ElementX/Sources/Modules/Other/Coordinator.swift new file mode 100755 index 000000000..3345c2f94 --- /dev/null +++ b/ElementX/Sources/Modules/Other/Coordinator.swift @@ -0,0 +1,51 @@ +/* + Copyright 2019 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 UIKit + +/// Protocol describing a [Coordinator](http://khanlou.com/2015/10/coordinators-redux/). +/// Coordinators are the objects which control the navigation flow of the application. +/// It helps to isolate and reuse view controllers and pass dependencies down the navigation hierarchy. +protocol Coordinator: AnyObject { + + /// Starts job of the coordinator. + func start() + + /// Child coordinators to retain. Prevent them from getting deallocated. + var childCoordinators: [Coordinator] { get set } + + /// Stores coordinator to the `childCoordinators` array. + /// + /// - Parameter childCoordinator: Child coordinator to store. + func add(childCoordinator: Coordinator) + + /// Remove coordinator from the `childCoordinators` array. + /// + /// - Parameter childCoordinator: Child coordinator to remove. + func remove(childCoordinator: Coordinator) +} + +// `Coordinator` default implementation +extension Coordinator { + + func add(childCoordinator coordinator: Coordinator) { + childCoordinators.append(coordinator) + } + + func remove(childCoordinator: Coordinator) { + self.childCoordinators = self.childCoordinators.filter { $0 !== childCoordinator } + } +} diff --git a/ElementX/Sources/Modules/Other/MXLog.swift b/ElementX/Sources/Modules/Other/MXLog.swift new file mode 100644 index 000000000..0afea7e62 --- /dev/null +++ b/ElementX/Sources/Modules/Other/MXLog.swift @@ -0,0 +1,179 @@ +// +// Copyright 2021 The Matrix.org Foundation C.I.C +// +// 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 +import SwiftyBeaver + +/// Various MXLog configuration options. Used in conjunction with `MXLog.configure()` +@objc public class MXLogConfiguration: NSObject { + + /// the desired log level. `.verbose` by default. + @objc public var logLevel: MXLogLevel = MXLogLevel.verbose + + /// whether logs should be written directly to files. `false` by default. + @objc public var redirectLogsToFiles: Bool = false + + /// the maximum total space to use for log files in bytes. `100MB` by default. + @objc public var logFilesSizeLimit: UInt = 100 * 1024 * 1024 // 100MB + + /// the maximum number of log files to use before rolling. `50` by default. + @objc public var maxLogFilesCount: UInt = 50 + + /// the subname for log files. Files will be named as 'console-[subLogName].log'. `nil` by default + @objc public var subLogName: String? +} + +/// MXLog logging levels. Use .none to disable logging entirely. +@objc public enum MXLogLevel: UInt { + case none + case verbose + case debug + case info + case warning + case error +} + +private var logger: SwiftyBeaver.Type = { + let logger = SwiftyBeaver.self + MXLog.configureLogger(logger, withConfiguration: MXLogConfiguration()) + return logger +}() + +/** + Logging utility that provies multiple logging levels as well as file output and rolling. + Its purpose is to provide a common entry for customizing logging and should be used throughout the code. + Please see `MXLog.h` for Objective-C options. + */ +@objc public class MXLog: NSObject { + + /// Method used to customize MXLog's behavior. + /// Called automatically when first accessing the logger with the default values. + /// Please see `MXLogConfiguration` for all available options. + /// - Parameters: + /// - configuration: the `MXLogConfiguration` instance to use + @objc static public func configure(_ configuration: MXLogConfiguration) { + configureLogger(logger, withConfiguration: configuration) + } + + public static func verbose(_ message: @autoclosure () -> Any, + _ file: String = #file, + _ function: String = #function, + line: Int = #line, + context: Any? = nil) { + logger.verbose(message(), file, function, line: line, context: context) + } + + @available(swift, obsoleted: 5.4) + @objc public static func logVerbose(_ message: String, file: String, function: String, line: Int) { + logger.verbose(message, file, function, line: line) + } + + public static func debug(_ message: @autoclosure () -> Any, + _ file: String = #file, + _ function: String = #function, + line: Int = #line, + context: Any? = nil) { + logger.debug(message(), file, function, line: line, context: context) + } + + @available(swift, obsoleted: 5.4) + @objc public static func logDebug(_ message: String, file: String, function: String, line: Int) { + logger.debug(message, file, function, line: line) + } + + public static func info(_ message: @autoclosure () -> Any, + _ file: String = #file, + _ function: String = #function, + line: Int = #line, + context: Any? = nil) { + logger.info(message(), file, function, line: line, context: context) + } + + @available(swift, obsoleted: 5.4) + @objc public static func logInfo(_ message: String, file: String, function: String, line: Int) { + logger.info(message, file, function, line: line) + } + + public static func warning(_ message: @autoclosure () -> Any, _ + file: String = #file, + _ function: String = #function, + line: Int = #line, + context: Any? = nil) { + logger.warning(message(), file, function, line: line, context: context) + } + + @available(swift, obsoleted: 5.4) + @objc public static func logWarning(_ message: String, file: String, function: String, line: Int) { + logger.warning(message, file, function, line: line) + } + + public static func error(_ message: @autoclosure () -> Any, + _ file: String = #file, + _ function: String = #function, + line: Int = #line, + context: Any? = nil) { + logger.error(message(), file, function, line: line, context: context) + } + + @available(swift, obsoleted: 5.4) + @objc public static func logError(_ message: String, file: String, function: String, line: Int) { + logger.error(message, file, function, line: line) + } + + // MARK: - Private + + fileprivate static func configureLogger(_ logger: SwiftyBeaver.Type, withConfiguration configuration: MXLogConfiguration) { +// if let subLogName = configuration.subLogName { +// MXLogger.setSubLogName(subLogName) +// } +// +// MXLogger.redirectNSLog(toFiles: configuration.redirectLogsToFiles, +// numberOfFiles: configuration.maxLogFilesCount, +// sizeLimit: configuration.logFilesSizeLimit) +// + guard configuration.logLevel != .none else { + return + } + + let consoleDestination = ConsoleDestination() + consoleDestination.useNSLog = true + consoleDestination.asynchronously = false + consoleDestination.format = "$C $N.$F():$l $M" + consoleDestination.levelColor.verbose = "" + consoleDestination.levelColor.debug = "" + consoleDestination.levelColor.info = "" + consoleDestination.levelColor.warning = "⚠️" + consoleDestination.levelColor.error = "🚨" + + switch configuration.logLevel { + case .verbose: + consoleDestination.minLevel = .verbose + case .debug: + consoleDestination.minLevel = .debug + case .info: + consoleDestination.minLevel = .info + case .warning: + consoleDestination.minLevel = .warning + case .error: + consoleDestination.minLevel = .error + case .none: + break + } + + logger.removeAllDestinations() + logger.addDestination(consoleDestination) + } +} diff --git a/ElementX/Sources/Modules/Other/SwiftUI/ViewModel/BindableState.swift b/ElementX/Sources/Modules/Other/SwiftUI/ViewModel/BindableState.swift new file mode 100644 index 000000000..b5d2785d6 --- /dev/null +++ b/ElementX/Sources/Modules/Other/SwiftUI/ViewModel/BindableState.swift @@ -0,0 +1,37 @@ +// +// Copyright 2021 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 + +/// Represents a specific portion of the ViewState that can be bound to with SwiftUI's [2-way binding](https://developer.apple.com/documentation/swiftui/binding). +protocol BindableState { + /// The associated type of the Bindable State. Defaults to Void. + associatedtype BindStateType = Void + var bindings: BindStateType { get set } +} + +extension BindableState where BindStateType == Void { + /// We provide a default implementation for the Void type so that we can have `ViewState` that + /// just doesn't include/take advantage of the bindings. + var bindings: Void { + get { + + } + set { + fatalError("Can't bind to the default Void binding.") + } + } +} diff --git a/ElementX/Sources/Modules/Other/SwiftUI/ViewModel/StateStoreViewModel.swift b/ElementX/Sources/Modules/Other/SwiftUI/ViewModel/StateStoreViewModel.swift new file mode 100644 index 000000000..cc4a7dd33 --- /dev/null +++ b/ElementX/Sources/Modules/Other/SwiftUI/ViewModel/StateStoreViewModel.swift @@ -0,0 +1,140 @@ +// +// Copyright 2021 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 +import Combine + +/// A constrained and concise interface for interacting with the ViewModel. +/// +/// This class is closely bound to`StateStoreViewModel`. It provides the exact interface the view should need to interact +/// ViewModel (as modelled on our previous template architecture with the addition of two-way binding): +/// - The ability read/observe view state +/// - The ability to send view events +/// - The ability to bind state to a specific portion of the view state safely. +/// This class was brought about a little bit by necessity. The most idiomatic way of interacting with SwiftUI is via `@Published` +/// properties which which are property wrappers and therefore can't be defined within protocols. +/// A similar approach is taken in libraries like [CombineFeedback](https://github.com/sergdort/CombineFeedback). +/// It provides a nice layer of consistency and also safety. As we are not passing the `ViewModel` to the view directly, shortcuts/hacks +/// can't be made into the `ViewModel`. +@available(iOS 14, *) +@dynamicMemberLookup +class ViewModelContext: ObservableObject { + // MARK: - Properties + + // MARK: Private + + fileprivate let viewActions: PassthroughSubject + + // MARK: Public + + /// Get-able/Observable `Published` property for the `ViewState` + @Published fileprivate(set) var viewState: ViewState + + /// Set-able/Bindable access to the bindable state. + subscript(dynamicMember keyPath: WritableKeyPath) -> T { + get { viewState.bindings[keyPath: keyPath] } + set { viewState.bindings[keyPath: keyPath] = newValue } + } + + // MARK: Setup + + init(initialViewState: ViewState) { + self.viewActions = PassthroughSubject() + self.viewState = initialViewState + } + + // MARK: Public + + /// Send a `ViewAction` to the `ViewModel` for processing. + /// - Parameter viewAction: The `ViewAction` to send to the `ViewModel`. + func send(viewAction: ViewAction) { + viewActions.send(viewAction) + } +} + +/// A common ViewModel implementation for handling of `State`, `StateAction`s and `ViewAction`s +/// +/// Generic type State is constrained to the BindableState protocol in that it may contain (but doesn't have to) +/// a specific portion of state that can be safely bound to. +/// If we decide to add more features to our state management (like doing state processing off the main thread) +/// we can do it in this centralised place. +@available(iOS 14, *) +class StateStoreViewModel { + + typealias Context = ViewModelContext + + // MARK: - Properties + + // MARK: Public + + /// For storing subscription references. + /// + /// Left as public for `ViewModel` implementations convenience. + var cancellables = Set() + + /// Constrained interface for passing to Views. + var context: Context + + var state: State { + get { context.viewState } + set { context.viewState = newValue } + } + + // MARK: Setup + + init(initialViewState: State) { + self.context = Context(initialViewState: initialViewState) + self.context.viewActions.sink { [weak self] action in + guard let self = self else { return } + self.process(viewAction: action) + } + .store(in: &cancellables) + } + + /// Send state actions to modify the state within the reducer. + /// - Parameter action: The state action to send to the reducer. + @available(*, deprecated, message: "Mutate state directly instead") + func dispatch(action: StateAction) { + Self.reducer(state: &context.viewState, action: action) + } + + /// Send state actions from a publisher to modify the state within the reducer. + /// - Parameter actionPublisher: The publisher that produces actions to be sent to the reducer + @available(*, deprecated, message: "Mutate state directly instead") + func dispatch(actionPublisher: AnyPublisher) { + actionPublisher.sink { [weak self] action in + guard let self = self else { return } + Self.reducer(state: &self.context.viewState, action: action) + } + .store(in: &cancellables) + } + + /// Override to handle mutations to the `State` + /// + /// A redux style reducer, all modifications to state happen here. + /// - Parameters: + /// - state: The `inout` state to be modified, + /// - action: The action that defines which state modification should take place. + class func reducer(state: inout State, action: StateAction) { + // Default implementation, -no-op + } + + /// Override to handles incoming `ViewAction`s from the `ViewModel`. + /// - Parameter viewAction: The `ViewAction` to be processed in `ViewModel` implementation. + func process(viewAction: ViewAction) { + // Default implementation, -no-op + } +} diff --git a/ElementX/Sources/Modules/Other/WeakDictionary/WeakDictionary.swift b/ElementX/Sources/Modules/Other/WeakDictionary/WeakDictionary.swift new file mode 100644 index 000000000..d6cd8efbd --- /dev/null +++ b/ElementX/Sources/Modules/Other/WeakDictionary/WeakDictionary.swift @@ -0,0 +1,104 @@ +// +// WeakDictionary.swift +// WeakDictionary +// +// Created by Nicholas Cross on 19/10/2016. +// Copyright © 2016 Nicholas Cross. All rights reserved. +// + +import Foundation + +public struct WeakDictionary { + + private var storage: [Key: WeakDictionaryReference] + + public init() { + self.init(storage: [Key: WeakDictionaryReference]()) + } + + public init(dictionary: [Key: Value]) { + var newStorage = [Key: WeakDictionaryReference]() + dictionary.forEach({ key, value in newStorage[key] = WeakDictionaryReference(value: value) }) + self.init(storage: newStorage) + } + + private init(storage: [Key: WeakDictionaryReference]) { + self.storage = storage + } + + public mutating func reap() { + storage = weakDictionary().storage + } + + public func weakDictionary() -> WeakDictionary { + return self[startIndex ..< endIndex] + } + + public func dictionary() -> [Key: Value] { + var newStorage = [Key: Value]() + + storage.forEach { key, value in + if let retainedValue = value.value { + newStorage[key] = retainedValue + } + } + + return newStorage + } +} + +extension WeakDictionary: Collection { + + public typealias Index = DictionaryIndex> + + public var startIndex: Index { + return storage.startIndex + } + + public var endIndex: Index { + return storage.endIndex + } + + public func index(after index: Index) -> Index { + return storage.index(after: index) + } + + public subscript(position: Index) -> (Key, WeakDictionaryReference) { + return storage[position] + } + + public subscript(key: Key) -> Value? { + get { + guard let valueRef = storage[key] else { + return nil + } + + return valueRef.value + } + + set { + guard let value = newValue else { + storage[key] = nil + return + } + + storage[key] = WeakDictionaryReference(value: value) + } + } + + public subscript(bounds: Range) -> WeakDictionary { + let subStorage = storage[bounds.lowerBound ..< bounds.upperBound] + var newStorage = [Key: WeakDictionaryReference]() + + subStorage.filter { _, value in return value.value != nil } + .forEach { key, value in newStorage[key] = value } + + return WeakDictionary(storage: newStorage) + } +} + +extension Dictionary where Value: AnyObject { + public func weakDictionary() -> WeakDictionary { + return WeakDictionary(dictionary: self) + } +} diff --git a/ElementX/Sources/Modules/Other/WeakDictionary/WeakDictionaryKeyReference.swift b/ElementX/Sources/Modules/Other/WeakDictionary/WeakDictionaryKeyReference.swift new file mode 100644 index 000000000..6d74e6a41 --- /dev/null +++ b/ElementX/Sources/Modules/Other/WeakDictionary/WeakDictionaryKeyReference.swift @@ -0,0 +1,36 @@ +// +// WeakDictionaryKeyReference.swift +// WeakDictionary-iOS +// +// Created by Nicholas Cross on 2/1/19. +// Copyright © 2019 Nicholas Cross. All rights reserved. +// + +import Foundation + +public struct WeakDictionaryKey : Hashable { + + private weak var baseKey: Key? + private let hash: Int + private var retainedValue: Value? + private let nilKeyHash = UUID().hashValue + + public init(key: Key, value: Value? = nil) { + baseKey = key + retainedValue = value + hash = key.hashValue + } + + public static func == (lhs: WeakDictionaryKey, rhs: WeakDictionaryKey) -> Bool { + return (lhs.baseKey != nil && rhs.baseKey != nil && lhs.baseKey == rhs.baseKey) + || lhs.hashValue == rhs.hashValue + } + + public var hashValue: Int { + return baseKey != nil ? hash : nilKeyHash + } + + public var key: Key? { + return baseKey + } +} diff --git a/ElementX/Sources/Modules/Other/WeakDictionary/WeakDictionaryReference.swift b/ElementX/Sources/Modules/Other/WeakDictionary/WeakDictionaryReference.swift new file mode 100644 index 000000000..c1789da4f --- /dev/null +++ b/ElementX/Sources/Modules/Other/WeakDictionary/WeakDictionaryReference.swift @@ -0,0 +1,21 @@ +// +// WeakDictionaryReference.swift +// WeakDictionary-iOS +// +// Created by Nicholas Cross on 2/1/19. +// Copyright © 2019 Nicholas Cross. All rights reserved. +// + +import Foundation + +public struct WeakDictionaryReference { + private weak var referencedValue: Value? + + init(value: Value) { + referencedValue = value + } + + public var value: Value? { + return referencedValue + } +} diff --git a/ElementX/Sources/Modules/Other/WeakDictionary/WeakKeyDictionary.swift b/ElementX/Sources/Modules/Other/WeakDictionary/WeakKeyDictionary.swift new file mode 100644 index 000000000..7d804c161 --- /dev/null +++ b/ElementX/Sources/Modules/Other/WeakDictionary/WeakKeyDictionary.swift @@ -0,0 +1,124 @@ +// +// WeakKeyDictionary.swift +// WeakDictionary-iOS +// +// Created by Nicholas Cross on 2/1/19. +// Copyright © 2019 Nicholas Cross. All rights reserved. +// + +import Foundation + +public struct WeakKeyDictionary { + + private var storage: WeakDictionary, Value> + private let valuesRetainedByKey: Bool + + public init(valuesRetainedByKey: Bool = false) { + self.init( + storage: WeakDictionary, Value>(), + valuesRetainedByKey: valuesRetainedByKey + ) + } + + public init(dictionary: [Key: Value], valuesRetainedByKey: Bool = false) { + var newStorage = WeakDictionary, Value>() + + dictionary.forEach { key, value in + var keyRef: WeakDictionaryKey! + + if valuesRetainedByKey { + keyRef = WeakDictionaryKey(key: key, value: value) + } else { + keyRef = WeakDictionaryKey(key: key) + } + + newStorage[keyRef] = value + } + + self.init(storage: newStorage, valuesRetainedByKey: valuesRetainedByKey) + } + + private init(storage: WeakDictionary, Value>, valuesRetainedByKey: Bool = false) { + self.storage = storage + self.valuesRetainedByKey = valuesRetainedByKey + } + + public mutating func reap() { + storage = weakKeyDictionary().storage + } + + public func weakDictionary() -> WeakDictionary { + return dictionary().weakDictionary() + } + + public func weakKeyDictionary() -> WeakKeyDictionary { + return self[startIndex ..< endIndex] + } + + public func dictionary() -> [Key: Value] { + var newStorage = [Key: Value]() + + storage.forEach { key, value in + if let retainedKey = key.key, let retainedValue = value.value { + newStorage[retainedKey] = retainedValue + } + } + + return newStorage + } +} + +extension WeakKeyDictionary: Collection { + + public typealias Index = DictionaryIndex, WeakDictionaryReference> + + public var startIndex: Index { + return storage.startIndex + } + + public var endIndex: Index { + return storage.endIndex + } + + public func index(after index: Index) -> Index { + return storage.index(after: index) + } + + public subscript(position: Index) -> (WeakDictionaryKey, WeakDictionaryReference) { + return storage[position] + } + + public subscript(key: Key) -> Value? { + get { + return storage[WeakDictionaryKey(key: key)] + } + + set { + let retainedValue = valuesRetainedByKey ? newValue : nil + let weakKey = WeakDictionaryKey(key: key, value: retainedValue) + storage[weakKey] = newValue + } + } + + public subscript(bounds: Range) -> WeakKeyDictionary { + let subStorage = storage[bounds.lowerBound ..< bounds.upperBound] + var newStorage = WeakDictionary, Value>() + + subStorage.filter { key, value in return key.key != nil && value.value != nil } + .forEach { key, value in newStorage[key] = value.value } + + return WeakKeyDictionary(storage: newStorage) + } +} + +extension WeakDictionary where Key: AnyObject { + public func weakKeyDictionary(valuesRetainedByKey: Bool = false) -> WeakKeyDictionary { + return WeakKeyDictionary(dictionary: dictionary(), valuesRetainedByKey: valuesRetainedByKey) + } +} + +extension Dictionary where Key: AnyObject, Value: AnyObject { + public func weakKeyDictionary(valuesRetainedByKey: Bool = false) -> WeakKeyDictionary { + return WeakKeyDictionary(dictionary: self, valuesRetainedByKey: valuesRetainedByKey) + } +} diff --git a/ElementX/Sources/Modules/Routers/NavigationModule.swift b/ElementX/Sources/Modules/Routers/NavigationModule.swift new file mode 100644 index 000000000..9ad20d3cb --- /dev/null +++ b/ElementX/Sources/Modules/Routers/NavigationModule.swift @@ -0,0 +1,36 @@ +// +// Copyright 2021 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 + +/// Structure used to pass modules to routers with pop completion blocks. +struct NavigationModule { + /// Actual presentable of the module + let presentable: Presentable + + /// Block to be called when the module is popped + let popCompletion: (() -> Void)? +} + +// MARK: - CustomStringConvertible + +extension NavigationModule: CustomStringConvertible { + + var description: String { + return "NavigationModule: \(presentable), pop completion: \(String(describing: popCompletion))" + } + +} diff --git a/ElementX/Sources/Modules/Routers/NavigationRouter.swift b/ElementX/Sources/Modules/Routers/NavigationRouter.swift new file mode 100755 index 000000000..974f935b8 --- /dev/null +++ b/ElementX/Sources/Modules/Routers/NavigationRouter.swift @@ -0,0 +1,405 @@ +/* + Copyright 2019 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 UIKit + +/// `NavigationRouter` is a concrete implementation of NavigationRouterType. +final class NavigationRouter: NSObject, NavigationRouterType { + + // MARK: - Properties + + // MARK: Private + + private var completions: [UIViewController : () -> Void] + private let navigationController: UINavigationController + + /// Stores the association between the added Presentable and his view controller. + /// They can be the same if the controller is not added via his Coordinator or it is a simple UIViewController. + private var storedModules = WeakDictionary() + + // MARK: Public + + /// Returns the presentables associated to each view controller + var modules: [Presentable] { + return self.viewControllers.map { (viewController) -> Presentable in + return self.module(for: viewController) + } + } + + /// Return the view controllers stack + var viewControllers: [UIViewController] { + return navigationController.viewControllers + } + + // MARK: - Setup + + init(navigationController: UINavigationController) { + self.navigationController = navigationController + self.completions = [:] + super.init() + self.navigationController.delegate = self + + // Post local notification on NavigationRouter creation + let userInfo: [String: Any] = [NavigationRouter.NotificationUserInfoKey.navigationRouter: self, + NavigationRouter.NotificationUserInfoKey.navigationController: navigationController] + NotificationCenter.default.post(name: NavigationRouter.didCreate, object: self, userInfo: userInfo) + } + + deinit { + // Post local notification on NavigationRouter deinit + let userInfo: [String: Any] = [NavigationRouter.NotificationUserInfoKey.navigationRouter: self, + NavigationRouter.NotificationUserInfoKey.navigationController: navigationController] + NotificationCenter.default.post(name: NavigationRouter.willDestroy, object: self, userInfo: userInfo) + } + + // MARK: - Public + + func present(_ module: Presentable, animated: Bool = true) { + MXLog.debug("[NavigationRouter] Present \(module)") + navigationController.present(module.toPresentable(), animated: animated, completion: nil) + } + + func dismissModule(animated: Bool = true, completion: (() -> Void)? = nil) { + MXLog.debug("[NavigationRouter] Dismiss presented module") + navigationController.dismiss(animated: animated, completion: completion) + } + + func setRootModule(_ module: Presentable, hideNavigationBar: Bool = false, animated: Bool = false, popCompletion: (() -> Void)? = nil) { + MXLog.debug("[NavigationRouter] Set root module \(module)") + + let controller = module.toPresentable() + + // Avoid setting a UINavigationController onto stack + guard controller is UINavigationController == false else { + MXLog.error("Cannot add a UINavigationController to NavigationRouter") + return + } + + self.addModule(module, for: controller) + + let controllersToPop = self.navigationController.viewControllers.reversed() + + controllersToPop.forEach { + self.willPopViewController($0) + } + + if let popCompletion = popCompletion { + completions[controller] = popCompletion + } + + self.willPushViewController(controller) + + navigationController.setViewControllers([controller], animated: animated) + navigationController.isNavigationBarHidden = hideNavigationBar + + // Pop old view controllers + controllersToPop.forEach { + self.didPopViewController($0) + } + + // Add again controller to module association, in case same module instance is added back + self.addModule(module, for: controller) + + self.didPushViewController(controller) + } + + func setModules(_ modules: [NavigationModule], hideNavigationBar: Bool, animated: Bool) { + + MXLog.debug("[NavigationRouter] Set modules \(modules)") + + let controllers = modules.map { (module) -> UIViewController in + let controller = module.presentable.toPresentable() + self.addModule(module.presentable, for: controller) + return controller + } + + let controllersToPop = self.navigationController.viewControllers.reversed() + + controllersToPop.forEach { + self.willPopViewController($0) + } + + controllers.forEach { + self.willPushViewController($0) + } + + // Set new view controllers + navigationController.setViewControllers(controllers, animated: animated) + navigationController.isNavigationBarHidden = hideNavigationBar + + // Pop old view controllers + controllersToPop.forEach { + self.didPopViewController($0) + } + + // Add again controller to module association, in case same modules instance are added back + modules.forEach { (module) in + self.addModule(module.presentable, for: module.presentable.toPresentable()) + } + + controllers.forEach { + self.didPushViewController($0) + } + } + + func popToRootModule(animated: Bool) { + MXLog.debug("[NavigationRouter] Pop to root module") + + let controllers = self.navigationController.viewControllers + + if controllers.count > 1 { + let controllersToPop = controllers[1.. Void)? = nil) { + MXLog.debug("[NavigationRouter] Push module \(module)") + + let controller = module.toPresentable() + + // Avoid pushing UINavigationController onto stack + guard controller is UINavigationController == false else { + MXLog.error("Cannot push a UINavigationController to NavigationRouter") + return + } + + self.addModule(module, for: controller) + + if let completion = popCompletion { + completions[controller] = completion + } + + self.willPushViewController(controller) + + navigationController.pushViewController(controller, animated: animated) + + self.didPushViewController(controller) + } + + func push(_ modules: [NavigationModule], animated: Bool) { + MXLog.debug("[NavigationRouter] Push modules \(modules)") + + // Avoid pushing any UINavigationController onto stack + guard modules.first(where: { $0.presentable.toPresentable() is UINavigationController }) == nil else { + MXLog.error("Cannot push a UINavigationController to NavigationRouter") + return + } + + for module in modules { + let controller = module.presentable.toPresentable() + self.addModule(module.presentable, for: controller) + + if let completion = module.popCompletion { + completions[controller] = completion + } + + self.willPushViewController(controller) + } + + var viewControllers = navigationController.viewControllers + viewControllers.append(contentsOf: modules.map({ $0.presentable.toPresentable() })) + navigationController.setViewControllers(viewControllers, animated: animated) + + for module in modules { + let controller = module.presentable.toPresentable() + self.didPushViewController(controller) + } + } + + func popModule(animated: Bool = true) { + MXLog.debug("[NavigationRouter] Pop module") + + if let lastController = navigationController.viewControllers.last { + self.willPopViewController(lastController) + } + + if let controller = navigationController.popViewController(animated: animated) { + self.didPopViewController(controller) + } + } + + func popAllModules(animated: Bool) { + MXLog.debug("[NavigationRouter] Pop all modules") + + let controllersToPop = self.navigationController.viewControllers.reversed() + + controllersToPop.forEach { + self.willPopViewController($0) + } + + navigationController.setViewControllers([], animated: animated) + + controllersToPop.forEach { + self.didPopViewController($0) + } + } + + func contains(_ module: Presentable) -> Bool { + + let controller = module.toPresentable() + return self.navigationController.viewControllers.contains(controller) + } + + // MARK: Presentable + + func toPresentable() -> UIViewController { + return navigationController + } + + // MARK: - Private + + private func module(for viewController: UIViewController) -> Presentable { + + guard let module = self.storedModules[viewController] as? Presentable else { + return viewController + } + return module + } + + private func addModule(_ module: Presentable, for viewController: UIViewController) { + self.storedModules[viewController] = module as AnyObject + } + + private func removeModule(for viewController: UIViewController) { + self.storedModules[viewController] = nil + } + + private func runCompletion(for controller: UIViewController) { + guard let completion = completions[controller] else { + return + } + completion() + completions.removeValue(forKey: controller) + } + + private func willPushViewController(_ viewController: UIViewController) { + self.postNotification(withName: NavigationRouter.willPushModule, for: viewController) + } + + private func didPushViewController(_ viewController: UIViewController) { + self.postNotification(withName: NavigationRouter.didPushModule, for: viewController) + } + + private func willPopViewController(_ viewController: UIViewController) { + self.postNotification(withName: NavigationRouter.willPopModule, for: viewController) + } + + private func didPopViewController(_ viewController: UIViewController) { + self.postNotification(withName: NavigationRouter.didPopModule, for: viewController) + + // Call completion closure associated to the view controller + // So associated coordinator can be deallocated + runCompletion(for: viewController) + + self.removeModule(for: viewController) + } + + private func postNotification(withName name: Notification.Name, for viewController: UIViewController) { + + let module = self.module(for: viewController) + + let userInfo: [String: Any] = [ + NotificationUserInfoKey.navigationRouter: self, + NotificationUserInfoKey.module: module, + NotificationUserInfoKey.viewController: viewController + ] + NotificationCenter.default.post(name: name, object: self, userInfo: userInfo) + } +} + +// MARK: - UINavigationControllerDelegate +extension NavigationRouter: UINavigationControllerDelegate { + + func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { + + // TODO: Try to post `NavigationRouter.willPopModule` notification here + } + + func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { + + // Ensure the view controller is popping + guard let poppedViewController = navigationController.transitionCoordinator?.viewController(forKey: .from), + !navigationController.viewControllers.contains(poppedViewController) else { + return + } + + MXLog.debug("[NavigationRouter] Popped module: \(poppedViewController)") + + self.didPopViewController(poppedViewController) + } +} + +// MARK: - NavigationRouter notification constants +extension NavigationRouter { + + // MARK: Notification names + + public static let willPushModule = Notification.Name("NavigationRouterWillPushModule") + public static let didPushModule = Notification.Name("NavigationRouterDidPushModule") + public static let willPopModule = Notification.Name("NavigationRouterWillPopModule") + public static let didPopModule = Notification.Name("NavigationRouterDidPopModule") + + public static let didCreate = Notification.Name("NavigationRouterDidCreate") + public static let willDestroy = Notification.Name("NavigationRouterWillDestroy") + + // MARK: Notification keys + + public struct NotificationUserInfoKey { + + /// The associated view controller (UIViewController). + static let viewController = "viewController" + + /// The associated module (Presentable), can the view controller itself or is Coordinator + static let module = "module" + + /// The navigation router that send the notification (NavigationRouterType) + static let navigationRouter = "navigationRouter" + + /// The navigation controller (UINavigationController) associated to the navigation router + static let navigationController = "navigationController" + } +} diff --git a/ElementX/Sources/Modules/Routers/NavigationRouterStore.swift b/ElementX/Sources/Modules/Routers/NavigationRouterStore.swift new file mode 100644 index 000000000..8101eeed1 --- /dev/null +++ b/ElementX/Sources/Modules/Routers/NavigationRouterStore.swift @@ -0,0 +1,96 @@ +// +// Copyright 2021 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 UIKit + +/// `NavigationRouterStore` enables to get a NavigationRouter from a UINavigationController instance. +class NavigationRouterStore: NavigationRouterStoreProtocol { + + // MARK: - Constants + + static let shared = NavigationRouterStore() + + // MARK: - Properties + + // FIXME: WeakDictionary does not work with protocol + // Find a way to use NavigationRouterType as value + private var navigationRouters = WeakDictionary() + + // MARK: - Setup + + /// As we are ensuring that there is only one navigation controller per NavigationRouter, the class here should be used as a singleton. + private init() { + self.registerNavigationRouterNotifications() + } + + // MARK: - Public + + func navigationRouter(for navigationController: UINavigationController) -> NavigationRouterType { + + if let existingNavigationRouter = self.findNavigationRouter(for: navigationController) { + return existingNavigationRouter + } + + let navigationRouter = NavigationRouter(navigationController: UINavigationController()) + return navigationRouter + } + + // MARK: - Private + + private func findNavigationRouter(for navigationController: UINavigationController) -> NavigationRouterType? { + return self.navigationRouters[navigationController] + } + + private func removeNavigationRouter(for navigationController: UINavigationController) { + self.navigationRouters[navigationController] = nil + } + + private func registerNavigationRouterNotifications() { + NotificationCenter.default.addObserver(self, selector: #selector(navigationRouterDidCreate(_:)), name: NavigationRouter.didCreate, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(navigationRouterWillDestroy(_:)), name: NavigationRouter.willDestroy, object: nil) + } + + @objc private func navigationRouterDidCreate(_ notification: Notification) { + + guard let userInfo = notification.userInfo, + let navigationRouter = userInfo[NavigationRouter.NotificationUserInfoKey.navigationRouter] as? NavigationRouterType, + let navigationController = userInfo[NavigationRouter.NotificationUserInfoKey.navigationController] as? UINavigationController else { + return + } + + if let existingNavigationRouter = self.findNavigationRouter(for: navigationController) { + fatalError("\(existingNavigationRouter) is already tied to the same navigation controller as \(navigationRouter). We should have only one NavigationRouter per navigation controller") + } else { + // FIXME: WeakDictionary does not work with protocol + // Find a way to avoid this cast + self.navigationRouters[navigationController] = navigationRouter as? NavigationRouter + } + } + + @objc private func navigationRouterWillDestroy(_ notification: Notification) { + guard let userInfo = notification.userInfo, + let navigationRouter = userInfo[NavigationRouter.NotificationUserInfoKey.navigationRouter] as? NavigationRouterType, + let navigationController = userInfo[NavigationRouter.NotificationUserInfoKey.navigationController] as? UINavigationController else { + return + } + + if let existingNavigationRouter = self.findNavigationRouter(for: navigationController), existingNavigationRouter !== navigationRouter { + fatalError("\(existingNavigationRouter) is already tied to the same navigation controller as \(navigationRouter). We should have only one NavigationRouter per navigation controller") + } + + self.removeNavigationRouter(for: navigationController) + } +} diff --git a/ElementX/Sources/Modules/Routers/NavigationRouterStoreProtocol.swift b/ElementX/Sources/Modules/Routers/NavigationRouterStoreProtocol.swift new file mode 100644 index 000000000..112f4b95b --- /dev/null +++ b/ElementX/Sources/Modules/Routers/NavigationRouterStoreProtocol.swift @@ -0,0 +1,25 @@ +// +// Copyright 2021 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 UIKit + +/// `NavigationRouterStoreProtocol` describes a structure that enables to get a NavigationRouter from a UINavigationController instance. +protocol NavigationRouterStoreProtocol { + + /// Gets the existing navigation router for the supplied controller, creating a new one if it doesn't yet exist. + /// Note: The store only holds a weak reference to the returned router. It is the caller's responsibility to retain it. + func navigationRouter(for navigationController: UINavigationController) -> NavigationRouterType +} diff --git a/ElementX/Sources/Modules/Routers/NavigationRouterType.swift b/ElementX/Sources/Modules/Routers/NavigationRouterType.swift new file mode 100755 index 000000000..484590088 --- /dev/null +++ b/ElementX/Sources/Modules/Routers/NavigationRouterType.swift @@ -0,0 +1,135 @@ +/* + Copyright 2019 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 UIKit + +/// Protocol describing a router that wraps a UINavigationController and add convenient completion handlers. Completions are called when a Presentable is removed. +/// Routers are used to be passed between coordinators. They handles only `physical` navigation. +protocol NavigationRouterType: AnyObject, Presentable { + + /// Present modally a view controller on the navigation controller + /// + /// - Parameter module: The Presentable to present. + /// - Parameter animated: Specify true to animate the transition. + func present(_ module: Presentable, animated: Bool) + + /// Dismiss presented view controller from navigation controller + /// + /// - Parameter animated: Specify true to animate the transition. + /// - Parameter completion: Animation completion (not the pop completion). + func dismissModule(animated: Bool, completion: (() -> Void)?) + + /// Set root view controller of navigation controller + /// + /// - Parameter module: The Presentable to set as root. + /// - Parameter hideNavigationBar: Specify true to hide the UINavigationBar. + /// - Parameter animated: Specify true to animate the transition. + /// - Parameter popCompletion: Completion called when `module` is removed from the navigation stack. + func setRootModule(_ module: Presentable, hideNavigationBar: Bool, animated: Bool, popCompletion: (() -> Void)?) + + /// Set view controllers stack of navigation controller + /// - Parameters: + /// - modules: The modules stack to set. + /// - hideNavigationBar: Specify true to hide the UINavigationBar. + /// - animated: Specify true to animate the transition. + func setModules(_ modules: [NavigationModule], hideNavigationBar: Bool, animated: Bool) + + /// Pop to root view controller of navigation controller and remove all others + /// + /// - Parameter animated: Specify true to animate the transition. + func popToRootModule(animated: Bool) + + /// Pops view controllers until the specified view controller is at the top of the navigation stack + /// + /// - Parameter module: The Presentable that should to be at the top of the stack. + /// - Parameter animated: Specify true to animate the transition. + func popToModule(_ module: Presentable, animated: Bool) + + /// Push a view controller on navigation controller stack + /// + /// - Parameter animated: Specify true to animate the transition. + /// - Parameter popCompletion: Completion called when `module` is removed from the navigation stack. + func push(_ module: Presentable, animated: Bool, popCompletion: (() -> Void)?) + + /// Push some view controllers on navigation controller stack + /// + /// - Parameter modules: Modules to push + /// - Parameter animated: Specify true to animate the transition. + func push(_ modules: [NavigationModule], animated: Bool) + + /// Pop last view controller from navigation controller stack + /// + /// - Parameter animated: Specify true to animate the transition. + func popModule(animated: Bool) + + /// Pops all view controllers + /// + /// - Parameter animated: Specify true to animate the transition. + func popAllModules(animated: Bool) + + /// Returns the modules that are currently in the navigation stack + var modules: [Presentable] { get } + + /// Check if the navigation controller contains the given presentable. + /// - Parameter module: The presentable for which to check the existence. + func contains(_ module: Presentable) -> Bool +} + +// `NavigationRouterType` default implementation +extension NavigationRouterType { + + func setRootModule(_ module: Presentable) { + setRootModule(module, hideNavigationBar: false, animated: false, popCompletion: nil) + } + + func setRootModule(_ module: Presentable, popCompletion: (() -> Void)?) { + setRootModule(module, hideNavigationBar: false, animated: false, popCompletion: popCompletion) + } + + func setModules(_ modules: [NavigationModule], animated: Bool) { + setModules(modules, hideNavigationBar: false, animated: animated) + } + + func setModules(_ modules: [Presentable], animated: Bool) { + setModules(modules, hideNavigationBar: false, animated: animated) + } + +} + +// MARK: - Presentable <--> NavigationModule Transitive Methods + +extension NavigationRouterType { + + func setRootModule(_ module: NavigationModule) { + setRootModule(module.presentable, popCompletion: module.popCompletion) + } + + func push(_ module: NavigationModule, animated: Bool) { + push(module.presentable, animated: animated, popCompletion: module.popCompletion) + } + + func setModules(_ modules: [Presentable], hideNavigationBar: Bool, animated: Bool) { + setModules(modules.map { $0.toModule() }, + hideNavigationBar: hideNavigationBar, + animated: animated) + } + + func push(_ modules: [Presentable], animated: Bool) { + push(modules.map { $0.toModule() }, + animated: animated) + } + +} diff --git a/ElementX/Sources/Modules/Routers/Presentable.swift b/ElementX/Sources/Modules/Routers/Presentable.swift new file mode 100755 index 000000000..e9f760393 --- /dev/null +++ b/ElementX/Sources/Modules/Routers/Presentable.swift @@ -0,0 +1,38 @@ +/* + Copyright 2019 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 UIKit + +/// Protocol used to pass UIViewControllers to routers +protocol Presentable { + func toPresentable() -> UIViewController +} + +extension UIViewController: Presentable { + public func toPresentable() -> UIViewController { + return self + } +} + +extension Presentable { + + /// Returns a new module from the presentable without a pop completion block + /// - Returns: Module + func toModule() -> NavigationModule { + return NavigationModule(presentable: self, popCompletion: nil) + } + +} diff --git a/ElementX/Sources/Modules/Routers/RootRouter.swift b/ElementX/Sources/Modules/Routers/RootRouter.swift new file mode 100755 index 000000000..dd9f76e91 --- /dev/null +++ b/ElementX/Sources/Modules/Routers/RootRouter.swift @@ -0,0 +1,87 @@ +/* + Copyright 2020 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 UIKit + +/// `RootRouter` is a concrete implementation of RootRouterType. +final class RootRouter: RootRouterType { + + // MARK: - Constants + + // `rootViewController` animation constants + private enum RootViewControllerUpdateAnimation { + static let duration: TimeInterval = 0.3 + static let options: UIView.AnimationOptions = .transitionCrossDissolve + } + + // MARK: - Properties + + private var presentedModule: Presentable? + + let window: UIWindow + + /// The root view controller currently presented + var rootViewController: UIViewController? { + return self.window.rootViewController + } + + // MARK: - Setup + + init(window: UIWindow) { + self.window = window + } + + // MARK: - Public methods + + func setRootModule(_ module: Presentable) { + self.updateRootViewController(rootViewController: module.toPresentable(), animated: false, completion: nil) + self.window.makeKeyAndVisible() + } + + func dismissRootModule(animated: Bool, completion: (() -> Void)?) { + self.updateRootViewController(rootViewController: nil, animated: animated, completion: completion) + } + + func presentModule(_ module: Presentable, animated: Bool, completion: (() -> Void)?) { + let viewControllerPresenter = self.rootViewController?.presentedViewController ?? self.rootViewController + + viewControllerPresenter?.present(module.toPresentable(), animated: animated, completion: completion) + self.presentedModule = module + } + + func dismissModule(animated: Bool, completion: (() -> Void)?) { + self.presentedModule?.toPresentable().dismiss(animated: animated, completion: completion) + } + + // MARK: - Private methods + + private func updateRootViewController(rootViewController: UIViewController?, animated: Bool, completion: (() -> Void)?) { + + if animated { + UIView.transition(with: window, duration: RootViewControllerUpdateAnimation.duration, options: RootViewControllerUpdateAnimation.options, animations: { + let oldState: Bool = UIView.areAnimationsEnabled + UIView.setAnimationsEnabled(false) + self.window.rootViewController = rootViewController + UIView.setAnimationsEnabled(oldState) + }, completion: { (_: Bool) -> Void in + completion?() + }) + } else { + self.window.rootViewController = rootViewController + completion?() + } + } +} diff --git a/ElementX/Sources/Modules/Routers/RootRouterType.swift b/ElementX/Sources/Modules/Routers/RootRouterType.swift new file mode 100755 index 000000000..633da610c --- /dev/null +++ b/ElementX/Sources/Modules/Routers/RootRouterType.swift @@ -0,0 +1,49 @@ +/* + Copyright 2020 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 UIKit + +/// Protocol describing a router that wraps the root navigation of the application. +/// Routers are used to be passed between coordinators. They handles only `physical` navigation. +protocol RootRouterType: AnyObject { + + /// Update the root view controller + /// + /// - Parameter module: The new root view controller to set + func setRootModule(_ module: Presentable) + + /// Dismiss the root view controller + /// + /// - Parameters: + /// - animated: Specify true to animate the transition. + /// - completion: The closure executed after the view controller is dismissed. + func dismissRootModule(animated: Bool, completion: (() -> Void)?) + + /// Present modally a view controller on the root view controller + /// + /// - Parameters: + /// - module: Specify true to animate the transition. + /// - animated: Specify true to animate the transition. + /// - completion: Animation completion. + func presentModule(_ module: Presentable, animated: Bool, completion: (() -> Void)?) + + /// Dismiss modally presented view controller from root view controller + /// + /// - Parameters: + /// - animated: Specify true to animate the transition. + /// - completion: Animation completion. + func dismissModule(animated: Bool, completion: (() -> Void)?) +} diff --git a/ElementX/Sources/Modules/Splash/SplashViewController.swift b/ElementX/Sources/Modules/Splash/SplashViewController.swift new file mode 100644 index 000000000..4d36f3a09 --- /dev/null +++ b/ElementX/Sources/Modules/Splash/SplashViewController.swift @@ -0,0 +1,12 @@ +// +// SplashViewController.swift +// ElementX +// +// Created by Stefan Ceriu on 14.02.2022. +// + +import UIKit + +class SplashViewController: UIViewController { + +} diff --git a/ElementX/Sources/Modules/Splash/SplashViewController.xib b/ElementX/Sources/Modules/Splash/SplashViewController.xib new file mode 100644 index 000000000..c67a5d8be --- /dev/null +++ b/ElementX/Sources/Modules/Splash/SplashViewController.xib @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ElementX/Sources/Modules/Templates/SimpleScreenExample/Coordinator/TemplateSimpleScreenCoordinator.swift b/ElementX/Sources/Modules/Templates/SimpleScreenExample/Coordinator/TemplateSimpleScreenCoordinator.swift new file mode 100644 index 000000000..394ab913a --- /dev/null +++ b/ElementX/Sources/Modules/Templates/SimpleScreenExample/Coordinator/TemplateSimpleScreenCoordinator.swift @@ -0,0 +1,64 @@ +// +// Copyright 2021 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 + +struct TemplateSimpleScreenCoordinatorParameters { + let promptType: TemplateSimpleScreenPromptType +} + +final class TemplateSimpleScreenCoordinator: Coordinator, Presentable { + + // MARK: - Properties + + // MARK: Private + + private let parameters: TemplateSimpleScreenCoordinatorParameters + private let templateSimpleScreenHostingController: UIViewController + private var templateSimpleScreenViewModel: TemplateSimpleScreenViewModelProtocol + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + var completion: ((TemplateSimpleScreenViewModelResult) -> Void)? + + // MARK: - Setup + + @available(iOS 14.0, *) + init(parameters: TemplateSimpleScreenCoordinatorParameters) { + self.parameters = parameters + + let viewModel = TemplateSimpleScreenViewModel(promptType: parameters.promptType) + let view = TemplateSimpleScreen(viewModel: viewModel.context) + templateSimpleScreenViewModel = viewModel + templateSimpleScreenHostingController = VectorHostingController(rootView: view) + } + + // MARK: - Public + func start() { + MXLog.debug("[TemplateSimpleScreenCoordinator] did start.") + templateSimpleScreenViewModel.completion = { [weak self] result in + MXLog.debug("[TemplateSimpleScreenCoordinator] TemplateSimpleScreenViewModel did complete with result: \(result).") + guard let self = self else { return } + self.completion?(result) + } + } + + func toPresentable() -> UIViewController { + return self.templateSimpleScreenHostingController + } +} diff --git a/ElementX/Sources/Modules/Templates/SimpleScreenExample/MockTemplateSimpleScreenScreenState.swift b/ElementX/Sources/Modules/Templates/SimpleScreenExample/MockTemplateSimpleScreenScreenState.swift new file mode 100644 index 000000000..7fd8fe6c9 --- /dev/null +++ b/ElementX/Sources/Modules/Templates/SimpleScreenExample/MockTemplateSimpleScreenScreenState.swift @@ -0,0 +1,57 @@ +// +// Copyright 2021 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 +import SwiftUI + +/// Using an enum for the screen allows you define the different state cases with +/// the relevant associated data for each case. +@available(iOS 14.0, *) +enum MockTemplateSimpleScreenScreenState: MockScreenState, CaseIterable { + // A case for each state you want to represent + // with specific, minimal associated data that will allow you + // mock that screen. + case promptType(TemplateSimpleScreenPromptType) + + /// The associated screen + var screenType: Any.Type { + TemplateSimpleScreen.self + } + + /// A list of screen state definitions + static var allCases: [MockTemplateSimpleScreenScreenState] { + // Each of the presence statuses + TemplateSimpleScreenPromptType.allCases.map(MockTemplateSimpleScreenScreenState.promptType) + } + + /// Generate the view struct for the screen state. + var screenView: ([Any], AnyView) { + let promptType: TemplateSimpleScreenPromptType + switch self { + case .promptType(let type): + promptType = type + } + let viewModel = TemplateSimpleScreenViewModel(promptType: promptType) + + // can simulate service and viewModel actions here if needs be. + + return ( + [promptType, viewModel], + AnyView(TemplateSimpleScreen(viewModel: viewModel.context) + .addDependency(MockAvatarService.example)) + ) + } +} diff --git a/ElementX/Sources/Modules/Templates/SimpleScreenExample/TemplateSimpleScreenModels.swift b/ElementX/Sources/Modules/Templates/SimpleScreenExample/TemplateSimpleScreenModels.swift new file mode 100644 index 000000000..2238fc709 --- /dev/null +++ b/ElementX/Sources/Modules/Templates/SimpleScreenExample/TemplateSimpleScreenModels.swift @@ -0,0 +1,67 @@ +// +// Copyright 2021 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 + +// MARK: - Coordinator + +enum TemplateSimpleScreenPromptType { + case regular + case upgrade +} + +extension TemplateSimpleScreenPromptType: Identifiable, CaseIterable { + var id: Self { self } + + var title: String { + switch self { + case .regular: + return VectorL10n.roomCreationMakePublicPromptTitle + case .upgrade: + return VectorL10n.roomDetailsHistorySectionPromptTitle + } + } + + var image: ImageAsset { + switch self { + case .regular: + return Asset.Images.appSymbol + case .upgrade: + return Asset.Images.keyVerificationSuccessShield + } + } +} + +// MARK: View model + +enum TemplateSimpleScreenViewModelResult { + case accept + case cancel +} + +// MARK: View + +struct TemplateSimpleScreenViewState: BindableState { + var promptType: TemplateSimpleScreenPromptType + var count: Int +} + +enum TemplateSimpleScreenViewAction { + case incrementCount + case decrementCount + case accept + case cancel +} diff --git a/ElementX/Sources/Modules/Templates/SimpleScreenExample/TemplateSimpleScreenViewModel.swift b/ElementX/Sources/Modules/Templates/SimpleScreenExample/TemplateSimpleScreenViewModel.swift new file mode 100644 index 000000000..b67286866 --- /dev/null +++ b/ElementX/Sources/Modules/Templates/SimpleScreenExample/TemplateSimpleScreenViewModel.swift @@ -0,0 +1,54 @@ +// +// Copyright 2021 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 + +@available(iOS 14, *) +typealias TemplateSimpleScreenViewModelType = StateStoreViewModel +@available(iOS 14, *) +class TemplateSimpleScreenViewModel: TemplateSimpleScreenViewModelType, TemplateSimpleScreenViewModelProtocol { + + // MARK: - Properties + + // MARK: Private + + // MARK: Public + + var completion: ((TemplateSimpleScreenViewModelResult) -> Void)? + + // MARK: - Setup + + init(promptType: TemplateSimpleScreenPromptType, initialCount: Int = 0) { + super.init(initialViewState: TemplateSimpleScreenViewState(promptType: promptType, count: 0)) + } + + // MARK: - Public + + override func process(viewAction: TemplateSimpleScreenViewAction) { + switch viewAction { + case .accept: + completion?(.accept) + case .cancel: + completion?(.cancel) + case .incrementCount: + state.count += 1 + case .decrementCount: + state.count -= 1 + } + } +} diff --git a/ElementX/Sources/Modules/Templates/SimpleScreenExample/TemplateSimpleScreenViewModelProtocol.swift b/ElementX/Sources/Modules/Templates/SimpleScreenExample/TemplateSimpleScreenViewModelProtocol.swift new file mode 100644 index 000000000..3c08f5292 --- /dev/null +++ b/ElementX/Sources/Modules/Templates/SimpleScreenExample/TemplateSimpleScreenViewModelProtocol.swift @@ -0,0 +1,24 @@ +// +// Copyright 2021 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 + +protocol TemplateSimpleScreenViewModelProtocol { + + var completion: ((TemplateSimpleScreenViewModelResult) -> Void)? { get set } + @available(iOS 14, *) + var context: TemplateSimpleScreenViewModelType.Context { get } +} diff --git a/ElementX/Sources/Modules/Templates/SimpleScreenExample/Test/UI/TemplateSimpleScreenUITests.swift b/ElementX/Sources/Modules/Templates/SimpleScreenExample/Test/UI/TemplateSimpleScreenUITests.swift new file mode 100644 index 000000000..112c39002 --- /dev/null +++ b/ElementX/Sources/Modules/Templates/SimpleScreenExample/Test/UI/TemplateSimpleScreenUITests.swift @@ -0,0 +1,45 @@ +// +// Copyright 2021 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 +import RiotSwiftUI + +@available(iOS 14.0, *) +class TemplateSimpleScreenUITests: MockScreenTest { + + override class var screenType: MockScreenState.Type { + return MockTemplateSimpleScreenScreenState.self + } + + override class func createTest() -> MockScreenTest { + return TemplateSimpleScreenUITests(selector: #selector(verifyTemplateSimpleScreenScreen)) + } + + func verifyTemplateSimpleScreenScreen() throws { + guard let screenState = screenState as? MockTemplateSimpleScreenScreenState else { fatalError("no screen") } + switch screenState { + case .promptType(let promptType): + verifyTemplateSimpleScreenPromptType(promptType: promptType) + } + } + + func verifyTemplateSimpleScreenPromptType(promptType: TemplateSimpleScreenPromptType) { + let title = app.staticTexts["title"] + XCTAssert(title.exists) + XCTAssertEqual(title.label, promptType.title) + } + +} diff --git a/ElementX/Sources/Modules/Templates/SimpleScreenExample/Test/Unit/TemplateSimpleScreenViewModelTests.swift b/ElementX/Sources/Modules/Templates/SimpleScreenExample/Test/Unit/TemplateSimpleScreenViewModelTests.swift new file mode 100644 index 000000000..5224ac3f6 --- /dev/null +++ b/ElementX/Sources/Modules/Templates/SimpleScreenExample/Test/Unit/TemplateSimpleScreenViewModelTests.swift @@ -0,0 +1,49 @@ +// +// Copyright 2021 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 RiotSwiftUI + +@available(iOS 14.0, *) +class TemplateSimpleScreenViewModelTests: XCTestCase { + private enum Constants { + static let counterInitialValue = 0 + } + + var viewModel: TemplateSimpleScreenViewModelProtocol! + var context: TemplateSimpleScreenViewModelType.Context! + + override func setUpWithError() throws { + viewModel = TemplateSimpleScreenViewModel(promptType: .regular, initialCount: Constants.counterInitialValue) + context = viewModel.context + } + + func testInitialState() { + XCTAssertEqual(context.viewState.count, Constants.counterInitialValue) + } + + func testCounter() throws { + context.send(viewAction: .incrementCount) + XCTAssertEqual(context.viewState.count, 1) + + context.send(viewAction: .incrementCount) + XCTAssertEqual(context.viewState.count, 2) + + context.send(viewAction: .decrementCount) + XCTAssertEqual(context.viewState.count, 1) + } +} diff --git a/ElementX/Sources/Modules/Templates/SimpleScreenExample/View/TemplateSimpleScreen.swift b/ElementX/Sources/Modules/Templates/SimpleScreenExample/View/TemplateSimpleScreen.swift new file mode 100644 index 000000000..e3fe82e86 --- /dev/null +++ b/ElementX/Sources/Modules/Templates/SimpleScreenExample/View/TemplateSimpleScreen.swift @@ -0,0 +1,113 @@ +// +// Copyright 2021 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 + +@available(iOS 14.0, *) +struct TemplateSimpleScreen: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + + private var horizontalPadding: CGFloat { + horizontalSizeClass == .regular ? 50 : 16 + } + + // MARK: Public + + @ObservedObject var viewModel: TemplateSimpleScreenViewModel.Context + + // MARK: Views + + var body: some View { + GeometryReader { geometry in + VStack { + ScrollView(showsIndicators: false) { + mainContent + .padding(.top, 50) + .padding(.horizontal, horizontalPadding) + } + + buttons + .padding(.horizontal, horizontalPadding) + .padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 16) + } + } + .background(theme.colors.background.ignoresSafeArea()) + .accentColor(theme.colors.accent) + } + + /// The main content of the view to be shown in a scroll view. + var mainContent: some View { + VStack(spacing: 36) { + Text(viewModel.viewState.promptType.title) + .font(theme.fonts.title1B) + .foregroundColor(theme.colors.primaryContent) + .accessibilityIdentifier("title") + + Image(viewModel.viewState.promptType.image.name) + .resizable() + .scaledToFit() + .frame(width: 100) + .foregroundColor(theme.colors.accent) + + HStack { + Text("Counter: \(viewModel.viewState.count)") + .foregroundColor(theme.colors.primaryContent) + + Button("-") { + viewModel.send(viewAction: .decrementCount) + } + + Button("+") { + viewModel.send(viewAction: .incrementCount) + } + } + .font(theme.fonts.title3) + } + } + + /// The action buttons shown at the bottom of the view. + var buttons: some View { + VStack { + Button { viewModel.send(viewAction: .accept) } label: { + Text("Accept") + .font(theme.fonts.bodySB) + } + .buttonStyle(PrimaryActionButtonStyle()) + + Button { viewModel.send(viewAction: .cancel) } label: { + Text("Cancel") + .font(theme.fonts.body) + .padding(.vertical, 12) + } + } + } +} + +// MARK: - Previews + +@available(iOS 14.0, *) +struct TemplateSimpleScreen_Previews: PreviewProvider { + static let stateRenderer = MockTemplateSimpleScreenScreenState.stateRenderer + static var previews: some View { + stateRenderer.screenGroup() + } +} diff --git a/ElementX/Supporting Files/Assets.xcassets/AccentColor.colorset/Contents.json b/ElementX/Supporting Files/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/ElementX/Supporting Files/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ElementX/Supporting Files/Assets.xcassets/AppIcon.appiconset/Contents.json b/ElementX/Supporting Files/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..9221b9bb1 --- /dev/null +++ b/ElementX/Supporting Files/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ElementX/Supporting Files/Assets.xcassets/Contents.json b/ElementX/Supporting Files/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/ElementX/Supporting Files/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ElementX/Supporting Files/Assets.xcassets/app-logo.imageset/Contents.json b/ElementX/Supporting Files/Assets.xcassets/app-logo.imageset/Contents.json new file mode 100644 index 000000000..332aaf86f --- /dev/null +++ b/ElementX/Supporting Files/Assets.xcassets/app-logo.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "launch_screen_logo.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "launch_screen_logo@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "launch_screen_logo@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/ElementX/Supporting Files/Assets.xcassets/app-logo.imageset/launch_screen_logo.png b/ElementX/Supporting Files/Assets.xcassets/app-logo.imageset/launch_screen_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..85d7a6a012b81e4f774b0c8587634e20131c5b27 GIT binary patch literal 6163 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 literal 0 HcmV?d00001 diff --git a/ElementX/Supporting Files/Assets.xcassets/app-logo.imageset/launch_screen_logo@2x.png b/ElementX/Supporting Files/Assets.xcassets/app-logo.imageset/launch_screen_logo@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..ac3f21dfb847ad56b7613f73290a1196d958684e GIT binary patch literal 11812 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 XJHN_rTF^&b00000NkvXXu0mjfr&_K- literal 0 HcmV?d00001 diff --git a/ElementX/Supporting Files/Base.lproj/LaunchScreen.storyboard b/ElementX/Supporting Files/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000..f25e499e7 --- /dev/null +++ b/ElementX/Supporting Files/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ElementX/Supporting Files/ElementX.entitlements b/ElementX/Supporting Files/ElementX.entitlements new file mode 100644 index 000000000..ee95ab7e5 --- /dev/null +++ b/ElementX/Supporting Files/ElementX.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/ElementXTests/ElementXTests.swift b/ElementXTests/ElementXTests.swift new file mode 100644 index 000000000..6a9d07719 --- /dev/null +++ b/ElementXTests/ElementXTests.swift @@ -0,0 +1,36 @@ +// +// ElementXTests.swift +// ElementXTests +// +// Created by Stefan Ceriu on 11.02.2022. +// + +import XCTest +@testable import ElementX + +class ElementXTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/ElementXUITests/ElementXUITests.swift b/ElementXUITests/ElementXUITests.swift new file mode 100644 index 000000000..196619b6f --- /dev/null +++ b/ElementXUITests/ElementXUITests.swift @@ -0,0 +1,42 @@ +// +// ElementXUITests.swift +// ElementXUITests +// +// Created by Stefan Ceriu on 11.02.2022. +// + +import XCTest + +class ElementXUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use recording to get started writing UI tests. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/ElementXUITests/ElementXUITestsLaunchTests.swift b/ElementXUITests/ElementXUITestsLaunchTests.swift new file mode 100644 index 000000000..dd7d02865 --- /dev/null +++ b/ElementXUITests/ElementXUITestsLaunchTests.swift @@ -0,0 +1,32 @@ +// +// ElementXUITestsLaunchTests.swift +// ElementXUITests +// +// Created by Stefan Ceriu on 11.02.2022. +// + +import XCTest + +class ElementXUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} diff --git a/Scripts/createSwiftUISimpleScreen.sh b/Scripts/createSwiftUISimpleScreen.sh new file mode 100755 index 000000000..0e510f899 --- /dev/null +++ b/Scripts/createSwiftUISimpleScreen.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +if [ ! $# -eq 2 ]; then + echo "Usage: ./createSwiftUISimpleScreen.sh Folder MyScreenName" + exit 1 +fi + +MODULE_DIR="../ElementX/Sources/Modules" +OUTPUT_DIR=$MODULE_DIR/$1 +SCREEN_NAME=$2 +SCREEN_VAR_NAME=`echo $SCREEN_NAME | awk '{ print tolower(substr($0, 1, 1)) substr($0, 2) }'` +TEMPLATE_DIR=$MODULE_DIR/Templates/SimpleScreenExample/ +if [ -e $OUTPUT_DIR ]; then + echo "Error: Folder ${OUTPUT_DIR} already exists" + exit 1 +fi + +echo "Create folder ${OUTPUT_DIR}" + +mkdir -p $OUTPUT_DIR +cp -R $TEMPLATE_DIR $OUTPUT_DIR/ + +cd $OUTPUT_DIR +for file in $(find * -type f -print) +do + echo "Building ${file/TemplateSimpleScreen/$SCREEN_NAME}..." + perl -p -i -e "s/TemplateSimpleScreen/"$SCREEN_NAME"/g" $file + perl -p -i -e "s/templateSimpleScreen/"$SCREEN_VAR_NAME"/g" $file + + mv ${file} ${file/TemplateSimpleScreen/$SCREEN_NAME} +done