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 000000000..85d7a6a01 Binary files /dev/null and b/ElementX/Supporting Files/Assets.xcassets/app-logo.imageset/launch_screen_logo.png differ 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 000000000..ac3f21dfb Binary files /dev/null and b/ElementX/Supporting Files/Assets.xcassets/app-logo.imageset/launch_screen_logo@2x.png differ diff --git a/ElementX/Supporting Files/Assets.xcassets/app-logo.imageset/launch_screen_logo@3x.png b/ElementX/Supporting Files/Assets.xcassets/app-logo.imageset/launch_screen_logo@3x.png new file mode 100644 index 000000000..e049f4e98 Binary files /dev/null and b/ElementX/Supporting Files/Assets.xcassets/app-logo.imageset/launch_screen_logo@3x.png differ 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