Initial project setup.

This commit is contained in:
Stefan Ceriu 2022-02-14 18:05:21 +02:00
parent a499570f39
commit a3fcc0f612
66 changed files with 4474 additions and 0 deletions

22
.gitignore vendored Normal file
View 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
View 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

View 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 */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -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>

View File

@ -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
}

View 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() {
}
}

View 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
}
}

View File

@ -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
}
}

View File

@ -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")
}
}
}

View File

@ -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()
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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)))
}
}
}

View File

@ -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 }
}

View File

@ -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)
}
}

View 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 XCTest
@testable import ElementX
@available(iOS 14.0, *)
class LoginScreenViewModelTests: XCTestCase {
override func setUpWithError() throws {
}
func testInitialState() {
}
}

View File

@ -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)
}
}

View 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
}
}
}

View File

@ -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
}
}

View 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
}

View File

@ -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) {
}
}

View File

@ -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 }
}

View File

@ -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)
}
}

View File

@ -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() {
}
}

View 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)
}
}

View 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
}

View 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 }
}
}

View 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)
}
}

View File

@ -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.")
}
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View 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))"
}
}

View 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"
}
}

View 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)
}
}

View File

@ -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
}

View 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)
}
}

View 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)
}
}

View 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?()
}
}
}

View 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)?)
}

View File

@ -0,0 +1,12 @@
//
// SplashViewController.swift
// ElementX
//
// Created by Stefan Ceriu on 14.02.2022.
//
import UIKit
class SplashViewController: UIViewController {
}

View 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>

View File

@ -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
}
}

View File

@ -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))
)
}
}

View File

@ -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
}

View File

@ -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
}
}
}

View File

@ -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 }
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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()
}
}

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View 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>

View 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>

View 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.
}
}
}

View 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 its 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()
}
}
}
}

View 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)
}
}

View 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