mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 21:39:12 +00:00
Initial project setup.
This commit is contained in:
parent
a499570f39
commit
a3fcc0f612
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
@ -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
|
50
.swiftlint.yml
Executable file
50
.swiftlint.yml
Executable file
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
995
ElementX.xcodeproj/project.pbxproj
Normal file
995
ElementX.xcodeproj/project.pbxproj
Normal file
@ -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 = "<group>"; };
|
||||||
|
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 = "<group>"; };
|
||||||
|
1850254A27B6918D002E6B18 /* ElementXUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementXUITestsLaunchTests.swift; sourceTree = "<group>"; };
|
||||||
|
1850256227B6A135002E6B18 /* AppCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = "<group>"; };
|
||||||
|
1850256527B6A135002E6B18 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
1850256727B6A135002E6B18 /* ElementX.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = ElementX.entitlements; sourceTree = "<group>"; };
|
||||||
|
1850256827B6A135002E6B18 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
1850256A27B6A135002E6B18 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
|
1863A40E27BA716A00B52E4D /* AuthenticationCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationCoordinator.swift; sourceTree = "<group>"; };
|
||||||
|
1863A40F27BA716A00B52E4D /* UserSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserSession.swift; sourceTree = "<group>"; };
|
||||||
|
1863A41027BA716A00B52E4D /* KeychainController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainController.swift; sourceTree = "<group>"; };
|
||||||
|
1863A41127BA716A00B52E4D /* KeychainControllerProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainControllerProtocol.swift; sourceTree = "<group>"; };
|
||||||
|
1863A41B27BA76B900B52E4D /* MXLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MXLog.swift; sourceTree = "<group>"; };
|
||||||
|
1863A41F27BA784300B52E4D /* LoginScreenViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
1863A42427BA784300B52E4D /* LoginScreenUITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginScreenUITests.swift; sourceTree = "<group>"; };
|
||||||
|
1863A42627BA784300B52E4D /* LoginScreenViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||||
|
1863A42727BA784300B52E4D /* LoginScreenViewModelProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||||
|
1863A42827BA784300B52E4D /* LoginScreenModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginScreenModels.swift; sourceTree = "<group>"; };
|
||||||
|
1863A42A27BA784300B52E4D /* LoginScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginScreen.swift; sourceTree = "<group>"; };
|
||||||
|
1863A43827BA789800B52E4D /* StateStoreViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StateStoreViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
1863A43927BA789800B52E4D /* BindableState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BindableState.swift; sourceTree = "<group>"; };
|
||||||
|
1863A43D27BA790000B52E4D /* Coordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = "<group>"; };
|
||||||
|
1863A44127BA79FF00B52E4D /* NavigationRouterStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationRouterStore.swift; sourceTree = "<group>"; };
|
||||||
|
1863A44227BA79FF00B52E4D /* NavigationRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationRouter.swift; sourceTree = "<group>"; };
|
||||||
|
1863A44327BA79FF00B52E4D /* RootRouterType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootRouterType.swift; sourceTree = "<group>"; };
|
||||||
|
1863A44427BA79FF00B52E4D /* NavigationRouterStoreProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationRouterStoreProtocol.swift; sourceTree = "<group>"; };
|
||||||
|
1863A44527BA79FF00B52E4D /* RootRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootRouter.swift; sourceTree = "<group>"; };
|
||||||
|
1863A44627BA79FF00B52E4D /* Presentable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Presentable.swift; sourceTree = "<group>"; };
|
||||||
|
1863A44727BA79FF00B52E4D /* NavigationModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationModule.swift; sourceTree = "<group>"; };
|
||||||
|
1863A44827BA79FF00B52E4D /* NavigationRouterType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationRouterType.swift; sourceTree = "<group>"; };
|
||||||
|
1863A45227BA7A7800B52E4D /* WeakDictionaryKeyReference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeakDictionaryKeyReference.swift; sourceTree = "<group>"; };
|
||||||
|
1863A45327BA7A7800B52E4D /* WeakDictionaryReference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeakDictionaryReference.swift; sourceTree = "<group>"; };
|
||||||
|
1863A45427BA7A7800B52E4D /* WeakDictionary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeakDictionary.swift; sourceTree = "<group>"; };
|
||||||
|
1863A45527BA7A7800B52E4D /* WeakKeyDictionary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeakKeyDictionary.swift; sourceTree = "<group>"; };
|
||||||
|
1863A45A27BA7B4700B52E4D /* LoginScreenCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||||
|
1863A45D27BAA60300B52E4D /* SplashViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashViewController.swift; sourceTree = "<group>"; };
|
||||||
|
1863A45E27BAA60300B52E4D /* SplashViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SplashViewController.xib; sourceTree = "<group>"; };
|
||||||
|
1863A47927BAA8A900B52E4D /* HomeScreenCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||||
|
1863A47C27BAA8A900B52E4D /* HomeScreenUITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeScreenUITests.swift; sourceTree = "<group>"; };
|
||||||
|
1863A47E27BAA8A900B52E4D /* HomeScreenViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||||
|
1863A47F27BAA8A900B52E4D /* HomeScreenViewModelProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||||
|
1863A48027BAA8A900B52E4D /* HomeScreenModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeScreenModels.swift; sourceTree = "<group>"; };
|
||||||
|
1863A48227BAA8A900B52E4D /* HomeScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeScreen.swift; sourceTree = "<group>"; };
|
||||||
|
1863A48427BAA8A900B52E4D /* HomeScreenViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
1863A49327BAAA6700B52E4D /* RoomModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomModel.swift; sourceTree = "<group>"; };
|
||||||
|
/* 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 = "<group>";
|
||||||
|
};
|
||||||
|
1850252527B6918C002E6B18 /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1850252427B6918C002E6B18 /* ElementX.app */,
|
||||||
|
1850253A27B6918D002E6B18 /* ElementXTests.xctest */,
|
||||||
|
1850254427B6918D002E6B18 /* ElementXUITests.xctest */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
1850252627B6918C002E6B18 /* ElementX */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1850256127B6A135002E6B18 /* Sources */,
|
||||||
|
1850256627B6A135002E6B18 /* Supporting Files */,
|
||||||
|
);
|
||||||
|
path = ElementX;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
1850253D27B6918D002E6B18 /* ElementXTests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1850253E27B6918D002E6B18 /* ElementXTests.swift */,
|
||||||
|
);
|
||||||
|
path = ElementXTests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
1850254727B6918D002E6B18 /* ElementXUITests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1850254827B6918D002E6B18 /* ElementXUITests.swift */,
|
||||||
|
1850254A27B6918D002E6B18 /* ElementXUITestsLaunchTests.swift */,
|
||||||
|
);
|
||||||
|
path = ElementXUITests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
1850256127B6A135002E6B18 /* Sources */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1850256527B6A135002E6B18 /* AppDelegate.swift */,
|
||||||
|
1850256227B6A135002E6B18 /* AppCoordinator.swift */,
|
||||||
|
1863A40927BA716A00B52E4D /* Modules */,
|
||||||
|
);
|
||||||
|
path = Sources;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
1850256627B6A135002E6B18 /* Supporting Files */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1850256727B6A135002E6B18 /* ElementX.entitlements */,
|
||||||
|
1850256827B6A135002E6B18 /* Assets.xcassets */,
|
||||||
|
1850256927B6A135002E6B18 /* LaunchScreen.storyboard */,
|
||||||
|
);
|
||||||
|
path = "Supporting Files";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
1863A40927BA716A00B52E4D /* Modules */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1863A45C27BAA5F100B52E4D /* Splash */,
|
||||||
|
1863A40D27BA716A00B52E4D /* Authentication */,
|
||||||
|
1863A47827BAA8A900B52E4D /* HomeScreen */,
|
||||||
|
1863A49227BAAA6700B52E4D /* Models */,
|
||||||
|
1863A44027BA79FF00B52E4D /* Routers */,
|
||||||
|
1863A41A27BA76B900B52E4D /* Other */,
|
||||||
|
);
|
||||||
|
path = Modules;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
1863A40D27BA716A00B52E4D /* Authentication */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1863A40E27BA716A00B52E4D /* AuthenticationCoordinator.swift */,
|
||||||
|
1863A40F27BA716A00B52E4D /* UserSession.swift */,
|
||||||
|
1863A41027BA716A00B52E4D /* KeychainController.swift */,
|
||||||
|
1863A41127BA716A00B52E4D /* KeychainControllerProtocol.swift */,
|
||||||
|
1863A41D27BA784300B52E4D /* LoginScreen */,
|
||||||
|
);
|
||||||
|
path = Authentication;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
1863A41A27BA76B900B52E4D /* Other */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1863A43D27BA790000B52E4D /* Coordinator.swift */,
|
||||||
|
1863A41B27BA76B900B52E4D /* MXLog.swift */,
|
||||||
|
1863A43627BA789800B52E4D /* SwiftUI */,
|
||||||
|
1863A45127BA7A7800B52E4D /* WeakDictionary */,
|
||||||
|
);
|
||||||
|
path = Other;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
1863A41D27BA784300B52E4D /* LoginScreen */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1863A45A27BA7B4700B52E4D /* LoginScreenCoordinator.swift */,
|
||||||
|
1863A42827BA784300B52E4D /* LoginScreenModels.swift */,
|
||||||
|
1863A41F27BA784300B52E4D /* LoginScreenViewModel.swift */,
|
||||||
|
1863A42727BA784300B52E4D /* LoginScreenViewModelProtocol.swift */,
|
||||||
|
1863A42227BA784300B52E4D /* Test */,
|
||||||
|
1863A42927BA784300B52E4D /* View */,
|
||||||
|
);
|
||||||
|
path = LoginScreen;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
1863A42227BA784300B52E4D /* Test */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1863A42327BA784300B52E4D /* UI */,
|
||||||
|
1863A42527BA784300B52E4D /* Unit */,
|
||||||
|
);
|
||||||
|
path = Test;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
1863A42327BA784300B52E4D /* UI */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1863A42427BA784300B52E4D /* LoginScreenUITests.swift */,
|
||||||
|
);
|
||||||
|
path = UI;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
1863A42527BA784300B52E4D /* Unit */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1863A42627BA784300B52E4D /* LoginScreenViewModelTests.swift */,
|
||||||
|
);
|
||||||
|
path = Unit;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
1863A42927BA784300B52E4D /* View */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1863A42A27BA784300B52E4D /* LoginScreen.swift */,
|
||||||
|
);
|
||||||
|
path = View;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
1863A43627BA789800B52E4D /* SwiftUI */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1863A43727BA789800B52E4D /* ViewModel */,
|
||||||
|
);
|
||||||
|
path = SwiftUI;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
1863A43727BA789800B52E4D /* ViewModel */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1863A43827BA789800B52E4D /* StateStoreViewModel.swift */,
|
||||||
|
1863A43927BA789800B52E4D /* BindableState.swift */,
|
||||||
|
);
|
||||||
|
path = ViewModel;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
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 = "<group>";
|
||||||
|
};
|
||||||
|
1863A45127BA7A7800B52E4D /* WeakDictionary */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1863A45227BA7A7800B52E4D /* WeakDictionaryKeyReference.swift */,
|
||||||
|
1863A45327BA7A7800B52E4D /* WeakDictionaryReference.swift */,
|
||||||
|
1863A45427BA7A7800B52E4D /* WeakDictionary.swift */,
|
||||||
|
1863A45527BA7A7800B52E4D /* WeakKeyDictionary.swift */,
|
||||||
|
);
|
||||||
|
path = WeakDictionary;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
1863A45C27BAA5F100B52E4D /* Splash */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1863A45D27BAA60300B52E4D /* SplashViewController.swift */,
|
||||||
|
1863A45E27BAA60300B52E4D /* SplashViewController.xib */,
|
||||||
|
);
|
||||||
|
path = Splash;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
1863A47827BAA8A900B52E4D /* HomeScreen */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1863A47927BAA8A900B52E4D /* HomeScreenCoordinator.swift */,
|
||||||
|
1863A48027BAA8A900B52E4D /* HomeScreenModels.swift */,
|
||||||
|
1863A48427BAA8A900B52E4D /* HomeScreenViewModel.swift */,
|
||||||
|
1863A47F27BAA8A900B52E4D /* HomeScreenViewModelProtocol.swift */,
|
||||||
|
1863A47A27BAA8A900B52E4D /* Test */,
|
||||||
|
1863A48127BAA8A900B52E4D /* View */,
|
||||||
|
);
|
||||||
|
path = HomeScreen;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
1863A47A27BAA8A900B52E4D /* Test */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1863A47B27BAA8A900B52E4D /* UI */,
|
||||||
|
1863A47D27BAA8A900B52E4D /* Unit */,
|
||||||
|
);
|
||||||
|
path = Test;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
1863A47B27BAA8A900B52E4D /* UI */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1863A47C27BAA8A900B52E4D /* HomeScreenUITests.swift */,
|
||||||
|
);
|
||||||
|
path = UI;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
1863A47D27BAA8A900B52E4D /* Unit */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1863A47E27BAA8A900B52E4D /* HomeScreenViewModelTests.swift */,
|
||||||
|
);
|
||||||
|
path = Unit;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
1863A48127BAA8A900B52E4D /* View */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1863A48227BAA8A900B52E4D /* HomeScreen.swift */,
|
||||||
|
);
|
||||||
|
path = View;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
1863A49227BAAA6700B52E4D /* Models */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1863A49327BAAA6700B52E4D /* RoomModel.swift */,
|
||||||
|
);
|
||||||
|
path = Models;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* 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 = "<group>";
|
||||||
|
};
|
||||||
|
/* 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 */;
|
||||||
|
}
|
7
ElementX.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
7
ElementX.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
@ -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
|
||||||
|
}
|
78
ElementX/Sources/AppCoordinator.swift
Normal file
78
ElementX/Sources/AppCoordinator.swift
Normal file
@ -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() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
20
ElementX/Sources/AppDelegate.swift
Normal file
20
ElementX/Sources/AppDelegate.swift
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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<LoginScreenViewState,
|
||||||
|
Never,
|
||||||
|
LoginScreenViewAction>
|
||||||
|
@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)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 }
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
44
ElementX/Sources/Modules/Authentication/UserSession.swift
Normal file
44
ElementX/Sources/Modules/Authentication/UserSession.swift
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
31
ElementX/Sources/Modules/HomeScreen/HomeScreenModels.swift
Normal file
31
ElementX/Sources/Modules/HomeScreen/HomeScreenModels.swift
Normal file
@ -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
|
||||||
|
}
|
@ -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<HomeScreenViewState,
|
||||||
|
Never,
|
||||||
|
HomeScreenViewAction>
|
||||||
|
@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) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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 }
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
41
ElementX/Sources/Modules/HomeScreen/View/HomeScreen.swift
Normal file
41
ElementX/Sources/Modules/HomeScreen/View/HomeScreen.swift
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
13
ElementX/Sources/Modules/Models/RoomModel.swift
Normal file
13
ElementX/Sources/Modules/Models/RoomModel.swift
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
//
|
||||||
|
// RoomModel.swift
|
||||||
|
// ElementX
|
||||||
|
//
|
||||||
|
// Created by Stefan Ceriu on 14.02.2022.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
struct RoomModel {
|
||||||
|
let displayName: String
|
||||||
|
}
|
51
ElementX/Sources/Modules/Other/Coordinator.swift
Executable file
51
ElementX/Sources/Modules/Other/Coordinator.swift
Executable file
@ -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 }
|
||||||
|
}
|
||||||
|
}
|
179
ElementX/Sources/Modules/Other/MXLog.swift
Normal file
179
ElementX/Sources/Modules/Other/MXLog.swift
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<ViewState: BindableState, ViewAction>: ObservableObject {
|
||||||
|
// MARK: - Properties
|
||||||
|
|
||||||
|
// MARK: Private
|
||||||
|
|
||||||
|
fileprivate let viewActions: PassthroughSubject<ViewAction, Never>
|
||||||
|
|
||||||
|
// 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<T>(dynamicMember keyPath: WritableKeyPath<ViewState.BindStateType, T>) -> 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<State: BindableState, StateAction, ViewAction> {
|
||||||
|
|
||||||
|
typealias Context = ViewModelContext<State, ViewAction>
|
||||||
|
|
||||||
|
// MARK: - Properties
|
||||||
|
|
||||||
|
// MARK: Public
|
||||||
|
|
||||||
|
/// For storing subscription references.
|
||||||
|
///
|
||||||
|
/// Left as public for `ViewModel` implementations convenience.
|
||||||
|
var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
/// 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<StateAction, Never>) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -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<Key: Hashable, Value: AnyObject> {
|
||||||
|
|
||||||
|
private var storage: [Key: WeakDictionaryReference<Value>]
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
self.init(storage: [Key: WeakDictionaryReference<Value>]())
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(dictionary: [Key: Value]) {
|
||||||
|
var newStorage = [Key: WeakDictionaryReference<Value>]()
|
||||||
|
dictionary.forEach({ key, value in newStorage[key] = WeakDictionaryReference<Value>(value: value) })
|
||||||
|
self.init(storage: newStorage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private init(storage: [Key: WeakDictionaryReference<Value>]) {
|
||||||
|
self.storage = storage
|
||||||
|
}
|
||||||
|
|
||||||
|
public mutating func reap() {
|
||||||
|
storage = weakDictionary().storage
|
||||||
|
}
|
||||||
|
|
||||||
|
public func weakDictionary() -> WeakDictionary<Key, Value> {
|
||||||
|
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<Key, WeakDictionaryReference<Value>>
|
||||||
|
|
||||||
|
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<Value>) {
|
||||||
|
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: value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public subscript(bounds: Range<Index>) -> WeakDictionary<Key, Value> {
|
||||||
|
let subStorage = storage[bounds.lowerBound ..< bounds.upperBound]
|
||||||
|
var newStorage = [Key: WeakDictionaryReference<Value>]()
|
||||||
|
|
||||||
|
subStorage.filter { _, value in return value.value != nil }
|
||||||
|
.forEach { key, value in newStorage[key] = value }
|
||||||
|
|
||||||
|
return WeakDictionary<Key, Value>(storage: newStorage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Dictionary where Value: AnyObject {
|
||||||
|
public func weakDictionary() -> WeakDictionary<Key, Value> {
|
||||||
|
return WeakDictionary<Key, Value>(dictionary: self)
|
||||||
|
}
|
||||||
|
}
|
@ -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<Key: AnyObject & Hashable, Value: AnyObject> : 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
|
||||||
|
}
|
||||||
|
}
|
@ -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<Value: AnyObject> {
|
||||||
|
private weak var referencedValue: Value?
|
||||||
|
|
||||||
|
init(value: Value) {
|
||||||
|
referencedValue = value
|
||||||
|
}
|
||||||
|
|
||||||
|
public var value: Value? {
|
||||||
|
return referencedValue
|
||||||
|
}
|
||||||
|
}
|
@ -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<Key: AnyObject & Hashable, Value: AnyObject> {
|
||||||
|
|
||||||
|
private var storage: WeakDictionary<WeakDictionaryKey<Key, Value>, Value>
|
||||||
|
private let valuesRetainedByKey: Bool
|
||||||
|
|
||||||
|
public init(valuesRetainedByKey: Bool = false) {
|
||||||
|
self.init(
|
||||||
|
storage: WeakDictionary<WeakDictionaryKey<Key, Value>, Value>(),
|
||||||
|
valuesRetainedByKey: valuesRetainedByKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(dictionary: [Key: Value], valuesRetainedByKey: Bool = false) {
|
||||||
|
var newStorage = WeakDictionary<WeakDictionaryKey<Key, Value>, Value>()
|
||||||
|
|
||||||
|
dictionary.forEach { key, value in
|
||||||
|
var keyRef: WeakDictionaryKey<Key, Value>!
|
||||||
|
|
||||||
|
if valuesRetainedByKey {
|
||||||
|
keyRef = WeakDictionaryKey<Key, Value>(key: key, value: value)
|
||||||
|
} else {
|
||||||
|
keyRef = WeakDictionaryKey<Key, Value>(key: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
newStorage[keyRef] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
self.init(storage: newStorage, valuesRetainedByKey: valuesRetainedByKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
private init(storage: WeakDictionary<WeakDictionaryKey<Key, Value>, Value>, valuesRetainedByKey: Bool = false) {
|
||||||
|
self.storage = storage
|
||||||
|
self.valuesRetainedByKey = valuesRetainedByKey
|
||||||
|
}
|
||||||
|
|
||||||
|
public mutating func reap() {
|
||||||
|
storage = weakKeyDictionary().storage
|
||||||
|
}
|
||||||
|
|
||||||
|
public func weakDictionary() -> WeakDictionary<Key, Value> {
|
||||||
|
return dictionary().weakDictionary()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func weakKeyDictionary() -> WeakKeyDictionary<Key, Value> {
|
||||||
|
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<WeakDictionaryKey<Key, Value>, WeakDictionaryReference<Value>>
|
||||||
|
|
||||||
|
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<Key, Value>, WeakDictionaryReference<Value>) {
|
||||||
|
return storage[position]
|
||||||
|
}
|
||||||
|
|
||||||
|
public subscript(key: Key) -> Value? {
|
||||||
|
get {
|
||||||
|
return storage[WeakDictionaryKey<Key, Value>(key: key)]
|
||||||
|
}
|
||||||
|
|
||||||
|
set {
|
||||||
|
let retainedValue = valuesRetainedByKey ? newValue : nil
|
||||||
|
let weakKey = WeakDictionaryKey<Key, Value>(key: key, value: retainedValue)
|
||||||
|
storage[weakKey] = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public subscript(bounds: Range<Index>) -> WeakKeyDictionary<Key, Value> {
|
||||||
|
let subStorage = storage[bounds.lowerBound ..< bounds.upperBound]
|
||||||
|
var newStorage = WeakDictionary<WeakDictionaryKey<Key, Value>, Value>()
|
||||||
|
|
||||||
|
subStorage.filter { key, value in return key.key != nil && value.value != nil }
|
||||||
|
.forEach { key, value in newStorage[key] = value.value }
|
||||||
|
|
||||||
|
return WeakKeyDictionary<Key, Value>(storage: newStorage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension WeakDictionary where Key: AnyObject {
|
||||||
|
public func weakKeyDictionary(valuesRetainedByKey: Bool = false) -> WeakKeyDictionary<Key, Value> {
|
||||||
|
return WeakKeyDictionary<Key, Value>(dictionary: dictionary(), valuesRetainedByKey: valuesRetainedByKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Dictionary where Key: AnyObject, Value: AnyObject {
|
||||||
|
public func weakKeyDictionary(valuesRetainedByKey: Bool = false) -> WeakKeyDictionary<Key, Value> {
|
||||||
|
return WeakKeyDictionary<Key, Value>(dictionary: self, valuesRetainedByKey: valuesRetainedByKey)
|
||||||
|
}
|
||||||
|
}
|
36
ElementX/Sources/Modules/Routers/NavigationModule.swift
Normal file
36
ElementX/Sources/Modules/Routers/NavigationModule.swift
Normal file
@ -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))"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
405
ElementX/Sources/Modules/Routers/NavigationRouter.swift
Executable file
405
ElementX/Sources/Modules/Routers/NavigationRouter.swift
Executable file
@ -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<UIViewController, AnyObject>()
|
||||||
|
|
||||||
|
// 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..<controllers.count]
|
||||||
|
|
||||||
|
controllersToPop.reversed().forEach {
|
||||||
|
self.willPopViewController($0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let controllers = navigationController.popToRootViewController(animated: animated) {
|
||||||
|
controllers.reversed().forEach {
|
||||||
|
self.didPopViewController($0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func popToModule(_ module: Presentable, animated: Bool) {
|
||||||
|
MXLog.debug("[NavigationRouter] Pop to module \(module)")
|
||||||
|
|
||||||
|
let controller = module.toPresentable()
|
||||||
|
let controllersBeforePop = self.navigationController.viewControllers
|
||||||
|
|
||||||
|
if let controllerIndex = controllersBeforePop.firstIndex(of: controller) {
|
||||||
|
let controllersToPop = controllersBeforePop[controllerIndex..<controllersBeforePop.count]
|
||||||
|
|
||||||
|
controllersToPop.reversed().forEach {
|
||||||
|
self.willPopViewController($0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let controllers = navigationController.popToViewController(controller, animated: animated) {
|
||||||
|
controllers.reversed().forEach {
|
||||||
|
self.didPopViewController($0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func push(_ module: Presentable, animated: Bool = true, popCompletion: (() -> 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"
|
||||||
|
}
|
||||||
|
}
|
96
ElementX/Sources/Modules/Routers/NavigationRouterStore.swift
Normal file
96
ElementX/Sources/Modules/Routers/NavigationRouterStore.swift
Normal file
@ -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<UINavigationController, NavigationRouter>()
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
135
ElementX/Sources/Modules/Routers/NavigationRouterType.swift
Executable file
135
ElementX/Sources/Modules/Routers/NavigationRouterType.swift
Executable file
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
38
ElementX/Sources/Modules/Routers/Presentable.swift
Executable file
38
ElementX/Sources/Modules/Routers/Presentable.swift
Executable file
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
87
ElementX/Sources/Modules/Routers/RootRouter.swift
Executable file
87
ElementX/Sources/Modules/Routers/RootRouter.swift
Executable file
@ -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?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
49
ElementX/Sources/Modules/Routers/RootRouterType.swift
Executable file
49
ElementX/Sources/Modules/Routers/RootRouterType.swift
Executable file
@ -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)?)
|
||||||
|
}
|
12
ElementX/Sources/Modules/Splash/SplashViewController.swift
Normal file
12
ElementX/Sources/Modules/Splash/SplashViewController.swift
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
//
|
||||||
|
// SplashViewController.swift
|
||||||
|
// ElementX
|
||||||
|
//
|
||||||
|
// Created by Stefan Ceriu on 14.02.2022.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SplashViewController: UIViewController {
|
||||||
|
|
||||||
|
}
|
45
ElementX/Sources/Modules/Splash/SplashViewController.xib
Normal file
45
ElementX/Sources/Modules/Splash/SplashViewController.xib
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="SplashViewController" customModule="ElementX" customModuleProvider="target">
|
||||||
|
<connections>
|
||||||
|
<outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
|
||||||
|
</connections>
|
||||||
|
</placeholder>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="i5M-Pr-FkT">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="app-logo" translatesAutoresizingMaskIntoConstraints="NO" id="ue7-fB-5XS">
|
||||||
|
<rect key="frame" x="87" y="328" width="240" height="240"/>
|
||||||
|
<color key="tintColor" red="0.050980392156862744" green="0.74117647058823533" blue="0.54509803921568623" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</imageView>
|
||||||
|
</subviews>
|
||||||
|
<viewLayoutGuide key="safeArea" id="fnl-2z-Ty3"/>
|
||||||
|
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="fnl-2z-Ty3" firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="ue7-fB-5XS" secondAttribute="bottom" constant="16" id="Cip-Sc-gaF"/>
|
||||||
|
<constraint firstItem="ue7-fB-5XS" firstAttribute="top" relation="greaterThanOrEqual" secondItem="fnl-2z-Ty3" secondAttribute="top" constant="16" id="Clt-cS-YAr"/>
|
||||||
|
<constraint firstItem="ue7-fB-5XS" firstAttribute="centerY" secondItem="i5M-Pr-FkT" secondAttribute="centerY" id="N3w-Jf-MRA"/>
|
||||||
|
<constraint firstItem="fnl-2z-Ty3" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="ue7-fB-5XS" secondAttribute="trailing" constant="16" id="WfN-3K-kpr"/>
|
||||||
|
<constraint firstItem="ue7-fB-5XS" firstAttribute="centerX" secondItem="i5M-Pr-FkT" secondAttribute="centerX" id="ujr-SL-AyX"/>
|
||||||
|
<constraint firstItem="ue7-fB-5XS" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="fnl-2z-Ty3" secondAttribute="leading" constant="16" id="yNl-Wu-AES"/>
|
||||||
|
</constraints>
|
||||||
|
<point key="canvasLocation" x="137.68115942028987" y="153.34821428571428"/>
|
||||||
|
</view>
|
||||||
|
</objects>
|
||||||
|
<resources>
|
||||||
|
<image name="app-logo" width="240" height="240"/>
|
||||||
|
<systemColor name="systemBackgroundColor">
|
||||||
|
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
</systemColor>
|
||||||
|
</resources>
|
||||||
|
</document>
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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<TemplateSimpleScreenViewState,
|
||||||
|
Never,
|
||||||
|
TemplateSimpleScreenViewAction>
|
||||||
|
@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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 }
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
6
ElementX/Supporting Files/Assets.xcassets/Contents.json
Normal file
6
ElementX/Supporting Files/Assets.xcassets/Contents.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
26
ElementX/Supporting Files/Assets.xcassets/app-logo.imageset/Contents.json
vendored
Normal file
26
ElementX/Supporting Files/Assets.xcassets/app-logo.imageset/Contents.json
vendored
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
BIN
ElementX/Supporting Files/Assets.xcassets/app-logo.imageset/launch_screen_logo.png
vendored
Normal file
BIN
ElementX/Supporting Files/Assets.xcassets/app-logo.imageset/launch_screen_logo.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.0 KiB |
BIN
ElementX/Supporting Files/Assets.xcassets/app-logo.imageset/launch_screen_logo@2x.png
vendored
Normal file
BIN
ElementX/Supporting Files/Assets.xcassets/app-logo.imageset/launch_screen_logo@2x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
ElementX/Supporting Files/Assets.xcassets/app-logo.imageset/launch_screen_logo@3x.png
vendored
Normal file
BIN
ElementX/Supporting Files/Assets.xcassets/app-logo.imageset/launch_screen_logo@3x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
32
ElementX/Supporting Files/Base.lproj/LaunchScreen.storyboard
Normal file
32
ElementX/Supporting Files/Base.lproj/LaunchScreen.storyboard
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||||
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--View Controller-->
|
||||||
|
<scene sceneID="EHf-IW-A2E">
|
||||||
|
<objects>
|
||||||
|
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||||
|
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||||
|
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="53" y="375"/>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
<resources>
|
||||||
|
<systemColor name="systemBackgroundColor">
|
||||||
|
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
</systemColor>
|
||||||
|
</resources>
|
||||||
|
</document>
|
10
ElementX/Supporting Files/ElementX.entitlements
Normal file
10
ElementX/Supporting Files/ElementX.entitlements
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.app-sandbox</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
36
ElementXTests/ElementXTests.swift
Normal file
36
ElementXTests/ElementXTests.swift
Normal file
@ -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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
42
ElementXUITests/ElementXUITests.swift
Normal file
42
ElementXUITests/ElementXUITests.swift
Normal file
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
ElementXUITests/ElementXUITestsLaunchTests.swift
Normal file
32
ElementXUITests/ElementXUITestsLaunchTests.swift
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
31
Scripts/createSwiftUISimpleScreen.sh
Executable file
31
Scripts/createSwiftUISimpleScreen.sh
Executable file
@ -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
|
Loading…
x
Reference in New Issue
Block a user