├── .gitignore ├── FunCorpSteamApp.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── swiftpm │ └── Package.resolved ├── Modules ├── App │ ├── App.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── App.xcscheme │ ├── Resources │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── steamAuth.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── steamAuth.png │ │ │ └── tabbar │ │ │ │ ├── Contents.json │ │ │ │ ├── tabbarNews.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── news.png │ │ │ │ ├── tabbarProfile.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── profile.png │ │ │ │ └── tabbarSessions.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── sessions.png │ │ ├── Base.lproj │ │ │ └── LaunchScreen.storyboard │ │ ├── Info.plist │ │ ├── en.lproj │ │ │ └── Localizable.strings │ │ └── ru.lproj │ │ │ └── Localizable.strings │ └── Sources │ │ ├── initialization │ │ ├── AppDelegate.swift │ │ ├── AppDependency.swift │ │ ├── AppStartPoint.swift │ │ ├── Application.swift │ │ └── StartPoints.swift │ │ ├── logger │ │ ├── ConsoleLogDestination.swift │ │ ├── LogFileDestination.swift │ │ ├── LogInitialization.swift │ │ └── XcodeDebugLogDestination.swift │ │ └── routing │ │ └── AppRouter.swift ├── Common │ ├── Package.swift │ ├── README.md │ └── Sources │ │ ├── Localization.swift │ │ ├── logs │ │ ├── LogDestination.swift │ │ ├── LogLevel.swift │ │ ├── Logger+Levels.swift │ │ └── Logger.swift │ │ ├── multithreads │ │ ├── DispatchQueue+mainSync.swift │ │ └── FastLock.swift │ │ ├── ui │ │ ├── ChangeableImage.swift │ │ └── ConstImage.swift │ │ └── utils │ │ ├── Getter.swift │ │ ├── ModelsDiff.swift │ │ ├── Notifier.swift │ │ ├── Weak.swift │ │ └── WeakArray.swift ├── Core │ ├── .swiftpm │ │ └── xcode │ │ │ └── package.xcworkspace │ │ │ └── contents.xcworkspacedata │ ├── Package.swift │ ├── README.md │ └── Sources │ │ ├── CoreStartPoint.swift │ │ ├── deeplinks │ │ └── DeepLink.swift │ │ ├── module_initialization │ │ ├── CommonStartPoint.swift │ │ ├── UIModuleName.swift │ │ └── UIStartPoint.swift │ │ └── routing │ │ ├── IRouter.swift │ │ ├── Navigator.swift │ │ ├── RoutingParamaters.swift │ │ └── Screen.swift ├── Domain │ ├── Entities │ │ ├── Package.swift │ │ ├── README.md │ │ └── Sources │ │ │ ├── ServiceCompletion.swift │ │ │ ├── ServiceError.swift │ │ │ ├── SteamIDs.swift │ │ │ ├── SteamLocalization.swift │ │ │ └── StorageResult.swift │ ├── UseCases │ │ ├── Package.swift │ │ ├── README.md │ │ └── Sources │ │ │ ├── ImageService.swift │ │ │ ├── auth │ │ │ ├── SteamAuthService.swift │ │ │ └── models │ │ │ │ └── SteamAuthErrors.swift │ │ │ ├── games │ │ │ ├── dota │ │ │ │ ├── SteamDotaCompletion.swift │ │ │ │ ├── SteamDotaService.swift │ │ │ │ ├── SteamDotaServiceCalculator.swift │ │ │ │ └── models │ │ │ │ │ ├── DotaHero.swift │ │ │ │ │ ├── DotaItem.swift │ │ │ │ │ ├── DotaLeaverStatus.swift │ │ │ │ │ ├── DotaLobby.swift │ │ │ │ │ ├── DotaMatch.swift │ │ │ │ │ ├── DotaMatchDetails.swift │ │ │ │ │ ├── DotaSide.swift │ │ │ │ │ ├── SteamDotaError.swift │ │ │ │ │ └── calculated │ │ │ │ │ ├── DotaAvgScores.swift │ │ │ │ │ ├── DotaPopularHero.swift │ │ │ │ │ └── DotaWinLose.swift │ │ │ ├── game │ │ │ │ ├── SteamAchievementService.swift │ │ │ │ ├── SteamGameService.swift │ │ │ │ └── models │ │ │ │ │ ├── SteamAchievementsSummary.swift │ │ │ │ │ ├── SteamGameInfo.swift │ │ │ │ │ ├── SteamGameProgress.swift │ │ │ │ │ └── SteamGameScheme.swift │ │ │ └── sessions │ │ │ │ ├── SteamSessionsService.swift │ │ │ │ └── models │ │ │ │ └── SteamSession.swift │ │ │ └── profile │ │ │ ├── SteamFriendsService.swift │ │ │ ├── SteamProfileGamesService.swift │ │ │ ├── SteamProfileService.swift │ │ │ └── models │ │ │ ├── SteamFriend.swift │ │ │ ├── SteamProfile.swift │ │ │ └── SteamProfileGameInfo.swift │ └── UseCasesContracts │ │ ├── .swiftpm │ │ └── xcode │ │ │ └── package.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ ├── Package.swift │ │ ├── README.md │ │ └── Sources │ │ ├── auth │ │ ├── SteamAuthNetwork.swift │ │ └── SteamAuthStorage.swift │ │ ├── games │ │ ├── dota │ │ │ ├── SteamDotaNetwork.swift │ │ │ └── SteamDotaStorage.swift │ │ ├── game │ │ │ ├── SteamGameNetwork.swift │ │ │ └── SteamGameStorage.swift │ │ └── sessions │ │ │ ├── SteamSessionsNetwork.swift │ │ │ └── SteamSessionsStorage.swift │ │ └── profile │ │ ├── SteamProfileNetwork.swift │ │ └── SteamProfileStorage.swift ├── Infrastructure │ ├── Network │ │ ├── Package.swift │ │ ├── README.md │ │ └── Sources │ │ │ ├── NetworkDependency.swift │ │ │ ├── NetworkStartPoint.swift │ │ │ ├── auth │ │ │ └── SteamAuthNetworkImpl.swift │ │ │ ├── games │ │ │ ├── dota │ │ │ │ └── SteamDotaNetworkImpl.swift │ │ │ ├── game │ │ │ │ └── SteamGameNetworkImpl.swift │ │ │ └── sessions │ │ │ │ └── SteamSessionsNetworkImpl.swift │ │ │ ├── profile │ │ │ └── SteamProfileNetworkImpl.swift │ │ │ └── root │ │ │ ├── Int64+ToDate.swift │ │ │ ├── NetworkSession.swift │ │ │ ├── Request.swift │ │ │ ├── Response.swift │ │ │ ├── SteamLocalization+ToString.swift │ │ │ └── SteamRequest.swift │ ├── Storage │ │ ├── Package.swift │ │ ├── README.md │ │ └── Sources │ │ │ ├── StorageDependency.swift │ │ │ ├── StorageStartPoint.swift │ │ │ ├── auth │ │ │ ├── SteamAuthData.swift │ │ │ └── SteamAuthStorageImpl.swift │ │ │ ├── games │ │ │ ├── dota │ │ │ │ ├── DotaLobby+Convert.swift │ │ │ │ ├── DotaSide+Convert.swift │ │ │ │ ├── SteamDotaHeroData.swift │ │ │ │ ├── SteamDotaMatchData.swift │ │ │ │ ├── SteamDotaMatchDetailsData.swift │ │ │ │ └── SteamDotaStorageImpl.swift │ │ │ ├── game │ │ │ │ ├── SteamGameProgressData.swift │ │ │ │ ├── SteamGameSchemeData.swift │ │ │ │ └── SteamGameStorageImpl.swift │ │ │ └── sessions │ │ │ │ ├── SteamSessionData.swift │ │ │ │ └── SteamSessionsStorageImpl.swift │ │ │ ├── profile │ │ │ ├── SteamFriendData.swift │ │ │ ├── SteamProfileData.swift │ │ │ ├── SteamProfileGameData.swift │ │ │ └── SteamProfileStorageImpl.swift │ │ │ └── support │ │ │ ├── DataConverters.swift │ │ │ ├── Realm+threadSafe.swift │ │ │ └── TimeInterval+minutes.swift │ └── UseCasesImpl │ │ ├── Package.swift │ │ ├── README.md │ │ └── Sources │ │ ├── UseCasesImplDependency.swift │ │ ├── UseCasesImplStartPoint.swift │ │ └── usecases │ │ ├── ImageServiceImpl.swift │ │ ├── auth │ │ └── SteamAuthServiceImpl.swift │ │ ├── base │ │ └── UniversalServiceImpl.swift │ │ ├── games │ │ ├── dota │ │ │ ├── SteamDotaDetailsSynchronizer.swift │ │ │ ├── SteamDotaHistorySynchronizer.swift │ │ │ ├── SteamDotaServiceCalculatorImpl.swift │ │ │ ├── SteamDotaServiceImpl.swift │ │ │ └── SteamDotaSynchonizers.swift │ │ ├── game │ │ │ ├── SteamAchievementServiceImpl.swift │ │ │ └── SteamGameServiceImpl.swift │ │ └── sessions │ │ │ └── SteamSessionsServiceImpl.swift │ │ └── profile │ │ ├── SteamFriendsServiceImpl.swift │ │ ├── SteamProfileGamesServiceImpl.swift │ │ └── SteamProfileServiceImpl.swift └── UIs │ ├── AppUIComponents │ ├── Package.swift │ ├── README.md │ └── Sources │ │ ├── AppUIComponentsStartPoint.swift │ │ ├── SteamAvatarView.swift │ │ └── SteamGameImageView.swift │ ├── Design │ ├── Package.swift │ ├── README.md │ └── Sources │ │ ├── Common │ │ ├── Gradient.swift │ │ ├── TimeInterval+String.swift │ │ └── UIColor+Hex.swift │ │ ├── DesignStartPoint.swift │ │ └── Style │ │ ├── Style.swift │ │ ├── StyleMaker.swift │ │ ├── StylizingView.swift │ │ ├── StylizingViewsContainer.swift │ │ └── consts │ │ ├── ConstColors.swift │ │ ├── ConstsAnimation.swift │ │ └── ConstsFonts.swift │ ├── Modules │ ├── Auth │ │ ├── Package.swift │ │ ├── README.md │ │ └── Sources │ │ │ ├── AuthDependency.swift │ │ │ ├── AuthRouter.swift │ │ │ ├── AuthStartPoint.swift │ │ │ └── auth │ │ │ ├── AuthScreenPresenter.swift │ │ │ └── AuthScreenView.swift │ ├── GameInformation │ │ ├── Dota │ │ │ ├── Package.swift │ │ │ ├── README.md │ │ │ └── Sources │ │ │ │ ├── DotaDependency.swift │ │ │ │ ├── DotaRouter.swift │ │ │ │ ├── DotaStartPoint.swift │ │ │ │ ├── gameInfo │ │ │ │ ├── DotaGameInfoPresenter.swift │ │ │ │ ├── last2WeeksSummary │ │ │ │ │ ├── Dota2WeeksDetailsViewModel.swift │ │ │ │ │ ├── Dota2WeeksGamesCountViewModel.swift │ │ │ │ │ ├── Dota2WeeksSummaryCell.swift │ │ │ │ │ └── Dota2WeeksSummaryConfigurator.swift │ │ │ │ └── lastGame │ │ │ │ │ ├── DotaLastMatchCell.swift │ │ │ │ │ ├── DotaLastMatchConfigurator.swift │ │ │ │ │ └── DotaLastMatchViewModel.swift │ │ │ │ └── statistics │ │ │ │ ├── DotaStatisticViewModel.swift │ │ │ │ ├── DotaStatisticsScreenPresenter.swift │ │ │ │ └── view │ │ │ │ ├── DotaGraphic.swift │ │ │ │ ├── DotaGraphicHint.swift │ │ │ │ ├── DotaStatisticCell.swift │ │ │ │ ├── DotaStatisticsScreenView.swift │ │ │ │ └── DotaStatisticsTableView.swift │ │ └── GameInformation │ │ │ ├── Package.swift │ │ │ ├── README.md │ │ │ └── Sources │ │ │ ├── GameInfoDependency.swift │ │ │ ├── GameInfoRouter.swift │ │ │ ├── GameInfoStartPoint.swift │ │ │ ├── custom │ │ │ ├── CustomGameInfoPresenter.swift │ │ │ ├── CustomGameInfoRouter.swift │ │ │ ├── CustomGameInfoViewContract.swift │ │ │ ├── CustomTableCellConfigurator.swift │ │ │ └── universal │ │ │ │ └── CustomGameInfoPresenterConfigurator.swift │ │ │ └── gameInfo │ │ │ ├── AchievementsSummaryViewModel.swift │ │ │ ├── GameInfoScreen.swift │ │ │ ├── GameInfoScreenPresenter.swift │ │ │ ├── GameInfoViewModel.swift │ │ │ └── view │ │ │ ├── AchievementsSummaryCell.swift │ │ │ ├── GameInfoCell.swift │ │ │ ├── GameInfoScreenView.swift │ │ │ └── GameInfoTableView.swift │ ├── Menu │ │ ├── Package.swift │ │ ├── README.md │ │ └── Sources │ │ │ ├── MenuDependency.swift │ │ │ ├── MenuRouter.swift │ │ │ ├── MenuStartPoint.swift │ │ │ └── menu │ │ │ ├── MenuScreenPresenter.swift │ │ │ ├── MenuScreenView.swift │ │ │ └── MenuViewModel.swift │ ├── News │ │ ├── Package.swift │ │ ├── README.md │ │ └── Sources │ │ │ ├── NewsDependency.swift │ │ │ ├── NewsRouter.swift │ │ │ ├── NewsStartPoint.swift │ │ │ └── ribbon │ │ │ ├── RibbonScreenPresenter.swift │ │ │ └── view │ │ │ └── RibbonScreenView.swift │ ├── Profile │ │ ├── Package.swift │ │ ├── README.md │ │ └── Sources │ │ │ ├── ProfileDependency.swift │ │ │ ├── ProfileRouter.swift │ │ │ ├── ProfileStartPoint.swift │ │ │ ├── friends │ │ │ ├── FriendViewModel.swift │ │ │ ├── FriendsScreenPresenter.swift │ │ │ └── view │ │ │ │ ├── FriendCell.swift │ │ │ │ ├── FriendsScreenView.swift │ │ │ │ └── FriendsTableView.swift │ │ │ └── profile │ │ │ ├── ProfileGameInfoViewModel.swift │ │ │ ├── ProfileScreenPresenter.swift │ │ │ ├── ProfileViewModel.swift │ │ │ └── view │ │ │ ├── ProfileCell.swift │ │ │ ├── ProfileGameCell.swift │ │ │ ├── ProfileScreenView.swift │ │ │ └── ProfileTableView.swift │ └── Sessions │ │ ├── Package.swift │ │ ├── README.md │ │ └── Sources │ │ ├── SessionsDependency.swift │ │ ├── SessionsRouter.swift │ │ ├── SessionsStartPoint.swift │ │ └── sessions │ │ ├── SessionViewModel.swift │ │ ├── SessionsScreenPresenter.swift │ │ └── view │ │ ├── SessionCell.swift │ │ ├── SessionsScreenView.swift │ │ └── SessionsTableView.swift │ └── UIComponents │ ├── Package.swift │ ├── README.md │ └── Sources │ ├── UIComponentsStartPoint.swift │ ├── alerts │ └── ErrorAlert.swift │ ├── base │ ├── ApHapticFeedback.swift │ ├── ApNavigationController.swift │ ├── ApTabBarController.swift │ ├── ApViewController.swift │ └── table │ │ ├── ApCollectionView.swift │ │ ├── ApSectionTitleView.swift │ │ ├── ApTableView.swift │ │ └── ApTableViewCell.swift │ ├── skeleton │ ├── SkeletonView.swift │ ├── SkeletonViewModel.swift │ └── UIView+Skeleton.swift │ └── views │ ├── AvatarView.swift │ ├── GradientView.swift │ └── IdImageView.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots/**/*.png 68 | fastlane/test_output 69 | -------------------------------------------------------------------------------- /FunCorpSteamApp.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "DITranquillity", 6 | "repositoryURL": "https://github.com/ivlevAstef/DITranquillity.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "2a366d93b213d42d6b32d724f488feba996bdd53", 10 | "version": "3.9.2" 11 | } 12 | }, 13 | { 14 | "package": "Realm", 15 | "repositoryURL": "https://github.com/realm/realm-cocoa.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "11d7853b750f367525b174331e72f5431e15dfcc", 19 | "version": "4.1.1" 20 | } 21 | }, 22 | { 23 | "package": "RealmCore", 24 | "repositoryURL": "https://github.com/realm/realm-core", 25 | "state": { 26 | "branch": null, 27 | "revision": "d2f6573960c84ebea3e0236047b3d976f95a5d7a", 28 | "version": "5.23.6" 29 | } 30 | }, 31 | { 32 | "package": "SnapKit", 33 | "repositoryURL": "https://github.com/SnapKit/SnapKit.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "d458564516e5676af9c70b4f4b2a9178294f1bc6", 37 | "version": "5.0.1" 38 | } 39 | }, 40 | { 41 | "package": "SwiftLazy", 42 | "repositoryURL": "https://github.com/ivlevAstef/SwiftLazy.git", 43 | "state": { 44 | "branch": null, 45 | "revision": "988bd1bc5850115a8f3ae20e4db937963b076960", 46 | "version": "1.1.5" 47 | } 48 | } 49 | ] 50 | }, 51 | "version": 1 52 | } 53 | -------------------------------------------------------------------------------- /Modules/App/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Modules/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Modules/App/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Modules/App/Resources/Assets.xcassets/steamAuth.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "steamAuth.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Modules/App/Resources/Assets.xcassets/steamAuth.imageset/steamAuth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivlevAstef/FunCorpSteamApp/b7137ea140c578322aaf2af908481e8415695293/Modules/App/Resources/Assets.xcassets/steamAuth.imageset/steamAuth.png -------------------------------------------------------------------------------- /Modules/App/Resources/Assets.xcassets/tabbar/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Modules/App/Resources/Assets.xcassets/tabbar/tabbarNews.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "news.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Modules/App/Resources/Assets.xcassets/tabbar/tabbarNews.imageset/news.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivlevAstef/FunCorpSteamApp/b7137ea140c578322aaf2af908481e8415695293/Modules/App/Resources/Assets.xcassets/tabbar/tabbarNews.imageset/news.png -------------------------------------------------------------------------------- /Modules/App/Resources/Assets.xcassets/tabbar/tabbarProfile.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "profile.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Modules/App/Resources/Assets.xcassets/tabbar/tabbarProfile.imageset/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivlevAstef/FunCorpSteamApp/b7137ea140c578322aaf2af908481e8415695293/Modules/App/Resources/Assets.xcassets/tabbar/tabbarProfile.imageset/profile.png -------------------------------------------------------------------------------- /Modules/App/Resources/Assets.xcassets/tabbar/tabbarSessions.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "sessions.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Modules/App/Resources/Assets.xcassets/tabbar/tabbarSessions.imageset/sessions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivlevAstef/FunCorpSteamApp/b7137ea140c578322aaf2af908481e8415695293/Modules/App/Resources/Assets.xcassets/tabbar/tabbarSessions.imageset/sessions.png -------------------------------------------------------------------------------- /Modules/App/Resources/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Modules/App/Sources/initialization/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ApLife 4 | // 5 | // Created by Alexander Ivlev on 22/09/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Common 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate 14 | { 15 | var window: UIWindow? 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | AppStartPoint.configure() 19 | log.info("configuration finished") 20 | AppStartPoint.reg() 21 | log.info("registration and validate dependency finished") 22 | AppStartPoint.initialize() 23 | log.info("initialize modules finished") 24 | 25 | let window = UIWindow(frame: UIScreen.main.bounds) 26 | self.window = window 27 | AppStartPoint.app.configureWindow(window) 28 | AppStartPoint.app.start(launchOptions: launchOptions) 29 | window.makeKeyAndVisible() 30 | 31 | log.info("setup windows and started application") 32 | 33 | return true 34 | } 35 | 36 | func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { 37 | return AppStartPoint.app.openUrl(url) 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /Modules/App/Sources/initialization/AppDependency.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDependency.swift 3 | // ApLife 4 | // 5 | // Created by Alexander Ivlev on 22/09/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import DITranquillity 10 | import Core 11 | import Common 12 | 13 | final class AppDependency 14 | { 15 | static func configure() { 16 | DISetting.Defaults.injectToSubviews = false 17 | DISetting.Defaults.lifeTime = .prototype 18 | DISetting.Log.level = .verbose 19 | DISetting.Log.tab = " " 20 | DISetting.Log.fun = Self.logMethod 21 | } 22 | 23 | static func reg(container: DIContainer) { 24 | container.append(framework: AppFramework.self) 25 | } 26 | 27 | static func validate(container: DIContainer) { 28 | #if DEBUG 29 | if !container.validate(checkGraphCycles: false) { 30 | log.fatal("DI graph validation failed") 31 | } 32 | #endif 33 | } 34 | 35 | private static func logMethod(level: DILogLevel, msg: String) { 36 | switch level { 37 | case .error: 38 | log.error(msg) 39 | case .warning: 40 | log.warning(msg) 41 | case .info: 42 | log.debug(msg) 43 | case .verbose: 44 | log.trace(msg) 45 | case .none: 46 | break 47 | } 48 | 49 | } 50 | } 51 | 52 | private class AppFramework: DIFramework 53 | { 54 | static func load(container: DIContainer) { 55 | container.register(Application.init) 56 | .lifetime(.perRun(.strong)) 57 | 58 | container.register(AppRouter.init) 59 | .as(IRouter.self) 60 | .lifetime(.objectGraph) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Modules/App/Sources/initialization/AppStartPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppStartPoint.swift 3 | // ApLife 4 | // 5 | // Created by Alexander Ivlev on 23/09/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Core 10 | import DITranquillity 11 | 12 | final class AppStartPoint 13 | { 14 | static var app: Application { 15 | return container.resolve() 16 | } 17 | 18 | private static let container = DIContainer() 19 | 20 | static func configure() { 21 | LogInitialization.configure() 22 | AppDependency.configure() 23 | 24 | for startPoint in StartPoints.common + StartPoints.ui.values.map({ $0 as CommonStartPoint }) { 25 | startPoint.configure() 26 | } 27 | } 28 | 29 | static func reg() { 30 | AppDependency.reg(container: container) 31 | 32 | for startPoint in StartPoints.common + StartPoints.ui.values.map({ $0 as CommonStartPoint }) { 33 | startPoint.reg(container: container) 34 | } 35 | 36 | AppDependency.validate(container: container) 37 | } 38 | 39 | static func initialize() { 40 | for startPoint in StartPoints.common + StartPoints.ui.values.map({ $0 as CommonStartPoint }) { 41 | startPoint.initialize() 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Modules/App/Sources/initialization/Application.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Application.swift 3 | // ApLife 4 | // 5 | // Created by Alexander Ivlev on 22/09/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Core 11 | import UseCases 12 | import SwiftLazy 13 | 14 | // Start point 15 | final class Application 16 | { 17 | private let router: AppRouter 18 | 19 | init(router: AppRouter) { 20 | self.router = router 21 | } 22 | 23 | func configureWindow(_ window: UIWindow) { 24 | window.rootViewController = router.rootViewController 25 | } 26 | 27 | func start(launchOptions: [UIApplication.LaunchOptionsKey: Any]?) { 28 | // TODO: support parse launch options 29 | router.start(parameters: RoutingParamaters()) 30 | } 31 | 32 | func openUrl(_ url: URL) -> Bool { 33 | // TODO: support parse url 34 | return false 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Modules/App/Sources/initialization/StartPoints.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StartPoints.swift 3 | // ApLife 4 | // 5 | // Created by Alexander Ivlev on 24/09/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Core 10 | import Design 11 | import UIComponents 12 | import AppUIComponents 13 | 14 | import Storage 15 | import Network 16 | import UseCasesImpl 17 | 18 | import Auth 19 | import Menu 20 | 21 | import News 22 | import Profile 23 | import GameInformation 24 | import Sessions 25 | 26 | import Dota 27 | 28 | enum StartPoints 29 | { 30 | static let auth = AuthStartPoint() 31 | static let menu = MenuStartPoint() 32 | 33 | static let news = NewsStartPoint() 34 | static let profile = ProfileStartPoint() 35 | static let gameInfo = GameInfoStartPoint() 36 | static let sessions = SessionsStartPoint() 37 | 38 | // Порядок инициализации этих точек важен - всегда идем от низа к вверху 39 | static let common: [CommonStartPoint] = [ 40 | CoreStartPoint(), 41 | DesignStartPoint(), 42 | UIComponentsStartPoint(), 43 | AppUIComponentsStartPoint(), 44 | StorageStartPoint(), 45 | NetworkStartPoint(), 46 | UseCasesImplStartPoint(), 47 | 48 | DotaStartPoint(), 49 | ] 50 | 51 | static let ui: [UIModuleName: UIStartPoint] = [ 52 | AuthStartPoint.name: auth, 53 | MenuStartPoint.name: menu, 54 | NewsStartPoint.name: news, 55 | ProfileStartPoint.name: profile, 56 | GameInfoStartPoint.name: gameInfo, 57 | SessionsStartPoint.name: sessions, 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /Modules/App/Sources/logger/ConsoleLogDestination.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConsoleLogDestination.swift 3 | // Logger 4 | // 5 | // Created by Alexander Ivlev on 25/02/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Common 11 | 12 | final class ConsoleLogDestination: LogDestination 13 | { 14 | let format: String 15 | let limitOutputLevel: LogLevel 16 | 17 | init(format: String = " [%L] %F:%l: %m", limitOutputLevel: LogLevel = .trace) { 18 | self.format = format 19 | self.limitOutputLevel = limitOutputLevel 20 | } 21 | 22 | func process(_ msg: String, level: LogLevel) { 23 | // Не print ибо print не показывается в консоле девайса 24 | // То есть print имеет смысл только при запуске прям из xCode, а просто посмотреть логи на устройстве с помощью print нельзя 25 | NSLog(msg) 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /Modules/App/Sources/logger/LogFileDestination.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogFileDestination.swift 3 | // Logger 4 | // 5 | // Created by Alexander Ivlev on 25/02/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Common 11 | 12 | final class LogFileDestination: LogDestination 13 | { 14 | let format: String 15 | let limitOutputLevel: LogLevel 16 | 17 | private let fileHandler: FileHandle? 18 | 19 | init(format: String = "%Df [%s]: %m", limitOutputLevel: LogLevel = .info) { 20 | self.format = format 21 | self.limitOutputLevel = limitOutputLevel 22 | guard let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { 23 | self.fileHandler = nil 24 | return 25 | } 26 | let dateFormatter = DateFormatter() 27 | dateFormatter.dateFormat = "yyyy_MM_dd_HH_mm_ss" 28 | let fileURL = documentsURL.appendingPathComponent("log_\(dateFormatter.string(from: Date())).log") 29 | 30 | if !FileManager.default.createFile(atPath: fileURL.path, contents: nil, attributes: nil) { 31 | self.fileHandler = nil 32 | return 33 | } 34 | 35 | self.fileHandler = try? FileHandle(forWritingTo: fileURL) 36 | } 37 | 38 | deinit { 39 | self.fileHandler?.synchronizeFile() 40 | self.fileHandler?.closeFile() 41 | } 42 | 43 | func process(_ msg: String, level: LogLevel) { 44 | guard let fileHandler = self.fileHandler, let data = "\(msg)\n".data(using: .utf8) else { 45 | return 46 | } 47 | 48 | fileHandler.write(data) 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Modules/App/Sources/logger/LogInitialization.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogInitialization.swift 3 | // ApLife 4 | // 5 | // Created by Alexander Ivlev on 02/10/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Common 10 | 11 | final class LogInitialization 12 | { 13 | static func configure() { 14 | #if DEBUG 15 | log.addDestination(XcodeDebugLogDestination(format: " [%e] %F:%l: %m", limitOutputLevel: .trace)) 16 | #else 17 | log.addDestination(ConsoleLogDestination(format: " [%s] %F:%l: %m", limitOutputLevel: .info)) 18 | log.addDestination(LogFileDestination(format: "%Df [%s]: %m", limitOutputLevel: .info)) 19 | #endif 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Modules/App/Sources/logger/XcodeDebugLogDestination.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XcodeDebugLogDestination.swift 3 | // Logger 4 | // 5 | // Created by Alexander Ivlev on 25/02/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Common 11 | 12 | final class XcodeDebugLogDestination: LogDestination 13 | { 14 | let format: String 15 | let limitOutputLevel: LogLevel 16 | 17 | init(format: String = " [%e] %F:%l: %m", limitOutputLevel: LogLevel = .trace) { 18 | self.format = format 19 | self.limitOutputLevel = limitOutputLevel 20 | } 21 | 22 | func process(_ msg: String, level: LogLevel) { 23 | print(msg) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Modules/Common/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Common", 8 | platforms: [.iOS(.v10)], 9 | products: [ 10 | .library(name: "Common", targets: ["Common"]), 11 | ], 12 | dependencies: [ 13 | ], 14 | targets: [ 15 | .target(name: "Common", dependencies: [], path: "./Sources"), 16 | ] 17 | ) 18 | -------------------------------------------------------------------------------- /Modules/Common/README.md: -------------------------------------------------------------------------------- 1 | # Common 2 | 3 | Пакет с кучей вспомогательных программных сущностей. Может переноситься без зазрения совести - ни от чего не зависит, и все сущности универсальные. 4 | 5 | Правда сюда еще логгер забрался, несмотря на то что это архитектурное решение - мне так удобней, ктомуже если не тут, то отдельной либой бы был. 6 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Localization.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Localization.swift 3 | // Common 4 | // 5 | // Created by Alexander Ivlev on 28/10/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public let loc = Localization() 12 | 13 | /// Простецкая локализация 14 | public final class Localization 15 | { 16 | private let notFound = "!@#can_not_found_key!@#" 17 | 18 | public subscript(_ key: String) -> String { 19 | let result = NSLocalizedString(key, value: notFound, comment: key) 20 | if result == notFound { 21 | log.assert("can't found localization for key: \(key)") 22 | return key 23 | } 24 | 25 | return result 26 | } 27 | 28 | public var languageCode: String { 29 | return Locale.current.languageCode ?? "en" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Modules/Common/Sources/logs/LogDestination.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogDestination.swift 3 | // Logger 4 | // 5 | // Created by Alexander Ivlev on 22/02/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | /// Протокол от которого должны наследоваться все потоки вывода. 10 | public protocol LogDestination: class 11 | { 12 | // формат: 13 | // %Dx - вывод даты варианты: 14 | // %Dd (debug) - mm:ss.SSS 15 | // %Dn (nano) - HH:mm:ss.SSS 16 | // %Da (any) - yyyy-MM-dd HH:mm:ss.SSS 17 | // %Df (file) - MM-dd HH:mm:ss 18 | // %L - вывод уровня логирования 19 | // %s - вывод уровня логирования в коротком варианте 20 | // %e - вывод уровня логирования используя emoji 21 | // %F - вывод имени файла 22 | // %l - вывод строчки 23 | // %M - вывод метода 24 | // %m - вывод сообщения 25 | var format: String { get } 26 | 27 | /// Максимально допустимый уровень логирования - выше этого уровня логи не будут приходить в функцию process 28 | var limitOutputLevel: LogLevel { get } 29 | 30 | /// Обработчик нового сообщения от логгера. Сообщение приходит в конечном варианте - отформатированным 31 | /// 32 | /// - Parameters: 33 | /// - msg: Отформатированное сообщение 34 | /// - level: Уровень логирования для этого сообщения 35 | func process(_ msg: String, level: LogLevel) 36 | } 37 | -------------------------------------------------------------------------------- /Modules/Common/Sources/logs/LogLevel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogLevel.swift 3 | // Logger 4 | // 5 | // Created by Alexander Ivlev on 22/02/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | 10 | /// Уровни логирования 11 | public enum LogLevel 12 | { 13 | /// Полностью отключает логирование 14 | case none 15 | 16 | /// Критическая ошибка. После возникновения критической ошибки программа точно не может работать дальше. 17 | case fatal 18 | 19 | /// assert. По своей сути похожа на обычную ошибку, но предназначенную для дебага. 20 | /// То есть это вид ошибок которые помогают отлавливать несогласование контрактов вызова функций 21 | case assert 22 | 23 | /// Обычная Ошибка. После возникновения обычной ошибки программа может продолжить исполнение, но корректность ее работы не гарантируется. 24 | case error 25 | 26 | /// Предупреждение. После возникновения предупреждения программа продолжает корректное исполение, но ситуация в которой прозошла проблема скорей всего не будет закончена корректна. 27 | case warning 28 | 29 | /// Информационное. Данный уровень используется для написания какой либо полезной информации, но которая не является переодической. 30 | /// Более того если нужно написать много данных то сам факт наличия данных пишется на этом уровне, а данные переносятся на уровень trace. 31 | case info 32 | 33 | /// Дебаг. Эквивалент информационному сообщению, но используется исключительно для дебага. 34 | /// На этом уровне можно писать некоторые приватные данные, так как данный уровень не будет использоваться у клиетов и при выпуске. 35 | case debug 36 | 37 | /// Свалка. Данный уровень используется для чего угодно. Позволяет подробно проанализировать ситуацию, но обычно является избыточным и сильно большим. 38 | case trace 39 | 40 | internal var priority: Int { 41 | switch self { 42 | case .none: return 0 43 | case .fatal: return 100 44 | case .assert: return 150 45 | case .error: return 200 46 | case .warning: return 300 47 | case .info: return 400 48 | case .debug: return 450 49 | case .trace: return 500 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Modules/Common/Sources/multithreads/DispatchQueue+mainSync.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DispatchQueue+mainSync.swift 3 | // Common 4 | // 5 | // Created by Alexander Ivlev on 29/09/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension DispatchQueue 12 | { 13 | static func mainSync(execute: () -> Void) { 14 | if Thread.isMainThread { 15 | execute() 16 | } else { 17 | DispatchQueue.main.sync(execute: execute) 18 | } 19 | } 20 | 21 | static func mainSync(execute: () throws -> T) rethrows -> T { 22 | if Thread.isMainThread { 23 | return try execute() 24 | } else { 25 | return try DispatchQueue.main.sync(execute: execute) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Modules/Common/Sources/multithreads/FastLock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FastLock.swift 3 | // Common 4 | // 5 | // Created by Alexander Ivlev on 26/09/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public final class FastLock 12 | { 13 | private var monitor: os_unfair_lock = os_unfair_lock() 14 | 15 | public init() { 16 | 17 | } 18 | 19 | public func lock() { 20 | os_unfair_lock_lock(&monitor) 21 | } 22 | 23 | public func unlock() { 24 | os_unfair_lock_unlock(&monitor) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Modules/Common/Sources/ui/ConstImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConstImage.swift 3 | // Common 4 | // 5 | // Created by Alexander Ivlev on 19/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public final class ConstImage 12 | { 13 | public let image: UIImage? 14 | 15 | public init(image: UIImage? = nil) { 16 | self.image = image 17 | } 18 | 19 | public init(named name: String) { 20 | self.image = UIImage(named: name) 21 | log.assert(self.image != nil, "Can't initialize image by name: \(name)") 22 | } 23 | 24 | func apply(to imageView: UIImageView) { 25 | imageView.image = image 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Modules/Common/Sources/utils/ModelsDiff.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModelsDiff.swift 3 | // Common 4 | // 5 | // Created by Alexander Ivlev on 30/09/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | @available(iOS 13, *) 10 | public struct ModelsDiff 11 | { 12 | public enum Change { 13 | case insert(T, at: Int) 14 | case remove(T, at: Int) 15 | } 16 | public let old: [T] 17 | public let new: [T] 18 | 19 | public let diff: [Change] 20 | 21 | public func make(old: [T], new: [T]) -> ModelsDiff { 22 | let systemDiff = new.difference(from: old) 23 | var diff: [ModelsDiff.Change] = [] 24 | for systemAction in systemDiff { 25 | switch systemAction { 26 | case .insert(let offset, let element, _): 27 | diff.append(.insert(element, at: offset)) 28 | case .remove(let offset, let element, _): 29 | diff.append(.remove(element, at: offset)) 30 | } 31 | } 32 | 33 | return ModelsDiff(old: old, new: new, diff: diff) 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /Modules/Common/Sources/utils/Weak.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Weak.swift 3 | // Common 4 | // 5 | // Created by Alexander Ivlev on 01/10/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | public class Weak 10 | { 11 | public var value: T? { 12 | return _value as? T 13 | } 14 | 15 | private weak var _value: AnyObject? 16 | 17 | public init(_ value: T) { 18 | _value = value as AnyObject 19 | } 20 | } 21 | 22 | public class WeakRef 23 | { 24 | public private(set) weak var value: T? 25 | 26 | public init(_ value: T) { 27 | self.value = value 28 | } 29 | } 30 | 31 | extension Weak: Equatable where T: Equatable 32 | { 33 | public static func ==(_ lhs: Weak, _ rhs: Weak) -> Bool { 34 | return lhs.value == rhs.value 35 | } 36 | } 37 | 38 | extension Weak: Hashable where T: Hashable 39 | { 40 | public func hash(into hasher: inout Hasher) { 41 | hasher.combine(value) 42 | } 43 | } 44 | 45 | extension WeakRef: Equatable where T: Equatable 46 | { 47 | public static func ==(_ lhs: WeakRef, _ rhs: WeakRef) -> Bool { 48 | return lhs.value == rhs.value 49 | } 50 | } 51 | 52 | extension WeakRef: Hashable where T: Hashable 53 | { 54 | public func hash(into hasher: inout Hasher) { 55 | hasher.combine(value) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Modules/Common/Sources/utils/WeakArray.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeakArray.swift 3 | // Common 4 | // 5 | // Created by Alexander Ivlev on 14/10/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | public struct WeakArray: Sequence 10 | { 11 | public typealias Iterator = StrongIterator 12 | public struct StrongIterator: IteratorProtocol { 13 | public typealias Element = T 14 | 15 | private let data: [T] 16 | private var index: Int = 0 17 | 18 | fileprivate init(data: [T]) { 19 | self.data = data 20 | } 21 | 22 | public mutating func next() -> T? { 23 | if index < data.count { 24 | defer { index += 1 } 25 | return data[index] 26 | } 27 | return nil 28 | } 29 | } 30 | 31 | private var data: [Weak] = [] 32 | 33 | public init() { 34 | 35 | } 36 | 37 | public mutating func append(_ object: T) { 38 | data.removeAll(where: { $0.value == nil }) 39 | data.append(Weak(object)) 40 | } 41 | 42 | public mutating func remove(_ object: T) { 43 | data.removeAll(where: { $0.value as AnyObject === object as AnyObject || $0.value == nil }) 44 | } 45 | 46 | public func makeIterator() -> Self.Iterator { 47 | return StrongIterator(data: data.compactMap { $0.value }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Modules/Core/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Modules/Core/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Core", 8 | platforms: [.iOS(.v10)], 9 | products: [ 10 | .library(name: "Core", targets: ["Core"]), 11 | ], 12 | dependencies: [ 13 | .package(path: "../Common"), 14 | .package(url: "https://github.com/ivlevAstef/DITranquillity.git", from: "3.9.2") 15 | ], 16 | targets: [ 17 | .target(name: "Core", dependencies: [ 18 | .product(name: "Common"), 19 | .product(name: "DITranquillity") 20 | ], path: "./Sources"), 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /Modules/Core/README.md: -------------------------------------------------------------------------------- 1 | # Core 2 | 3 | Сердце архитектуры. Тут находятся все базовые классы/протоколы, без которых архитектура развалиться. 4 | Соответственно переносим только на те проекты где планируется использоваться эквивалентная архитектура. 5 | -------------------------------------------------------------------------------- /Modules/Core/Sources/CoreStartPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreStartPoint.swift 3 | // Core 4 | // 5 | // Created by Alexander Ivlev on 01/10/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | 10 | import UIKit 11 | import DITranquillity 12 | 13 | public final class CoreStartPoint: CommonStartPoint 14 | { 15 | public init() { 16 | 17 | } 18 | 19 | public func configure() { 20 | 21 | } 22 | 23 | public func reg(container: DIContainer) { 24 | container.register(Navigator.init) 25 | .lifetime(.prototype) 26 | } 27 | 28 | public func initialize() { 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /Modules/Core/Sources/deeplinks/DeepLink.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeepLink.swift 3 | // Core 4 | // 5 | // Created by Alexander Ivlev on 22/09/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | public struct DeepLink 10 | { 11 | public typealias Name = String 12 | } 13 | -------------------------------------------------------------------------------- /Modules/Core/Sources/module_initialization/CommonStartPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommonStartPoint.swift 3 | // Core 4 | // 5 | // Created by Alexander Ivlev on 23/09/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import DITranquillity 10 | 11 | public protocol CommonStartPoint 12 | { 13 | /// Pre configure module. Called before reg 14 | func configure() 15 | 16 | /// Registrate dependencies 17 | /// - Parameter container: common container for registration dependencies 18 | func reg(container: DIContainer) 19 | 20 | /// Post configure module. Called after reg 21 | func initialize() 22 | } 23 | 24 | -------------------------------------------------------------------------------- /Modules/Core/Sources/module_initialization/UIModuleName.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIModuleName.swift 3 | // Core 4 | // 5 | // Created by Alexander Ivlev on 30/09/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | public enum UIModuleName { 10 | case auth 11 | case menu 12 | 13 | case news 14 | case profile 15 | case gameInfo 16 | case sessions 17 | 18 | public var deeplinkName: DeepLink.Name { 19 | switch self { 20 | case .auth: return "auth" 21 | case .menu: return "menu" 22 | case .news: return "news" 23 | case .profile: return "profile" 24 | case .gameInfo: return "gameInfo" 25 | case .sessions: return "sessions" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Modules/Core/Sources/module_initialization/UIStartPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIStartPoint.swift 3 | // Core 4 | // 5 | // Created by Alexander Ivlev on 23/09/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | public protocol UIStartPoint: CommonStartPoint 10 | { 11 | static var name: UIModuleName { get } 12 | 13 | /// check for can make and start this module for paramater 14 | /// - Parameter parameters: start parameters 15 | func isSupportOpen(with parameters: RoutingParamaters) -> Bool 16 | 17 | /// make router for startup project 18 | func makeRouter(use navigator: Navigator) -> IRouter 19 | } 20 | -------------------------------------------------------------------------------- /Modules/Core/Sources/routing/IRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IRouter.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 22/09/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol IRouter 12 | { 13 | func start(parameters: RoutingParamaters) 14 | } 15 | 16 | public extension IRouter 17 | { 18 | func start() { 19 | start(parameters: RoutingParamaters()) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Modules/Core/Sources/routing/RoutingParamaters.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RoutingParamaters.swift 3 | // Core 4 | // 5 | // Created by Alexander Ivlev on 22/09/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | public struct RoutingParamaters 10 | { 11 | public var isEmpty: Bool { 12 | return moduleName != nil 13 | } 14 | public let moduleName: UIModuleName? 15 | public let options: [String: String] 16 | 17 | public init() { 18 | self.moduleName = nil 19 | self.options = [:] 20 | } 21 | 22 | public init(moduleName: UIModuleName, options: [String: String] = [:]) { 23 | self.moduleName = moduleName 24 | self.options = options 25 | } 26 | } 27 | 28 | extension RoutingParamaters: Equatable 29 | { 30 | public static func ==(lhs: RoutingParamaters, rhs: RoutingParamaters) -> Bool { 31 | return lhs.moduleName == rhs.moduleName && lhs.options == rhs.options 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Modules/Core/Sources/routing/Screen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Screen.swift 3 | // Core 4 | // 5 | // Created by Alexander Ivlev on 28/09/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | private /*static*/var associatedForRetainKey: UInt8 = 0 13 | private /*static*/var associatedForRouterRetainKey: UInt8 = 0 14 | 15 | public class Screen 16 | { 17 | public let view: View 18 | public let presenter: Presenter 19 | 20 | public init(view: View, presenter: Presenter) { 21 | self.view = view 22 | self.presenter = presenter 23 | 24 | // yes it's not good, but optimization all code 25 | objc_setAssociatedObject(view, &associatedForRetainKey, presenter, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 26 | } 27 | 28 | public func setRouter(_ router: IRouter) { 29 | // yes it's not good, but optimization all code 30 | objc_setAssociatedObject(view, &associatedForRouterRetainKey, router, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Modules/Domain/Entities/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Entities", 8 | platforms: [.iOS(.v10)], 9 | products: [ 10 | .library(name: "Entities", targets: ["Entities"]), 11 | ], 12 | dependencies: [ 13 | .package(path: "../../Common"), 14 | ], 15 | targets: [ 16 | .target(name: "Entities", dependencies: [ 17 | .product(name: "Common"), 18 | ], path: "./Sources"), 19 | ] 20 | ) 21 | -------------------------------------------------------------------------------- /Modules/Domain/Entities/README.md: -------------------------------------------------------------------------------- 1 | # Entities 2 | -------------------------------------------------------------------------------- /Modules/Domain/Entities/Sources/ServiceCompletion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceCompletion.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 03/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | 10 | public enum ServiceCompletion 11 | { 12 | // Данные взяты из базы данных 13 | 14 | case notRelevant(T) 15 | case db(T) 16 | 17 | // Данные взяты по сети 18 | 19 | case network(T) 20 | case failure(ServiceError) 21 | 22 | public var content: T? { 23 | switch self { 24 | case .notRelevant(let content): 25 | return content 26 | case .db(let content): 27 | return content 28 | case .network(let content): 29 | return content 30 | case .failure: 31 | return nil 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Modules/Domain/Entities/Sources/ServiceError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceError.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 23/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | public enum ServiceError: Error { 10 | case notConnection 11 | case incorrectResponse 12 | case customError(Error) 13 | case cancelled 14 | } 15 | -------------------------------------------------------------------------------- /Modules/Domain/Entities/Sources/SteamIDs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamIDs.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 28/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | public typealias SteamID = UInt64 10 | public typealias AccountID = UInt32 11 | 12 | public typealias SteamGameID = UInt32 13 | 14 | public typealias SteamAchievementID = String 15 | public typealias SteamStatID = String 16 | 17 | 18 | private let magicSteamIDMapNumber: SteamID = 76561197960265728 19 | 20 | extension SteamID { 21 | public var accountId: AccountID { AccountID(self - magicSteamIDMapNumber) } 22 | } 23 | 24 | extension AccountID { 25 | public var steamId: SteamID { SteamID(self) + magicSteamIDMapNumber } 26 | } 27 | -------------------------------------------------------------------------------- /Modules/Domain/Entities/Sources/SteamLocalization.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamLocalization.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 24/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Common 10 | 11 | public enum SteamLocalization 12 | { 13 | case en 14 | case ru 15 | 16 | public static var current: SteamLocalization { 17 | return SteamLocalization(current: ()) 18 | } 19 | } 20 | 21 | extension SteamLocalization 22 | { 23 | public init(current: Void) { 24 | switch loc.languageCode { 25 | case "en": 26 | self = .en 27 | case "ru": 28 | self = .ru 29 | default: 30 | self = .en 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Modules/Domain/Entities/Sources/StorageResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StorageResult.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 28/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | public enum StorageResult: ExpressibleByNilLiteral 10 | { 11 | case none 12 | case noRelevant(Content) 13 | case done(Content) 14 | 15 | public init(nilLiteral: ()) { 16 | self = .none 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Modules/Domain/UseCases/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "UseCases", 8 | platforms: [.iOS(.v10)], 9 | products: [ 10 | .library(name: "UseCases", targets: ["UseCases"]), 11 | ], 12 | dependencies: [ 13 | .package(path: "../../Core"), 14 | .package(path: "../Entities"), 15 | ], 16 | targets: [ 17 | .target(name: "UseCases", dependencies: [ 18 | .product(name: "Core"), 19 | .product(name: "Entities"), 20 | ], path: "./Sources"), 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /Modules/Domain/UseCases/README.md: -------------------------------------------------------------------------------- 1 | # UseCases 2 | 3 | Пакет содержит сервисы - то есть протоколый нужные для обеспечения бизнес логики. 4 | Также в пакете хранятся структуры данных необходимые для поддержания этой бизнес логики. 5 | -------------------------------------------------------------------------------- /Modules/Domain/UseCases/Sources/ImageService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageService.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 23/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Common 11 | 12 | public protocol ImageService: class 13 | { 14 | func fetch(url: URL, to changeableImage: ChangeableImage) 15 | } 16 | 17 | extension ImageService 18 | { 19 | public func fetch(url: URL?, to changeableImage: ChangeableImage) { 20 | if let url = url { 21 | fetch(url: url, to: changeableImage) 22 | } 23 | } 24 | 25 | public func deferredFetch(url: URL?, to changeableImage: ChangeableImage) { 26 | if let url = url { 27 | changeableImage.fetcher = { [weak self, weak changeableImage] in 28 | if let changeableImage = changeableImage { 29 | self?.fetch(url: url, to: changeableImage) 30 | } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Modules/Domain/UseCases/Sources/auth/SteamAuthService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamAuthService.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 20/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Entities 10 | 11 | /// Протокол содержащий бизнес логику авторизации через стим. 12 | public protocol SteamAuthService: class 13 | { 14 | /// Залогинен ли пользователь 15 | var isLogined: Bool { get } 16 | /// Уникальный id пользователя. Пустое только если пользователь не залогинен 17 | var steamId: SteamID? { get } 18 | /// Уникальный id пользователя - используется в играх. Пустое только если пользователь не залогинен 19 | var accountId: AccountID? { get } 20 | 21 | /// Позволяет начать процесс авторизации в стим. 22 | /// - Parameter completion: Результат авторизации. В случае успеха вернется SteamID 23 | func login(completion: @escaping (Result) -> Void) 24 | /// Позволяет разлогинится из steam. 25 | /// - Parameter completion: оповещает об завершении выхода. В случае успеха вернет SteamID с которого был произведен выход 26 | func logout(completion: @escaping (Result) -> Void) 27 | } 28 | -------------------------------------------------------------------------------- /Modules/Domain/UseCases/Sources/auth/models/SteamAuthErrors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamAuthErrors.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 20/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | 10 | public enum SteamLoginError: Error 11 | { 12 | case yourLoggedIn 13 | case userCancel 14 | case applicationIncorrectConfigured 15 | case incorrectResponse 16 | } 17 | 18 | public enum SteamLogoutError: Error 19 | { 20 | case yourLoggedOut 21 | } 22 | -------------------------------------------------------------------------------- /Modules/Domain/UseCases/Sources/games/dota/SteamDotaCompletion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamDotaCompletion.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 03/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Entities 11 | 12 | public enum SteamDotaCompletion 13 | { 14 | case notActual(T) 15 | case actual(T) 16 | case failure(ServiceError) 17 | } 18 | -------------------------------------------------------------------------------- /Modules/Domain/UseCases/Sources/games/dota/SteamDotaService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamDotaService.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 24/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Entities 11 | 12 | /// Позволяет получить более подробную информацию о Dota 2 13 | public protocol SteamDotaService: class 14 | { 15 | /// Возвращает идентификатор игры "Dota 2" 16 | var gameId: SteamGameID { get } 17 | 18 | /// Возвращает сколько игр было сыграно за последние две недели 19 | func matchesInLast2weeks(for accountId: AccountID, completion: @escaping (SteamDotaCompletion) -> Void) 20 | 21 | /// Возвращает информацию по последней сыгранной игре. nil если игр нет 22 | func lastMatch(for accountId: AccountID, completion: @escaping (SteamDotaCompletion) -> Void) 23 | 24 | /// Возвращает детализацию за последние две недели 25 | func detailsInLast2weeks(for accountId: AccountID, completion: @escaping (SteamDotaCompletion<[DotaMatchDetails]>) -> Void) 26 | 27 | /// Возвращает детализацию за период 28 | func detailsInPeriod(for accountId: AccountID, from: Date, to: Date, completion: @escaping (SteamDotaCompletion<[DotaMatchDetails]>) -> Void) 29 | 30 | /// Возвращает описание героя. nil если по запрашиваему id героя нет 31 | func getHero(for heroId: DotaHeroID, loc: SteamLocalization, completion: @escaping (SteamDotaCompletion) -> Void) 32 | } 33 | -------------------------------------------------------------------------------- /Modules/Domain/UseCases/Sources/games/dota/SteamDotaServiceCalculator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamDotaService.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 04/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Entities 10 | 11 | /// Различные функции подсчета данных 12 | public protocol SteamDotaServiceCalculator: class 13 | { 14 | func player(for accountId: AccountID, in details: DotaMatchDetails) -> DotaMatchDetails.Player? 15 | 16 | /// Подсчитывает статистику побед/поражений из переданных игр. unknown это то количество, в скольких играх не нашелся данный игрок 17 | func winLoseCount(for accountId: AccountID, details: [DotaMatchDetails]) -> DotaWinLose 18 | 19 | /// Подсчитывает статистику усредненных показателей из переданных игр. 20 | func avgScores(for accountId: AccountID, details: [DotaMatchDetails]) -> DotaAvgScores 21 | } 22 | -------------------------------------------------------------------------------- /Modules/Domain/UseCases/Sources/games/dota/models/DotaHero.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DotaHero.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 02/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Entities 11 | 12 | public typealias DotaHeroID = UInt16 13 | 14 | public typealias DotaHeroesResult = Result<[DotaHero], ServiceError> 15 | 16 | public struct DotaHero 17 | { 18 | public let id: DotaHeroID 19 | public let name: String 20 | 21 | /// 205x105 22 | public let iconURL: URL? 23 | /// 256x144 24 | public let iconFullHorizontalURL: URL? 25 | /// 235x272 26 | public let iconFullVerticalURL: URL? 27 | 28 | private init() { fatalError("Not support empty initialization") } 29 | } 30 | 31 | extension DotaHero 32 | { 33 | public init( 34 | id: DotaHeroID, 35 | name: String, 36 | iconURL: URL?, 37 | iconFullHorizontalURL: URL?, 38 | iconFullVerticalURL: URL? 39 | ) { 40 | self.id = id 41 | self.name = name 42 | self.iconURL = iconURL 43 | self.iconFullHorizontalURL = iconFullHorizontalURL 44 | self.iconFullVerticalURL = iconFullVerticalURL 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Modules/Domain/UseCases/Sources/games/dota/models/DotaItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DotaItem.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 02/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | public typealias DotaItemID = Int 10 | -------------------------------------------------------------------------------- /Modules/Domain/UseCases/Sources/games/dota/models/DotaLeaverStatus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DotaLeaverStatus.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 02/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | public enum DotaLeaverStatus 10 | { 11 | case notLeave 12 | case disconnected 13 | case disconnectedTooLong // more 5 minutes 14 | case abandoned 15 | case afk 16 | case neverConnected 17 | case neverConnectedTooLong 18 | } 19 | -------------------------------------------------------------------------------- /Modules/Domain/UseCases/Sources/games/dota/models/DotaLobby.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DotaLobby.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 02/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | 10 | public enum DotaLobby 11 | { 12 | case `public` 13 | case practice 14 | case tournament 15 | case tutorial 16 | case coopWithBots 17 | case teamMatch 18 | case soloQueue 19 | case ranked 20 | case soloMid1v1 21 | } 22 | -------------------------------------------------------------------------------- /Modules/Domain/UseCases/Sources/games/dota/models/DotaMatch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DotaMatch.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 02/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Entities 11 | 12 | public typealias DotaMatchID = UInt64 13 | 14 | public typealias DotaMatchHistoryResult = Result<[DotaMatch], ServiceError> 15 | 16 | public struct DotaMatch 17 | { 18 | public struct Player { 19 | /// Иногда он не приходит, а иногда приходит равным UInt32.max... для простоты все к UInt32.max свожу 20 | public let accountId: AccountID 21 | public let heroId: DotaHeroID 22 | public let side: DotaSide 23 | 24 | private init() { fatalError("Not support empty initialization") } 25 | } 26 | 27 | public let matchId: DotaMatchID 28 | public let startTime: Date 29 | public let seqNumber: Int64 30 | public let lobby: DotaLobby? 31 | public let players: [Player] 32 | 33 | private init() { fatalError("Not support empty initialization") } 34 | } 35 | 36 | extension DotaMatch 37 | { 38 | public init( 39 | matchId: DotaMatchID, 40 | startTime: Date, 41 | seqNumber: Int64, 42 | lobby: DotaLobby?, 43 | players: [Player] 44 | ) { 45 | self.matchId = matchId 46 | self.startTime = startTime 47 | self.seqNumber = seqNumber 48 | self.lobby = lobby 49 | self.players = players 50 | } 51 | } 52 | 53 | extension DotaMatch.Player 54 | { 55 | public init( 56 | accountId: AccountID, 57 | heroId: DotaHeroID, 58 | side: DotaSide 59 | ) { 60 | self.accountId = accountId 61 | self.heroId = heroId 62 | self.side = side 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Modules/Domain/UseCases/Sources/games/dota/models/DotaSide.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DotaSide.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 02/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | public enum DotaSide 10 | { 11 | case radiant 12 | case dire 13 | } 14 | -------------------------------------------------------------------------------- /Modules/Domain/UseCases/Sources/games/dota/models/SteamDotaError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamDotaError.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 03/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum SteamDotaError: Error 12 | { 13 | /// Пользователь не разрешил просматривать данные по игре. 14 | case notAllowed 15 | } 16 | -------------------------------------------------------------------------------- /Modules/Domain/UseCases/Sources/games/dota/models/calculated/DotaAvgScores.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DotaAvgScores.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 05/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | public struct DotaAvgScores 10 | { 11 | public let kills: Double 12 | public let deaths: Double 13 | public let assists: Double 14 | public let lastHits: Double 15 | public let denies: Double 16 | /// gold per minute 17 | public let gpm: Double 18 | /// xp per minute 19 | public let xpm: Double 20 | 21 | public let level: Double 22 | 23 | private init() { fatalError("Not support empty initialization") } 24 | } 25 | 26 | extension DotaAvgScores 27 | { 28 | public init( 29 | kills: Double, 30 | deaths: Double, 31 | assists: Double, 32 | lastHits: Double, 33 | denies: Double, 34 | gpm: Double, 35 | xpm: Double, 36 | level: Double 37 | ) { 38 | self.kills = kills 39 | self.deaths = deaths 40 | self.assists = assists 41 | self.lastHits = lastHits 42 | self.denies = denies 43 | self.gpm = gpm 44 | self.xpm = xpm 45 | self.level = level 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Modules/Domain/UseCases/Sources/games/dota/models/calculated/DotaPopularHero.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DotaPopularHero.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 02/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | public enum DotaPopularHero 10 | { 11 | case one(DotaHeroID) 12 | case multi 13 | case fewGames 14 | } 15 | -------------------------------------------------------------------------------- /Modules/Domain/UseCases/Sources/games/dota/models/calculated/DotaWinLose.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DotaWinLose.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 05/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | public struct DotaWinLose 10 | { 11 | public let win: Int 12 | public let lose: Int 13 | public let unknown: Int 14 | 15 | private init() { fatalError("Not support empty initialization") } 16 | } 17 | 18 | extension DotaWinLose 19 | { 20 | public init( 21 | win: Int, 22 | lose: Int, 23 | unknown: Int 24 | ) { 25 | self.win = win 26 | self.lose = lose 27 | self.unknown = unknown 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Modules/Domain/UseCases/Sources/games/game/SteamAchievementService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamAchievementService.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 27/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Common 10 | import Entities 11 | 12 | /// Позволяет получить общую универсальную информацию по любой игре 13 | public protocol SteamAchievementService: class 14 | { 15 | /// Позволяет получить суммарную информацию об ачивках, и реагировать на ее изменение 16 | func getAchievementsSummaryNotifier(for gameId: SteamGameID, steamId: SteamID) -> Notifier 17 | /// Обновляет суммарную информацию об ачивках. Обновление состоит из получения схемы и игрового прогресса. 18 | func refreshAchievementsSummary(for gameId: SteamGameID, steamId: SteamID, completion: ((Bool) -> Void)?) 19 | } 20 | 21 | extension SteamAchievementService 22 | { 23 | public func refreshAchievementsSummary(for gameId: SteamGameID, steamId: SteamID) { 24 | refreshAchievementsSummary(for: gameId, steamId: steamId, completion: nil) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Modules/Domain/UseCases/Sources/games/game/SteamGameService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamGameService.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 24/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Common 10 | import Entities 11 | 12 | /// Позволяет получить общую универсальную информацию по любой игре 13 | public protocol SteamGameService: class 14 | { 15 | /// Схема выдает основную информацию о внутренностях игры - список ачивок, и список показателей. 16 | /// Универсально для любой игры. 17 | /// Схема не меняется, поэтому ей всяких нотификаторов не надо. 18 | func getScheme(for gameId: SteamGameID, loc: SteamLocalization, completion: @escaping (SteamGameSchemeCompletion) -> Void) 19 | 20 | /// Позволяет получить универсальный прогресс по игре - ачивки, и некоторую универсальную статистиску 21 | func getGameProgressNotifier(for gameId: SteamGameID, steamId: SteamID) -> Notifier 22 | /// Обновляет универсальный прогресс по игре - ачивки, и некоторую универсальную статистиску 23 | func refreshGameProgress(for gameId: SteamGameID, steamId: SteamID, completion: ((Bool) -> Void)?) 24 | 25 | /// Выдает универсальную историю прогресса - то есть как менялось состояние ачивок, и универсальной статистики 26 | func getGameProgressHistory(for gameId: SteamGameID, steamId: SteamID, 27 | completion: @escaping (SteamGameProgressHistoryCompletion) -> Void) 28 | } 29 | 30 | extension SteamGameService 31 | { 32 | public func refreshGameProgress(for gameId: SteamGameID, steamId: SteamID) { 33 | refreshGameProgress(for: gameId, steamId: steamId, completion: nil) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Modules/Domain/UseCases/Sources/games/game/models/SteamAchievementsSummary.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamAchievementsSummary.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 27/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Entities 10 | 11 | public typealias SteamAchievementsSummaryResult = Result 12 | 13 | public struct SteamAchievementsSummary { 14 | public let current: Set 15 | public let any: Set 16 | public let onlyVisible: Set 17 | 18 | public init(current: Set, 19 | any: Set, 20 | onlyVisible: Set) { 21 | self.current = current 22 | self.any = any 23 | self.onlyVisible = onlyVisible 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Modules/Domain/UseCases/Sources/games/game/models/SteamGameInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamGameInfo.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 24/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Entities 11 | 12 | public struct SteamGameInfo 13 | { 14 | public let gameId: SteamGameID 15 | public let name: String 16 | public let iconUrl: URL? 17 | public let logoUrl: URL? 18 | 19 | private init() { fatalError("Not support empty initialization") } 20 | } 21 | 22 | extension SteamGameInfo 23 | { 24 | public init( 25 | gameId: SteamGameID, 26 | name: String, 27 | iconUrl: URL?, 28 | logoUrl: URL? 29 | ) { 30 | self.gameId = gameId 31 | self.name = name 32 | self.iconUrl = iconUrl 33 | self.logoUrl = logoUrl 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Modules/Domain/UseCases/Sources/games/game/models/SteamGameProgress.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamGameAchievementProgress.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 24/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Entities 11 | 12 | public typealias SteamGameProgressResult = Result 13 | public typealias SteamGameProgressHistoryCompletion = Result<[SteamGameProgress], ServiceError> 14 | 15 | public struct SteamGameProgress: Equatable 16 | { 17 | public struct Achievement: Equatable { 18 | public let achieved: Bool 19 | 20 | private init() { fatalError("Not support empty initialization") } 21 | } 22 | 23 | public struct Stat: Equatable { 24 | public let count: Double 25 | 26 | private init() { fatalError("Not support empty initialization") } 27 | } 28 | 29 | public let gameId: SteamGameID 30 | public let achievements: [SteamAchievementID: Achievement] 31 | public let stats: [SteamStatID: Stat] 32 | /// Начальное время когда точно было такое состояние прогресса 33 | public let stateDateBegin: Date 34 | /// Конечное время когда точно было такое состояние прогресса 35 | public let stateDateEnd: Date 36 | 37 | private init() { fatalError("Not support empty initialization") } 38 | 39 | public static func ==(lhs: SteamGameProgress, rhs: SteamGameProgress) -> Bool { 40 | return lhs.gameId == rhs.gameId && lhs.achievements == rhs.achievements && lhs.stats == rhs.stats 41 | } 42 | } 43 | 44 | extension SteamGameProgress 45 | { 46 | public init( 47 | gameId: SteamGameID, 48 | achievements: [SteamAchievementID: Achievement], 49 | stats: [SteamStatID: Stat], 50 | stateDateBegin: Date, 51 | stateDateEnd: Date 52 | ) { 53 | self.gameId = gameId 54 | self.achievements = achievements 55 | self.stats = stats 56 | self.stateDateBegin = stateDateBegin 57 | self.stateDateEnd = stateDateEnd 58 | } 59 | } 60 | 61 | extension SteamGameProgress.Achievement 62 | { 63 | public init(achieved: Bool) { 64 | self.achieved = achieved 65 | } 66 | } 67 | 68 | extension SteamGameProgress.Stat 69 | { 70 | public init(count: Double) { 71 | self.count = count 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Modules/Domain/UseCases/Sources/games/game/models/SteamGameScheme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamGameScheme.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 24/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Entities 11 | 12 | public typealias SteamGameSchemeResult = Result 13 | public typealias SteamGameSchemeCompletion = ServiceCompletion 14 | 15 | public struct SteamGameScheme 16 | { 17 | public struct Achivement { 18 | public let id: SteamAchievementID 19 | public let hidden: Bool 20 | 21 | public let localizedName: String 22 | public let localizedDescription: String 23 | 24 | public let iconUrl: URL? 25 | public let iconGrayUrl: URL? 26 | 27 | private init() { fatalError("Not support empty initialization") } 28 | } 29 | 30 | public struct Stat { 31 | public let id: SteamStatID 32 | public let localizedName: String 33 | 34 | private init() { fatalError("Not support empty initialization") } 35 | } 36 | 37 | public let gameId: SteamGameID 38 | 39 | public let achivements: [Achivement] 40 | public let stats: [Stat] 41 | 42 | private init() { fatalError("Not support empty initialization") } 43 | } 44 | 45 | extension SteamGameScheme 46 | { 47 | public init( 48 | gameId: SteamGameID, 49 | achivements: [Achivement], 50 | stats: [Stat] 51 | ) { 52 | self.gameId = gameId 53 | self.achivements = achivements 54 | self.stats = stats 55 | } 56 | } 57 | 58 | extension SteamGameScheme.Achivement 59 | { 60 | public init( 61 | id: String, 62 | hidden: Bool, 63 | localizedName: String, 64 | localizedDescription: String, 65 | iconUrl: URL?, 66 | iconGrayUrl: URL? 67 | ) { 68 | self.id = id 69 | self.hidden = hidden 70 | self.localizedName = localizedName 71 | self.localizedDescription = localizedDescription 72 | self.iconUrl = iconUrl 73 | self.iconGrayUrl = iconGrayUrl 74 | } 75 | } 76 | 77 | extension SteamGameScheme.Stat 78 | { 79 | public init( 80 | id: String, 81 | localizedName: String 82 | ) { 83 | self.id = id 84 | self.localizedName = localizedName 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Modules/Domain/UseCases/Sources/games/sessions/SteamSessionsService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamSessionsService.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 01/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Common 10 | import Entities 11 | 12 | /// Позволяет получить информацию о последних сессиях. В реальности пока это последние игры в которые заходил игрок 13 | public protocol SteamSessionsService: class 14 | { 15 | func getSessionsNotifier(for steamId: SteamID) -> Notifier 16 | 17 | func refreshSessions(for steamId: SteamID, completion: ((Bool) -> Void)?) 18 | } 19 | 20 | extension SteamSessionsService 21 | { 22 | public func refreshSessions(for steamId: SteamID) { 23 | refreshSessions(for: steamId, completion: nil) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Modules/Domain/UseCases/Sources/games/sessions/models/SteamSession.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamSession.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 01/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Entities 11 | 12 | public typealias SteamSessionsResult = Result<[SteamSession], ServiceError> 13 | 14 | public struct SteamSession 15 | { 16 | public let gameInfo: SteamGameInfo 17 | 18 | public let playtimeForever: TimeInterval 19 | public let playtime2weeks: TimeInterval 20 | 21 | private init() { fatalError("Not support empty initialization") } 22 | } 23 | 24 | extension SteamSession 25 | { 26 | public init(gameInfo: SteamGameInfo, 27 | playtimeForever: TimeInterval, 28 | playtime2weeks: TimeInterval) { 29 | self.gameInfo = gameInfo 30 | self.playtimeForever = playtimeForever 31 | self.playtime2weeks = playtime2weeks 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Modules/Domain/UseCases/Sources/profile/SteamFriendsService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamFriendsService.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 30/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Common 10 | import Entities 11 | 12 | public protocol SteamFriendsService: class 13 | { 14 | func getNotifier(for steamId: SteamID) -> Notifier 15 | 16 | func refresh(for steamId: SteamID, completion: ((Bool) -> Void)?) 17 | } 18 | 19 | 20 | extension SteamFriendsService 21 | { 22 | public func refresh(for steamId: SteamID) { 23 | refresh(for: steamId, completion: nil) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Modules/Domain/UseCases/Sources/profile/SteamProfileGamesService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamProfileGamesService.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 23/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Common 10 | import Entities 11 | 12 | public protocol SteamProfileGamesService: class 13 | { 14 | func getGameNotifier(for steamId: SteamID, gameId: SteamGameID) -> Notifier 15 | func refreshGame(for steamId: SteamID, gameId: SteamGameID, completion: ((Bool) -> Void)?) 16 | 17 | func getGamesNotifier(for steamId: SteamID) -> Notifier 18 | func refreshGames(for steamId: SteamID, completion: ((Bool) -> Void)?) 19 | } 20 | 21 | extension SteamProfileGamesService 22 | { 23 | public func refreshGame(for steamId: SteamID, gameId: SteamGameID) { 24 | refreshGame(for: steamId, gameId: gameId, completion: nil) 25 | } 26 | 27 | public func refreshGames(for steamId: SteamID) { 28 | refreshGames(for: steamId, completion: nil) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Modules/Domain/UseCases/Sources/profile/SteamProfileService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamProfileService.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 23/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Common 10 | import Entities 11 | 12 | public protocol SteamProfileService: class 13 | { 14 | func getNotifier(for steamId: SteamID) -> Notifier 15 | 16 | func refresh(for steamId: SteamID, completion: ((Bool) -> Void)?) 17 | 18 | func getProfile(for steamId: SteamID, completion: @escaping (SteamProfileCompletion) -> Void) 19 | } 20 | 21 | extension SteamProfileService 22 | { 23 | public func refresh(for steamId: SteamID) { 24 | refresh(for: steamId, completion: nil) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Modules/Domain/UseCases/Sources/profile/models/SteamFriend.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamFriend.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 30/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Entities 11 | 12 | public typealias SteamFriendsResult = Result<[SteamFriend], ServiceError> 13 | 14 | public struct SteamFriend: Equatable 15 | { 16 | public let ownerSteamId: SteamID 17 | 18 | public let steamId: SteamID 19 | public let friendSince: Date? 20 | 21 | private init() { fatalError("Not support empty initialization") } 22 | } 23 | 24 | extension SteamFriend 25 | { 26 | public init( 27 | ownerSteamId: SteamID, 28 | steamId: SteamID, 29 | friendSince: Date? 30 | ) { 31 | self.ownerSteamId = ownerSteamId 32 | self.steamId = steamId 33 | self.friendSince = friendSince 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Modules/Domain/UseCases/Sources/profile/models/SteamProfileGameInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamProfileGameInfo.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 23/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Entities 11 | 12 | public typealias SteamProfileGamesInfoResult = Result<[SteamProfileGameInfo], ServiceError> 13 | public typealias SteamProfileGameInfoResult = Result 14 | 15 | public struct SteamProfileGameInfo 16 | { 17 | public let steamId: SteamID 18 | 19 | public let playtimeForever: TimeInterval 20 | public let playtime2weeks: TimeInterval 21 | 22 | public let gameInfo: SteamGameInfo 23 | 24 | private init() { fatalError("Not support empty initialization") } 25 | } 26 | 27 | extension SteamProfileGameInfo 28 | { 29 | public init( 30 | steamId: SteamID, 31 | playtimeForever: TimeInterval, 32 | playtime2weeks: TimeInterval, 33 | gameInfo: SteamGameInfo 34 | ) { 35 | self.steamId = steamId 36 | self.playtimeForever = playtimeForever 37 | self.playtime2weeks = playtime2weeks 38 | self.gameInfo = gameInfo 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Modules/Domain/UseCasesContracts/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Modules/Domain/UseCasesContracts/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "UseCasesContracts", 8 | platforms: [.iOS(.v10)], 9 | products: [ 10 | .library(name: "UseCasesContracts", targets: ["UseCasesContracts"]), 11 | ], 12 | dependencies: [ 13 | .package(path: "../UseCases"), 14 | .package(path: "../Entities"), 15 | ], 16 | targets: [ 17 | .target(name: "UseCasesContracts", dependencies: [ 18 | .product(name: "UseCases"), 19 | .product(name: "Entities"), 20 | ], path: "./Sources"), 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /Modules/Domain/UseCasesContracts/README.md: -------------------------------------------------------------------------------- 1 | # UseCasesContracts 2 | Необходимые протоколы которые нужно реализовать, для работоспособности UseCases 3 | 4 | -------------------------------------------------------------------------------- /Modules/Domain/UseCasesContracts/Sources/auth/SteamAuthNetwork.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamAuthNetwork.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 22/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Entities 11 | 12 | public protocol SteamAuthNetwork: class 13 | { 14 | var loginUrl: URL { get } 15 | 16 | func isRedirect(url: URL) -> Bool 17 | func parseRedirect(url: URL) -> SteamID? 18 | } 19 | -------------------------------------------------------------------------------- /Modules/Domain/UseCasesContracts/Sources/auth/SteamAuthStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamAuthStorage.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 22/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Entities 10 | 11 | public protocol SteamAuthStorage: class 12 | { 13 | var steamId: SteamID? { set get } 14 | } 15 | -------------------------------------------------------------------------------- /Modules/Domain/UseCasesContracts/Sources/games/dota/SteamDotaNetwork.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamDotaNetwork.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 02/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Entities 10 | import UseCases 11 | 12 | public protocol SteamDotaNetwork: class 13 | { 14 | func requestHistory(accountId: AccountID, count: UInt, heroId: Int?, from: DotaMatchID?, completion: @escaping (DotaMatchHistoryResult) -> Void) 15 | 16 | func requestDetails(matchId: DotaMatchID, completion: @escaping (DotaMatchDetailsResult) -> Void) 17 | 18 | func requestHeroes(loc: SteamLocalization, completion: @escaping (DotaHeroesResult) -> Void) 19 | } 20 | -------------------------------------------------------------------------------- /Modules/Domain/UseCasesContracts/Sources/games/dota/SteamDotaStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamDotaStorage.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 02/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Entities 10 | import UseCases 11 | 12 | public protocol SteamDotaStorage: class 13 | { 14 | func put(matches: [DotaMatch], for accountId: AccountID) 15 | func put(details: DotaMatchDetails, for accountId: AccountID) 16 | func put(heroes: [DotaHero], loc: SteamLocalization) 17 | 18 | func fetchMatches(for accountId: AccountID) -> [DotaMatch] 19 | func fetchDetails(for accountId: AccountID, matchId: DotaMatchID) -> DotaMatchDetails? 20 | func fetchDetailsList(for accountId: AccountID) -> [DotaMatchDetails] 21 | func fetchHeroes(loc: SteamLocalization) -> StorageResult<[DotaHero]> 22 | } 23 | -------------------------------------------------------------------------------- /Modules/Domain/UseCasesContracts/Sources/games/game/SteamGameNetwork.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamGameNetwork.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 24/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Entities 10 | import UseCases 11 | 12 | public protocol SteamGameNetwork: class 13 | { 14 | func requestScheme(by gameId: SteamGameID, loc: SteamLocalization, completion: @escaping (SteamGameSchemeResult) -> Void) 15 | 16 | func requestGameProgress(by gameId: SteamGameID, steamId: SteamID, completion: @escaping (SteamGameProgressResult) -> Void) 17 | } 18 | -------------------------------------------------------------------------------- /Modules/Domain/UseCasesContracts/Sources/games/game/SteamGameStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamGameStorage.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 24/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Entities 10 | import UseCases 11 | 12 | public protocol SteamGameStorage: class 13 | { 14 | func put(scheme: SteamGameScheme, loc: SteamLocalization) 15 | func put(gameProgress: SteamGameProgress, steamId: SteamID) 16 | 17 | func fetchScheme(by gameId: SteamGameID, loc: SteamLocalization) -> StorageResult 18 | func fetchGameProgress(by gameId: SteamGameID, steamId: SteamID) -> StorageResult 19 | func fetchGameProgressHistory(by gameId: SteamGameID, steamId: SteamID) -> [SteamGameProgress] 20 | } 21 | -------------------------------------------------------------------------------- /Modules/Domain/UseCasesContracts/Sources/games/sessions/SteamSessionsNetwork.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamSessionsNetwork.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 01/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Entities 10 | import UseCases 11 | 12 | public protocol SteamSessionsNetwork: class 13 | { 14 | func requestSessions(for steamId: SteamID, completion: @escaping (SteamSessionsResult) -> Void) 15 | } 16 | 17 | -------------------------------------------------------------------------------- /Modules/Domain/UseCasesContracts/Sources/games/sessions/SteamSessionsStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamSessionsStorage.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 01/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Entities 10 | import UseCases 11 | 12 | public protocol SteamSessionsStorage: class 13 | { 14 | func put(sessions: [SteamSession], steamId: SteamID) 15 | 16 | func fetchSessions(for steamId: SteamID) -> StorageResult<[SteamSession]> 17 | } 18 | -------------------------------------------------------------------------------- /Modules/Domain/UseCasesContracts/Sources/profile/SteamProfileNetwork.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamProfileNetwork.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 22/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Entities 10 | import UseCases 11 | 12 | public protocol SteamProfileNetwork: class 13 | { 14 | func requestProfile(by steamId: SteamID, completion: @escaping (SteamProfileResult) -> Void) 15 | 16 | func requestFriends(for steamId: SteamID, completion: @escaping (SteamFriendsResult) -> Void) 17 | 18 | func requestGames(by steamId: SteamID, completion: @escaping (SteamProfileGamesInfoResult) -> Void) 19 | 20 | func requestGame(by steamId: SteamID, gameId: SteamGameID, completion: @escaping (SteamProfileGameInfoResult) -> Void) 21 | } 22 | -------------------------------------------------------------------------------- /Modules/Domain/UseCasesContracts/Sources/profile/SteamProfileStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamProfileStorage.swift 3 | // UseCases 4 | // 5 | // Created by Alexander Ivlev on 23/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Entities 11 | import UseCases 12 | 13 | public protocol SteamProfileStorage: class 14 | { 15 | func put(profile: SteamProfile) 16 | func put(friends: [SteamFriend]) 17 | func put(games: [SteamProfileGameInfo]) 18 | func put(game: SteamProfileGameInfo) 19 | 20 | func fetchProfile(by steamId: SteamID) -> StorageResult 21 | func fetchFriends(for steamId: SteamID) -> StorageResult<[SteamFriend]> 22 | func fetchGames(by steamId: SteamID) -> StorageResult<[SteamProfileGameInfo]> 23 | func fetchGame(by steamId: SteamID, gameId: SteamGameID) -> StorageResult 24 | } 25 | -------------------------------------------------------------------------------- /Modules/Infrastructure/Network/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Network", 8 | platforms: [.iOS(.v10)], 9 | products: [ 10 | .library(name: "Network", targets: ["Network"]), 11 | ], 12 | dependencies: [ 13 | .package(path: "../../Domain/UseCasesContracts"), 14 | .package(path: "../../Core"), 15 | .package(path: "../../Common"), 16 | ], 17 | targets: [ 18 | .target(name: "Network", dependencies: [ 19 | "UseCasesContracts", 20 | "Core", 21 | "Common" 22 | ], path: "./Sources"), 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /Modules/Infrastructure/Network/README.md: -------------------------------------------------------------------------------- 1 | # Network 2 | 3 | Реализация протоколов по работе с получением данных по сети. 4 | На прямую к этому пакету никто не должен обращаться - все через протоколы объявленные уровнем ниже. 5 | 6 | -------------------------------------------------------------------------------- /Modules/Infrastructure/Network/Sources/NetworkDependency.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkDependency.swift 3 | // Network 4 | // 5 | // Created by Alexander Ivlev on 22/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import DITranquillity 10 | import UseCasesContracts 11 | 12 | final class NetworkDependency: DIFramework 13 | { 14 | static func load(container: DIContainer) { 15 | container.register(NetworkSession.init) 16 | .lifetime(.perRun(.strong)) 17 | 18 | container.register(SteamAuthNetworkImpl.init) 19 | .as(SteamAuthNetwork.self) 20 | .lifetime(.prototype) 21 | 22 | container.register(SteamProfileNetworkImpl.init) 23 | .as(SteamProfileNetwork.self) 24 | .lifetime(.prototype) 25 | 26 | container.register(SteamGameNetworkImpl.init) 27 | .as(SteamGameNetwork.self) 28 | .lifetime(.prototype) 29 | 30 | container.register(SteamSessionsNetworkImpl.init) 31 | .as(SteamSessionsNetwork.self) 32 | .lifetime(.prototype) 33 | 34 | // Games 35 | container.register(SteamDotaNetworkImpl.init) 36 | .as(SteamDotaNetwork.self) 37 | .lifetime(.prototype) 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /Modules/Infrastructure/Network/Sources/NetworkStartPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkStartPoint.swift 3 | // Network 4 | // 5 | // Created by Alexander Ivlev on 22/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Core 10 | import DITranquillity 11 | 12 | public final class NetworkStartPoint: CommonStartPoint 13 | { 14 | public init() { 15 | 16 | } 17 | 18 | public func configure() { 19 | 20 | } 21 | 22 | public func reg(container: DIContainer) { 23 | container.append(framework: NetworkDependency.self) 24 | } 25 | 26 | public func initialize() { 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Modules/Infrastructure/Network/Sources/root/Int64+ToDate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Int64+ToDate.swift 3 | // Network 4 | // 5 | // Created by Alexander Ivlev on 23/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Int64 { 12 | var unixTimeToDate: Date { 13 | return Date(timeIntervalSince1970: TimeInterval(self)) 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /Modules/Infrastructure/Network/Sources/root/Request.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Request.swift 3 | // Network 4 | // 5 | // Created by Alexander Ivlev on 27/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol RequestInfo { 12 | var interface: String { get } 13 | var method: String { get } 14 | var version: Int { get } 15 | 16 | var fields: [String: Any] { get } 17 | } 18 | 19 | class Request: RequestInfo { 20 | let interface: String 21 | let method: String 22 | let version: Int 23 | 24 | let fields: [String: Any] 25 | let completion: (Result) -> Void 26 | 27 | init(interface: String, 28 | method: String, 29 | version: Int, 30 | fields: [String: Any] = [:], 31 | completion: @escaping (Result) -> Void) { 32 | self.interface = interface 33 | self.method = method 34 | self.version = version 35 | self.fields = fields 36 | self.completion = completion 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Modules/Infrastructure/Network/Sources/root/Response.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Response.swift 3 | // Network 4 | // 5 | // Created by Alexander Ivlev on 22/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Response: Decodable { 12 | let response: Type 13 | } 14 | -------------------------------------------------------------------------------- /Modules/Infrastructure/Network/Sources/root/SteamLocalization+ToString.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamLocalization+ToString.swift 3 | // Network 4 | // 5 | // Created by Alexander Ivlev on 27/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Entities 10 | 11 | extension SteamLocalization 12 | { 13 | var toString: String { 14 | switch self { 15 | case .en: 16 | return "english" // or not? 17 | case .ru: 18 | return "russian" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Modules/Infrastructure/Network/Sources/root/SteamRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamRequest.swift 3 | // Network 4 | // 5 | // Created by Alexander Ivlev on 27/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Entities 10 | 11 | class SteamRequest: Request { 12 | init(interface: String, 13 | method: String, 14 | version: Int, 15 | fields: [String: Any] = [:], 16 | parse: @escaping (Content) -> Result, 17 | completion: @escaping (Result) -> Void) { 18 | super.init(interface: interface, method: method, version: version, fields: fields, completion: { result in 19 | switch result { 20 | case .success(let content): 21 | completion(parse(content)) 22 | case .failure(.cancelled): 23 | completion(.failure(.cancelled)) 24 | case .failure(.notConnection), .failure(.timeout): 25 | completion(.failure(.notConnection)) 26 | case .failure: 27 | completion(.failure(.incorrectResponse)) 28 | } 29 | }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Modules/Infrastructure/Storage/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Storage", 8 | platforms: [.iOS(.v10)], 9 | products: [ 10 | .library(name: "Storage", targets: ["Storage"]), 11 | ], 12 | dependencies: [ 13 | .package(path: "../../Domain/UseCasesContracts"), 14 | .package(path: "../../Core"), 15 | .package(path: "../../Common"), 16 | .package(url: "https://github.com/realm/realm-cocoa.git", from: "4.1.1") 17 | ], 18 | targets: [ 19 | .target(name: "Storage", dependencies: [ 20 | "UseCasesContracts", 21 | "Core", 22 | "Common", 23 | "RealmSwift" 24 | ], path: "./Sources"), 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /Modules/Infrastructure/Storage/README.md: -------------------------------------------------------------------------------- 1 | # Storage 2 | 3 | Реализация протоколов по работе с хранением данных. 4 | На прямую к этому пакету никто не должен обращаться - все через протоколы объявленные уровнем ниже. 5 | -------------------------------------------------------------------------------- /Modules/Infrastructure/Storage/Sources/StorageDependency.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StorageDependency.swift 3 | // Storage 4 | // 5 | // Created by Alexander Ivlev on 22/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import DITranquillity 10 | import UseCasesContracts 11 | import RealmSwift 12 | 13 | final class StorageDependency: DIFramework 14 | { 15 | static func load(container: DIContainer) { 16 | /// В силу ограничений времени, запись в БД игнорирует ошибки - то есть если не удалось сохранить, то этот факт просто проигнорируется 17 | /// Благо оно всегда записывается... Да и в прочем, а что еще делать? не выдавать же пользователю ошибку... 18 | container.register { 19 | try! Realm() 20 | }.lifetime(.perRun(.strong)) 21 | 22 | container.register(SteamAuthStorageImpl.init) 23 | .as(SteamAuthStorage.self) 24 | .lifetime(.prototype) 25 | 26 | container.register(SteamProfileStorageImpl.init) 27 | .as(SteamProfileStorage.self) 28 | .lifetime(.prototype) 29 | 30 | container.register(SteamGameStorageImpl.init) 31 | .as(SteamGameStorage.self) 32 | .lifetime(.prototype) 33 | 34 | container.register(SteamSessionsStorageImpl.init) 35 | .as(SteamSessionsStorage.self) 36 | .lifetime(.prototype) 37 | 38 | container.register(SteamDotaStorageImpl.init) 39 | .as(SteamDotaStorage.self) 40 | .lifetime(.prototype) 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /Modules/Infrastructure/Storage/Sources/StorageStartPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StorageStartPoint.swift 3 | // Storage 4 | // 5 | // Created by Alexander Ivlev on 22/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Core 10 | import DITranquillity 11 | 12 | public final class StorageStartPoint: CommonStartPoint 13 | { 14 | public init() { 15 | 16 | } 17 | 18 | public func configure() { 19 | 20 | } 21 | 22 | public func reg(container: DIContainer) { 23 | container.append(framework: StorageDependency.self) 24 | } 25 | 26 | public func initialize() { 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Modules/Infrastructure/Storage/Sources/auth/SteamAuthData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamAuthData.swift 3 | // Storage 4 | // 5 | // Created by Alexander Ivlev on 28/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | import Entities 12 | 13 | final class SteamAuthData: Object { 14 | static let singlePrimaryKey = 1 15 | // sessionId нужен просто, чтобы сохранять один объект. Ну или в будущем возможно сделать мульти вход... 16 | @objc dynamic var _sessionId: Int = singlePrimaryKey 17 | 18 | @objc dynamic var _steamId: String = "" 19 | 20 | override static func primaryKey() -> String? { 21 | return "_sessionId" 22 | } 23 | } 24 | 25 | extension SteamAuthData { 26 | convenience init(steamId: SteamID) { 27 | self.init() 28 | _steamId = "\(steamId)" 29 | } 30 | 31 | var steamId: SteamID? { 32 | return SteamID(_steamId) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Modules/Infrastructure/Storage/Sources/auth/SteamAuthStorageImpl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamAuthStorageImpl.swift 3 | // Storage 4 | // 5 | // Created by Alexander Ivlev on 22/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | import Entities 12 | import UseCasesContracts 13 | 14 | class SteamAuthStorageImpl: SteamAuthStorage { 15 | //var steamId: SteamID? = 76561198073561699 // my steam id 16 | //var steamId: SteamID? = 76561197960434622 // more games steam id 17 | //var steamId: SteamID? = 76561198047329396 // core2duo steam id 18 | 19 | var steamId: SteamID? { 20 | set { 21 | if let steamId = newValue { 22 | realm.threadSafeWrite { realm in 23 | let data = SteamAuthData(steamId: steamId) 24 | realm.add(data, update: .all) 25 | } 26 | } else if let object = realm.ts?.object(ofType: SteamAuthData.self, forPrimaryKey: SteamAuthData.singlePrimaryKey) { 27 | realm.threadSafeWrite { realm in 28 | realm.delete(object) 29 | } 30 | } 31 | } 32 | get { 33 | realm.ts?.object(ofType: SteamAuthData.self, forPrimaryKey: SteamAuthData.singlePrimaryKey)?.steamId 34 | } 35 | } 36 | 37 | private let realm: Realm 38 | 39 | init(realm: Realm) { 40 | self.realm = realm 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Modules/Infrastructure/Storage/Sources/games/dota/DotaLobby+Convert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DotaLobby+Convert.swift 3 | // Storage 4 | // 5 | // Created by Alexander Ivlev on 03/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UseCases 10 | 11 | extension DotaLobby { 12 | init?(_ int: Int) { 13 | switch int { 14 | case 0: self = .public 15 | case 1: self = .practice 16 | case 2: self = .tournament 17 | case 3: self = .tutorial 18 | case 4: self = .coopWithBots 19 | case 5: self = .teamMatch 20 | case 6: self = .soloQueue 21 | case 7: self = .ranked 22 | case 8: self = .soloMid1v1 23 | default: 24 | return nil 25 | } 26 | } 27 | 28 | var toInt: Int { 29 | switch self { 30 | case .`public`: return 0 31 | case .practice: return 1 32 | case .tournament: return 2 33 | case .tutorial: return 3 34 | case .coopWithBots: return 4 35 | case .teamMatch: return 5 36 | case .soloQueue: return 6 37 | case .ranked: return 7 38 | case .soloMid1v1: return 8 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Modules/Infrastructure/Storage/Sources/games/dota/DotaSide+Convert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DotaSide+Convert.swift 3 | // Storage 4 | // 5 | // Created by Alexander Ivlev on 03/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UseCases 10 | 11 | extension DotaSide { 12 | init?(_ int: Int) { 13 | switch int { 14 | case 0: self = .radiant 15 | case 1: self = .dire 16 | default: 17 | return nil 18 | } 19 | } 20 | 21 | var toInt: Int { 22 | switch self { 23 | case .radiant: return 0 24 | case .dire: return 1 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Modules/Infrastructure/Storage/Sources/games/dota/SteamDotaHeroData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamDotaHeroData.swift 3 | // Storage 4 | // 5 | // Created by Alexander Ivlev on 03/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | import Entities 12 | import UseCases 13 | 14 | final class SteamDotaHeroData: Object, LimitedUpdated { 15 | static func generatePrimaryKey(heroId: DotaHeroID, loc: SteamLocalization) -> String { 16 | return "\(heroId)_\(loc)" 17 | } 18 | 19 | @objc dynamic var _primaryKey: String = "" 20 | @objc dynamic var _heroId: Int = 0 21 | @objc dynamic var _loc: String = "" 22 | @objc dynamic var _name: String = "" 23 | 24 | @objc dynamic var _iconURL: String? = nil 25 | @objc dynamic var _iconFullHorizontalURL: String? = nil 26 | @objc dynamic var _iconFullVerticalURL: String? = nil 27 | 28 | @objc dynamic var lastUpdateTime: Date = Date() 29 | 30 | override static func primaryKey() -> String? { 31 | return "_primaryKey" 32 | } 33 | } 34 | 35 | 36 | extension SteamDotaHeroData 37 | { 38 | convenience init(hero: DotaHero, loc: SteamLocalization) { 39 | self.init() 40 | 41 | _primaryKey = Self.generatePrimaryKey(heroId: hero.id, loc: loc) 42 | _heroId = Int(hero.id) 43 | _loc = "\(loc)" 44 | _name = hero.name 45 | 46 | _iconURL = hero.iconURL?.absoluteString 47 | _iconFullHorizontalURL = hero.iconFullHorizontalURL?.absoluteString 48 | _iconFullVerticalURL = hero.iconFullVerticalURL?.absoluteString 49 | } 50 | 51 | var hero: DotaHero? { 52 | return DotaHero(id: DotaHeroID(_heroId), 53 | name: _name, 54 | iconURL: _iconURL.flatMap { URL(string: $0) }, 55 | iconFullHorizontalURL: _iconFullHorizontalURL.flatMap { URL(string: $0) }, 56 | iconFullVerticalURL: _iconFullVerticalURL.flatMap { URL(string: $0) }) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Modules/Infrastructure/Storage/Sources/games/sessions/SteamSessionsStorageImpl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamSessionsStorageImpl.swift 3 | // Storage 4 | // 5 | // Created by Alexander Ivlev on 01/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | import Entities 12 | import UseCases 13 | import UseCasesContracts 14 | 15 | private let sessionsUpdateInterval: TimeInterval = .minutes(5) 16 | 17 | final class SteamSessionsStorageImpl: SteamSessionsStorage 18 | { 19 | private let realm: Realm 20 | 21 | init(realm: Realm) { 22 | self.realm = realm 23 | } 24 | 25 | // MARK: - sessions 26 | 27 | func put(sessions: [SteamSession], steamId: SteamID) { 28 | realm.threadSafeWrite { realm in 29 | let data = SteamSessionsData(sessions: sessions, steamId: steamId) 30 | realm.add(data, update: .all) 31 | } 32 | } 33 | 34 | func fetchSessions(for steamId: SteamID) -> StorageResult<[SteamSession]> { 35 | let primaryKey = "\(steamId)" 36 | let data = realm.ts?.object(ofType: SteamSessionsData.self, forPrimaryKey: primaryKey) 37 | return dataToResult(data, updateInterval: sessionsUpdateInterval, map: { $0.sessions }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Modules/Infrastructure/Storage/Sources/profile/SteamFriendData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamFriendData.swift 3 | // Storage 4 | // 5 | // Created by Alexander Ivlev on 30/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | import Entities 12 | import UseCases 13 | 14 | final class SteamFriendData: Object, LimitedUpdated { 15 | static func generatePrimaryKey(ownerSteamId: SteamID, steamId: SteamID) -> String { 16 | return "\(ownerSteamId)_\(steamId)" 17 | } 18 | 19 | @objc dynamic var _ownerSteamId: String = "" 20 | @objc dynamic var _steamId: String = "" 21 | @objc dynamic var _primaryKey: String = "" 22 | 23 | @objc dynamic var _friendSince: Date? = nil 24 | 25 | @objc dynamic var lastUpdateTime: Date = Date() 26 | 27 | override static func primaryKey() -> String? { 28 | return "_primaryKey" 29 | } 30 | } 31 | 32 | extension SteamFriendData { 33 | convenience init(friend: SteamFriend) { 34 | self.init() 35 | 36 | _ownerSteamId = "\(friend.ownerSteamId)" 37 | _steamId = "\(friend.steamId)" 38 | _primaryKey = Self.generatePrimaryKey(ownerSteamId: friend.ownerSteamId, steamId: friend.steamId) 39 | 40 | _friendSince = friend.friendSince 41 | } 42 | 43 | var friend: SteamFriend? { 44 | guard let ownerSteamId = SteamID(_ownerSteamId), let steamId = SteamID(_steamId) else { 45 | return nil 46 | } 47 | 48 | return SteamFriend(ownerSteamId: ownerSteamId, steamId: steamId, friendSince: _friendSince) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Modules/Infrastructure/Storage/Sources/support/Realm+threadSafe.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Alexander Ivlev on 29/11/2019. 6 | // 7 | 8 | import Foundation 9 | import RealmSwift 10 | import Common 11 | 12 | extension Realm { 13 | var ts: Realm? { threadSafe } 14 | 15 | var threadSafe: Realm? { 16 | if Thread.isMainThread { 17 | return self 18 | } 19 | 20 | guard let realm = try? Realm(configuration: self.configuration) else { 21 | log.assert("Can't make realm for thread safe") 22 | return nil 23 | } 24 | return realm 25 | } 26 | 27 | func threadSafeWrite(_ closure: (_ realm: Realm) -> Void) { 28 | guard let realm = self.threadSafe else { 29 | return 30 | } 31 | try? realm.write { 32 | closure(realm) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Modules/Infrastructure/Storage/Sources/support/TimeInterval+minutes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeInterval+minutes.swift 3 | // Storage 4 | // 5 | // Created by Alexander Ivlev on 28/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension TimeInterval 12 | { 13 | static func minutes(_ minutes: Int) -> TimeInterval { 14 | return TimeInterval(minutes) * 60.0 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Modules/Infrastructure/UseCasesImpl/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "UseCasesImpl", 8 | platforms: [.iOS(.v10)], 9 | products: [ 10 | .library(name: "UseCasesImpl", targets: ["UseCasesImpl"]), 11 | ], 12 | dependencies: [ 13 | .package(path: "../../Domain/UseCases"), 14 | .package(path: "../../Domain/UseCasesContracts"), 15 | .package(path: "../../Domain/Entities"), 16 | .package(path: "../../Core"), 17 | .package(path: "../../Common"), 18 | ], 19 | targets: [ 20 | .target(name: "UseCasesImpl", dependencies: [ 21 | .product(name: "Entities"), 22 | .product(name: "UseCases"), 23 | .product(name: "UseCasesContracts"), 24 | .product(name: "Core"), 25 | .product(name: "Common") 26 | ], path: "./Sources"), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /Modules/Infrastructure/UseCasesImpl/README.md: -------------------------------------------------------------------------------- 1 | # UseCasesImpl 2 | 3 | Реализация всех сервисов. Конечно в идеальном мире реализации нужно хранить в отдельных модулях. 4 | Но для проекта на три недели плодить кучу пакетов, с реализациями по 1-2 класса не удобно. 5 | 6 | -------------------------------------------------------------------------------- /Modules/Infrastructure/UseCasesImpl/Sources/UseCasesImplDependency.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UseCasesImplDependency.swift 3 | // UseCasesImpl 4 | // 5 | // Created by Alexander Ivlev on 20/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import DITranquillity 10 | import UseCases 11 | 12 | final class UseCasesImplDependency: DIFramework 13 | { 14 | static func load(container: DIContainer) { 15 | container.register(ImageServiceImpl.init) 16 | .as(ImageService.self) 17 | .lifetime(.perRun(.weak)) 18 | 19 | container.register(SteamAuthServiceImpl.init) 20 | .as(SteamAuthService.self) 21 | .lifetime(.perRun(.weak)) 22 | 23 | container.register(SteamProfileServiceImpl.init) 24 | .as(SteamProfileService.self) 25 | .lifetime(.perRun(.weak)) 26 | 27 | container.register(SteamProfileGamesServiceImpl.init) 28 | .as(SteamProfileGamesService.self) 29 | .lifetime(.perRun(.weak)) 30 | 31 | container.register(SteamGameServiceImpl.init) 32 | .as(SteamGameService.self) 33 | .lifetime(.perRun(.weak)) 34 | 35 | container.register(SteamAchievementServiceImpl.init) 36 | .as(SteamAchievementService.self) 37 | .lifetime(.perRun(.weak)) 38 | 39 | container.register(SteamFriendsServiceImpl.init) 40 | .as(SteamFriendsService.self) 41 | .lifetime(.perRun(.weak)) 42 | 43 | container.register(SteamSessionsServiceImpl.init) 44 | .as(SteamSessionsService.self) 45 | .lifetime(.perRun(.weak)) 46 | 47 | container.register(SteamDotaServiceImpl.init) 48 | .as(SteamDotaService.self) 49 | .lifetime(.perRun(.weak)) 50 | container.register(SteamDotaServiceCalculatorImpl.init) 51 | .as(SteamDotaServiceCalculator.self) 52 | .lifetime(.prototype) 53 | 54 | container.register { SteamDotaHistorySynchronizer(network: $0, storage: $1, accountId: arg($2)) } 55 | .lifetime(.prototype) 56 | container.register { SteamDotaDetailsSynchronizer(network: $0, storage: $1, accountId: arg($2)) } 57 | .lifetime(.prototype) 58 | container.register(SteamDotaSynchronizers.init) 59 | .lifetime(.perRun(.strong)) 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /Modules/Infrastructure/UseCasesImpl/Sources/UseCasesImplStartPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UseCasesImplStartPoint.swift 3 | // UseCasesImpl 4 | // 5 | // Created by Alexander Ivlev on 20/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Core 10 | import DITranquillity 11 | 12 | public final class UseCasesImplStartPoint: CommonStartPoint 13 | { 14 | public init() { 15 | 16 | } 17 | 18 | public func configure() { 19 | 20 | } 21 | 22 | public func reg(container: DIContainer) { 23 | container.append(framework: UseCasesImplDependency.self) 24 | } 25 | 26 | public func initialize() { 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Modules/Infrastructure/UseCasesImpl/Sources/usecases/games/dota/SteamDotaSynchonizers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamDotaSynchronizer.swift 3 | // UseCasesImpl 4 | // 5 | // Created by Alexander Ivlev on 04/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Common 11 | import Entities 12 | import UseCases 13 | import SwiftLazy 14 | 15 | final class SteamDotaSynchronizers { 16 | private let historySynchronizerProvider: Provider1 17 | private let detailsSynchronizerProvider: Provider1 18 | 19 | private var historySynchonizersByAccount: [AccountID: SteamDotaHistorySynchronizer] = [:] 20 | private var detailsSynchonizersByAccount: [AccountID: SteamDotaDetailsSynchronizer] = [:] 21 | 22 | private var lock = FastLock() 23 | 24 | init(historySynchronizerProvider: Provider1, 25 | detailsSynchronizerProvider: Provider1) { 26 | self.historySynchronizerProvider = historySynchronizerProvider 27 | self.detailsSynchronizerProvider = detailsSynchronizerProvider 28 | } 29 | 30 | func historySynchronizer(for accountId: AccountID) -> SteamDotaHistorySynchronizer { 31 | lock.lock() 32 | defer { lock.unlock() } 33 | 34 | if let synchronizer = historySynchonizersByAccount[accountId] { 35 | return synchronizer 36 | } 37 | let synchronizer = historySynchronizerProvider.value(accountId) 38 | historySynchonizersByAccount[accountId] = synchronizer 39 | 40 | return synchronizer 41 | } 42 | 43 | func detailsSynchronizer(for accountId: AccountID) -> SteamDotaDetailsSynchronizer { 44 | lock.lock() 45 | defer { lock.unlock() } 46 | 47 | if let synchronizer = detailsSynchonizersByAccount[accountId] { 48 | return synchronizer 49 | } 50 | let synchronizer = detailsSynchronizerProvider.value(accountId) 51 | detailsSynchonizersByAccount[accountId] = synchronizer 52 | 53 | return synchronizer 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Modules/Infrastructure/UseCasesImpl/Sources/usecases/games/sessions/SteamSessionsServiceImpl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamSessionsServiceImpl.swift 3 | // UseCasesImpl 4 | // 5 | // Created by Alexander Ivlev on 01/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Common 11 | import Entities 12 | import UseCases 13 | import UseCasesContracts 14 | 15 | final class SteamSessionsServiceImpl: SteamSessionsService 16 | { 17 | private lazy var universalService = { [unowned self] in 18 | UniversalServiceImpl( 19 | fetcher: { steamId in 20 | self.storage.fetchSessions(for: steamId) 21 | }, updater: { (steamId, completion) in 22 | self.network.requestSessions(for: steamId, completion: completion) 23 | }, saver: { (steamId, sessions) in 24 | self.storage.put(sessions: sessions, steamId: steamId) 25 | } 26 | ) 27 | }() 28 | 29 | private let network: SteamSessionsNetwork 30 | private let storage: SteamSessionsStorage 31 | 32 | init(network: SteamSessionsNetwork, storage: SteamSessionsStorage) { 33 | self.network = network 34 | self.storage = storage 35 | } 36 | 37 | func refreshSessions(for steamId: SteamID, completion: ((Bool) -> Void)?) { 38 | universalService.refresh(for: steamId, completion: completion) 39 | } 40 | 41 | func getSessionsNotifier(for steamId: SteamID) -> Notifier { 42 | universalService.getNotifier(for: steamId) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Modules/Infrastructure/UseCasesImpl/Sources/usecases/profile/SteamFriendsServiceImpl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamFriendsServiceImpl.swift 3 | // UseCasesImpl 4 | // 5 | // Created by Alexander Ivlev on 30/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Common 11 | import Entities 12 | import UseCases 13 | import UseCasesContracts 14 | 15 | final class SteamFriendsServiceImpl: SteamFriendsService 16 | { 17 | 18 | private lazy var universalService = { [unowned self] in 19 | UniversalServiceImpl( 20 | fetcher: { steamId in 21 | self.storage.fetchFriends(for: steamId) 22 | }, updater: { (steamId, completion) in 23 | self.network.requestFriends(for: steamId, completion: completion) 24 | }, saver: { (_, friends) in 25 | self.storage.put(friends: friends) 26 | } 27 | ) 28 | }() 29 | 30 | private let network: SteamProfileNetwork 31 | private let storage: SteamProfileStorage 32 | 33 | init(network: SteamProfileNetwork, storage: SteamProfileStorage) { 34 | self.network = network 35 | self.storage = storage 36 | } 37 | 38 | func refresh(for steamId: SteamID, completion: ((Bool) -> Void)?) { 39 | universalService.refresh(for: steamId, completion: completion) 40 | } 41 | 42 | func getNotifier(for steamId: SteamID) -> Notifier { 43 | universalService.getNotifier(for: steamId) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Modules/Infrastructure/UseCasesImpl/Sources/usecases/profile/SteamProfileServiceImpl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamProfileServiceImpl.swift 3 | // UseCasesImpl 4 | // 5 | // Created by Alexander Ivlev on 23/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Common 11 | import Entities 12 | import UseCases 13 | import UseCasesContracts 14 | 15 | final class SteamProfileServiceImpl: SteamProfileService 16 | { 17 | 18 | private lazy var universalService = { [unowned self] in 19 | UniversalServiceImpl( 20 | fetcher: { steamId in 21 | self.storage.fetchProfile(by: steamId) 22 | }, updater: { (steamId, completion) in 23 | self.network.requestProfile(by: steamId, completion: completion) 24 | }, saver: { (_, profile) in 25 | self.storage.put(profile: profile) 26 | } 27 | ) 28 | }() 29 | 30 | private let network: SteamProfileNetwork 31 | private let storage: SteamProfileStorage 32 | 33 | init(network: SteamProfileNetwork, storage: SteamProfileStorage) { 34 | self.network = network 35 | self.storage = storage 36 | } 37 | 38 | func refresh(for steamId: SteamID, completion: ((Bool) -> Void)?) { 39 | universalService.refresh(for: steamId, completion: completion) 40 | } 41 | 42 | func getNotifier(for steamId: SteamID) -> Notifier { 43 | universalService.getNotifier(for: steamId) 44 | } 45 | 46 | func getProfile(for steamId: SteamID, completion: @escaping (SteamProfileCompletion) -> Void) { 47 | universalService.refresh(for: steamId, contentCompletion: completion) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Modules/UIs/AppUIComponents/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "AppUIComponents", 8 | platforms: [.iOS(.v10)], 9 | products: [ 10 | .library(name: "AppUIComponents", targets: ["AppUIComponents"]), 11 | ], 12 | dependencies: [ 13 | .package(path: "../../Domain/UseCases"), 14 | .package(path: "../UIComponents"), 15 | .package(path: "../Design"), 16 | .package(path: "../../Core"), 17 | .package(path: "../../Common"), 18 | ], 19 | targets: [ 20 | .target(name: "AppUIComponents", dependencies: [ 21 | .product(name: "UseCases"), 22 | .product(name: "UIComponents"), 23 | .product(name: "Design"), 24 | .product(name: "Core"), 25 | .product(name: "Common"), 26 | ], path: "./Sources"), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /Modules/UIs/AppUIComponents/README.md: -------------------------------------------------------------------------------- 1 | # UIComponents 2 | 3 | Пакет с UI компонентами. Компоненты никак не привязаны к бизнес логике, и могут спокойно переноситься из проекта в проект. 4 | Максиум на что они завязаны - на дизайн, который может иногда меняться не только в цветах, но и в списке разрешенных имен. 5 | -------------------------------------------------------------------------------- /Modules/UIs/AppUIComponents/Sources/AppUIComponentsStartPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppUIComponentsStartPoint.swift 3 | // AppUIComponents 4 | // 5 | // Created by Alexander Ivlev on 25/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import DITranquillity 11 | import Core 12 | import UIComponents 13 | 14 | public final class AppUIComponentsStartPoint: CommonStartPoint 15 | { 16 | public init() { 17 | 18 | } 19 | 20 | public func configure() { 21 | 22 | } 23 | 24 | public func reg(container: DIContainer) { 25 | } 26 | 27 | public func initialize() { 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Modules/UIs/AppUIComponents/Sources/SteamGameImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SteamGameImageView.swift 3 | // UIComponents 4 | // 5 | // Created by Alexander Ivlev on 23/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import UIComponents 11 | import Design 12 | 13 | public class SteamGameImageView: IdImageView 14 | { 15 | public func apply(use style: Style, size: CGFloat) { 16 | backgroundColor = style.colors.shadowColor 17 | layer.cornerRadius = 0.12 * size 18 | clipsToBounds = true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Modules/UIs/Design/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Design", 8 | platforms: [.iOS(.v10)], 9 | products: [ 10 | .library(name: "Design", targets: ["Design"]), 11 | ], 12 | dependencies: [ 13 | .package(path: "../../Core"), 14 | .package(path: "../../Common"), 15 | ], 16 | targets: [ 17 | .target(name: "Design", dependencies: [ 18 | .product(name: "Core"), 19 | .product(name: "Common"), 20 | ], path: "./Sources"), 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /Modules/UIs/Design/README.md: -------------------------------------------------------------------------------- 1 | # Design 2 | 3 | Пакет с дизайном. Тут в первую очередь цветовая палитра, используемые шрифты, настройки анимации, и базовый лайаутинг. 4 | Переносим между проектами, но скорей всего потребует модификаций - всеже универсальный список вариантов цветов/шрифтов сделать сложно. 5 | -------------------------------------------------------------------------------- /Modules/UIs/Design/Sources/Common/Gradient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Gradient.swift 3 | // Design 4 | // 5 | // Created by Alexander Ivlev on 25/10/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public struct Gradient: Equatable 12 | { 13 | public let from: UIColor 14 | public let to: UIColor 15 | 16 | public init(from: UIColor, to: UIColor) { 17 | self.from = from 18 | self.to = to 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Modules/UIs/Design/Sources/Common/TimeInterval+String.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeInterval+String.swift 3 | // Design 4 | // 5 | // Created by Alexander Ivlev on 23/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Common 11 | 12 | extension TimeInterval { 13 | public var adaptiveString: String { 14 | if self < 60 { // seconds 15 | let seconds = Int(self) 16 | return "\(seconds) \(loc["TimeInterval.short.seconds"])" 17 | } else if self < 60 * 60 { // minutes 18 | let minutes = Int(self / 60) 19 | return "\(minutes) \(loc["TimeInterval.short.minutes"])" 20 | } else { // hours 21 | let hours = Int(self / (60 * 60)) 22 | return "\(hours) \(loc["TimeInterval.short.hours"])" 23 | } 24 | } 25 | 26 | public var detailsAdaptiveString: String { 27 | let hours: Int = Int(self / (60.0 * 60.0)) 28 | let minutes: Int = Int(self / 60.0) - (hours * 60) 29 | let seconds: Int = Int(self.rounded()) % 60 30 | 31 | if hours > 0 { 32 | return "\(hours):\(minutes):\(seconds)" 33 | } 34 | 35 | if minutes > 0 { 36 | return "\(minutes):\(seconds)" 37 | } 38 | 39 | return "\(seconds) \(loc["TimeInterval.short.seconds"])" 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /Modules/UIs/Design/Sources/Common/UIColor+Hex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Hex.swift 3 | // Design 4 | // 5 | // Created by Alexander Ivlev on 25/10/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIColor 12 | { 13 | public convenience init(hex24 hex: UInt32, alpha: CGFloat = 1.0) { 14 | let r: UInt32 = (hex >> 16) & 0x000000FF 15 | let g: UInt32 = (hex >> 8) & 0x000000FF 16 | let b: UInt32 = (hex >> 0) & 0x000000FF 17 | self.init(red: CGFloat(r) / 255.0, green: CGFloat(g) / 255.0, blue: CGFloat(b) / 255.0, alpha: alpha) 18 | } 19 | 20 | public convenience init(hex32 hex: UInt32) { 21 | let r: UInt32 = (hex >> 24) & 0x000000FF 22 | let g: UInt32 = (hex >> 16) & 0x000000FF 23 | let b: UInt32 = (hex >> 8) & 0x000000FF 24 | let a: UInt32 = (hex >> 0) & 0x000000FF 25 | self.init(red: CGFloat(r) / 255.0, green: CGFloat(g) / 255.0, blue: CGFloat(b) / 255.0, alpha: CGFloat(a) / 255.0) 26 | } 27 | } 28 | 29 | func color(hex24: UInt32, alpha: CGFloat = 1.0) -> UIColor { 30 | return UIColor(hex24: hex24, alpha: alpha) 31 | } 32 | 33 | func color(hex32: UInt32) -> UIColor { 34 | return UIColor(hex32: hex32) 35 | } 36 | 37 | func gradient(from: UInt32, to: UInt32) -> Gradient { 38 | return Gradient(from: UIColor(hex24: from), to: UIColor(hex24: to)) 39 | } 40 | -------------------------------------------------------------------------------- /Modules/UIs/Design/Sources/DesignStartPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DesignStartPoint.swift 3 | // Design 4 | // 5 | // Created by Alexander Ivlev on 15/10/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Core 10 | import DITranquillity 11 | 12 | public final class DesignStartPoint: CommonStartPoint 13 | { 14 | public init() { 15 | 16 | } 17 | 18 | public func configure() { 19 | 20 | } 21 | 22 | public func reg(container: DIContainer) { 23 | } 24 | 25 | public func initialize() { 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Modules/UIs/Design/Sources/Style/StylizingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StylizingView.swift 3 | // Design 4 | // 5 | // Created by Alexander Ivlev on 27/10/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Common 10 | 11 | public protocol StylizingView: class 12 | { 13 | var stylizingSubviews: WeakArray { get } 14 | 15 | func apply(use style: Style) 16 | } 17 | 18 | extension StylizingView { 19 | public var stylizingSubviews: WeakArray { WeakArray() } 20 | } 21 | -------------------------------------------------------------------------------- /Modules/UIs/Design/Sources/Style/StylizingViewsContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StylizingViewsContainer.swift 3 | // UIComponents 4 | // 5 | // Created by Alexander Ivlev on 26/10/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Common 11 | 12 | public final class StylizingViewsContainer 13 | { 14 | private var style: Style? 15 | 16 | private var viewsForStylizing = WeakArray() 17 | 18 | public init() { 19 | 20 | } 21 | 22 | public func addView(_ view: StylizingView, immediately: Bool) { 23 | if !viewsForStylizing.contains(where: { $0 === view }) { 24 | viewsForStylizing.append(view) 25 | } 26 | 27 | if let style = style, immediately { 28 | view.apply(use: style) 29 | apply(style, viewsForStylizing: view.stylizingSubviews) 30 | } 31 | } 32 | 33 | public func styleDidChange(_ style: Style) { 34 | self.style = style 35 | 36 | apply(style, viewsForStylizing: viewsForStylizing) 37 | } 38 | 39 | private func apply(_ style: Style, viewsForStylizing: WeakArray) { 40 | for stylizingView in viewsForStylizing.reversed() { 41 | stylizingView.apply(use: style) 42 | apply(style, viewsForStylizing: stylizingView.stylizingSubviews) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Modules/UIs/Design/Sources/Style/consts/ConstColors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConstColors.swift 3 | // Design 4 | // 5 | // Created by Alexander Ivlev on 19/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public enum ConstColors 12 | { 13 | public static let defaultColors = Style.Colors( 14 | background: color(hex24: 0x000000), 15 | accent: color(hex24: 0x1c1c1e), 16 | separator: color(hex24: 0x2c2c2e), 17 | skeleton: gradient(from: 0x39383e, to: 0x707070), 18 | skeletonFailed: color(hex24: 0xc73225), 19 | barStyle: .black, 20 | tint: color(hex24: 0xFFFFFF), 21 | mainText: color(hex24: 0xFFFFFF), 22 | notAccentText: color(hex24: 0xc3c3c3), 23 | contentText: color(hex24: 0xc3c3c3), 24 | blurStyle: .dark, 25 | preferredStatusBarStyle: .lightContent, 26 | 27 | shadowColor: color(hex24: 0x000000), 28 | shadowOpacity: 1.0 29 | ) 30 | 31 | public static let darkColors = Style.Colors( 32 | background: color(hex24: 0x000000), 33 | accent: color(hex24: 0x1c1c1e), 34 | separator: color(hex24: 0x2c2c2e), 35 | skeleton: gradient(from: 0x39383e, to: 0x707070), 36 | skeletonFailed: color(hex24: 0xc73225), 37 | barStyle: .default, 38 | tint: color(hex24: 0xFFFFFF), 39 | mainText: color(hex24: 0xFFFFFF), 40 | notAccentText: color(hex24: 0xc3c3c3), 41 | contentText: color(hex24: 0xc3c3c3), 42 | blurStyle: .dark, 43 | preferredStatusBarStyle: .default, 44 | 45 | shadowColor: color(hex24: 0x000000), 46 | shadowOpacity: 1.0 47 | ) 48 | 49 | public static let lightColors = Style.Colors( 50 | background: color(hex24: 0xf3f2f8), 51 | accent: color(hex24: 0xffffff), 52 | separator: color(hex24: 0xe6e5eb), 53 | skeleton: gradient(from: 0xbdbdbd, to: 0xe0e0e0), 54 | skeletonFailed: color(hex24: 0xf64d3f), 55 | barStyle: .default, 56 | tint: color(hex24: 0x000000), 57 | mainText: color(hex24: 0x000000), 58 | notAccentText: color(hex24: 0x3c3c3c), 59 | contentText: color(hex24: 0x3c3c3c), 60 | blurStyle: .light, 61 | preferredStatusBarStyle: .default, 62 | 63 | shadowColor: color(hex24: 0x000000), 64 | shadowOpacity: 1.0 65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /Modules/UIs/Design/Sources/Style/consts/ConstsAnimation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConstsAnimation.swift 3 | // Design 4 | // 5 | // Created by Alexander Ivlev on 26/10/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public enum ConstsAnimation 12 | { 13 | public static let `default` = Style.Animation( 14 | animationTime: 0.25, 15 | transitionTime: 0.5 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /Modules/UIs/Design/Sources/Style/consts/ConstsFonts.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConstsFonts.swift 3 | // Design 4 | // 5 | // Created by Alexander Ivlev on 25/10/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public enum ConstsFonts 12 | { 13 | public static let `default` = Style.Fonts( 14 | navLarge: UIFont.systemFont(ofSize: 36.0), 15 | navDefault: UIFont.systemFont(ofSize: 30.0), 16 | large: UIFont.systemFont(ofSize: 28.0), 17 | avatar: UIFont.systemFont(ofSize: 20.0, weight: .bold), 18 | title: UIFont.systemFont(ofSize: 18.0), 19 | subtitle: UIFont.systemFont(ofSize: 14.0), 20 | content: UIFont.systemFont(ofSize: 12.0) 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/Auth/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Auth", 8 | platforms: [.iOS(.v10)], 9 | products: [ 10 | .library(name: "Auth", targets: ["Auth"]), 11 | ], 12 | dependencies: [ 13 | .package(path: "../../UIComponents"), 14 | .package(path: "../../Design"), 15 | .package(path: "../../../Core"), 16 | .package(path: "../../../Common"), 17 | ], 18 | targets: [ 19 | .target(name: "Auth", dependencies: [ 20 | .product(name: "UIComponents"), 21 | .product(name: "Design"), 22 | .product(name: "Core"), 23 | .product(name: "Common"), 24 | ], path: "./Sources") 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/Auth/README.md: -------------------------------------------------------------------------------- 1 | # Profile 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/Auth/Sources/AuthDependency.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthDependency.swift 3 | // Auth 4 | // 5 | // Created by Alexander Ivlev on 19/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import DITranquillity 10 | 11 | final class AuthDependency: DIFramework 12 | { 13 | static func load(container: DIContainer) { 14 | container.register { AuthRouter(navigator: arg($0), authService: $1) } 15 | .injection(\.authScreenProvider) 16 | .lifetime(.objectGraph) 17 | 18 | container.register(AuthScreen.init) 19 | .lifetime(.prototype) 20 | container.register { AuthScreenView() } 21 | .as(AuthScreenViewContract.self) 22 | .lifetime(.objectGraph) 23 | container.register(AuthScreenPresenter.init) 24 | .lifetime(.objectGraph) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/Auth/Sources/AuthRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthRouter.swift 3 | // Auth 4 | // 5 | // Created by Alexander Ivlev on 19/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Core 11 | import Common 12 | import SwiftLazy 13 | import UIComponents 14 | import UseCases 15 | 16 | typealias AuthScreen = Screen 17 | 18 | final class AuthRouter: IRouter 19 | { 20 | let authSuccessNotifier = Notifier() 21 | 22 | /*dependency*/var authScreenProvider = Provider() 23 | 24 | private let navigator: Navigator 25 | private let authService: SteamAuthService 26 | 27 | init(navigator: Navigator, authService: SteamAuthService) { 28 | self.navigator = navigator 29 | self.authService = authService 30 | } 31 | 32 | func start(parameters: RoutingParamaters) { 33 | log.assert(!authService.isLogined, "Unsupport show auth if your logined") 34 | 35 | showAuthScreen(parameters: parameters) 36 | } 37 | 38 | private func showAuthScreen(parameters: RoutingParamaters) { 39 | let screen = authScreenProvider.value 40 | screen.setRouter(self) 41 | screen.presenter.authSuccessNotifier.join(authSuccessNotifier, map: { _ in 42 | return parameters 43 | }) 44 | 45 | navigator.push(screen.view) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/Auth/Sources/AuthStartPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthStartPoint.swift 3 | // Auth 4 | // 5 | // Created by Alexander Ivlev on 19/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Common 10 | import Core 11 | import DITranquillity 12 | import SwiftLazy 13 | 14 | public final class AuthStartPoint: UIStartPoint 15 | { 16 | public static let name: UIModuleName = .auth 17 | 18 | public struct Subscribers { 19 | public let authSuccessNotifier: Notifier 20 | } 21 | public var subscribersFiller: (_ navigator: Navigator, _ subscribers: Subscribers) -> Void = { _, _ in } 22 | 23 | private var routerProvider = Provider1() 24 | 25 | public init() { 26 | 27 | } 28 | 29 | public func configure() { 30 | 31 | } 32 | 33 | public func reg(container: DIContainer) { 34 | container.append(framework: AuthDependency.self) 35 | routerProvider = container.resolve() 36 | } 37 | 38 | public func initialize() { 39 | 40 | } 41 | 42 | public func isSupportOpen(with parameters: RoutingParamaters) -> Bool { 43 | return parameters.moduleName == Self.name 44 | } 45 | 46 | public func makeRouter(use navigator: Navigator) -> IRouter { 47 | let router = routerProvider.value(navigator) 48 | subscribersFiller(navigator, Subscribers( 49 | authSuccessNotifier: router.authSuccessNotifier 50 | )) 51 | 52 | return router 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/GameInformation/Dota/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Dota", 8 | platforms: [.iOS(.v10)], 9 | products: [ 10 | .library(name: "Dota", targets: ["Dota"]), 11 | ], 12 | dependencies: [ 13 | .package(path: "../GameInformation") 14 | ], 15 | targets: [ 16 | .target(name: "Dota", dependencies: [ 17 | .product(name: "GameInformation"), 18 | ], path: "./Sources"), 19 | ] 20 | ) 21 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/GameInformation/Dota/README.md: -------------------------------------------------------------------------------- 1 | # Dota 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/GameInformation/Dota/Sources/DotaDependency.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DotaDependency.swift 3 | // Dota 4 | // 5 | // Created by Alexander Ivlev on 06/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import DITranquillity 10 | import GameInformation //TODO: CustomGameInformation 11 | 12 | public final class DotaDependency: DIFramework 13 | { 14 | public static func load(container: DIContainer) { 15 | container.register(DotaRouter.init) 16 | .as(CustomGameInfoRouter.self) 17 | .injection(\.statisticsProvider) 18 | .lifetime(.objectGraph) 19 | 20 | container.register(DotaGameInfoPresenter.init) 21 | .lifetime(.objectGraph) 22 | 23 | container.register(DotaStatisticsScreen.init) 24 | .lifetime(.prototype) 25 | container.register(DotaStatisticsScreenPresenter.init) 26 | .lifetime(.objectGraph) 27 | container.register { DotaStatisticsScreenView() } 28 | .as(DotaStatisticsScreenViewContract.self) 29 | .lifetime(.objectGraph) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/GameInformation/Dota/Sources/DotaRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DotaRouter.swift 3 | // Dota 4 | // 5 | // Created by Alexander Ivlev on 06/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Common 10 | import Core 11 | import Entities 12 | import UseCases 13 | import SwiftLazy 14 | import GameInformation 15 | 16 | typealias DotaStatisticsScreen = Screen 17 | 18 | final class DotaRouter: CustomGameInfoRouter 19 | { 20 | var statisticsProvider = Provider() 21 | 22 | private var navigator: Navigator? 23 | private let gameInfoPresenter: DotaGameInfoPresenter 24 | private let dotaService: SteamDotaService 25 | 26 | init(gameInfoPresenter: DotaGameInfoPresenter, dotaService: SteamDotaService) { 27 | self.gameInfoPresenter = gameInfoPresenter 28 | self.dotaService = dotaService 29 | } 30 | 31 | func configure(navigator: Navigator, steamId: SteamID, gameId: SteamGameID) -> [CustomGameInfoPresenter] { 32 | if gameId == dotaService.gameId { 33 | self.navigator = navigator 34 | configureCustomPresenters(steamId: steamId, gameId: gameId) 35 | return [gameInfoPresenter] 36 | } 37 | return [] 38 | } 39 | 40 | private func showDotaStatistics(for steamId: SteamID) { 41 | log.assert(navigator != nil, "Call show dota statistics but navigator is nil") 42 | 43 | let screen = statisticsProvider.value 44 | 45 | screen.presenter.configure(steamId: steamId) 46 | 47 | navigator?.push(screen.view, animated: true) 48 | } 49 | 50 | private func configureCustomPresenters(steamId: SteamID, gameId: SteamGameID) { 51 | gameInfoPresenter.tapSummaryNotifier.weakJoin(listener: { (self, _) in 52 | self.showDotaStatistics(for: steamId) 53 | }, owner: self) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/GameInformation/Dota/Sources/DotaStartPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DotaStartPoint.swift 3 | // Dota 4 | // 5 | // Created by Alexander Ivlev on 07/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Core 10 | import DITranquillity 11 | 12 | public final class DotaStartPoint: CommonStartPoint 13 | { 14 | public init() { 15 | 16 | } 17 | 18 | public func configure() { 19 | 20 | } 21 | 22 | public func reg(container: DIContainer) { 23 | container.append(framework: DotaDependency.self) 24 | } 25 | 26 | public func initialize() { 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/GameInformation/Dota/Sources/gameInfo/last2WeeksSummary/Dota2WeeksDetailsViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dota2WeeksDetailsViewModel.swift 3 | // Dota 4 | // 5 | // Created by Alexander Ivlev on 05/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Common 11 | 12 | struct Dota2WeeksDetailsViewModel 13 | { 14 | let winPrefix: String 15 | let win: Int 16 | 17 | let avgKillsPrefix: String 18 | let avgKills: Int 19 | 20 | let avgDeathsPrefix: String 21 | let avgDeaths: Int 22 | 23 | let avgAssistsPrefix: String 24 | let avgAssists: Int 25 | 26 | let avgLastHitsPrefix: String 27 | let avgLastHits: Int 28 | 29 | let avgDeniesPrefix: String 30 | let avgDenies: Int 31 | 32 | let avgGPMPrefix: String 33 | let avgGPM: Int 34 | } 35 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/GameInformation/Dota/Sources/gameInfo/last2WeeksSummary/Dota2WeeksGamesCountViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dota2WeeksGamesCountViewModel.swift 3 | // Dota 4 | // 5 | // Created by Alexander Ivlev on 05/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Common 11 | 12 | struct Dota2WeeksGamesCountViewModel 13 | { 14 | let prefix: String 15 | let count: Int 16 | } 17 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/GameInformation/Dota/Sources/gameInfo/last2WeeksSummary/Dota2WeeksSummaryConfigurator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dota2WeeksSummaryConfigurator.swift 3 | // Dota 4 | // 5 | // Created by Alexander Ivlev on 05/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import UIComponents 11 | import Common 12 | import GameInformation //TODO: CustomGameInformation 13 | 14 | final class Dota2WeeksSummaryConfigurator: CustomTableCellConfigurator 15 | { 16 | var gamesCountViewModel: SkeletonViewModel = .loading 17 | var detailsViewModel: SkeletonViewModel = .loading 18 | 19 | let tapNotifier = Notifier() 20 | 21 | var isSelectable: Bool { 22 | /// Пока не загрузилось количество игр, то нажимать нельзя. А детали, уже не обязательно - на следующем экране всеравно их больше чем две недели будет. 23 | if case .done = gamesCountViewModel { 24 | return tapNotifier.hasListeners() 25 | } 26 | return false 27 | } 28 | 29 | var registeredCell: (type: UITableViewCell.Type, identifier: String) { 30 | return (type: DotaLast2WeeksSummaryCell.self, identifier: DotaLast2WeeksSummaryCell.identifier) 31 | } 32 | 33 | func calculateHeightCell() -> CGFloat { 34 | return DotaLast2WeeksSummaryCell.preferredHeight 35 | } 36 | 37 | func makeCell(in tableView: UITableView, indexPath: IndexPath) -> UITableViewCell { 38 | return tableView.dequeueReusableCell(withIdentifier: DotaLast2WeeksSummaryCell.identifier, for: indexPath) 39 | } 40 | 41 | func configureCell(_ cell: UITableViewCell) { 42 | if let summaryCell = cell as? DotaLast2WeeksSummaryCell { 43 | summaryCell.configure(gamesCountViewModel) 44 | summaryCell.configure(detailsViewModel) 45 | return 46 | } 47 | } 48 | 49 | func select() { 50 | DispatchQueue.main.async { [tapNotifier] in 51 | tapNotifier.notify(()) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/GameInformation/Dota/Sources/gameInfo/lastGame/DotaLastMatchConfigurator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DotaLastMatchConfigurator.swift 3 | // Dota 4 | // 5 | // Created by Alexander Ivlev on 05/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import UIComponents 11 | import GameInformation //TODO: CustomGameInformation 12 | 13 | final class DotaLastMatchConfigurator: CustomTableCellConfigurator 14 | { 15 | var lastMatchViewModel: SkeletonViewModel = .loading 16 | 17 | let isSelectable: Bool = false 18 | 19 | var registeredCell: (type: UITableViewCell.Type, identifier: String) { 20 | return (type: DotaLastMatchCell.self, identifier: DotaLastMatchCell.identifier) 21 | } 22 | 23 | func calculateHeightCell() -> CGFloat { 24 | return DotaLastMatchCell.preferredHeight 25 | } 26 | 27 | func makeCell(in tableView: UITableView, indexPath: IndexPath) -> UITableViewCell { 28 | return tableView.dequeueReusableCell(withIdentifier: DotaLastMatchCell.identifier, for: indexPath) 29 | } 30 | 31 | func configureCell(_ cell: UITableViewCell) { 32 | if let lastMatchCell = cell as? DotaLastMatchCell { 33 | lastMatchCell.configure(lastMatchViewModel) 34 | return 35 | } 36 | } 37 | 38 | func select() { 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/GameInformation/Dota/Sources/gameInfo/lastGame/DotaLastMatchViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DotaLastMatchViewModel.swift 3 | // Dota 4 | // 5 | // Created by Alexander Ivlev on 05/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Common 11 | 12 | struct DotaLastMatchViewModel 13 | { 14 | let heroImage: ChangeableImage 15 | let heroName: String 16 | 17 | /// kills, deaths, assists 18 | let kdaText: String 19 | let kills: Int 20 | let deaths: Int 21 | let assists: Int 22 | 23 | let startTime: Date 24 | let durationText: String 25 | let duration: TimeInterval 26 | 27 | let resultText: String 28 | let isWin: Bool 29 | let winText: String 30 | let loseText: String 31 | } 32 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/GameInformation/Dota/Sources/statistics/view/DotaStatisticsScreenView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DotaStatisticsScreenView.swift 3 | // Dota 4 | // 5 | // Created by Alexander Ivlev on 06/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Common 11 | import Design 12 | import UIComponents 13 | 14 | final class DotaStatisticsScreenView: ApViewController, DotaStatisticsScreenViewContract 15 | { 16 | private let tableView = DotaStatisticsTableView() 17 | 18 | override init() { 19 | super.init() 20 | } 21 | 22 | required init?(coder: NSCoder) { 23 | fatalError("init(coder:) has not been implemented") 24 | } 25 | 26 | override func viewDidLoad() { 27 | super.viewDidLoad() 28 | 29 | title = loc["Games.Dota2.GameStatistic.title"] 30 | 31 | configureViews() 32 | } 33 | 34 | func setStatistics(_ viewModels: [DotaStatisticViewModel]) { 35 | tableView.updateStatistics(viewModels) 36 | } 37 | 38 | func showError(_ text: String) { 39 | ErrorAlert.show(text, on: self) 40 | } 41 | 42 | private func configureViews() { 43 | view.addSubview(tableView) 44 | 45 | tableView.clipsToBounds = true 46 | tableView.backgroundColor = .clear 47 | 48 | addViewForStylizing(tableView) 49 | 50 | tableView.snp.makeConstraints { maker in 51 | maker.top.equalToSuperview() 52 | maker.left.equalToSuperview() 53 | maker.right.equalToSuperview() 54 | maker.bottom.equalToSuperview() 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/GameInformation/GameInformation/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "GameInformation", 8 | platforms: [.iOS(.v10)], 9 | products: [ 10 | .library(name: "GameInformation", targets: ["GameInformation"]), 11 | ], 12 | dependencies: [ 13 | .package(path: "../../../AppUIComponents"), 14 | .package(path: "../../../UIComponents"), 15 | .package(path: "../../../Design"), 16 | .package(path: "../../../../Core"), 17 | .package(path: "../../../../Common"), 18 | ], 19 | targets: [ 20 | .target(name: "GameInformation", dependencies: [ 21 | .product(name: "AppUIComponents"), 22 | .product(name: "UIComponents"), 23 | .product(name: "Design"), 24 | .product(name: "Core"), 25 | .product(name: "Common"), 26 | ], path: "./Sources"), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/GameInformation/GameInformation/README.md: -------------------------------------------------------------------------------- 1 | # GameInformation 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/GameInformation/GameInformation/Sources/GameInfoDependency.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameInfoDependency.swift 3 | // GameInformation 4 | // 5 | // Created by Alexander Ivlev on 26/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import DITranquillity 10 | 11 | final class GameInfoDependency: DIFramework 12 | { 13 | static func load(container: DIContainer) { 14 | container.register { GameInfoRouter(navigator: arg($0)) } 15 | .injection(\.gameInfoScreenProvider) 16 | .lifetime(.objectGraph) 17 | 18 | container.register { GameInfoScreen(screen: $0, routerProviders: many($1)) } 19 | .lifetime(.prototype) 20 | container.register(EmbeddedGameInfoScreen.init) 21 | .lifetime(.prototype) 22 | 23 | container.register { GameInfoScreenView() } 24 | .as(GameInfoScreenViewContract.self) 25 | .as(CustomGameInfoViewContract.self) 26 | .lifetime(.objectGraph) 27 | 28 | container.register(GameInfoScreenPresenter.init) 29 | .lifetime(.objectGraph) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/GameInformation/GameInformation/Sources/GameInfoRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameInfoRouter.swift 3 | // GameInformation 4 | // 5 | // Created by Alexander Ivlev on 26/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Core 11 | import Common 12 | import SwiftLazy 13 | import UIComponents 14 | import Entities 15 | 16 | final class GameInfoRouter: IRouter 17 | { 18 | /*dependency*/var gameInfoScreenProvider = Provider() 19 | 20 | private let navigator: Navigator 21 | 22 | init(navigator: Navigator) { 23 | self.navigator = navigator 24 | } 25 | 26 | func start(parameters: RoutingParamaters) { 27 | let steamIdKey = GameInfoStartPoint.RoutingOptions.steamId 28 | let gameIdKey = GameInfoStartPoint.RoutingOptions.gameId 29 | 30 | if let steamId = parameters.options[steamIdKey].flatMap({ SteamID($0) }), 31 | let gameId = parameters.options[gameIdKey].flatMap({ SteamGameID($0) }) { 32 | showGameInfoScreen(steamId: steamId, gameId: gameId) 33 | } else { 34 | log.fatal("Unsupport show game info without steamId and game in options") 35 | } 36 | } 37 | 38 | private func showGameInfoScreen(steamId: SteamID, gameId: SteamGameID) { 39 | let screen = makeGameInfoScreen(steamId: steamId, gameId: gameId) 40 | 41 | navigator.push(screen.view) 42 | } 43 | 44 | private func makeGameInfoScreen(steamId: SteamID, gameId: SteamGameID) -> GameInfoScreen { 45 | let screen = gameInfoScreenProvider.value 46 | screen.setRouter(self) 47 | 48 | let presentersConfigurator = screen.configureRouters(navigator: navigator, steamId: steamId, gameId: gameId) 49 | screen.presenter.configure(steamId: steamId, gameId: gameId, presentersConfigurator: presentersConfigurator) 50 | 51 | return screen 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/GameInformation/GameInformation/Sources/GameInfoStartPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameInfoStartPoint.swift 3 | // GameInformation 4 | // 5 | // Created by Alexander Ivlev on 19/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Core 10 | import DITranquillity 11 | import SwiftLazy 12 | import Entities 13 | 14 | public final class GameInfoStartPoint: UIStartPoint 15 | { 16 | public enum RoutingOptions { 17 | public static let steamId = "SteamId" 18 | public static let gameId = "GameId" 19 | } 20 | 21 | public static let name: UIModuleName = .gameInfo 22 | 23 | private var routerProvider = Provider1() 24 | 25 | public init() { 26 | 27 | } 28 | 29 | public func makeParams(steamId: SteamID, gameId: SteamGameID) -> RoutingParamaters { 30 | return RoutingParamaters(moduleName: Self.name, options: [ 31 | RoutingOptions.steamId: "\(steamId)", 32 | RoutingOptions.gameId: "\(gameId)" 33 | ]) 34 | } 35 | 36 | public func configure() { 37 | 38 | } 39 | 40 | public func reg(container: DIContainer) { 41 | container.append(framework: GameInfoDependency.self) 42 | routerProvider = container.resolve() 43 | } 44 | 45 | public func initialize() { 46 | 47 | } 48 | 49 | public func isSupportOpen(with parameters: RoutingParamaters) -> Bool { 50 | return parameters.moduleName == Self.name 51 | } 52 | 53 | public func makeRouter(use navigator: Navigator) -> IRouter { 54 | return routerProvider.value(navigator) 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/GameInformation/GameInformation/Sources/custom/CustomGameInfoPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomGameInfoPresenter.swift 3 | // GameInformation 4 | // 5 | // Created by Alexander Ivlev on 01/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Entities 10 | 11 | public protocol CustomGameInfoPresenter: class 12 | { 13 | var priority: Int { get } 14 | 15 | var orders: [UInt] { set get } 16 | 17 | // Говорит сколько секций он должен занимать. Вернет ноль, если не поддерживает информацию об игре 18 | func requestSectionsCount(gameId: SteamGameID) -> UInt 19 | 20 | func configure(steamId: SteamID, gameId: SteamGameID) 21 | func refresh(steamId: SteamID, gameId: SteamGameID) 22 | } 23 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/GameInformation/GameInformation/Sources/custom/CustomGameInfoRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomGameInfoRouter.swift 3 | // GameInformation 4 | // 5 | // Created by Alexander Ivlev on 07/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Entities 10 | import Core 11 | 12 | // TODO: весь custom лучше бы в отдельный модуль, дабы Dota и другие кастомные модули не завязывались на прямую на GameInformation 13 | public protocol CustomGameInfoRouter: class 14 | { 15 | func configure(navigator: Navigator, steamId: SteamID, gameId: SteamGameID) -> [CustomGameInfoPresenter] 16 | } 17 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/GameInformation/GameInformation/Sources/custom/CustomGameInfoViewContract.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomGameInfoViewContract.swift 3 | // GameInformation 4 | // 5 | // Created by Alexander Ivlev on 01/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | public protocol CustomGameInfoViewContract: class 10 | { 11 | func addCustomSection(title: String?, order: UInt, configurators: [CustomTableCellConfigurator]) 12 | func removeCustomSection(order: UInt) 13 | 14 | func updateCustom(configurator: CustomTableCellConfigurator) 15 | 16 | func showError(_ text: String) 17 | } 18 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/GameInformation/GameInformation/Sources/custom/CustomTableCellConfigurator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomGameInfoViewContract.swift 3 | // GameInformation 4 | // 5 | // Created by Alexander Ivlev on 01/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import UIComponents 11 | 12 | public protocol CustomTableCellConfigurator: class 13 | { 14 | var registeredCell: (type: UITableViewCell.Type, identifier: String) { get } 15 | var isSelectable: Bool { get } 16 | 17 | func calculateHeightCell() -> CGFloat 18 | func makeCell(in tableView: UITableView, indexPath: IndexPath) -> UITableViewCell 19 | func configureCell(_ cell: UITableViewCell) 20 | 21 | func select() 22 | } 23 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/GameInformation/GameInformation/Sources/custom/universal/CustomGameInfoPresenterConfigurator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomGameInfoPresenterConfigurator.swift 3 | // GameInformation 4 | // 5 | // Created by Alexander Ivlev on 01/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Common 10 | import Entities 11 | 12 | final class CustomGameInfoPresenterConfigurator 13 | { 14 | private let customPresenters: [CustomGameInfoPresenter] 15 | 16 | init(customPresenters: [CustomGameInfoPresenter]) { 17 | self.customPresenters = customPresenters.sorted(by: { $0.priority > $1.priority }) 18 | } 19 | 20 | func configure(steamId: SteamID, gameId: SteamGameID) { 21 | var order: UInt = 0 22 | 23 | for presenter in customPresenters { 24 | var sectionCount = presenter.requestSectionsCount(gameId: gameId) 25 | presenter.orders = [] 26 | while sectionCount > 0 { 27 | presenter.orders.append(order) 28 | order += 1 29 | sectionCount -= 1 30 | } 31 | } 32 | 33 | for presenter in customPresenters { 34 | if !presenter.orders.isEmpty { 35 | presenter.configure(steamId: steamId, gameId: gameId) 36 | } 37 | } 38 | } 39 | 40 | func refresh(steamId: SteamID, gameId: SteamGameID) { 41 | for presenter in customPresenters { 42 | if !presenter.orders.isEmpty { 43 | presenter.refresh(steamId: steamId, gameId: gameId) 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/GameInformation/GameInformation/Sources/gameInfo/AchievementsSummaryViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AchievementsSummaryViewModel.swift 3 | // GameInformation 4 | // 5 | // Created by Alexander Ivlev on 27/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Common 11 | 12 | struct AchievementsSummaryViewModel 13 | { 14 | let prefix: String 15 | let current: Int 16 | let any: Int 17 | } 18 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/GameInformation/GameInformation/Sources/gameInfo/GameInfoViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameInfoViewModel.swift 3 | // GameInformation 4 | // 5 | // Created by Alexander Ivlev on 26/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Common 11 | 12 | struct GameInfoViewModel 13 | { 14 | let icon: ChangeableImage 15 | let name: String 16 | 17 | let playtimeForeverPrefix: String 18 | let playtimeForever: TimeInterval 19 | 20 | let playtime2weeksPrefix: String 21 | let playtime2weeks: TimeInterval 22 | let playtime2weeksSuffix: String 23 | } 24 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/Menu/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Menu", 8 | platforms: [.iOS(.v10)], 9 | products: [ 10 | .library(name: "Menu", targets: ["Menu"]), 11 | ], 12 | dependencies: [ 13 | .package(path: "../../UIComponents"), 14 | .package(path: "../../Design"), 15 | .package(path: "../../../Core"), 16 | .package(path: "../../../Common"), 17 | ], 18 | targets: [ 19 | .target(name: "Menu", dependencies: [ 20 | .product(name: "UIComponents"), 21 | .product(name: "Design"), 22 | .product(name: "Core"), 23 | .product(name: "Common"), 24 | ], path: "./Sources"), 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/Menu/README.md: -------------------------------------------------------------------------------- 1 | # Menu 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/Menu/Sources/MenuDependency.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuDependency.swift 3 | // Menu 4 | // 5 | // Created by Alexander Ivlev on 19/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import DITranquillity 10 | 11 | final class MenuDependency: DIFramework 12 | { 13 | static func load(container: DIContainer) { 14 | container.register { MenuRouter(navigator: arg($0)) } 15 | .injection(\.menuScreenProvider) 16 | .lifetime(.objectGraph) 17 | 18 | container.register(MenuScreen.init) 19 | .lifetime(.prototype) 20 | container.register{ MenuScreenView(navigatorProvider: $0) } 21 | .as(MenuScreenViewContract.self) 22 | .lifetime(.objectGraph) 23 | container.register(MenuScreenPresenter.init) 24 | .lifetime(.objectGraph) 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/Menu/Sources/MenuRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuRouter.swift 3 | // Menu 4 | // 5 | // Created by Alexander Ivlev on 19/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | 10 | import UIKit 11 | import Core 12 | import UIComponents 13 | import Common 14 | import SwiftLazy 15 | 16 | 17 | typealias MenuScreen = Screen 18 | 19 | final class MenuRouter: IRouter 20 | { 21 | let newsGetter = Getter() 22 | let myProfileGetter = Getter() 23 | let sessionsGetter = Getter() 24 | 25 | /*dependency*/var menuScreenProvider = Lazy() 26 | 27 | private let navigator: Navigator 28 | 29 | init(navigator: Navigator) { 30 | self.navigator = navigator 31 | } 32 | 33 | func start(parameters: RoutingParamaters) { 34 | let screen = menuScreenProvider.value 35 | configure(screen) 36 | 37 | navigator.push(screen.view) 38 | } 39 | 40 | private func configure(_ screen: MenuScreen) { 41 | screen.setRouter(self) 42 | 43 | screen.presenter.newsGetter.take(from: self.newsGetter) 44 | screen.presenter.myProfileGetter.take(from: self.myProfileGetter) 45 | screen.presenter.sessionsGetter.take(from: self.sessionsGetter) 46 | 47 | screen.presenter.start() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/Menu/Sources/MenuStartPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuStartPoint.swift 3 | // Menu 4 | // 5 | // Created by Alexander Ivlev on 19/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Core 10 | import Common 11 | import DITranquillity 12 | import SwiftLazy 13 | 14 | public final class MenuStartPoint: UIStartPoint 15 | { 16 | public static let name: UIModuleName = .menu 17 | 18 | public struct Subscribers { 19 | public let newsGetter: Getter 20 | public let myProfileGetter: Getter 21 | public let sessionsGetter: Getter 22 | } 23 | public var subscribersFiller: (_ navigator: Navigator, _ subscribers: Subscribers) -> Void = { _, _ in } 24 | 25 | private var routerProvider = Provider1() 26 | 27 | public init() { 28 | 29 | } 30 | 31 | public func configure() { 32 | 33 | } 34 | 35 | public func reg(container: DIContainer) { 36 | container.append(framework: MenuDependency.self) 37 | routerProvider = container.resolve() 38 | } 39 | 40 | public func initialize() { 41 | 42 | } 43 | 44 | public func isSupportOpen(with parameters: RoutingParamaters) -> Bool { 45 | return parameters.moduleName == Self.name 46 | } 47 | 48 | public func makeRouter(use navigator: Navigator) -> IRouter { 49 | let router = routerProvider.value(navigator) 50 | subscribersFiller(navigator, Subscribers( 51 | newsGetter: router.newsGetter, 52 | myProfileGetter: router.myProfileGetter, 53 | sessionsGetter: router.sessionsGetter 54 | )) 55 | 56 | return router 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/Menu/Sources/menu/MenuScreenPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuScreenPresenter.swift 3 | // Menu 4 | // 5 | // Created by Alexander Ivlev on 19/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Core 10 | import Common 11 | 12 | protocol MenuScreenViewContract: class 13 | { 14 | var viewModels: [MenuViewModel] { get set } 15 | } 16 | 17 | final class MenuScreenPresenter 18 | { 19 | let newsGetter = Getter() 20 | let myProfileGetter = Getter() 21 | let sessionsGetter = Getter() 22 | 23 | private weak var view: MenuScreenViewContract? 24 | 25 | init(view: MenuScreenViewContract) { 26 | self.view = view 27 | } 28 | 29 | func start() { 30 | configure() 31 | } 32 | 33 | private func configure() { 34 | view?.viewModels = [ 35 | MenuViewModel( 36 | icon: ConstImage(named: "tabbarNews"), 37 | title: loc["SteamMenu.News"], 38 | selected: false, 39 | viewGetter: newsGetter 40 | ), 41 | MenuViewModel( 42 | icon: ConstImage(named: "tabbarProfile"), 43 | title: loc["SteamMenu.Profile"], 44 | selected: true, 45 | viewGetter: myProfileGetter 46 | ), 47 | MenuViewModel( 48 | icon: ConstImage(named: "tabbarSessions"), 49 | title: loc["SteamMenu.Sessions"], 50 | selected: false, 51 | viewGetter: sessionsGetter 52 | ), 53 | ] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/Menu/Sources/menu/MenuScreenView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuScreenView.swift 3 | // Menu 4 | // 5 | // Created by Alexander Ivlev on 19/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Core 11 | import Common 12 | import UIComponents 13 | import Design 14 | import SwiftLazy 15 | 16 | final class MenuScreenView: ApTabBarController, UITabBarControllerDelegate, MenuScreenViewContract 17 | { 18 | var viewModels: [MenuViewModel] = [] { 19 | didSet { 20 | configureViewControllers() 21 | } 22 | } 23 | 24 | private let navigatorProvider: Provider 25 | 26 | init(navigatorProvider: Provider) { 27 | self.navigatorProvider = navigatorProvider 28 | super.init(nibName: nil, bundle: nil) 29 | } 30 | 31 | required init?(coder: NSCoder) { 32 | fatalError("init(coder:) has not been implemented") 33 | } 34 | 35 | override func styleDidChange(_ style: Style) { 36 | super.styleDidChange(style) 37 | 38 | tabBar.barStyle = style.colors.barStyle 39 | tabBar.tintColor = style.colors.tint 40 | } 41 | 42 | override func viewDidLoad() { 43 | super.viewDidLoad() 44 | 45 | self.delegate = self 46 | 47 | styleDidChange(style) 48 | } 49 | 50 | private func configureViewControllers() { 51 | var selectedIndex: Int = 0 52 | var newViewControllers: [UIViewController] = [] 53 | for viewModel in viewModels { 54 | if !viewModel.viewGetter.hasCallback() { 55 | continue 56 | } 57 | 58 | let navigator = navigatorProvider.value 59 | guard let router = viewModel.viewGetter.get(navigator) else { 60 | continue 61 | } 62 | router.start() 63 | 64 | let vc = navigator.controller 65 | vc.tabBarItem = UITabBarItem(title: viewModel.title, image: viewModel.icon.image, tag: 0) 66 | 67 | if viewModel.selected { 68 | selectedIndex = newViewControllers.count 69 | } 70 | 71 | newViewControllers.append(vc) 72 | } 73 | 74 | viewControllers = newViewControllers 75 | self.selectedIndex = selectedIndex 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/Menu/Sources/menu/MenuViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuViewModel.swift 3 | // Menu 4 | // 5 | // Created by Alexander Ivlev on 19/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Core 10 | import Common 11 | 12 | struct MenuViewModel { 13 | let icon: ConstImage 14 | let title: String 15 | let selected: Bool 16 | 17 | let viewGetter: Getter 18 | } 19 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/News/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "News", 8 | platforms: [.iOS(.v10)], 9 | products: [ 10 | .library(name: "News", targets: ["News"]), 11 | ], 12 | dependencies: [ 13 | .package(path: "../../AppUIComponents"), 14 | .package(path: "../../UIComponents"), 15 | .package(path: "../../Design"), 16 | .package(path: "../../../Core"), 17 | .package(path: "../../../Common"), 18 | ], 19 | targets: [ 20 | .target(name: "News", dependencies: [ 21 | .product(name: "AppUIComponents"), 22 | .product(name: "UIComponents"), 23 | .product(name: "Design"), 24 | .product(name: "Core"), 25 | .product(name: "Common"), 26 | ], path: "./Sources"), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/News/README.md: -------------------------------------------------------------------------------- 1 | # News 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/News/Sources/NewsDependency.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsDependency.swift 3 | // News 4 | // 5 | // Created by Alexander Ivlev on 19/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import DITranquillity 10 | 11 | final class NewsDependency: DIFramework 12 | { 13 | static func load(container: DIContainer) { 14 | container.register { NewsRouter(navigator: arg($0)) } 15 | .injection(\.ribbonScreenProvider) 16 | .lifetime(.objectGraph) 17 | 18 | container.register(RibbonScreen.init) 19 | .lifetime(.prototype) 20 | container.register { RibbonScreenView() } 21 | .as(RibbonScreenViewContract.self) 22 | .lifetime(.objectGraph) 23 | container.register(RibbonScreenPresenter.init) 24 | .lifetime(.objectGraph) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/News/Sources/NewsRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsRouter.swift 3 | // News 4 | // 5 | // Created by Alexander Ivlev on 19/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Core 11 | import Common 12 | import SwiftLazy 13 | import UIComponents 14 | 15 | typealias RibbonScreen = Screen 16 | 17 | final class NewsRouter: IRouter 18 | { 19 | /*dependency*/var ribbonScreenProvider = Provider() 20 | 21 | private let navigator: Navigator 22 | 23 | init(navigator: Navigator) { 24 | self.navigator = navigator 25 | } 26 | 27 | func start(parameters: RoutingParamaters) { 28 | let screen = ribbonScreenProvider.value 29 | configure(screen) 30 | navigator.push(screen.view) 31 | } 32 | 33 | private func configure(_ screen: RibbonScreen) { 34 | screen.setRouter(self) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/News/Sources/NewsStartPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsStartPoint.swift 3 | // News 4 | // 5 | // Created by Alexander Ivlev on 19/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Core 10 | import DITranquillity 11 | import SwiftLazy 12 | 13 | public final class NewsStartPoint: UIStartPoint 14 | { 15 | public static let name: UIModuleName = .news 16 | 17 | private var routerProvider = Provider1() 18 | 19 | public init() { 20 | 21 | } 22 | 23 | public func configure() { 24 | 25 | } 26 | 27 | public func reg(container: DIContainer) { 28 | container.append(framework: NewsDependency.self) 29 | routerProvider = container.resolve() 30 | } 31 | 32 | public func initialize() { 33 | 34 | } 35 | 36 | public func isSupportOpen(with parameters: RoutingParamaters) -> Bool { 37 | return parameters.moduleName == Self.name 38 | } 39 | 40 | public func makeRouter(use navigator: Navigator) -> IRouter { 41 | return routerProvider.value(navigator) 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/News/Sources/ribbon/RibbonScreenPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RibbonScreenPresenter.swift 3 | // News 4 | // 5 | // Created by Alexander Ivlev on 19/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Common 10 | 11 | protocol RibbonScreenViewContract: class 12 | { 13 | } 14 | 15 | final class RibbonScreenPresenter 16 | { 17 | private weak var view: RibbonScreenViewContract? 18 | 19 | init(view: RibbonScreenViewContract) { 20 | self.view = view 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/News/Sources/ribbon/view/RibbonScreenView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RibbonScreenView.swift 3 | // News 4 | // 5 | // Created by Alexander Ivlev on 19/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Common 11 | import UIComponents 12 | 13 | final class RibbonScreenView: ApViewController, RibbonScreenViewContract 14 | { 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | title = "Новости" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/Profile/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Profile", 8 | platforms: [.iOS(.v10)], 9 | products: [ 10 | .library(name: "Profile", targets: ["Profile"]), 11 | ], 12 | dependencies: [ 13 | .package(path: "../../AppUIComponents"), 14 | .package(path: "../../UIComponents"), 15 | .package(path: "../../Design"), 16 | .package(path: "../../../Core"), 17 | .package(path: "../../../Common"), 18 | ], 19 | targets: [ 20 | .target(name: "Profile", dependencies: [ 21 | .product(name: "AppUIComponents"), 22 | .product(name: "UIComponents"), 23 | .product(name: "Design"), 24 | .product(name: "Core"), 25 | .product(name: "Common"), 26 | ], path: "./Sources") 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/Profile/README.md: -------------------------------------------------------------------------------- 1 | # Profile 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/Profile/Sources/ProfileDependency.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileDependency.swift 3 | // Profile 4 | // 5 | // Created by Alexander Ivlev on 19/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import DITranquillity 10 | 11 | final class ProfileDependency: DIFramework 12 | { 13 | static func load(container: DIContainer) { 14 | container.register { ProfileRouter(navigator: arg($0), authService: $1) } 15 | .injection(\.profileScreenProvider) 16 | .injection(\.friendsScreenProvider) 17 | .lifetime(.objectGraph) 18 | 19 | container.register(ProfileScreen.init) 20 | .lifetime(.prototype) 21 | container.register { ProfileScreenView() } 22 | .as(ProfileScreenViewContract.self) 23 | .lifetime(.objectGraph) 24 | container.register(ProfileScreenPresenter.init) 25 | .lifetime(.objectGraph) 26 | 27 | // Вначале друзья были отдельным модулем, но потом я подумал, и решил - а зачем? 28 | container.register(FriendsScreen.init) 29 | .lifetime(.prototype) 30 | container.register { FriendsScreenView() } 31 | .as(FriendsScreenViewContract.self) 32 | .lifetime(.objectGraph) 33 | container.register(FriendsScreenPresenter.init) 34 | .lifetime(.objectGraph) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/Profile/Sources/ProfileStartPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileStartPoint.swift 3 | // Profile 4 | // 5 | // Created by Alexander Ivlev on 19/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Common 10 | import Core 11 | import DITranquillity 12 | import SwiftLazy 13 | import Entities 14 | 15 | public final class ProfileStartPoint: UIStartPoint 16 | { 17 | public enum RoutingOptions { 18 | public static let steamId = "SteamId" 19 | // TODO: в будущем можно будет поддержать переход сразу на друга, от себя (то есть с полной навигацией) 20 | } 21 | 22 | public struct Subscribers { 23 | public let tapOnGameNotifier: Notifier<(SteamID, SteamGameID, Navigator)> 24 | } 25 | public var subscribersFiller: (_ navigator: Navigator, _ subscribers: Subscribers) -> Void = { _, _ in } 26 | 27 | public static let name: UIModuleName = .profile 28 | 29 | private var routerProvider = Provider1() 30 | 31 | public init() { 32 | 33 | } 34 | 35 | public func makeParams(steamId: SteamID) -> RoutingParamaters { 36 | return RoutingParamaters(moduleName: Self.name, options: [ 37 | RoutingOptions.steamId: "\(steamId)" 38 | ]) 39 | } 40 | 41 | public func configure() { 42 | 43 | } 44 | 45 | public func reg(container: DIContainer) { 46 | container.append(framework: ProfileDependency.self) 47 | routerProvider = container.resolve() 48 | } 49 | 50 | public func initialize() { 51 | 52 | } 53 | 54 | public func isSupportOpen(with parameters: RoutingParamaters) -> Bool { 55 | return parameters.moduleName == Self.name 56 | } 57 | 58 | public func makeRouter(use navigator: Navigator) -> IRouter { 59 | let router = routerProvider.value(navigator) 60 | subscribersFiller(navigator, Subscribers( 61 | tapOnGameNotifier: router.tapOnGameNotifier 62 | )) 63 | 64 | return router 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/Profile/Sources/friends/FriendViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FriendViewModel.swift 3 | // Profile 4 | // 5 | // Created by Alexander Ivlev on 30/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Common 11 | import Entities 12 | 13 | struct FriendViewModel: Equatable 14 | { 15 | struct Content: Equatable { 16 | let avatar: ChangeableImage 17 | let avatarURL: URL? // только ради сравнения 18 | let avatarLetter: String 19 | 20 | let nick: String 21 | 22 | let tapNotifier: Notifier 23 | 24 | static func ==(lhs: Content, rhs: Content) -> Bool { 25 | return lhs.avatarURL == rhs.avatarURL && lhs.nick == rhs.nick 26 | } 27 | } 28 | 29 | enum State: Equatable { 30 | case loading 31 | case failed 32 | case done(Content) 33 | } 34 | 35 | let steamId: SteamID 36 | let state: State 37 | let needUpdateCallback: (() -> Void)? 38 | 39 | static func ==(lhs: FriendViewModel, rhs: FriendViewModel) -> Bool { 40 | return lhs.steamId == rhs.steamId && lhs.state == rhs.state 41 | } 42 | } 43 | 44 | extension FriendViewModel 45 | { 46 | init(empty state: State) { 47 | self.steamId = 0 48 | self.state = state 49 | self.needUpdateCallback = nil 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/Profile/Sources/friends/view/FriendsScreenView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FriendsScreenView.swift 3 | // Profile 4 | // 5 | // Created by Alexander Ivlev on 26/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AppUIComponents 11 | import UIComponents 12 | import Common 13 | import Design 14 | import SnapKit 15 | import UseCases 16 | 17 | final class FriendsScreenView: ApViewController, FriendsScreenViewContract 18 | { 19 | let needUpdateNotifier = Notifier() 20 | 21 | private let tableView = FriendsTableView() 22 | 23 | override init() { 24 | super.init() 25 | } 26 | 27 | required init?(coder: NSCoder) { 28 | fatalError("init(coder:) has not been implemented") 29 | } 30 | 31 | override func viewDidLoad() { 32 | super.viewDidLoad() 33 | 34 | title = loc["SteamFriends.Title"] 35 | 36 | configureViews() 37 | } 38 | 39 | override func viewWillAppear(_ animated: Bool) { 40 | super.viewWillAppear(animated) 41 | 42 | needUpdateNotifier.notify(()) 43 | } 44 | 45 | func setTitle(_ text: String) { 46 | title = text 47 | } 48 | 49 | func beginLoading() { 50 | tableView.updateFriends(Array(repeating: FriendViewModel(empty: .loading), count: 8)) 51 | } 52 | 53 | func failedLoading() { 54 | tableView.updateFriends(Array(repeating: FriendViewModel(empty: .failed), count: 8)) 55 | } 56 | 57 | func updateFriends(_ friends: [FriendViewModel]) { 58 | tableView.updateFriends(friends) 59 | } 60 | 61 | func updateFriend(_ friend: FriendViewModel) { 62 | tableView.updateFriend(friend) 63 | } 64 | 65 | // MARK: - other 66 | 67 | func showError(_ text: String) { 68 | ErrorAlert.show(text, on: self) 69 | } 70 | 71 | private func configureViews() { 72 | view.addSubview(tableView) 73 | 74 | tableView.clipsToBounds = true 75 | tableView.backgroundColor = .clear 76 | 77 | addViewForStylizing(tableView) 78 | 79 | tableView.snp.makeConstraints { maker in 80 | maker.top.equalToSuperview() 81 | maker.left.equalToSuperview() 82 | maker.right.equalToSuperview() 83 | maker.bottom.equalToSuperview() 84 | } 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/Profile/Sources/profile/ProfileGameInfoViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileGameViewModel.swift 3 | // Profile 4 | // 5 | // Created by Alexander Ivlev on 23/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Common 11 | 12 | struct ProfileGameInfoViewModel 13 | { 14 | let icon: ChangeableImage 15 | let name: String 16 | 17 | let playtimePrefix: String 18 | let playtime: TimeInterval 19 | 20 | let tapNotifier: Notifier 21 | } 22 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/Profile/Sources/profile/ProfileViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileViewModel.swift 3 | // Profile 4 | // 5 | // Created by Alexander Ivlev on 22/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Common 10 | 11 | struct ProfileViewModel 12 | { 13 | let avatar: ChangeableImage 14 | let avatarLetter: String 15 | 16 | let nick: String 17 | 18 | var isPrivate: Bool = true 19 | var realName: String = "" 20 | let privateText: String 21 | let notSetRealNameText: String 22 | 23 | let tapNotifier: Notifier 24 | } 25 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/Sessions/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Sessions", 8 | platforms: [.iOS(.v10)], 9 | products: [ 10 | .library(name: "Sessions", targets: ["Sessions"]), 11 | ], 12 | dependencies: [ 13 | .package(path: "../../AppUIComponents"), 14 | .package(path: "../../UIComponents"), 15 | .package(path: "../../Design"), 16 | .package(path: "../../../Core"), 17 | .package(path: "../../../Common"), 18 | ], 19 | targets: [ 20 | .target(name: "Sessions", dependencies: [ 21 | .product(name: "AppUIComponents"), 22 | .product(name: "UIComponents"), 23 | .product(name: "Design"), 24 | .product(name: "Core"), 25 | .product(name: "Common"), 26 | ], path: "./Sources"), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/Sessions/README.md: -------------------------------------------------------------------------------- 1 | # Sessions 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/Sessions/Sources/SessionsDependency.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionsDependency.swift 3 | // Sessions 4 | // 5 | // Created by Alexander Ivlev on 19/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import DITranquillity 10 | 11 | final class SessionsDependency: DIFramework 12 | { 13 | static func load(container: DIContainer) { 14 | container.register { SessionsRouter(navigator: arg($0), authService: $1) } 15 | .injection(\.sessionsScreenProvider) 16 | .lifetime(.objectGraph) 17 | 18 | container.register(SessionsScreen.init) 19 | .lifetime(.prototype) 20 | container.register { SessionsScreenView() } 21 | .as(SessionsScreenViewContract.self) 22 | .lifetime(.objectGraph) 23 | container.register(SessionsScreenPresenter.init) 24 | .lifetime(.objectGraph) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/Sessions/Sources/SessionsRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionsRouter.swift 3 | // Sessions 4 | // 5 | // Created by Alexander Ivlev on 19/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Core 11 | import UIComponents 12 | import Common 13 | import Entities 14 | import UseCases 15 | import SwiftLazy 16 | 17 | typealias SessionsScreen = Screen 18 | 19 | final class SessionsRouter: IRouter 20 | { 21 | let tapOnGameNotifier = Notifier<(SteamID, SteamGameID, Navigator)>() 22 | 23 | /*dependency*/var sessionsScreenProvider = Provider() 24 | 25 | private let navigator: Navigator 26 | private let authService: SteamAuthService 27 | 28 | private var currentRoutingParamaters: RoutingParamaters? 29 | 30 | init(navigator: Navigator, authService: SteamAuthService) { 31 | self.navigator = navigator 32 | self.authService = authService 33 | } 34 | 35 | func start(parameters: RoutingParamaters) { 36 | if currentRoutingParamaters == parameters { 37 | return 38 | } 39 | currentRoutingParamaters = parameters 40 | 41 | let steamIdKey = SessionsStartPoint.RoutingOptions.steamId 42 | if let steamId = parameters.options[steamIdKey].flatMap({ SteamID($0) }) { 43 | showSessionsScreen(steamId: steamId, on: navigator) 44 | } else if let steamId = authService.steamId { 45 | showSessionsScreen(steamId: steamId, on: navigator) 46 | } else { 47 | log.fatal("Unsupport show sessions without steamId for options or auth") 48 | } 49 | } 50 | 51 | private func showSessionsScreen(steamId: SteamID, on navigator: Navigator) { 52 | let screen = sessionsScreenProvider.value 53 | screen.setRouter(self) 54 | 55 | screen.presenter.tapOnGameNotifier.join(tapOnGameNotifier) { (steamId, gameId) in 56 | return (steamId, gameId, navigator) 57 | } 58 | 59 | screen.presenter.configure(steamId: steamId) 60 | 61 | navigator.push(screen.view) 62 | } 63 | 64 | } 65 | 66 | 67 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/Sessions/Sources/SessionsStartPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionsStartPoint.swift 3 | // Sessions 4 | // 5 | // Created by Alexander Ivlev on 19/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Common 10 | import Core 11 | import DITranquillity 12 | import SwiftLazy 13 | import Entities 14 | 15 | public final class SessionsStartPoint: UIStartPoint 16 | { 17 | public enum RoutingOptions { 18 | public static let steamId = "SteamId" 19 | // TODO: в будущем можно будет поддержать переход сразу на друга, от себя (то есть с полной навигацией) 20 | } 21 | 22 | public struct Subscribers { 23 | public let tapOnGameNotifier: Notifier<(SteamID, SteamGameID, Navigator)> 24 | } 25 | public var subscribersFiller: (_ navigator: Navigator, _ subscribers: Subscribers) -> Void = { _, _ in } 26 | 27 | public static let name: UIModuleName = .sessions 28 | 29 | private var routerProvider = Provider1() 30 | 31 | public init() { 32 | 33 | } 34 | 35 | public func makeParams(steamId: SteamID) -> RoutingParamaters { 36 | return RoutingParamaters(moduleName: Self.name, options: [ 37 | RoutingOptions.steamId: "\(steamId)" 38 | ]) 39 | } 40 | 41 | public func configure() { 42 | } 43 | 44 | public func reg(container: DIContainer) { 45 | container.append(framework: SessionsDependency.self) 46 | routerProvider = container.resolve() 47 | } 48 | 49 | public func initialize() { 50 | // TODO: setup style 51 | } 52 | 53 | public func isSupportOpen(with parameters: RoutingParamaters) -> Bool { 54 | return parameters.moduleName == Self.name 55 | } 56 | 57 | public func makeRouter(use navigator: Navigator) -> IRouter { 58 | let router = routerProvider.value(navigator) 59 | subscribersFiller(navigator, Subscribers( 60 | tapOnGameNotifier: router.tapOnGameNotifier 61 | )) 62 | 63 | return router 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/Sessions/Sources/sessions/SessionViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionViewModel.swift 3 | // Sessions 4 | // 5 | // Created by Alexander Ivlev on 01/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Common 11 | 12 | struct SessionViewModel 13 | { 14 | let icon: ChangeableImage 15 | let name: String 16 | 17 | let playtimeForeverPrefix: String 18 | let playtimeForever: TimeInterval 19 | 20 | let playtime2weeksPrefix: String 21 | let playtime2weeks: TimeInterval 22 | let playtime2weeksSuffix: String 23 | 24 | let tapNotifier: Notifier 25 | } 26 | -------------------------------------------------------------------------------- /Modules/UIs/Modules/Sessions/Sources/sessions/view/SessionsScreenView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionsScreenView.swift 3 | // Sessions 4 | // 5 | // Created by Alexander Ivlev on 24/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | 10 | import UIKit 11 | import Common 12 | import UIComponents 13 | 14 | final class SessionsScreenView: ApViewController, SessionsScreenViewContract 15 | { 16 | let needUpdateNotifier = Notifier() 17 | 18 | private let tableView = SessionsTableView() 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | 23 | title = loc["SteamSessions.Title"] 24 | 25 | configureViews() 26 | } 27 | 28 | override func viewWillAppear(_ animated: Bool) { 29 | super.viewWillAppear(animated) 30 | 31 | needUpdateNotifier.notify(()) 32 | } 33 | 34 | func beginLoading() { 35 | tableView.updateSessions(Array(repeating: .loading, count: 8)) 36 | } 37 | 38 | func failedLoadingSessions() { 39 | tableView.updateSessions(Array(repeating: .failed, count: 8)) 40 | } 41 | 42 | func showSessions(_ sessions: [SessionViewModel]) { 43 | tableView.updateSessions(sessions.map { .done($0) }) 44 | } 45 | 46 | // MARK: - other 47 | 48 | func showError(_ text: String) { 49 | ErrorAlert.show(text, on: self) 50 | } 51 | 52 | private func configureViews() { 53 | view.addSubview(tableView) 54 | 55 | tableView.clipsToBounds = true 56 | tableView.backgroundColor = .clear 57 | 58 | addViewForStylizing(tableView) 59 | 60 | tableView.snp.makeConstraints { maker in 61 | maker.top.equalToSuperview() 62 | maker.left.equalToSuperview() 63 | maker.right.equalToSuperview() 64 | maker.bottom.equalToSuperview() 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Modules/UIs/UIComponents/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "UIComponents", 8 | platforms: [.iOS(.v10)], 9 | products: [ 10 | .library(name: "UIComponents", targets: ["UIComponents"]), 11 | ], 12 | dependencies: [ 13 | .package(path: "../Design"), 14 | .package(path: "../../Core"), 15 | .package(path: "../../Common"), 16 | .package(url: "https://github.com/SnapKit/SnapKit.git", from: "5.0.1") 17 | ], 18 | targets: [ 19 | .target(name: "UIComponents", dependencies: [ 20 | .product(name: "Design"), 21 | .product(name: "Core"), 22 | .product(name: "Common"), 23 | .product(name: "SnapKit"), 24 | ], path: "./Sources"), 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /Modules/UIs/UIComponents/README.md: -------------------------------------------------------------------------------- 1 | # UIComponents 2 | 3 | Пакет с UI компонентами. Компоненты никак не привязаны к бизнес логике, и могут спокойно переноситься из проекта в проект. 4 | Максиум на что они завязаны - на дизайн, который может иногда меняться не только в цветах, но и в списке разрешенных имен. 5 | -------------------------------------------------------------------------------- /Modules/UIs/UIComponents/Sources/UIComponentsStartPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIComponentsStartPoint.swift 3 | // UIComponents 4 | // 5 | // Created by Alexander Ivlev on 04/10/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import DITranquillity 11 | import Core 12 | 13 | public final class UIComponentsStartPoint: CommonStartPoint 14 | { 15 | public init() { 16 | 17 | } 18 | 19 | public func configure() { 20 | 21 | } 22 | 23 | public func reg(container: DIContainer) { 24 | container.register { ApNavigationController(nibName: nil, bundle: nil) } 25 | .as(UINavigationController.self) 26 | .lifetime(.prototype) 27 | } 28 | 29 | public func initialize() { 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Modules/UIs/UIComponents/Sources/alerts/ErrorAlert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorAlert.swift 3 | // UIComponents 4 | // 5 | // Created by Alexander Ivlev on 21/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Common 11 | 12 | public final class ErrorAlert 13 | { 14 | private static var onShownViewController = WeakArray() 15 | 16 | public static func show(_ text: String, on viewController: UIViewController) { 17 | log.assert(Thread.isMainThread, "Thread.isMainThread") 18 | 19 | let onShownVC = viewController 20 | 21 | if onShownViewController.contains(where: { $0 === onShownVC }) { 22 | return 23 | } 24 | 25 | // Да не модно и не молодежно, но зато писать или подключать ничего не надо 26 | let alertController = AlertController(title: loc["Alert.Error"], 27 | message: text, 28 | preferredStyle: .alert) 29 | alertController.statusBarStyle = viewController.preferredStatusBarStyle 30 | alertController.addAction(UIAlertAction( 31 | title: loc["Alert.Ok"], 32 | style: .default, 33 | handler: { _ in 34 | onShownViewController.remove(onShownVC) 35 | } 36 | )) 37 | 38 | onShownViewController.append(onShownVC) 39 | onShownVC.present(alertController, animated: true) 40 | } 41 | } 42 | 43 | private final class AlertController: UIAlertController 44 | { 45 | override var preferredStatusBarStyle: UIStatusBarStyle { 46 | return statusBarStyle 47 | } 48 | 49 | var statusBarStyle: UIStatusBarStyle = .default 50 | } 51 | -------------------------------------------------------------------------------- /Modules/UIs/UIComponents/Sources/base/ApHapticFeedback.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApHapticFeedback.swift 3 | // UIComponents 4 | // 5 | // Created by Alexander Ivlev on 03/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CoreHaptics 11 | 12 | public enum ApFeedbackGenerator { 13 | case impact(UIImpactFeedbackGenerator, intensity: CGFloat) 14 | case notification(UINotificationFeedbackGenerator, type: UINotificationFeedbackGenerator.FeedbackType) 15 | case selection(UISelectionFeedbackGenerator) 16 | } 17 | 18 | public class AppHapticFeedback { 19 | private let feedback: ApFeedbackGenerator 20 | 21 | public static func test() { 22 | } 23 | 24 | public static func preview() -> AppHapticFeedback { 25 | // TODO: need use CoreHaptic for improve 26 | return AppHapticFeedback(feedback: .impact(UIImpactFeedbackGenerator(style: .heavy), intensity: 1.0)) 27 | } 28 | 29 | public func noise() { 30 | switch feedback { 31 | case let .impact(generator, intensity): 32 | if #available(iOS 13.0, *) { 33 | generator.impactOccurred(intensity: intensity) 34 | } else { 35 | generator.impactOccurred() 36 | } 37 | case let .notification(generator, type): 38 | generator.notificationOccurred(type) 39 | case let .selection(generator): 40 | generator.selectionChanged() 41 | } 42 | } 43 | 44 | public init(feedback: ApFeedbackGenerator) { 45 | self.feedback = feedback 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Modules/UIs/UIComponents/Sources/base/ApNavigationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApNavigationController.swift 3 | // UIComponents 4 | // 5 | // Created by Alexander Ivlev on 03/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class ApNavigationController: UINavigationController, UIGestureRecognizerDelegate 12 | { 13 | open override var preferredStatusBarStyle: UIStatusBarStyle { 14 | return (presentedViewController ?? topViewController)?.preferredStatusBarStyle ?? .default 15 | } 16 | 17 | open override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | interactivePopGestureRecognizer?.delegate = self 21 | } 22 | 23 | open func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { 24 | return viewControllers.count > 1 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Modules/UIs/UIComponents/Sources/base/ApTabBarController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApTabBarController.swift 3 | // UIComponents 4 | // 5 | // Created by Alexander Ivlev on 19/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Design 11 | 12 | open class ApTabBarController: UITabBarController 13 | { 14 | public private(set) lazy var style: Style = styleMaker.makeStyle(for: self) 15 | 16 | public override var preferredStatusBarStyle: UIStatusBarStyle { 17 | guard let tabVCs = viewControllers, !tabVCs.isEmpty else { 18 | return style.colors.preferredStatusBarStyle 19 | } 20 | return tabVCs[selectedIndex].preferredStatusBarStyle 21 | } 22 | 23 | private let styleMaker: StyleMaker = StyleMaker() 24 | 25 | open func styleDidChange(_ style: Style) { 26 | view.backgroundColor = style.colors.background 27 | } 28 | 29 | open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { 30 | super.traitCollectionDidChange(previousTraitCollection) 31 | 32 | style = styleMaker.makeStyle(for: self) 33 | styleDidChange(style) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Modules/UIs/UIComponents/Sources/base/table/ApCollectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApCollectionView.swift 3 | // UIComponents 4 | // 5 | // Created by Alexander Ivlev on 08/12/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Design 11 | import Common 12 | 13 | open class ApCollectionView: UICollectionView, StylizingView 14 | { 15 | private let stylizingViewsContainer = StylizingViewsContainer() 16 | 17 | public func addViewForStylizing(_ view: StylizingView, immediately: Bool = true) { 18 | stylizingViewsContainer.addView(view, immediately: immediately) 19 | } 20 | 21 | open func apply(use style: Design.Style) { 22 | backgroundColor = style.colors.background 23 | 24 | stylizingViewsContainer.styleDidChange(style) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Modules/UIs/UIComponents/Sources/base/table/ApSectionTitleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApSectionTitleView.swift 3 | // UIComponents 4 | // 5 | // Created by Alexander Ivlev on 26/10/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Design 11 | import Common 12 | 13 | public class ApSectionTitleView: UIView 14 | { 15 | public static let preferredHeight: CGFloat = 30.0 16 | 17 | private let titleLabel: UILabel = UILabel(frame: .zero) 18 | 19 | public init(text: String) { 20 | super.init(frame: .zero) 21 | 22 | commonInit(text: text) 23 | } 24 | 25 | required init?(coder: NSCoder) { 26 | fatalError("init(coder:) has not been implemented") 27 | } 28 | 29 | private func commonInit(text: String) { 30 | addSubview(titleLabel) 31 | 32 | titleLabel.text = text 33 | } 34 | } 35 | 36 | extension ApSectionTitleView: StylizingView 37 | { 38 | public func apply(use style: Style) { 39 | backgroundColor = .clear 40 | 41 | titleLabel.font = style.fonts.title 42 | titleLabel.textColor = style.colors.mainText 43 | 44 | titleLabel.snp.remakeConstraints { maker in 45 | maker.top.equalToSuperview().offset(4.0) 46 | maker.left.equalToSuperview().offset(style.layout.cellInnerInsets.left) 47 | maker.right.equalToSuperview().offset(-style.layout.cellInnerInsets.right) 48 | maker.bottom.equalToSuperview().offset(-4.0) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Modules/UIs/UIComponents/Sources/base/table/ApTableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApTableView.swift 3 | // UIComponents 4 | // 5 | // Created by Alexander Ivlev on 26/10/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Design 11 | import Common 12 | 13 | open class ApTableView: UITableView, StylizingView 14 | { 15 | public struct TableSection { 16 | public var title: String? 17 | public var content: Content 18 | 19 | public init(content: Content) { 20 | self.content = content 21 | } 22 | } 23 | 24 | private let stylizingViewsContainer = StylizingViewsContainer() 25 | 26 | public func addViewForStylizing(_ view: StylizingView, immediately: Bool = true) { 27 | stylizingViewsContainer.addView(view, immediately: immediately) 28 | } 29 | 30 | open func apply(use style: Design.Style) { 31 | backgroundColor = style.colors.background 32 | separatorColor = style.colors.separator 33 | 34 | separatorInset = .zero 35 | 36 | stylizingViewsContainer.styleDidChange(style) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Modules/UIs/UIComponents/Sources/base/table/ApTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApTableViewCell.swift 3 | // UIComponents 4 | // 5 | // Created by Alexander Ivlev on 26/10/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Design 11 | import Common 12 | 13 | open class ApTableViewCell: UITableViewCell, StylizingView 14 | { 15 | public var stylizingSubviews = WeakArray() 16 | 17 | open func apply(use style: Design.Style) { 18 | self.backgroundColor = style.colors.accent 19 | 20 | selectionStyle = .default 21 | let selectedView = UIView() 22 | selectedView.backgroundColor = style.colors.separator 23 | selectedBackgroundView = selectedView 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Modules/UIs/UIComponents/Sources/skeleton/SkeletonViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SkeletonViewModel.swift 3 | // UIComponents 4 | // 5 | // Created by Alexander Ivlev on 26/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | 10 | public enum SkeletonViewModel 11 | { 12 | case loading 13 | case done(ViewModel) 14 | case failed 15 | } 16 | -------------------------------------------------------------------------------- /Modules/UIs/UIComponents/Sources/skeleton/UIView+Skeleton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Skeleton.swift 3 | // UIComponents 4 | // 5 | // Created by Alexander Ivlev on 23/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SnapKit 11 | 12 | extension UIView { 13 | public var skeletonView: SkeletonView { 14 | let skeletons = subviews.compactMap { $0 as? SkeletonView} 15 | if let skeleton = skeletons.last { 16 | skeletons.forEach { bringSubviewToFront($0) } 17 | return skeleton 18 | } 19 | 20 | let skeleton = SkeletonView() 21 | addSubview(skeleton) 22 | 23 | skeleton.snp.makeConstraints { maker in 24 | maker.edges.equalToSuperview() 25 | } 26 | 27 | return skeleton 28 | } 29 | 30 | public func startSkeleton() { 31 | if isHidden { 32 | return 33 | } 34 | 35 | let skeletonViews = subviews.compactMap { $0 as? SkeletonView } 36 | UIView.animate(withDuration: 0.15, animations: { 37 | for subview in skeletonViews { 38 | subview.start() 39 | } 40 | }) 41 | } 42 | 43 | public func endSkeleton() { 44 | let skeletonViews = subviews.compactMap { $0 as? SkeletonView } 45 | UIView.animate(withDuration: 0.15, animations: { 46 | for subview in skeletonViews { 47 | subview.end() 48 | } 49 | }) 50 | } 51 | 52 | public func failedSkeleton() { 53 | let skeletonViews = subviews.compactMap { $0 as? SkeletonView } 54 | UIView.animate(withDuration: 0.25, animations: { 55 | for subview in skeletonViews { 56 | subview.failed() 57 | } 58 | }) 59 | } 60 | 61 | public func endSkeleton(success: Bool) { 62 | if success { 63 | endSkeleton() 64 | } else { 65 | failedSkeleton() 66 | } 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /Modules/UIs/UIComponents/Sources/views/AvatarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AvatarView.swift 3 | // UIComponents 4 | // 5 | // Created by Alexander Ivlev on 28/10/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Common 11 | import Design 12 | 13 | open class AvatarView: IdImageView { 14 | 15 | public init() { 16 | super.init(frame: .zero) 17 | } 18 | 19 | public required init?(coder: NSCoder) { 20 | fatalError("init(coder:) has not been implemented") 21 | } 22 | } 23 | 24 | extension AvatarView 25 | { 26 | public static func generateAvatar(letter: String, size: CGFloat, style: Style) -> UIImage { 27 | return UIGraphicsImageRenderer(size: CGSize(width: size, height: size)).image { context in 28 | let label = UILabel(frame: CGRect(x: 0, y: 0, width: size, height: size)) 29 | label.textAlignment = .center 30 | label.textColor = style.colors.mainText 31 | label.backgroundColor = style.colors.accent 32 | label.font = style.fonts.avatar.withSize(size * 0.5) 33 | label.text = letter 34 | 35 | label.layer.render(in: context.cgContext) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Modules/UIs/UIComponents/Sources/views/GradientView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GradientItemView.swift 3 | // UIComponents 4 | // 5 | // Created by Alexander Ivlev on 27/10/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Design 11 | import Common 12 | 13 | public class GradientView: UIView { 14 | public override class var layerClass: AnyClass { 15 | return CAGradientLayer.classForCoder() 16 | } 17 | 18 | public var gradient: Gradient? { 19 | didSet { updateGradient() } 20 | } 21 | 22 | public var startPoint: CGPoint = CGPoint(x: 0.5, y: 0.0) { 23 | didSet { updateGradient() } 24 | } 25 | 26 | public var endPoint: CGPoint = CGPoint(x: 0.5, y: 1.0) { 27 | didSet { updateGradient() } 28 | } 29 | 30 | public override init(frame: CGRect = .zero) { 31 | super.init(frame: frame) 32 | } 33 | 34 | required init?(coder: NSCoder) { 35 | fatalError("init(coder:) has not been implemented") 36 | } 37 | 38 | private func updateGradient() { 39 | guard let layer = self.layer as? CAGradientLayer else { 40 | log.assert("Gradient view layer have incorrect type") 41 | return 42 | } 43 | 44 | guard let gradient = self.gradient else { 45 | layer.colors = [UIColor.clear.cgColor, UIColor.clear.cgColor] 46 | return 47 | } 48 | 49 | layer.startPoint = startPoint 50 | layer.endPoint = endPoint 51 | layer.colors = [gradient.from.cgColor, gradient.to.cgColor] 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /Modules/UIs/UIComponents/Sources/views/IdImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IdImageView.swift 3 | // UIComponents 4 | // 5 | // Created by Alexander Ivlev on 23/11/2019. 6 | // Copyright © 2019 ApostleLife. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Common 11 | 12 | open class IdImageView: UIImageView 13 | { 14 | fileprivate var owner: ChangeableImage? 15 | fileprivate var unique = AbstractImageUnique() 16 | } 17 | 18 | // MARK: - UIImageView 19 | extension ChangeableImage { 20 | public func join(imageView: IdImageView, completion: (() -> Void)? = nil, file: String = #file, line: UInt = #line) { 21 | imageView.image = image 22 | imageView.owner = self 23 | 24 | imageView.unique = AbstractImageUnique() 25 | weakJoin(listener: { [weak self, weak imageView] (_, image) in 26 | if imageView?.owner === self { 27 | imageView?.image = image 28 | completion?() 29 | } 30 | }, owner: imageView.unique, file: file, line: line) 31 | } 32 | } 33 | --------------------------------------------------------------------------------