├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ └── main.yml ├── .gitignore ├── .swiftlint.yml ├── ADD_NEW_UTIL_TUTORIAL.md ├── BuildTools └── swiftlint_0_47_1 ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE ├── Makefile ├── Package.resolved ├── Package.swift ├── README.md ├── SurfUtils.podspec ├── TechDocs ├── Pictures │ ├── badge.png │ ├── beans1.gif │ ├── beans2.gif │ ├── commonButton.gif │ ├── initials.png │ ├── skeleton1.gif │ └── skeleton2.gif ├── service_utils.md └── uikit_utils.md ├── Utils.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ └── Utils.xcscheme ├── Utils ├── BeanPageControl │ └── BeanPageControl.swift ├── BrightSide │ └── BrightSide.swift ├── CommonButton │ └── CommonButton.swift ├── CustomSwitch │ ├── CustomSwitch.swift │ └── CustomSwitchExternalConfigurations.swift ├── Dictionary │ └── Dictionary+QueryStringBuilder.swift ├── GeolocationService │ ├── GeolocationService.swift │ ├── GeolocationServiceInterface.swift │ └── Support │ │ ├── GeolocationAccuracy.swift │ │ ├── GeolocationAuthResult.swift │ │ ├── GeolocationResult.swift │ │ └── LocationManagerInterface.swift ├── Info.plist ├── ItemsScrollManager │ └── ItemsScrollManager.swift ├── KeyboardPresentable │ ├── CommonKeyboardPresentable.swift │ ├── FullKeyboardPresentable.swift │ ├── KeyboardNotificationsObserver.swift │ ├── KeyboardNotificationsObserverPool.swift │ ├── KeyboardObservable.swift │ └── Notification.swift ├── LayoutHelper │ └── LayoutHelper.swift ├── LoadingView │ ├── BaseLoadingView.swift │ ├── LoadingDataProvider.swift │ ├── LoadingSubview │ │ ├── LoadingSubview.swift │ │ └── LoadingSubviewConfigurable.swift │ ├── LoadingView.swift │ ├── LoadingViewBlock │ │ ├── BaseLoadingViewBlock.swift │ │ └── LoadingViewBlock.swift │ └── Models │ │ ├── DefaultLoadingModel.swift │ │ └── LoadingViewConfig.swift ├── LocalStorage │ └── LocalStorage.swift ├── MailSender │ ├── MailUtil.swift │ ├── Modles │ │ └── MailSenderPayload.swift │ ├── Protocols │ │ ├── MailSenderError.swift │ │ ├── MailSenderErrorDisplaying.swift │ │ ├── MailSenderPayloadProvider.swift │ │ └── MailSenderRouterHelper.swift │ └── Support │ │ └── InsideAppMailViewController.swift ├── MapRoutingService │ ├── MapApplication.swift │ ├── MapRoutingLocationServiceInterface.swift │ ├── MapRoutingService.swift │ └── MapRoutingServiceInterface.swift ├── MoneyModel │ └── MoneyModel.swift ├── RouteMeasurer │ └── RouteMeasurer.swift ├── SecurityService │ ├── Crypto │ │ ├── CryptoBox │ │ │ ├── CryptoBox.swift │ │ │ ├── CryptoBoxCommonError.swift │ │ │ ├── HackWrapperCryptoBox.swift │ │ │ ├── PinCryptoBox.swift │ │ │ └── PinHackCryptoBox.swift │ │ ├── CryptoService │ │ │ ├── BlowfishCryptoService.swift │ │ │ └── SymmetricCryptoService.swift │ │ ├── Hash │ │ │ ├── HashProvider.swift │ │ │ └── SHA3.swift │ │ └── Utils │ │ │ └── CryptoCommon.swift │ └── Store │ │ ├── GenericPasswordQueryable.swift │ │ ├── InMemorySecureStore.swift │ │ ├── KeyChainSecureStore.swift │ │ ├── SecureStore.swift │ │ └── SecureStoreError.swift ├── SettingsRouter │ └── SettingsRouter.swift ├── SkeletonView │ └── SkeletonView.swift ├── String │ ├── String+Attributes.swift │ └── StringBuilder.swift ├── UIControl │ └── TouchableControl.swift ├── UIDevice │ ├── Support │ │ ├── Size.swift │ │ ├── Type.swift │ │ ├── Version.swift │ │ ├── iOS │ │ │ └── Device.swift │ │ └── macOS │ │ │ └── DeviceMacOS.swift │ └── UIDevice.swift ├── UIImage │ ├── UIImage+badgedImage.swift │ └── UIImageExtensions.swift ├── UINavigationController │ └── UINavigationController+AdvancedNavigationStackManagement.swift ├── UIStyle │ ├── AnyStyle.swift │ └── UIStyle.swift ├── UIView │ ├── UIView+BlurBuilder.swift │ ├── UIView+Masking.swift │ └── UIView+XibSetup.swift ├── Utils.h ├── VibrationFeedbackManager │ ├── UIDevice+feedbackType.swift │ ├── UIDevice+hasHapticFeedback.swift │ ├── UIDevice+hasTapticEngine.swift │ └── VibrationFeedbackManager.swift └── WordDeclinationSelector │ └── WordDeclinationSelector.swift ├── UtilsExample ├── .swiftlint.yml ├── UtilsExample.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved ├── UtilsExample │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── Info.plist │ └── Playbook │ │ ├── Coordinators │ │ ├── BrightSideCoordinator.swift │ │ ├── CustomSwitchCoordinator.swift │ │ ├── GeolocationServiceCoordinator.swift │ │ ├── KeyboardPresentableCoordinator.swift │ │ ├── MoneyModelCoordinator.swift │ │ ├── QueryStringBuilderCoordinator.swift │ │ ├── RouteMeasurerCoordinator.swift │ │ ├── SettingsRouterCoordinator.swift │ │ ├── SkeletonViewCoordinator.swift │ │ ├── StringAttributesCoordinator.swift │ │ ├── UIDeviceCoordinator.swift │ │ └── WordDeclinationSelectorCoordinator.swift │ │ ├── Flows │ │ ├── BrightSide │ │ │ ├── Configurator │ │ │ │ └── BrightSideModuleConfigurator.swift │ │ │ ├── Presenter │ │ │ │ ├── BrightSideModuleInput.swift │ │ │ │ ├── BrightSideModuleOutput.swift │ │ │ │ └── BrightSidePresenter.swift │ │ │ └── View │ │ │ │ ├── BrightSideViewController.swift │ │ │ │ ├── BrightSideViewController.xib │ │ │ │ ├── BrightSideViewInput.swift │ │ │ │ └── BrightSideViewOutput.swift │ │ ├── CustomSwitch │ │ │ ├── Configurator │ │ │ │ └── CustomSwitchModuleConfigurator.swift │ │ │ ├── Presenter │ │ │ │ ├── CustomSwitchModuleInput.swift │ │ │ │ ├── CustomSwitchModuleOutput.swift │ │ │ │ └── CustomSwitchPresenter.swift │ │ │ └── View │ │ │ │ ├── CustomSwitchViewController.swift │ │ │ │ ├── CustomSwitchViewController.xib │ │ │ │ ├── CustomSwitchViewInput.swift │ │ │ │ └── CustomSwitchViewOutput.swift │ │ ├── GeolocationService │ │ │ ├── Configurator │ │ │ │ └── GeolocationServiceModuleConfigurator.swift │ │ │ ├── Presenter │ │ │ │ ├── GeolocationServiceModuleInput.swift │ │ │ │ ├── GeolocationServiceModuleOutput.swift │ │ │ │ └── GeolocationServicePresenter.swift │ │ │ └── View │ │ │ │ ├── GeolocationServiceViewController.swift │ │ │ │ ├── GeolocationServiceViewController.xib │ │ │ │ ├── GeolocationServiceViewInput.swift │ │ │ │ └── GeolocationServiceViewOutput.swift │ │ ├── KeyboardPresentable │ │ │ ├── Configurator │ │ │ │ └── KeyboardPresentableModuleConfigurator.swift │ │ │ ├── Presenter │ │ │ │ ├── KeyboardPresentableModuleInput.swift │ │ │ │ ├── KeyboardPresentableModuleOutput.swift │ │ │ │ └── KeyboardPresentablePresenter.swift │ │ │ └── View │ │ │ │ ├── KeyboardPresentableViewController.swift │ │ │ │ ├── KeyboardPresentableViewController.xib │ │ │ │ ├── KeyboardPresentableViewInput.swift │ │ │ │ └── KeyboardPresentableViewOutput.swift │ │ ├── MoneyModel │ │ │ ├── Configurator │ │ │ │ └── MoneyModelModuleConfigurator.swift │ │ │ ├── Presenter │ │ │ │ ├── MoneyModelModuleInput.swift │ │ │ │ ├── MoneyModelModuleOutput.swift │ │ │ │ └── MoneyModelPresenter.swift │ │ │ └── View │ │ │ │ ├── MoneyModelViewController.swift │ │ │ │ ├── MoneyModelViewController.xib │ │ │ │ ├── MoneyModelViewInput.swift │ │ │ │ └── MoneyModelViewOutput.swift │ │ ├── QueryStringBuilder │ │ │ ├── Configurator │ │ │ │ └── QueryStringBuilderModuleConfigurator.swift │ │ │ ├── Presenter │ │ │ │ ├── QueryStringBuilderModuleInput.swift │ │ │ │ ├── QueryStringBuilderModuleOutput.swift │ │ │ │ └── QueryStringBuilderPresenter.swift │ │ │ └── View │ │ │ │ ├── QueryStringBuilderViewController.swift │ │ │ │ ├── QueryStringBuilderViewController.xib │ │ │ │ ├── QueryStringBuilderViewInput.swift │ │ │ │ └── QueryStringBuilderViewOutput.swift │ │ ├── RouteMeasurer │ │ │ ├── Configurator │ │ │ │ └── RouteMeasurerModuleConfigurator.swift │ │ │ ├── Presenter │ │ │ │ ├── RouteMeasurerModuleInput.swift │ │ │ │ ├── RouteMeasurerModuleOutput.swift │ │ │ │ └── RouteMeasurerPresenter.swift │ │ │ └── View │ │ │ │ ├── RouteMeasurerViewController.swift │ │ │ │ ├── RouteMeasurerViewController.xib │ │ │ │ ├── RouteMeasurerViewInput.swift │ │ │ │ └── RouteMeasurerViewOutput.swift │ │ ├── SettingsRouter │ │ │ ├── Configurator │ │ │ │ └── SettingsRouterModuleConfigurator.swift │ │ │ ├── Presenter │ │ │ │ ├── SettingsRouterModuleInput.swift │ │ │ │ ├── SettingsRouterModuleOutput.swift │ │ │ │ └── SettingsRouterPresenter.swift │ │ │ └── View │ │ │ │ ├── SettingsRouterViewController.swift │ │ │ │ ├── SettingsRouterViewController.xib │ │ │ │ ├── SettingsRouterViewInput.swift │ │ │ │ └── SettingsRouterViewOutput.swift │ │ ├── SkeletonView │ │ │ ├── Configurator │ │ │ │ └── SkeletonViewModuleConfigurator.swift │ │ │ ├── Presenter │ │ │ │ ├── SkeletonViewModuleInput.swift │ │ │ │ ├── SkeletonViewModuleOutput.swift │ │ │ │ └── SkeletonViewPresenter.swift │ │ │ └── View │ │ │ │ ├── SkeletonViewViewController.swift │ │ │ │ ├── SkeletonViewViewController.xib │ │ │ │ ├── SkeletonViewViewInput.swift │ │ │ │ └── SkeletonViewViewOutput.swift │ │ ├── StringAttributes │ │ │ ├── Configurator │ │ │ │ └── StringAttributesModuleConfigurator.swift │ │ │ ├── Presenter │ │ │ │ ├── StringAttributesModuleInput.swift │ │ │ │ ├── StringAttributesModuleOutput.swift │ │ │ │ └── StringAttributesPresenter.swift │ │ │ └── View │ │ │ │ ├── StringAttributesViewController.swift │ │ │ │ ├── StringAttributesViewController.xib │ │ │ │ ├── StringAttributesViewInput.swift │ │ │ │ └── StringAttributesViewOutput.swift │ │ ├── UIDevice │ │ │ ├── Configurator │ │ │ │ └── UIDeviceModuleConfigurator.swift │ │ │ ├── Presenter │ │ │ │ ├── UIDeviceModuleInput.swift │ │ │ │ ├── UIDeviceModuleOutput.swift │ │ │ │ └── UIDevicePresenter.swift │ │ │ └── View │ │ │ │ ├── UIDeviceViewController.swift │ │ │ │ ├── UIDeviceViewController.xib │ │ │ │ ├── UIDeviceViewInput.swift │ │ │ │ └── UIDeviceViewOutput.swift │ │ └── WordDeclinationSelector │ │ │ ├── Configurator │ │ │ └── WordDeclinationSelectorModuleConfigurator.swift │ │ │ ├── Presenter │ │ │ ├── WordDeclinationSelectorModuleInput.swift │ │ │ ├── WordDeclinationSelectorModuleOutput.swift │ │ │ └── WordDeclinationSelectorPresenter.swift │ │ │ └── View │ │ │ ├── WordDeclinationSelectorViewController.swift │ │ │ ├── WordDeclinationSelectorViewController.xib │ │ │ ├── WordDeclinationSelectorViewInput.swift │ │ │ └── WordDeclinationSelectorViewOutput.swift │ │ ├── Library │ │ ├── Extensions │ │ │ ├── Presentable.swift │ │ │ └── UIApplication.swift │ │ └── Router │ │ │ ├── MainRouter.swift │ │ │ └── Router.swift │ │ └── UIKitPages │ │ └── MainPage.swift ├── UtilsExampleTests │ └── UtilsExampleTests.swift └── UtilsExampleUITests │ ├── UtilsExampleUITests.swift │ └── UtilsExampleUITestsLaunchTests.swift └── UtilsTests ├── GeolocationServiceTests.swift ├── Info.plist ├── LocalStorageTests.swift ├── MoneyModelTests.swift ├── RouteMeasurerTests.swift ├── SecurityService ├── KeyChainSecureStoreTests.swift ├── PinCryptoBoxTests.swift └── PinHackCryptoBoxTests.swift └── WordDeclinationSelectorTests.swift /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: chausovSurfStudio 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Library version and device type** 27 | - iOS version: [e.g. 14.0] 28 | - device type: [e.g. iPhone 6+] 29 | - Version [e.g. 1.0.0] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: update 6 | assignees: chausovSurfStudio 7 | 8 | --- 9 | 10 | # General description of the problem 11 | **Is your feature request related to a problem? Please describe.** 12 | A clear and concise description of what the problem is. 13 | Ex. I'm always frustrated when [...] 14 | 15 | # Playback Steps 16 | **A detailed description of how to reproduce the problem.** 17 | Be sure to specify the settings for the input field - for example, with what parameters the mask for the input field is applied, what is the height policy, etc. 18 | - step 1 19 | - step 2 20 | 21 | # Expected behavior 22 | **Describe the expected behavior that you expect to see by reproducing the problem through the indicated steps** 23 | 24 | # Actual behavior 25 | **What is really happening and why it seems wrong to you** 26 | 27 | **Describe the solution you'd like** 28 | A clear and concise description of what you want to happen. 29 | 30 | **Describe alternatives you've considered** 31 | A clear and concise description of any alternative solutions or features you've considered. 32 | 33 | **Additional context** 34 | Add any other context or screenshots about the feature request here. 35 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # What is done 2 | 3 | - first 4 | - second 5 | - third 6 | 7 | # What to look for 8 | 9 | - first 10 | - second 11 | - third 12 | 13 | # How to check 14 | 15 | - first 16 | - second 17 | - third -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | # Controls when the action will run. 4 | on: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | branches: 10 | - '**' 11 | types: [ opened, edited, synchronize, reopened ] 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: macos-12 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Init 21 | run: | 22 | make init 23 | - name: Force select xcode 13 24 | run: | 25 | sudo xcode-select -switch /Applications/Xcode_13.4.app 26 | - name: Build 27 | run: | 28 | make build 29 | - name: SPM Build 30 | run: | 31 | make spm_build 32 | - name: Tests 33 | run: | 34 | make test 35 | - name: Example Build 36 | run: | 37 | make example_build 38 | - name: Upload coverage to Codecov 39 | uses: codecov/codecov-action@v1.2.1 40 | - name: Pod Lib Lint 41 | run: | 42 | pod lib lint 43 | -------------------------------------------------------------------------------- /.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 | Gemfile.lock 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | *.dSYM.zip 30 | *.dSYM 31 | 32 | ## Playgrounds 33 | timeline.xctimeline 34 | playground.xcworkspace 35 | 36 | # Swift Package Manager 37 | # 38 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 39 | # Packages/ 40 | # Package.pins 41 | # Package.resolved 42 | .build/ 43 | 44 | # CocoaPods 45 | # 46 | # We recommend against adding the Pods directory to your .gitignore. However 47 | # you should judge for yourself, the pros and cons are mentioned at: 48 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 49 | # 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 | Utils/fastlane/Preview.html 66 | Utils/fastlane/screenshots/**/*.png 67 | Utils/fastlane/local_config.sh 68 | Utils/fastlane/test_output 69 | Utils/fastlane/Provisioning 70 | Utils/fastlane/Build 71 | Utils/fastlane/report.xml 72 | Utils/buildData 73 | .swiftpm 74 | .DS_Store 75 | 76 | # Bundler 77 | .bundle/ 78 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | whitelist_rules: 2 | - attributes 3 | - class_delegate_protocol 4 | - closing_brace 5 | - closure_end_indentation 6 | - closure_spacing 7 | - colon 8 | - comma 9 | - cyclomatic_complexity 10 | - empty_count 11 | - empty_parameters 12 | - empty_parentheses_with_trailing_closure 13 | - explicit_init 14 | - explicit_self 15 | - first_where 16 | - force_cast 17 | - force_try 18 | - force_unwrapping 19 | - implicitly_unwrapped_optional 20 | - implicit_getter 21 | - leading_whitespace 22 | - legacy_cggeometry_functions 23 | - legacy_constant 24 | - legacy_constructor 25 | - legacy_nsgeometry_functions 26 | - mark 27 | - line_length 28 | - opening_brace 29 | - operator_usage_whitespace 30 | - redundant_discardable_let 31 | - redundant_nil_coalescing 32 | - redundant_void_return 33 | - return_arrow_whitespace 34 | - shorthand_operator 35 | - statement_position 36 | - syntactic_sugar 37 | - todo 38 | - trailing_comma 39 | - trailing_newline 40 | - trailing_semicolon 41 | - trailing_whitespace 42 | - type_name 43 | - type_body_length 44 | - unused_import 45 | - unused_declaration 46 | - vertical_whitespace 47 | - void_return 48 | - weak_delegate 49 | 50 | 51 | # logic 52 | # disabled_rules: # rule identifiers to exclude from running 53 | # - leading_whitespace 54 | # - line_length 55 | # - variable_name 56 | # - file_length 57 | # - missing_docs 58 | 59 | # # 60 | opt_in_rules: # some rules are only opt-in 61 | # - opening_brace 62 | # - closure_spacing 63 | # - colon 64 | # - comma 65 | # - conditional_returns_on_newline 66 | # - control_statement 67 | # - legacy_cggeometry_functions 68 | # - legacy_constant 69 | # - legacy_constructor 70 | # - mark 71 | # - overridden_super_call 72 | # - return_arrow_whitespace 73 | # - statement_position 74 | # - trailing_newline 75 | # - trailing_semicolon 76 | # - trailing_whitespace 77 | # - vertical_whitespace 78 | excluded: # paths to ignore during linting. Takes precedence over `included`. 79 | - UtilsExample 80 | - .build 81 | 82 | # configurable rules can be customized from this configuration file 83 | # binary rules can set their severity level 84 | force_cast: warning # implicitly 85 | force_try: 86 | severity: warning # explicitly 87 | # rules that have both warning and error levels, can set just the warning level 88 | # implicitly 89 | line_length: 120 90 | # they can set both implicitly with an array 91 | type_body_length: 92 | - 300 # warning 93 | - 400 # error 94 | # # or they can set both explicitly 95 | # file_length: 96 | # warning: 500 97 | # error: 1200 98 | # # naming rules can set warnings/errors for min_length and max_length 99 | # # additionally they can set excluded names 100 | type_name: 101 | min_length: 4 # only warning 102 | max_length: 50 103 | excluded: # excluded via string array 104 | - Id 105 | - iPhone 106 | 107 | reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit) 108 | -------------------------------------------------------------------------------- /BuildTools/swiftlint_0_47_1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/iOS-Utils/3d679adef6c6837f7f740506631c5eda2d13810a/BuildTools/swiftlint_0_47_1 -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Правила работы с репозиторием 2 | 3 | - Для того, чтобы добавить что-нибудь в этот репозиторий, не обязательно быть членом iOS команды Surf 4 | - Работа в этом репозитории ведется на русском языке 5 | 6 | ## Добавление новой утилиты 7 | 8 | Для того, чтобы добавить новую утилиту необходимо: 9 | 1) Убедиться, что такой утилиты еще нет 10 | 2) Ответвиться от `master` (либо создать fork этой библиотеки и провести работы там) 11 | 3) Добавить код 12 | 4) Добавить `subspec` для вашей утилиты 13 | 5) Поднять версию 14 | 6) Сделать PR и назначить на членов команды Surf 15 | 16 | ## Обновление утилиты 17 | 18 | Вы можете исправить баг или добавить функциональность к одной из написанных утилит. 19 | В качестве ревьювера GitHub автоматически добавит того человека, чей код вы изменили. 20 | 21 | После добавления изменений необходимо так же поднять версию всей `.podspec` 22 | 23 | Более подробно процесс добавления/изменения утилит описан в данном [документе](./ADD_NEW_UTIL_TUTORIAL.md) -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Ensure github repositories are fetched using HTTPS 4 | git_source(:github) do |repo_name| 5 | repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") 6 | puts(repo_name) 7 | "https://github.com/#{repo_name}.git" 8 | end if Gem::Version.new(Bundler::VERSION) < Gem::Version.new('2') 9 | 10 | gem 'xcpretty', "0.3.0" 11 | 12 | plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') 13 | eval_gemfile(plugins_path) if File.exist?(plugins_path) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Surf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | init: 2 | # Install bundler if not installed 3 | if ! gem spec bundler > /dev/null 2>&1; then\ 4 | echo "bundler gem is not installed!";\ 5 | -sudo gem install bundler -v "1.17.3";\ 6 | fi 7 | -bundle install --path .bundle 8 | 9 | ## Used to build target. Usually, it is not called manually, it is necessary for the CI to work. 10 | build: 11 | xcodebuild clean build -scheme Utils -sdk iphonesimulator | bundle exec xcpretty -c 12 | 13 | ## Used to build target with SPM dependencies. Usually, it is not called manually, it is necessary for the CI to work. 14 | spm_build: 15 | swift package clean 16 | swift build -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" -Xswiftc "-target" -Xswiftc "x86_64-apple-ios15.5-simulator" -Xswiftc "-lswiftUIKit" 17 | 18 | ## Run tests 19 | test: 20 | xcodebuild test -scheme Utils -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO -enableCodeCoverage YES -destination 'platform=iOS Simulator,name=iPhone 8,OS=15.5' | bundle exec xcpretty -c 21 | 22 | ## Run build for example project 23 | example_build: 24 | cd UtilsExample && xcodebuild clean build -scheme UtilsExample -sdk iphonesimulator | bundle exec xcpretty -c 25 | 26 | # COLORS 27 | GREEN := $(shell tput -Txterm setaf 2) 28 | YELLOW := $(shell tput -Txterm setaf 3) 29 | WHITE := $(shell tput -Txterm setaf 7) 30 | RESET := $(shell tput -Txterm sgr0) 31 | 32 | 33 | TARGET_MAX_CHAR_NUM=20 34 | ## Show help 35 | help: 36 | @echo '' 37 | @echo 'Usage:' 38 | @echo ' ${YELLOW}make${RESET} ${GREEN}${RESET}' 39 | @echo '' 40 | @echo 'Targets:' 41 | @awk '/^[a-zA-Z\-\_0-9]+:/ { \ 42 | helpMessage = match(lastLine, /^## (.*)/); \ 43 | if (helpMessage) { \ 44 | helpCommand = substr($$1, 0, index($$1, ":")-1); \ 45 | helpMessage = substr(lastLine, RSTART + 3, RLENGTH); \ 46 | printf " ${YELLOW}%-$(TARGET_MAX_CHAR_NUM)s${RESET} ${GREEN}%s${RESET}\n", helpCommand, helpMessage; \ 47 | } \ 48 | } \ 49 | { lastLine = $$0 }' $(MAKEFILE_LIST) 50 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CryptoSwift", 6 | "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift", 7 | "state": { 8 | "branch": null, 9 | "revision": "039f56c5d7960f277087a0be51f5eb04ed0ec073", 10 | "version": "1.5.1" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 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: "Utils", 8 | platforms: [.iOS(.v11), .macOS(.v10_12)], 9 | products: [ 10 | .library( 11 | name: "Utils", 12 | targets: ["Utils"]) 13 | ], 14 | dependencies: [ 15 | .package( 16 | name: "CryptoSwift", 17 | url: "https://github.com/krzyzanowskim/CryptoSwift", 18 | .exact("1.5.1") 19 | ) 20 | ], 21 | targets: [ 22 | .target( 23 | name: "Utils", 24 | dependencies: ["CryptoSwift"], 25 | path: "Utils", 26 | exclude: [ 27 | "Info.plist" 28 | ] 29 | ), 30 | .testTarget( 31 | name: "UtilsTests", 32 | dependencies: ["Utils"], 33 | path: "UtilsTests", 34 | exclude: [ 35 | "Info.plist" 36 | ] 37 | ) 38 | ] 39 | ) 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHubActions Build Status](https://github.com/surfstudio/iOS-Utils/workflows/CI/badge.svg)](https://github.com/surfstudio/iOS-Utils/actions) 2 | [![codecov](https://codecov.io/gh/surfstudio/iOS-Utils/branch/master/graph/badge.svg)](https://codecov.io/gh/surfstudio/iOS-Utils) 3 | [![CocoaPods Version](https://img.shields.io/cocoapods/v/SurfUtils.svg?style=flat)](https://cocoapods.org/pods/SurfUtils) 4 | [![SPM Compatible](https://img.shields.io/badge/SPM-compatible-blue.svg)](https://github.com/apple/swift-package-manager) 5 | # iOS-Utils 6 | 7 | Utils collection for iOS-development. Each utility is a small and frequently used piece of logic or UI component. 8 | 9 | [![iOS-Utils](https://i.ibb.co/vsGyFx7/Group-48095987.png)](https://github.com/surfstudio/iOS-Utils) 10 | 11 | ## About 12 | 13 | В повседневной работе часто применяются одни и те же устоявшиеся решения, участки логики. Именно они и вынесены в данную библиотеку: проверенные временем решения, охватывающие весь спектр разработки - от небольших хелперов или оберток над системными API, до полноценных UI-компонентов. 14 | 15 | ## Installation 16 | 17 | #### CocoaPods 18 | 19 | Для добавления всех утилит, добавьте в свой Podfile следующую строку, затем запустите `pod install` 20 | 21 | ```ruby 22 | pod 'SurfUtils' 23 | ``` 24 | 25 | Для установки конкретной утилиты $UTIL_NAME необходимо добавить следующий код в ваш `Podfile`, затем запустить `pod install` 26 | 27 | ```ruby 28 | pod 'SurfUtils/$UTIL_NAME$' 29 | ``` 30 | 31 | #### Swift Package Manager 32 | 33 | - В XCode пройдите в `File > Add Packages...` 34 | - Введите URL репозитория `https://github.com/surfstudio/iOS-Utils.git` 35 | 36 | ## Features 37 | 38 | - Различные UI-компоненты и утилиты, завязанные на UIKit - [документация](TechDocs/uikit_utils.md) 39 | - Хелперы, небольшие утилиты и сервисы - [документация](TechDocs/service_utils.md) 40 | 41 | Самое полезное и наиболее часто используемое: 42 | 43 | - [StringAttributes](TechDocs/service_utils.md#stringattributes) - упрощение работы с `NSAttributedString` 44 | - [KeyboardPresentable](TechDocs/uikit_utils.md#keyboardpresentable) - семейство протоколов для упрощения работы с клавиатурой и сокращения количества одинакового кода 45 | - [SkeletonView](TechDocs/uikit_utils.md#skeletonview) - cпециальная кастомная View для создания skeleton loader'ов 46 | - [XibView](TechDocs/uikit_utils.md#xibview) - для работы UIView + xib 47 | - [CommonButton](TechDocs/uikit_utils.md#commonbutton) - Базовый класс для кнопки 48 | - [UIDevice](TechDocs/uikit_utils.md#uidevice) – набор вспомогательных методов для определения типа девайса 49 | - [UIStyle](TechDocs/uikit_utils.md#uistyle) – класс для удобной работы с разными стилями UIView наследников 50 | - [LoadingView](TechDocs/uikit_utils.md#loadingview) - набор классов и протоколов для удобного отображения загрузочных состояний с шиммерами 51 | 52 | ## Example 53 | 54 | Все вышеперечисленное можно увидеть в Example-проекте. Для его корректного запуска и конфигурации скачайте репозиторий и выполните команду `make init` перед тем как его запустить. 55 | 56 | ## Changelog 57 | 58 | Список всех изменений можно посмотреть в этом [файле](./CHANGELOG.md). 59 | 60 | ## Contributing 61 | 62 | - Туториал по добавлению собственной утилиты можно найти [здесь](./ADD_NEW_UTIL_TUTORIAL.md) 63 | - Правила работы с репозиторием - [здесь](./CONTRIBUTING.md) 64 | 65 | ## License 66 | 67 | [MIT License](./LICENSE) -------------------------------------------------------------------------------- /TechDocs/Pictures/badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/iOS-Utils/3d679adef6c6837f7f740506631c5eda2d13810a/TechDocs/Pictures/badge.png -------------------------------------------------------------------------------- /TechDocs/Pictures/beans1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/iOS-Utils/3d679adef6c6837f7f740506631c5eda2d13810a/TechDocs/Pictures/beans1.gif -------------------------------------------------------------------------------- /TechDocs/Pictures/beans2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/iOS-Utils/3d679adef6c6837f7f740506631c5eda2d13810a/TechDocs/Pictures/beans2.gif -------------------------------------------------------------------------------- /TechDocs/Pictures/commonButton.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/iOS-Utils/3d679adef6c6837f7f740506631c5eda2d13810a/TechDocs/Pictures/commonButton.gif -------------------------------------------------------------------------------- /TechDocs/Pictures/initials.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/iOS-Utils/3d679adef6c6837f7f740506631c5eda2d13810a/TechDocs/Pictures/initials.png -------------------------------------------------------------------------------- /TechDocs/Pictures/skeleton1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/iOS-Utils/3d679adef6c6837f7f740506631c5eda2d13810a/TechDocs/Pictures/skeleton1.gif -------------------------------------------------------------------------------- /TechDocs/Pictures/skeleton2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surfstudio/iOS-Utils/3d679adef6c6837f7f740506631c5eda2d13810a/TechDocs/Pictures/skeleton2.gif -------------------------------------------------------------------------------- /Utils.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Utils.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Utils.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "cryptoswift", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/krzyzanowskim/CryptoSwift", 7 | "state" : { 8 | "revision" : "039f56c5d7960f277087a0be51f5eb04ed0ec073", 9 | "version" : "1.5.1" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /Utils/BrightSide/BrightSide.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrightSide.swift 3 | // Utils 4 | // 5 | // Created by Vlad Krupenko on 05.09.2018. 6 | // Copyright © 2018 Surf. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | public final class BrightSide { 13 | 14 | // MARK: - Public static methods 15 | 16 | /// Method return false, if we can detect some common for jailbroken deivce files or can write to device 17 | public static func isBright() -> Bool { 18 | // Check 1 : check if current device is simulator 19 | if isSimulator() { 20 | return true 21 | } 22 | 23 | // Check 2 : existence of files that are common for jailbroken devices 24 | if isJailbreakDirectoriesExist() || canOpenCydia() { 25 | return false 26 | } 27 | 28 | // Check 3 : Reading and writing in system directories (sandbox violation) 29 | let stringToWrite = "Jailbreak Test" 30 | do { 31 | try stringToWrite.write(toFile: "/private/JailbreakTest.txt", 32 | atomically: true, 33 | encoding: String.Encoding.utf8) 34 | //Device is jailbroken 35 | return false 36 | } catch { 37 | return true 38 | } 39 | } 40 | 41 | } 42 | 43 | // MARK: - Private help methods 44 | 45 | private extension BrightSide { 46 | 47 | /// Method will return true, if any of the files typical for the jailbreak exists 48 | private static func isJailbreakDirectoriesExist() -> Bool { 49 | let jailbreakDirectories = [ 50 | "/Applications/Cydia.app", 51 | "/Library/MobileSubstrate/MobileSubstrate.dylib", 52 | "/bin/bash", 53 | "/usr/sbin/sshd", 54 | "/etc/apt", 55 | "/private/var/lib/apt/" 56 | ] 57 | return jailbreakDirectories.map { FileManager.default.fileExists(atPath: $0) }.reduce(false, { $0 || $1 }) 58 | } 59 | 60 | /// Method will return true if we can open cydia package 61 | private static func canOpenCydia() -> Bool { 62 | guard let cydiaURL = URL(string: "cydia://package/com.example.package") else { 63 | return false 64 | } 65 | return UIApplication.shared.canOpenURL(cydiaURL) 66 | } 67 | 68 | /// Method will return true if current device is simulator 69 | private static func isSimulator() -> Bool { 70 | return ProcessInfo.processInfo.environment["SIMULATOR_DEVICE_NAME"] != nil 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /Utils/CustomSwitch/CustomSwitchExternalConfigurations.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomSwitchExternalConfigurations.swift 3 | // Utils 4 | // 5 | // Created by Artemii Shabanov on 13.08.2020. 6 | // Copyright © 2020 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// Holds shadow parameters 12 | public struct CSShadowConfiguration { 13 | let color: UIColor 14 | let offset: CGSize 15 | let radius: CGFloat 16 | let oppacity: Float 17 | 18 | public init(color: UIColor, offset: CGSize, radius: CGFloat, oppacity: Float) { 19 | self.color = color 20 | self.offset = offset 21 | self.radius = radius 22 | self.oppacity = oppacity 23 | } 24 | } 25 | 26 | /// Defines methods needed to apply color changes on CustomSwitch elements 27 | public protocol CSColorConfiguration { 28 | /// Applies color changes for view 29 | func applyColor(for view: UIView) 30 | } 31 | 32 | /// Simple configuration color for CustomSwitch element 33 | public struct CSSimpleColorConfiguration: CSColorConfiguration { 34 | let color: UIColor 35 | 36 | public func applyColor(for view: UIView) { 37 | view.backgroundColor = color 38 | } 39 | public init(color: UIColor) { 40 | self.color = color 41 | } 42 | } 43 | 44 | /// Gradient configuration for CustomSwitch element 45 | public struct CSGradientColorConfiguration: CSColorConfiguration { 46 | let colors: [UIColor] 47 | let locations: [NSNumber] 48 | 49 | public func applyColor(for view: UIView) { 50 | let gradient: CAGradientLayer = CAGradientLayer() 51 | gradient.cornerRadius = view.layer.cornerRadius 52 | gradient.frame = view.bounds 53 | gradient.colors = colors.map { $0.cgColor } 54 | gradient.locations = locations 55 | view.layer.insertSublayer(gradient, at: 0) 56 | } 57 | public init(colors: [UIColor] = [], locations: [NSNumber] = []) { 58 | self.colors = colors 59 | self.locations = locations 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Utils/Dictionary/Dictionary+QueryStringBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dictionary+QueryStringBuilder.swift 3 | // Utils 4 | // 5 | // Created by Chausov Alexander on 25/12/2018. 6 | // Copyright © 2018 Surf. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Dictionary where Key == String, Value == Any { 12 | 13 | /// Method allows you convert [String: Any] object into query string like "key1=value1&key3=true&key2=2.15" 14 | public func toQueryString() -> String? { 15 | let items = queryItems() 16 | let components = URLComponents(queryItems: items) 17 | return components.query 18 | } 19 | 20 | // MARK: - Private Methods 21 | 22 | /// Support methods to convert elements of dictionary into URLQueryItem array 23 | private func queryItems() -> [URLQueryItem] { 24 | return self.map { (element) -> URLQueryItem in 25 | let (key, value) = element 26 | let string = "\(value)" 27 | return URLQueryItem(name: key, value: string.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ) 28 | } 29 | } 30 | 31 | } 32 | 33 | // MARK: - URLComponents 34 | 35 | private extension URLComponents { 36 | 37 | /// Support init for toQueryString() method 38 | init(queryItems: [URLQueryItem]) { 39 | self.init() 40 | self.queryItems = queryItems 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /Utils/GeolocationService/GeolocationServiceInterface.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeolocationServiceInterface.swift 3 | // Utils 4 | // 5 | // Created by Александр Чаусов on 20/11/2019. 6 | // Copyright © 2019 Surf. All rights reserved. 7 | // 8 | 9 | // MARK: - Typealiases 10 | 11 | public typealias GeolocationCompletion = (GeolocationResult) -> Void 12 | public typealias GeolocationAuthCompletion = (GeolocationAuthResult) -> Void 13 | 14 | /// Service for working with user geoposition 15 | public protocol GeolocationServiceInterface { 16 | /// Methods returns user location at completion block 17 | /// or 'denied' if we haven't permission on user geoposition 18 | /// (or 'error' if some error occured) 19 | func getCurrentLocation(_ completion: @escaping GeolocationCompletion) 20 | /// Method allows you to know current geolocation permission state. 21 | /// If we haven't already request it - returns 'requesting' case 22 | func requestAuthorization(_ completion: @escaping GeolocationAuthCompletion) 23 | } 24 | -------------------------------------------------------------------------------- /Utils/GeolocationService/Support/GeolocationAccuracy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeolocationAccuracy.swift 3 | // Utils 4 | // 5 | // Created by Александр Чаусов on 20/11/2019. 6 | // Copyright © 2019 Surf. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | 11 | /// Available accuracy of the location data 12 | public enum GeolocationAccuracy { 13 | case bestForNavigation 14 | case best 15 | case tenMeters 16 | case hundredMeters 17 | case kilometer 18 | case threeKilometers 19 | 20 | // MARK: - Properties 21 | 22 | var coreValue: CLLocationAccuracy { 23 | switch self { 24 | case .bestForNavigation: 25 | return kCLLocationAccuracyBestForNavigation 26 | case .best: 27 | return kCLLocationAccuracyBest 28 | case .tenMeters: 29 | return kCLLocationAccuracyNearestTenMeters 30 | case .hundredMeters: 31 | return kCLLocationAccuracyHundredMeters 32 | case .kilometer: 33 | return kCLLocationAccuracyKilometer 34 | case .threeKilometers: 35 | return kCLLocationAccuracyThreeKilometers 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /Utils/GeolocationService/Support/GeolocationAuthResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeolocationAuthResult.swift 3 | // Utils 4 | // 5 | // Created by Александр Чаусов on 20/11/2019. 6 | // Copyright © 2019 Surf. All rights reserved. 7 | // 8 | 9 | /// Available results for request on user permission status 10 | public enum GeolocationAuthResult { 11 | /// user have permission on geolocation 12 | case success 13 | /// user haven't permission on geolocation 14 | case denied 15 | /// user doesn't give permission on his geolocation 16 | case failure 17 | /// system dialog reqeusts user permission at this moment 18 | case requesting 19 | } 20 | -------------------------------------------------------------------------------- /Utils/GeolocationService/Support/GeolocationResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeolocationResult.swift 3 | // Utils 4 | // 5 | // Created by Александр Чаусов on 20/11/2019. 6 | // Copyright © 2019 Surf. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | 11 | /// Available results for request on user location 12 | public enum GeolocationResult: Equatable { 13 | case success(CLLocation) 14 | /// denied access to the user location 15 | case denied 16 | /// some error occurred 17 | case error 18 | } 19 | -------------------------------------------------------------------------------- /Utils/GeolocationService/Support/LocationManagerInterface.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationManagerInterface.swift 3 | // Utils 4 | // 5 | // Created by Александр Чаусов on 20/11/2019. 6 | // Copyright © 2019 Surf. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | 11 | /// Protocol with 'CLLocationManager' interface for testing GeolocationService. 12 | public protocol LocationManagerInterface { 13 | /// The delegate object to receive update events. 14 | var delegate: CLLocationManagerDelegate? { get set } 15 | /// Returns the app’s authorization status for using location services. 16 | var status: CLAuthorizationStatus { get } 17 | /// The accuracy of the location data. 18 | var desiredAccuracy: CLLocationAccuracy { get set } 19 | 20 | /// Requests the one-time delivery of the user’s current location. 21 | func requestLocation() 22 | /// Requests the user’s permission to use location services while the app is in use. 23 | func requestWhenInUseAuthorization() 24 | } 25 | 26 | // MARK: - CLLocationManager 27 | 28 | extension CLLocationManager: LocationManagerInterface { 29 | 30 | public var status: CLAuthorizationStatus { 31 | return CLLocationManager.authorizationStatus() 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Utils/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Utils/KeyboardPresentable/CommonKeyboardPresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommonKeyboardPresentable.swift 3 | // Utils 4 | // 5 | // Created by Александр Чаусов on 16/01/2019. 6 | // Copyright © 2019 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | fileprivate enum Constants { 12 | static let animationDuration: TimeInterval = 0.25 13 | } 14 | 15 | /// This protocol allows you to get plain keyboard parameters on keyboard appear/disappear. 16 | /// But you have to use this protocol along with KeyboardObservable protocol. 17 | public protocol CommonKeyboardPresentable: AnyObject { 18 | 19 | /// This method is called when the keyboard appears on the device screen 20 | func keyboardWillBeShown(keyboardHeight: CGFloat, duration: TimeInterval) 21 | 22 | /// This method is called when the keyboard disappears from the device screen 23 | func keyboardWillBeHidden(duration: TimeInterval) 24 | 25 | /// This method is called after the keyboard appears on the device screen. Optional Method 26 | func keyboardWasShown(keyboardHeight: CGFloat, duration: TimeInterval) 27 | 28 | /// This method is called after the keyboard disappears from the device screen. Optional Method 29 | func keyboardWasHidden(duration: TimeInterval) 30 | 31 | } 32 | 33 | public extension CommonKeyboardPresentable where Self: KeyboardObservable { 34 | 35 | func keyboardWillBeShown(notification: Notification) { 36 | guard let keyboardHeight = notification.keyboardInfo.frameEnd?.height else { 37 | return 38 | } 39 | let duration = notification.keyboardInfo.animationDuration ?? Constants.animationDuration 40 | keyboardWillBeShown(keyboardHeight: keyboardHeight, duration: duration) 41 | } 42 | 43 | func keyboardWillBeHidden(notification: Notification) { 44 | let duration = notification.keyboardInfo.animationDuration ?? Constants.animationDuration 45 | keyboardWillBeHidden(duration: duration) 46 | } 47 | 48 | func keyboardWasShown(notification: Notification) { 49 | guard let keyboardHeight = notification.keyboardInfo.frameEnd?.height else { 50 | return 51 | } 52 | let duration = notification.keyboardInfo.animationDuration ?? Constants.animationDuration 53 | keyboardWasShown(keyboardHeight: keyboardHeight, duration: duration) 54 | } 55 | 56 | func keyboardWasHidden(notification: Notification) { 57 | let duration = notification.keyboardInfo.animationDuration ?? Constants.animationDuration 58 | keyboardWasHidden(duration: duration) 59 | } 60 | 61 | // MARK: - Optional Method 62 | 63 | func keyboardWasShown(keyboardHeight: CGFloat, duration: TimeInterval) { 64 | } 65 | 66 | func keyboardWasHidden(duration: TimeInterval) { 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /Utils/KeyboardPresentable/FullKeyboardPresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FullKeyboardPresentable.swift 3 | // Utils 4 | // 5 | // Created by Александр Чаусов on 16/01/2019. 6 | // Copyright © 2019 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// This protocol allows you to get full keyboard parameters on keyboard appear/disappear. 12 | /// But you have to use this protocol along with KeyboardObservable protocol. 13 | public protocol FullKeyboardPresentable: AnyObject { 14 | 15 | /// This method is called when the keyboard appears on the device screen 16 | func keyboardWillBeShown(keyboardInfo: Notification.KeyboardInfo) 17 | 18 | /// This method is called when the keyboard disappears from the device screen 19 | func keyboardWillBeHidden(keyboardInfo: Notification.KeyboardInfo) 20 | 21 | /// This method is called after the keyboard appears on the device screen 22 | func keyboardWasShown(keyboardInfo: Notification.KeyboardInfo) 23 | 24 | /// This method is called after the keyboard disappears from the device screen 25 | func keyboardWasHidden(keyboardInfo: Notification.KeyboardInfo) 26 | 27 | } 28 | 29 | public extension FullKeyboardPresentable where Self: KeyboardObservable { 30 | 31 | func keyboardWillBeShown(notification: Notification) { 32 | keyboardWillBeShown(keyboardInfo: notification.keyboardInfo) 33 | } 34 | 35 | func keyboardWillBeHidden(notification: Notification) { 36 | keyboardWillBeHidden(keyboardInfo: notification.keyboardInfo) 37 | } 38 | 39 | func keyboardWasShown(notification: Notification) { 40 | keyboardWasShown(keyboardInfo: notification.keyboardInfo) 41 | } 42 | 43 | func keyboardWasHidden(notification: Notification) { 44 | keyboardWasHidden(keyboardInfo: notification.keyboardInfo) 45 | } 46 | 47 | // MARK: - Optional Method 48 | 49 | func keyboardWasShown(keyboardInfo: Notification.KeyboardInfo) { 50 | } 51 | 52 | func keyboardWasHidden(keyboardInfo: Notification.KeyboardInfo) { 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /Utils/KeyboardPresentable/KeyboardNotificationsObserver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardNotificationsObserver.swift 3 | // Utils 4 | // 5 | // Created by Александр Чаусов on 12/01/2019. 6 | // Copyright © 2019 Surf. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// An instance of this class is responsible for handling notifications when the keyboard appears and disappears. 12 | /// Its necessity is caused by the fact that in the protocol extension you cannot declare methods with 13 | /// the @objc identifier. 14 | final class KeyboardNotificationsObserver { 15 | 16 | // MARK: - Private Properties 17 | 18 | private weak var view: KeyboardObservable? 19 | 20 | // MARK: - Properties 21 | 22 | var isInvalid: Bool { 23 | return view == nil 24 | } 25 | 26 | // MARK: - Initialization 27 | 28 | init(view: KeyboardObservable) { 29 | self.view = view 30 | } 31 | 32 | // MARK: - Internal Methods 33 | 34 | @objc 35 | func keyboardWillBeShown(notification: Notification) { 36 | view?.keyboardWillBeShown(notification: notification) 37 | } 38 | 39 | @objc 40 | func keyboardWillBeHidden(notification: Notification) { 41 | view?.keyboardWillBeHidden(notification: notification) 42 | } 43 | 44 | @objc 45 | func keyboardWasShown(notification: Notification) { 46 | view?.keyboardWasShown(notification: notification) 47 | } 48 | 49 | @objc 50 | func keyboardWasHidden(notification: Notification) { 51 | view?.keyboardWasHidden(notification: notification) 52 | } 53 | 54 | func isLinked(to view: KeyboardObservable) -> Bool { 55 | guard let guardedView = self.view else { 56 | return false 57 | } 58 | return guardedView === view 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /Utils/KeyboardPresentable/KeyboardNotificationsObserverPool.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardNotificationsObserverPool.swift 3 | // Utils 4 | // 5 | // Created by Александр Чаусов on 16/01/2019. 6 | // Copyright © 2019 Surf. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Pool of observers. Implemented as singltone for retain reference on observers. 12 | final class KeyboardNotificationsObserverPool { 13 | 14 | // MARK: - Properties 15 | 16 | static let shared = KeyboardNotificationsObserverPool() 17 | 18 | // MARK: - Private Properties 19 | 20 | private var observers: [KeyboardNotificationsObserver] = [] 21 | 22 | // MARK: - Internal Methods 23 | 24 | /// Returns new observer for view or nil if observer already exist 25 | func newObserver(for view: KeyboardObservable) -> KeyboardNotificationsObserver? { 26 | guard observers.first(where: { $0.isLinked(to: view) }) == nil else { 27 | return nil 28 | } 29 | let observer = KeyboardNotificationsObserver(view: view) 30 | observers.append(observer) 31 | return observer 32 | } 33 | 34 | /// Removes invalid observers, which have no view 35 | func removeInvalid() { 36 | for observer in observers.filter({ $0.isInvalid }) { 37 | NotificationCenter.default.removeObserver(observer) 38 | } 39 | observers.removeAll(where: { $0.isInvalid }) 40 | } 41 | 42 | /// Releases observer for given view 43 | func releaseObserver(for view: KeyboardObservable) { 44 | for observer in observers.filter({ $0.isLinked(to: view) }) { 45 | NotificationCenter.default.removeObserver(observer) 46 | } 47 | observers.removeAll(where: { $0.isLinked(to: view) }) 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /Utils/KeyboardPresentable/KeyboardObservable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardObservable.swift 3 | // Utils 4 | // 5 | // Created by Александр Чаусов on 16/01/2019. 6 | // Copyright © 2019 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// This protocol carries out all the necessary actions for subscribing / unsubscribing from keyboard notifications. 12 | /// You can use only this protocol, or you can use Common/FullKeyboardPresentable or you own implementation for getting 13 | /// necessary parameters. 14 | public protocol KeyboardObservable: AnyObject { 15 | 16 | /// Method for subscribing on keyboard notifications 17 | func subscribeOnKeyboardNotifications() 18 | 19 | /// Method for unsubscribing from keyboard notifications. 20 | /// You must call this method for unsubscriibing if you call subscribeOnKeyboardNotifications() method before 21 | func unsubscribeFromKeyboardNotifications() 22 | 23 | /// This method is called when the keyboard appears on the device screen 24 | func keyboardWillBeShown(notification: Notification) 25 | 26 | /// This method is called when the keyboard disappears from the device screen 27 | func keyboardWillBeHidden(notification: Notification) 28 | 29 | /// This method is called after the keyboard appears on the device screen. Optional Method 30 | func keyboardWasShown(notification: Notification) 31 | 32 | /// This method is called after the keyboard disappears from the device screen. Optional Method 33 | func keyboardWasHidden(notification: Notification) 34 | 35 | } 36 | 37 | public extension KeyboardObservable { 38 | 39 | // MARK: - Public Methods 40 | 41 | func subscribeOnKeyboardNotifications() { 42 | guard let notificationsObserver = KeyboardNotificationsObserverPool.shared.newObserver(for: self) else { 43 | // case when view already subscribed on notifications 44 | return 45 | } 46 | let center = NotificationCenter.default 47 | center.addObserver(notificationsObserver, 48 | selector: #selector(KeyboardNotificationsObserver.keyboardWillBeShown(notification:)), 49 | name: UIResponder.keyboardWillShowNotification, 50 | object: nil) 51 | center.addObserver(notificationsObserver, 52 | selector: #selector(KeyboardNotificationsObserver.keyboardWillBeHidden(notification:)), 53 | name: UIResponder.keyboardWillHideNotification, 54 | object: nil) 55 | center.addObserver(notificationsObserver, 56 | selector: #selector(KeyboardNotificationsObserver.keyboardWasShown(notification:)), 57 | name: UIResponder.keyboardDidShowNotification, 58 | object: nil) 59 | center.addObserver(notificationsObserver, 60 | selector: #selector(KeyboardNotificationsObserver.keyboardWasHidden(notification:)), 61 | name: UIResponder.keyboardDidHideNotification, 62 | object: nil) 63 | } 64 | 65 | func unsubscribeFromKeyboardNotifications() { 66 | KeyboardNotificationsObserverPool.shared.removeInvalid() 67 | KeyboardNotificationsObserverPool.shared.releaseObserver(for: self) 68 | } 69 | 70 | // MARK: - Optional Methods 71 | 72 | func keyboardWasShown(notification: Notification) { 73 | } 74 | 75 | func keyboardWasHidden(notification: Notification) { 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /Utils/KeyboardPresentable/Notification.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Notification.swift 3 | // Utils 4 | // 5 | // Created by Александр Чаусов on 16/01/2019. 6 | // Copyright © 2019 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension Notification { 12 | 13 | /// Instance of this structure keeps info about keyboard from notification 14 | public struct KeyboardInfo { 15 | public var frameBegin: CGRect? 16 | public var animationCurve: UInt? 17 | public var animationDuration: Double? 18 | public var frameEnd: CGRect? 19 | } 20 | 21 | // MARK: - Public Properties 22 | 23 | public var keyboardInfo: KeyboardInfo { 24 | return KeyboardInfo(frameBegin: keyboardFrameBegin, 25 | animationCurve: keyboradAnimationCurve, 26 | animationDuration: keyboardAnimationDuration, 27 | frameEnd: keyboardFrameEnd) 28 | } 29 | 30 | // MARK: - Private Properties 31 | 32 | private var keyboardFrameBegin: CGRect? { 33 | return userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? CGRect 34 | } 35 | 36 | private var keyboradAnimationCurve: UInt? { 37 | return userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt 38 | } 39 | 40 | private var keyboardAnimationDuration: Double? { 41 | return userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double 42 | } 43 | 44 | private var keyboardFrameEnd: CGRect? { 45 | return userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /Utils/LayoutHelper/LayoutHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayoutHelper.swift 3 | // Utils 4 | // 5 | // Created by Vladislav Krupenko on 03/01/2020. 6 | // Copyright © 2020 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class LayoutHelper: NSLayoutConstraint { 12 | 13 | @IBInspectable public var isSmallPhone: CGFloat = 0.0 { 14 | didSet { 15 | guard UIDevice.isSmallPhone else { 16 | return 17 | } 18 | constant = isSmallPhone 19 | } 20 | } 21 | 22 | @IBInspectable public var isNormal: CGFloat = 0.0 { 23 | didSet { 24 | guard !UIDevice.isSmallPhone && !UIDevice.isXPhone else { 25 | return 26 | } 27 | constant = isNormal 28 | } 29 | } 30 | 31 | @IBInspectable public var isXPhone: CGFloat = 0.0 { 32 | didSet { 33 | guard UIDevice.isXPhone else { 34 | return 35 | } 36 | constant = isXPhone 37 | } 38 | } 39 | 40 | @IBInspectable public var isIPad: CGFloat = 0.0 { 41 | didSet { 42 | guard UIDevice.isPad else { 43 | return 44 | } 45 | constant = isIPad 46 | } 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /Utils/LoadingView/LoadingDataProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingDataProvider.swift 3 | // Utils 4 | // 5 | // Created by Никита Гагаринов on 18.08.2020. 6 | // Copyright © 2020 Surf. All rights reserved. 7 | // 8 | 9 | public protocol LoadingDataProvider { 10 | var config: LoadingViewConfig { get } 11 | 12 | func getBlocks() -> [LoadingViewBlock] 13 | } 14 | -------------------------------------------------------------------------------- /Utils/LoadingView/LoadingSubview/LoadingSubview.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingSubview.swift 3 | // Utils 4 | // 5 | // Created by Никита Гагаринов on 18.08.2020. 6 | // Copyright © 2020 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol LoadingSubview: UIView { 12 | var height: CGFloat { get } 13 | 14 | /// Configuration for placeholders color 15 | func configure(color: UIColor) 16 | } 17 | -------------------------------------------------------------------------------- /Utils/LoadingView/LoadingSubview/LoadingSubviewConfigurable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingSubviewConfigurable.swift 3 | // Utils 4 | // 5 | // Created by Никита Гагаринов on 18.08.2020. 6 | // Copyright © 2020 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol LoadingSubviewConfigurable: UIView { 12 | associatedtype Model 13 | 14 | /// Changes offsets or required parameters 15 | func configure(model: Model) 16 | } 17 | 18 | extension LoadingSubviewConfigurable where Model == DefaultLoadingModel { 19 | func configure(model: Model) { } 20 | } 21 | -------------------------------------------------------------------------------- /Utils/LoadingView/LoadingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingView.swift 3 | // Utils 4 | // 5 | // Created by Никита Гагаринов on 19.08.2020. 6 | // Copyright © 2020 Surf. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | public protocol LoadingView: UIView { 13 | func setNeedAnimating(_ needAnimating: Bool) 14 | } 15 | -------------------------------------------------------------------------------- /Utils/LoadingView/LoadingViewBlock/BaseLoadingViewBlock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingViewBlock.swift 3 | // Utils 4 | // 5 | // Created by Никита Гагаринов on 19.08.2020. 6 | // Copyright © 2020 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public final class BaseLoadingViewBlock: LoadingViewBlock { 12 | 13 | // MARK: - Public Properties 14 | 15 | public let repeatCount: Int 16 | public let model: Subview.Model 17 | 18 | // MARK: - Private Properties 19 | 20 | private lazy var subviews: [LoadingSubview] = { 21 | return produce() 22 | }() 23 | 24 | // MARK: - Initialization 25 | 26 | public init(model: Subview.Model, repeatCount: Int = 1) { 27 | self.model = model 28 | self.repeatCount = repeatCount 29 | } 30 | 31 | // MARK: - Public Methods 32 | 33 | public func configure(color: UIColor) { 34 | subviews.forEach { $0.configure(color: color) } 35 | } 36 | 37 | public func reconfigure(repeatCount: Int) -> BaseLoadingViewBlock { 38 | return BaseLoadingViewBlock(model: model, repeatCount: repeatCount) 39 | } 40 | 41 | public func getSubviews() -> [LoadingSubview] { 42 | return subviews 43 | } 44 | 45 | } 46 | 47 | // MARK: - Private Methods 48 | 49 | private extension BaseLoadingViewBlock { 50 | 51 | func produce() -> [LoadingSubview] { 52 | return (0.. [LoadingSubview] 14 | func configure(color: UIColor) 15 | func reconfigure(repeatCount: Int) -> Self 16 | } 17 | -------------------------------------------------------------------------------- /Utils/LoadingView/Models/DefaultLoadingModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultLoadingModel.swift 3 | // Utils 4 | // 5 | // Created by Никита Гагаринов on 18.08.2020. 6 | // Copyright © 2020 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | /// Default model for configuring paddings 11 | public struct DefaultLoadingModel { 12 | public let topOffset: CGFloat 13 | public let bottomOffset: CGFloat 14 | 15 | public init(topOffset: CGFloat = 0, bottomOffset: CGFloat = 0) { 16 | self.topOffset = topOffset 17 | self.bottomOffset = bottomOffset 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Utils/LoadingView/Models/LoadingViewConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingViewConfig.swift 3 | // Utils 4 | // 5 | // Created by Никита Гагаринов on 18.08.2020. 6 | // Copyright © 2020 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public struct LoadingViewConfig { 12 | 13 | // MARK: - Properties 14 | 15 | /// Top offset for first block 16 | public let topOffset: CGFloat 17 | public let placeholderColor: UIColor 18 | public let shimmerColor: UIColor 19 | public let shimmerRatio: Double 20 | public let movingAnimationDuration: CFTimeInterval 21 | /// If true it will nicely reduce the alpha to the bottom 22 | public let needGradient: Bool 23 | /// Will repeat the last block to the end of the screen 24 | public let needRepeatLast: Bool 25 | 26 | // MARK: - Initialization 27 | 28 | public init(topOffset: CGFloat = 0, 29 | placeholderColor: UIColor, 30 | shimmerColor: UIColor = UIColor.white, 31 | shimmerRatio: Double = 0.5, 32 | movingAnimationDuration: CFTimeInterval = 1, 33 | needGradient: Bool = false, 34 | needRepeatLast: Bool = false) { 35 | self.topOffset = topOffset 36 | self.placeholderColor = placeholderColor 37 | self.shimmerColor = shimmerColor 38 | self.shimmerRatio = shimmerRatio 39 | self.movingAnimationDuration = movingAnimationDuration 40 | self.needGradient = needGradient 41 | self.needRepeatLast = needRepeatLast 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /Utils/MailSender/Modles/MailSenderPayload.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2020 Surf. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | /// Struct that holds mail properties 8 | public struct MailSenderPayload { 9 | 10 | // MARK: - Public Properties 11 | 12 | public let recipient: String 13 | public let subject: String? 14 | public let body: String 15 | 16 | // MARK: - Initializaion 17 | 18 | public init(recipient: String, 19 | subject: String?, 20 | body: String) { 21 | self.recipient = recipient 22 | self.subject = subject 23 | self.body = body 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Utils/MailSender/Protocols/MailSenderError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2020 Surf. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | /// Error that describes all kinds of mail util errors 8 | public enum MailSenderError: LocalizedError { 9 | case thereIsNoAbilityToSendMail 10 | case system(Error) 11 | } 12 | -------------------------------------------------------------------------------- /Utils/MailSender/Protocols/MailSenderErrorDisplaying.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2020 Surf. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | /// Protocol for displaying mail error 8 | public protocol MailSenderErrorDisplaying { 9 | 10 | /// Method for displaying mail error 11 | /// - Parameter error: mail util error 12 | func display(error: MailSenderError) 13 | 14 | } 15 | -------------------------------------------------------------------------------- /Utils/MailSender/Protocols/MailSenderPayloadProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2020 Surf. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | /// Protocol for getting mail payload 8 | public protocol MailSenderPayloadProvider { 9 | 10 | /// Methods for getting mail payload 11 | func getPayload() -> MailSenderPayload 12 | 13 | } 14 | -------------------------------------------------------------------------------- /Utils/MailSender/Protocols/MailSenderRouterHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2020 Surf. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | /// Protocol for supporting routing for UIViewController 8 | public protocol MailSenderRouterHelper { 9 | 10 | /// Method for presenting UIViewController 11 | /// - Parameter viewController: UIViewController object to present 12 | func present(_ viewController: UIViewController) 13 | 14 | /// Method for dismissing current UIViewController 15 | func dismiss() 16 | 17 | } 18 | -------------------------------------------------------------------------------- /Utils/MailSender/Support/InsideAppMailViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2020 Surf. All rights reserved. 3 | // 4 | 5 | import MessageUI 6 | 7 | /// ViewController to send mail that can be shown inside app 8 | final class InsideAppMailViewController: MFMailComposeViewController { 9 | 10 | // MARK: - Properties 11 | 12 | /// Closure that will be fired after successful of unsuccessful mail sending 13 | var onClose: ((Error?) -> Void)? 14 | 15 | // MARK: - UIViewController 16 | 17 | override func viewDidAppear(_ animated: Bool) { 18 | super.viewDidAppear(animated) 19 | mailComposeDelegate = self 20 | } 21 | 22 | } 23 | 24 | // MARK: - Methods 25 | 26 | extension InsideAppMailViewController { 27 | 28 | /// Method for configuring with payload 29 | /// - Parameters: 30 | /// - recipient: recipient of mail 31 | /// - subject: subject of mail 32 | /// - body: body of mail 33 | func setupInitialState(recipient: String, subject: String?, body: String) { 34 | setToRecipients([recipient]) 35 | setSubject(subject ?? "") 36 | setMessageBody(body, isHTML: false) 37 | } 38 | 39 | } 40 | 41 | // MARK: - MFMailComposeViewControllerDelegate 42 | 43 | extension InsideAppMailViewController: MFMailComposeViewControllerDelegate { 44 | 45 | func mailComposeController(_ controller: MFMailComposeViewController, 46 | didFinishWith result: MFMailComposeResult, 47 | error: Error?) { 48 | onClose?(error) 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Utils/MapRoutingService/MapRoutingLocationServiceInterface.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapRoutingLocationServiceInterface.swift 3 | // Utils 4 | // 5 | // Created by Александр Чаусов on 30.10.2021. 6 | // Copyright © 2021 Surf. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | 11 | /// Вспомогательный протокол для сервиса геолокации, 12 | /// необходимого для использования MapRoutingService 13 | public protocol MapRoutingLocationServiceInterface: AnyObject { 14 | /// Равно true, когда пользователь разрешил доступ к геопозиции 15 | var isLocationAccessAllowed: Bool { get } 16 | /// Равно true, когда пользователь разрешил использование точной геопозиции 17 | var isAllowedFullAccuracyLocation: Bool { get } 18 | /// Вовращает текущую геопозицию пользователя, если она известна, 19 | /// и nil во всех остальных случаях 20 | func getCurrentLocation(_ completion: @escaping ((CLLocationCoordinate2D?) -> Void)) 21 | } 22 | -------------------------------------------------------------------------------- /Utils/MapRoutingService/MapRoutingService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapRoutingService.swift 3 | // Utils 4 | // 5 | // Created by Александр Чаусов on 30.10.2021. 6 | // Copyright © 2021 Surf. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | import UIKit 11 | 12 | /// Сервис для построения маршрута в различных приложениях 13 | public final class MapRoutingService: MapRoutingServiceInterface { 14 | 15 | // MARK: - Private Propeties 16 | 17 | private let locationService: MapRoutingLocationServiceInterface 18 | 19 | // MARK: - Initialization 20 | 21 | public init(locationService: MapRoutingLocationServiceInterface) { 22 | self.locationService = locationService 23 | } 24 | 25 | // MARK: - MapRoutingServiceInterface 26 | 27 | public var availableApplications: [MapApplication] { 28 | let applications = MapApplication.allCases 29 | var available: [MapApplication] = [] 30 | for application in applications { 31 | guard 32 | let url = application.schemaUrl, 33 | UIApplication.shared.canOpenURL(url) 34 | else { 35 | continue 36 | } 37 | available.append(application) 38 | } 39 | 40 | // убираем ссылку на гугл карты в браузере, если есть приложение гугл карт 41 | if available.contains(.googleApp), let googleUrlIndex = available.firstIndex(of: .googleUrl) { 42 | available.remove(at: googleUrlIndex) 43 | } 44 | return available 45 | } 46 | 47 | public func buildRoute(to destination: CLLocationCoordinate2D, 48 | in application: MapApplication, 49 | onComplete: (() -> Void)?) { 50 | switch (locationService.isLocationAccessAllowed, locationService.isAllowedFullAccuracyLocation) { 51 | case (true, true): 52 | tryBuildRouteForCurrentLocation(to: destination, 53 | in: application, 54 | onComplete: onComplete) 55 | default: 56 | let url = application.routeUrl(startCoordinate: nil, 57 | endCoordinate: destination) 58 | onComplete?() 59 | open(url: url) 60 | } 61 | } 62 | 63 | } 64 | 65 | // MARK: - Private Methods 66 | 67 | private extension MapRoutingService { 68 | 69 | func tryBuildRouteForCurrentLocation(to destination: CLLocationCoordinate2D, 70 | in application: MapApplication, 71 | onComplete: (() -> Void)?) { 72 | locationService.getCurrentLocation { [weak self] userLocation in 73 | let url = application.routeUrl(startCoordinate: userLocation, 74 | endCoordinate: destination) 75 | onComplete?() 76 | self?.open(url: url) 77 | } 78 | } 79 | 80 | func open(url: URL?) { 81 | guard 82 | let url = url, 83 | UIApplication.shared.canOpenURL(url) 84 | else { 85 | return 86 | } 87 | UIApplication.shared.open(url, options: [:], completionHandler: nil) 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /Utils/MapRoutingService/MapRoutingServiceInterface.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapRoutingServiceInterface.swift 3 | // Utils 4 | // 5 | // Created by Александр Чаусов on 30.10.2021. 6 | // Copyright © 2021 Surf. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | 11 | /// Протокол для сервиса построения маршрута в различных приложениях 12 | public protocol MapRoutingServiceInterface { 13 | /// Возвращает список установленных приложений, через которые можно построить маршрут 14 | var availableApplications: [MapApplication] { get } 15 | /// Используется для построения маршрута в определенном приложении 16 | /// 17 | /// Если есть доступ к геопозиции и выбрана точная геопозиция, 18 | /// в приложение передаются координаты начала и конца маршрута. 19 | /// Если доступа нет или геопозиция не точная, 20 | /// то передается только координаты конца маршрута. 21 | func buildRoute(to destination: CLLocationCoordinate2D, 22 | in application: MapApplication, 23 | onComplete: (() -> Void)?) 24 | } 25 | -------------------------------------------------------------------------------- /Utils/SecurityService/Crypto/CryptoBox/CryptoBox.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CryptoBox.swift 3 | // Utils 4 | // 5 | // Created by Никита Гагаринов on 20.08.2020. 6 | // Copyright © 2020 Surf. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Interface for entity which can encrypt data and decrypt it. 12 | /// Also specfic implementation must saves ecnrypted data in some storage 13 | /// 14 | /// This interface means that single instants of box can work only with one piece of data and only with one key 15 | /// so ypu can't ecnrypr 2 different pieces of data, because each `encrypt` would rewrite previous. 16 | public protocol CryptoBox { 17 | /// Make encrypting operation on data with some auth item 18 | /// 19 | /// - Parameters: 20 | /// - data: Cryptotext 21 | /// - auth: Key whics was used to encrypt `data` 22 | func encrypt(data: String, auth: String) throws 23 | 24 | /// Make decryption operation on previously saved encrypted data 25 | /// 26 | /// - Parameters: 27 | /// - authL Key whics was used to encrypt saved data 28 | func decrypt(auth: String) throws -> String 29 | } 30 | -------------------------------------------------------------------------------- /Utils/SecurityService/Crypto/CryptoBox/CryptoBoxCommonError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CryptoBoxCommonError.swift 3 | // Utils 4 | // 5 | // Created by Никита Гагаринов on 20.08.2020. 6 | // Copyright © 2020 Surf. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum CryptoBoxCommonError: Error { 12 | case cantConvertStringToData 13 | case cantConvertDataToString 14 | } 15 | -------------------------------------------------------------------------------- /Utils/SecurityService/Crypto/CryptoBox/HackWrapperCryptoBox.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HackWrapperCryptoBox.swift 3 | // Utils 4 | // 5 | // Created by Никита Гагаринов on 20.08.2020. 6 | // Copyright © 2020 Surf. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Hack-box wrapper 12 | /// It is used to replace the cryptobox on the fly. 13 | public struct HackWrapperCryptoBox: CryptoBox { 14 | 15 | private let box: PinHackCryptoBox 16 | 17 | public init(box: PinHackCryptoBox) { 18 | self.box = box 19 | } 20 | 21 | public func encrypt(data: String, auth: String) throws { 22 | try self.box.encrypt(data: data) 23 | } 24 | 25 | public func decrypt(auth: String) throws -> String { 26 | try self.box.decrypt() 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /Utils/SecurityService/Crypto/CryptoBox/PinHackCryptoBox.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PinHackCryptoBox.swift 3 | // Utils 4 | // 5 | // Created by Никита Гагаринов on 21.08.2020. 6 | // Copyright © 2020 Surf. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Crypto-box used for refreshing security data 12 | public struct PinHackCryptoBox { 13 | 14 | private let secureStore: () -> SecureStore 15 | private let cryptoService: SymmetricCryptoService 16 | 17 | private let ivKey: String 18 | private let dataKey: String 19 | private let saltKey: String 20 | private let hashKey: String 21 | 22 | public init(secureStore: @escaping () -> SecureStore, 23 | cryptoService: SymmetricCryptoService, 24 | ivKey: String, 25 | dataKey: String, 26 | saltKey: String, 27 | hashKey: String) { 28 | 29 | self.secureStore = secureStore 30 | self.cryptoService = cryptoService 31 | 32 | self.ivKey = ivKey 33 | self.dataKey = dataKey 34 | self.saltKey = saltKey 35 | self.hashKey = hashKey 36 | } 37 | 38 | public func encrypt(data: String) throws { 39 | let hash: String = try self.secureStore().load(by: self.hashKey) 40 | let iv: String = try self.secureStore().load(by: self.ivKey) 41 | 42 | guard let bytes = data.data(using: .utf8, allowLossyConversion: false) else { 43 | throw CryptoBoxCommonError.cantConvertStringToData 44 | } 45 | 46 | let plain = try self.cryptoService.encrypt(data: bytes.bytes, key: hash, iv: iv) 47 | 48 | try self.secureStore().save(data: Data(plain).base64EncodedString(), by: self.dataKey) 49 | } 50 | 51 | public func decrypt() throws -> String { 52 | let data: String = try self.secureStore().load(by: self.dataKey) 53 | let hash: String = try self.secureStore().load(by: self.hashKey) 54 | let iv: String = try self.secureStore().load(by: self.ivKey) 55 | 56 | guard let decoded = Data(base64Encoded: data) else { 57 | throw CryptoBoxCommonError.cantConvertStringToData 58 | } 59 | 60 | let plain = try self.cryptoService.decrypt(data: decoded.bytes, key: hash, iv: iv) 61 | 62 | guard let str = String(data: Data(plain), encoding: .utf8) else { 63 | throw CryptoBoxCommonError.cantConvertDataToString 64 | } 65 | 66 | return str 67 | } 68 | 69 | } 70 | 71 | public extension PinHackCryptoBox { 72 | func erase() -> CryptoBox { 73 | return HackWrapperCryptoBox(box: self) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Utils/SecurityService/Crypto/CryptoService/BlowfishCryptoService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlowfishCryptoService.swift 3 | // Utils 4 | // 5 | // Created by Никита Гагаринов on 20.08.2020. 6 | // Copyright © 2020 Surf. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CryptoSwift 11 | 12 | public final class BlowfishCryptoService: SymmetricCryptoService { 13 | 14 | public init() {} 15 | 16 | public func encrypt(data: [UInt8], key: String, iv: String) throws -> [UInt8] { 17 | let alg = try Blowfish(key: key, iv: iv, padding: .pkcs7) 18 | return try alg.encrypt(data) 19 | } 20 | 21 | public func decrypt(data: [UInt8], key: String, iv: String) throws -> [UInt8] { 22 | let alg = try Blowfish(key: key, iv: iv, padding: .pkcs7) 23 | return try alg.decrypt(data) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Utils/SecurityService/Crypto/CryptoService/SymmetricCryptoService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SymmetricCryptoService.swift 3 | // Utils 4 | // 5 | // Created by Никита Гагаринов on 20.08.2020. 6 | // Copyright © 2020 Surf. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol SymmetricCryptoService { 12 | func encrypt(data: [UInt8], key: String, iv: String) throws -> [UInt8] 13 | func decrypt(data: [UInt8], key: String, iv: String) throws -> [UInt8] 14 | } 15 | -------------------------------------------------------------------------------- /Utils/SecurityService/Crypto/Hash/HashProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HashProvider.swift 3 | // Utils 4 | // 5 | // Created by Никита Гагаринов on 20.08.2020. 6 | // Copyright © 2020 Surf. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol HashProvider { 12 | func hash(data: [UInt8]) throws -> [UInt8] 13 | } 14 | -------------------------------------------------------------------------------- /Utils/SecurityService/Crypto/Hash/SHA3.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SHA3.swift 3 | // Utils 4 | // 5 | // Created by Никита Гагаринов on 20.08.2020. 6 | // Copyright © 2020 Surf. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CryptoSwift 11 | 12 | extension SHA3: HashProvider { 13 | public func hash(data: [UInt8]) throws -> [UInt8] { 14 | self.calculate(for: data) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Utils/SecurityService/Crypto/Utils/CryptoCommon.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CryptoCommon.swift 3 | // Utils 4 | // 5 | // Created by Никита Гагаринов on 20.08.2020. 6 | // Copyright © 2020 Surf. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum CryptoCommon { 12 | public static func random(count: Int) -> [UInt8] { 13 | var bytes = [UInt8](repeating: 0, count: count) 14 | _ = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) 15 | return bytes 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Utils/SecurityService/Store/GenericPasswordQueryable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GenericPasswordQueryable.swift 3 | // Utils 4 | // 5 | // Created by Никита Гагаринов on 20.08.2020. 6 | // Copyright © 2020 Surf. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Security 11 | 12 | // MARK: - SecureStoreQueryable 13 | 14 | /// Keychain request interface 15 | public protocol SecureStoreQueryable { 16 | var query: [String: Any] { get } 17 | } 18 | 19 | // MARK: - GenericPasswordQueryable 20 | 21 | public struct GenericPasswordQueryable { 22 | 23 | let service: String 24 | let accessGroup: String? 25 | 26 | public init(service: String, accessGroup: String? = nil) { 27 | self.service = service 28 | self.accessGroup = accessGroup 29 | } 30 | } 31 | 32 | // MARK: - GenericPasswordQueryable.SecureStoreQueryable 33 | 34 | extension GenericPasswordQueryable: SecureStoreQueryable { 35 | public var query: [String: Any] { 36 | var query: [String: Any] = [ 37 | kSecClass.plain: kSecClassGenericPassword, 38 | kSecAttrService.plain: service 39 | ] 40 | 41 | #if !targetEnvironment(simulator) 42 | if let accessGroup = accessGroup { 43 | query[kSecAttrAccessGroup.plain] = accessGroup 44 | } 45 | #endif 46 | return query 47 | } 48 | } 49 | 50 | // MARK: - Support 51 | 52 | extension CFString { 53 | var plain: String { 54 | return String(self) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Utils/SecurityService/Store/InMemorySecureStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InMemorySecureStore.swift 3 | // Utils 4 | // 5 | // Created by Никита Гагаринов on 20.08.2020. 6 | // Copyright © 2020 Surf. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class InMemorySecureStore: SecureStore { 12 | 13 | public init() {} 14 | 15 | public var memory = [String: String]() 16 | 17 | public func remove(by key: String) throws { 18 | _ = self.memory.removeValue(forKey: key) 19 | } 20 | 21 | public func removeAll() throws { 22 | self.memory = [String: String]() 23 | } 24 | 25 | public func save(data: String, by key: String) throws { 26 | self.memory[key] = data 27 | } 28 | 29 | public func load(by key: String) throws -> String { 30 | guard let res = self.memory[key] else { 31 | throw NSError(domain: "err.mem", code: -1, userInfo: nil) 32 | } 33 | return res 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Utils/SecurityService/Store/SecureStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SecureStore.swift 3 | // Utils 4 | // 5 | // Created by Никита Гагаринов on 20.08.2020. 6 | // Copyright © 2020 Surf. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Abstracts the entire protected store. 12 | /// 13 | /// - SeeAlso: SecureDataProvider 14 | public protocol SecureStore: SecureDataProvider { 15 | func remove(by key: String) throws 16 | /// Clears all storage from application data 17 | func removeAll() throws 18 | } 19 | 20 | /// Can save and read data to / from secure storage 21 | public protocol SecureDataProvider { 22 | func save(data: String, by key: String) throws 23 | func load(by key: String) throws -> String 24 | } 25 | -------------------------------------------------------------------------------- /Utils/SecurityService/Store/SecureStoreError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SecureStoreError.swift 3 | // Utils 4 | // 5 | // Created by Никита Гагаринов on 20.08.2020. 6 | // Copyright © 2020 Surf. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum SecureStoreError: Error { 12 | case stringToDataConversionError 13 | case dataToStringConversionError 14 | case unhandledError(message: String) 15 | } 16 | -------------------------------------------------------------------------------- /Utils/SettingsRouter/SettingsRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsRouter.swift 3 | // Utils 4 | // 5 | // Created by Александр Чаусов on 02/01/2019. 6 | // Copyright © 2019 Surf. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | /// Utils for opening various settings screens from the application 13 | public final class SettingsRouter { 14 | 15 | // MARK: - Constants 16 | 17 | private enum Constants { 18 | static let appSettingsUrl = UIApplication.openSettingsURLString 19 | } 20 | 21 | // MARK: - Public Methods 22 | 23 | public static func openAppSettings() { 24 | guard let url = URL(string: Constants.appSettingsUrl) else { 25 | return 26 | } 27 | openExpectedURL(url) 28 | } 29 | } 30 | 31 | // MARK: - Private Methods 32 | 33 | private extension SettingsRouter { 34 | 35 | static func openExpectedURL(_ url: URL) { 36 | guard UIApplication.shared.canOpenURL(url) else { 37 | return 38 | } 39 | UIApplication.shared.open(url, options: [:], completionHandler: nil) 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Utils/UIDevice/Support/Size.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Size.swift 3 | // Device 4 | // 5 | // Created by Lucas Ortis on 30/10/2015. 6 | // Copyright © 2015 Ekhoo. All rights reserved. 7 | // 8 | 9 | public enum Size: Int, Comparable { 10 | 11 | case unknownSize = 0 12 | #if os(iOS) 13 | /// iPhone 2G, 3G, 3GS, 4, 4s, iPod Touch 4th gen. 14 | case screen3_5Inch 15 | /// iPhone 5, 5s, 5c, SE, iPod Touch 5-7th gen. 16 | case screen4Inch 17 | /// iPhone 6, 6s, 7, 8, SE 2nd gen. 18 | case screen4_7Inch 19 | /// iPhone 12 Mini 20 | case screen5_4Inch 21 | /// iPhone 6+, 6s+, 7+, 8+ 22 | case screen5_5Inch 23 | /// iPhone X, Xs, 11 Pro 24 | case screen5_8Inch 25 | /// iPhone Xr, 11, 12, 12 Pro, 13, 13 Pro 26 | case screen6_1Inch 27 | /// iPhone Xs Max, 11 Pro Max 28 | case screen6_5Inch 29 | /// iPhone 12 Pro Max, 13 Pro Max 30 | case screen6_7Inch 31 | /// iPad Mini, iPad Mini 2, iPad Mini 3, iPad Mini 4 32 | case screen7_9Inch 33 | /// iPad mini 6 34 | case screen8_3Inch 35 | /// iPad, iPad 2, iPad 3, iPad 4, iPad 5, iPad 6, iPad Air, iPad Air 2, iPad Pro 9.7″ 36 | case screen9_7Inch 37 | /// iPad 7, iPad (10.2-inch) 38 | case screen10_2Inch 39 | /// iPad Air 3, iPad Pro (10.5-inch) 40 | case screen10_5Inch 41 | /// iPad Air 4, iPad Pro 12.9″ 42 | case screen10_9Inch 43 | /// iPad Pro 11″ 44 | case screen11Inch 45 | /// iPad Pro 12.9″ 46 | case screen12_9Inch 47 | #elseif os(OSX) 48 | case screen11Inch 49 | case screen12Inch 50 | case screen13Inch 51 | case screen14Inch 52 | case screen15Inch 53 | case screen16Inch 54 | case screen17Inch 55 | case screen20Inch 56 | case screen21_5Inch 57 | case screen24Inch 58 | case screen27Inch 59 | #endif 60 | 61 | public static func ==(lhs: Size, rhs: Size) -> Bool { 62 | return lhs.rawValue == rhs.rawValue 63 | } 64 | 65 | public static func <(lhs: Size, rhs: Size) -> Bool { 66 | return lhs.rawValue < rhs.rawValue 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Utils/UIDevice/Support/Type.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Type.swift 3 | // Device 4 | // 5 | // Created by Stefan Jansen on 08-12-15. 6 | // Copyright © 2015 Ekhoo. All rights reserved. 7 | // 8 | 9 | public enum DeviceType: String { 10 | #if os(iOS) 11 | case iPhone 12 | case iPad 13 | case iPod 14 | case simulator 15 | #elseif os(OSX) 16 | case iMac 17 | case macMini 18 | case macPro 19 | case macBook 20 | case macBookAir 21 | case macBookPro 22 | case xserve 23 | #endif 24 | case unknown 25 | } 26 | -------------------------------------------------------------------------------- /Utils/UIDevice/Support/Version.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Version.swift 3 | // Device 4 | // 5 | // Created by Lucas Ortis on 30/10/2015. 6 | // Copyright © 2015 Ekhoo. All rights reserved. 7 | // 8 | 9 | public enum Version: String { 10 | /*** iPhone ***/ 11 | case iPhone2G 12 | case iPhone3G 13 | case iPhone3GS 14 | case iPhone4 15 | case iPhone4S 16 | case iPhone5 17 | case iPhone5C 18 | case iPhone5S 19 | case iPhone6 20 | case iPhone6Plus 21 | case iPhone6S 22 | case iPhone6SPlus 23 | case iPhoneSE 24 | case iPhone7 25 | case iPhone7Plus 26 | case iPhone8 27 | case iPhone8Plus 28 | case iPhoneX 29 | case iPhoneXS 30 | case iPhoneXS_Max 31 | case iPhoneXR 32 | case iPhone11 33 | case iPhone11Pro 34 | case iPhone11Pro_Max 35 | case iPhoneSE2 36 | case iPhone12Mini 37 | case iPhone12 38 | case iPhone12Pro 39 | case iPhone12Pro_Max 40 | case iPhone13Mini 41 | case iPhone13 42 | case iPhone13Pro 43 | case iPhone13Pro_Max 44 | 45 | /*** iPad ***/ 46 | case iPad1 47 | case iPad2 48 | case iPad3 49 | case iPad4 50 | case iPad5 51 | case iPad6 52 | case iPad7 53 | case iPad8 54 | case iPad9 55 | case iPadAir 56 | case iPadAir2 57 | case iPadAir3 58 | case iPadAir4 59 | case iPadMini 60 | case iPadMini2 61 | case iPadMini3 62 | case iPadMini4 63 | case iPadMini5 64 | case iPadMini6 65 | 66 | /*** iPadPro ***/ 67 | case iPadPro9_7Inch 68 | case iPadPro12_9Inch 69 | case iPadPro10_5Inch 70 | case iPadPro12_9Inch2 71 | case iPadPro11_0Inch 72 | case iPadPro12_9Inch3 73 | case iPadPro11_0Inch2 74 | case iPadPro11_0Inch3 75 | case iPadPro12_9Inch4 76 | case iPadPro12_9Inch5 77 | 78 | /*** iPod ***/ 79 | case iPodTouch1Gen 80 | case iPodTouch2Gen 81 | case iPodTouch3Gen 82 | case iPodTouch4Gen 83 | case iPodTouch5Gen 84 | case iPodTouch6Gen 85 | case iPodTouch7Gen 86 | 87 | /*** simulator ***/ 88 | case simulator 89 | 90 | /*** unknown ***/ 91 | case unknown 92 | } 93 | -------------------------------------------------------------------------------- /Utils/UIDevice/Support/macOS/DeviceMacOS.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeviceMacOS.swift 3 | // Device 4 | // 5 | // Created by Tom Baranes on 16/08/16. 6 | // Copyright © 2016 Ekhoo. All rights reserved. 7 | // 8 | 9 | #if os(OSX) 10 | import Cocoa 11 | 12 | public class Device { 13 | 14 | static private func getVersionCode() -> String { 15 | var size: Int = 0 16 | sysctlbyname("hw.model", nil, &size, nil, 0) 17 | var model = [CChar](repeating: 0, count: Int(size)) 18 | sysctlbyname("hw.model", &model, &size, nil, 0) 19 | return String(validatingUTF8: model) ?? "" 20 | } 21 | 22 | static private func getType(code: String) -> DeviceType { 23 | if code.hasPrefix("MacPro") { 24 | return .macPro 25 | } else if code.hasPrefix("iMac") { 26 | return .iMac 27 | } else if code.hasPrefix("MacBookPro") { 28 | return .macBookPro 29 | } else if code.hasPrefix("MacBookAir") { 30 | return .macBookAir 31 | } else if code.hasPrefix("MacBook") { 32 | return .macBook 33 | } else if code.hasPrefix("MacMini") { 34 | return .macMini 35 | } else if code.hasPrefix("Xserve") { 36 | return .xserve 37 | } 38 | return .unknown 39 | } 40 | 41 | private static func sizeInInches() -> CGFloat { 42 | let screen = NSScreen.main 43 | let description = screen?.deviceDescription 44 | let displayPhysicalSize = CGDisplayScreenSize( 45 | description?[NSDeviceDescriptionKey(rawValue: "NSScreenNumber")] as? CGDirectDisplayID ?? 0 46 | ) 47 | return floor(sqrt(pow(displayPhysicalSize.width, 2) + pow(displayPhysicalSize.height, 2)) * 0.0393701) 48 | } 49 | 50 | // swiftlint:disable cyclomatic_complexity 51 | static public func size() -> Size { 52 | let sizeInInches = Device.sizeInInches() 53 | 54 | switch sizeInInches { 55 | case 11: 56 | return Size.screen11Inch 57 | case 12: 58 | return Size.screen12Inch 59 | case 13: 60 | return Size.screen13Inch 61 | case 14: 62 | return Size.screen14Inch 63 | case 15: 64 | return Size.screen15Inch 65 | case 16: 66 | return Size.screen16Inch 67 | case 17: 68 | return Size.screen17Inch 69 | case 20: 70 | return Size.screen20Inch 71 | case 21: 72 | return Size.screen21_5Inch 73 | case 24: 74 | return Size.screen24Inch 75 | case 27: 76 | return Size.screen27Inch 77 | default: 78 | return Size.unknownSize 79 | } 80 | } 81 | // swiftlint:enable cyclomatic_complexity 82 | 83 | static public func version() -> String { 84 | return String(describing: Device.type()) + " " + String(describing: Device.sizeInInches()) + "-inch" 85 | } 86 | 87 | static public func type() -> DeviceType { 88 | let versionName = Device.getVersionCode() 89 | return Device.getType(code: versionName) 90 | } 91 | 92 | } 93 | #endif 94 | -------------------------------------------------------------------------------- /Utils/UIDevice/UIDevice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIDevice.swift 3 | // Utils 4 | // 5 | // Created by Vladislav Krupenko on 03/01/2020. 6 | // Copyright © 2020 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public extension UIDevice { 12 | 13 | /// Returns 'true' if device have 4inch diagonal screen or smaller (5, 5s, 5c, SE) 14 | static var isSmallPhone: Bool { 15 | let smallSizes: [Size] = [ 16 | .screen3_5Inch, 17 | .screen4Inch 18 | ] 19 | return smallSizes.contains(Device.size()) 20 | } 21 | 22 | /// Returns `true` if device have screen size of X version 23 | static var isXPhone: Bool { 24 | let xSizes: [Size] = [ 25 | .screen5_8Inch, 26 | .screen6_1Inch, 27 | .screen6_5Inch 28 | ] 29 | return xSizes.contains(Device.size()) 30 | } 31 | 32 | /// Returns 'true' if device have 4.7inch and 5.5inch diagonal screen (6/7/8 normal or '+') 33 | static var isNormalPhone: Bool { 34 | let normalSizes: [Size] = [ 35 | .screen4_7Inch, 36 | .screen5_5Inch 37 | ] 38 | return normalSizes.contains(Device.size()) 39 | } 40 | 41 | /// Returns 'true' if current device is iPad 42 | static var isPad: Bool { 43 | let padSizes: [Size] = [ 44 | .screen7_9Inch, 45 | .screen8_3Inch, 46 | .screen9_7Inch, 47 | .screen10_2Inch, 48 | .screen10_5Inch, 49 | .screen10_9Inch, 50 | .screen11Inch, 51 | .screen12_9Inch 52 | ] 53 | return padSizes.contains(Device.size()) 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /Utils/UINavigationController/UINavigationController+AdvancedNavigationStackManagement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UINavigationController+AdvancedNavigationStackManagement.swift 3 | // Utils 4 | // 5 | // Created by Александр Чаусов on 02/01/2019. 6 | // Copyright © 2019 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public extension UINavigationController { 12 | 13 | /// Method for calling pushViewController(_, animated) method with completion closure 14 | func pushViewController(_ viewController: UIViewController, animated: Bool, completion: @escaping () -> Void) { 15 | pushViewController(viewController, animated: animated) 16 | guard animated, let coordinator = transitionCoordinator else { 17 | DispatchQueue.main.async { 18 | completion() 19 | } 20 | return 21 | } 22 | coordinator.animate(alongsideTransition: nil) { _ in 23 | completion() 24 | } 25 | } 26 | 27 | /// Method for calling popViewController(animated) method with completion closure 28 | func popViewController(animated: Bool, completion: @escaping () -> Void) { 29 | popViewController(animated: animated) 30 | guard animated, let coordinator = transitionCoordinator else { 31 | DispatchQueue.main.async { 32 | completion() 33 | } 34 | return 35 | } 36 | coordinator.animate(alongsideTransition: nil) { _ in 37 | completion() 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /Utils/UIStyle/AnyStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyStyle.swift 3 | // Utils 4 | // 5 | // Created by Vladislav Krupenko on 03/01/2020. 6 | // Copyright © 2020 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public struct AnyStyle: UIStyleProtocol { 12 | 13 | // MARK: - Public Properties 14 | 15 | public let box: BaseBox 16 | 17 | // MARK: - Initialization 18 | 19 | public init(style: T) where T: UIStyleProtocol, T.Control == Control { 20 | self.box = StyleBox(style: style) 21 | } 22 | 23 | // MARK: - Public Methods 24 | 25 | public func apply(for control: Control) { 26 | self.box.apply(for: control) 27 | } 28 | 29 | } 30 | 31 | public class BaseBox: UIStyleProtocol { 32 | 33 | // MARK: - Initialization 34 | 35 | public init() {} 36 | 37 | // MARK: - Public Methods 38 | 39 | public func apply(for control: Control) { 40 | fatalError("You can't use \(self.self)") 41 | } 42 | 43 | } 44 | 45 | public class StyleBox: BaseBox { 46 | 47 | // MARK: - Public Properties 48 | 49 | let style: Style 50 | 51 | // MARK: - Initialization 52 | 53 | public init(style: Style) { 54 | self.style = style 55 | super.init() 56 | } 57 | 58 | // MARK: - BaseBox 59 | 60 | override public func apply(for control: Control) { 61 | self.style.apply(for: control) 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /Utils/UIStyle/UIStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIStyle.swift 3 | // Utils 4 | // 5 | // Created by Vladislav Krupenko on 03/01/2020. 6 | // Copyright © 2020 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol UIStyleProtocol { 12 | associatedtype Control: UIView 13 | func apply(for: Control) 14 | } 15 | 16 | open class UIStyle: UIStyleProtocol { 17 | public init() {} 18 | open func apply(for: Control) {} 19 | } 20 | 21 | public protocol AttributableStyle { 22 | func attributes() -> [NSAttributedString.Key: Any] 23 | } 24 | -------------------------------------------------------------------------------- /Utils/UIView/UIView+BlurBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+BlurBuilder.swift 3 | // Utils 4 | // 5 | // Created by Александр Чаусов on 10/01/2019. 6 | // Copyright © 2019 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIView { 12 | 13 | /// Method allows you to add blur with custom color and style on needed view 14 | /// 15 | /// Example of usage: 16 | /// ``` 17 | /// bluredView.addBlur(color: UIColor.white.withAlphaComponent(0.1), style: .light) 18 | /// ``` 19 | /// 20 | /// - Parameters: 21 | /// - color: Color of the blur effect 22 | /// - style: Style of the blur effect, default value is .dark 23 | public func addBlur(color: UIColor, style: UIBlurEffect.Style = .dark) { 24 | self.backgroundColor = UIColor.clear 25 | 26 | let blurEffect = UIBlurEffect(style: style) 27 | let blurEffectView = UIVisualEffectView(effect: blurEffect) 28 | blurEffectView.frame = self.bounds 29 | blurEffectView.contentView.backgroundColor = color 30 | blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 31 | 32 | self.insertSubview(blurEffectView, at: 0) 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Utils/UIView/UIView+Masking.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Masking.swift 3 | // Utils 4 | // 5 | // Created by Artemii Shabanov on 21/01/2019. 6 | // Copyright © 2019 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIView { 12 | 13 | /// Apply given views as masks 14 | /// 15 | /// - Parameter views: Views to apply as mask. 16 | /// ## Note: The view calling this function must have all the views in the given array as subviews. 17 | public func setMaskingViews(_ views: [UIView]) { 18 | 19 | let mutablePath = CGMutablePath() 20 | 21 | //Append path for each subview 22 | views.forEach { (view) in 23 | guard self.subviews.contains(view) else { 24 | fatalError("View:\(view) is not a subView of \(self). Therefore, it cannot be a masking view.") 25 | } 26 | let path = UIBezierPath(roundedRect: view.frame, cornerRadius: view.layer.cornerRadius) 27 | mutablePath.addPath(path.cgPath) 28 | } 29 | 30 | let maskLayer = CAShapeLayer() 31 | maskLayer.path = mutablePath 32 | 33 | self.layer.mask = maskLayer 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Utils/UIView/UIView+XibSetup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+XibSetup.swift 3 | // Utils 4 | // 5 | // Created by Anton Dryakhlykh on 15.10.2019. 6 | // Copyright © 2019 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public extension UIView { 12 | func xibSetup(bundle: Bundle) { 13 | let view = loadFromNib(bundle: bundle) 14 | addSubview(view) 15 | stretch(view: view) 16 | } 17 | 18 | func loadFromNib(bundle: Bundle) -> T { 19 | let selfType = type(of: self) 20 | let nibName = String(describing: selfType) 21 | let nib = UINib(nibName: nibName, bundle: bundle) 22 | 23 | guard let view = nib.instantiate(withOwner: self, options: nil).first as? T else { 24 | return T() 25 | } 26 | 27 | return view 28 | } 29 | 30 | static func loadFromNib(bundle: Bundle) -> T { 31 | let nibName = String(describing: self) 32 | let nib = UINib(nibName: nibName, bundle: bundle) 33 | 34 | guard let view = nib.instantiate(withOwner: self, options: nil).first as? T else { 35 | return T() 36 | } 37 | 38 | return view 39 | } 40 | 41 | func xibSetup() { 42 | let view = loadFromNib() 43 | addSubview(view) 44 | stretch(view: view) 45 | } 46 | 47 | func loadFromNib() -> T { 48 | let bundle = Bundle(for: type(of: self)) 49 | return loadFromNib(bundle: bundle) 50 | } 51 | 52 | static func loadFromNib() -> T { 53 | let bundle = Bundle(for: self) 54 | return loadFromNib(bundle: bundle) 55 | } 56 | 57 | func stretch(view: UIView) { 58 | view.translatesAutoresizingMaskIntoConstraints = false 59 | NSLayoutConstraint.activate([ 60 | view.topAnchor.constraint(equalTo: topAnchor), 61 | view.leadingAnchor.constraint(equalTo: leadingAnchor), 62 | view.trailingAnchor.constraint(equalTo: trailingAnchor), 63 | view.bottomAnchor.constraint(equalTo: bottomAnchor) 64 | ]) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Utils/Utils.h: -------------------------------------------------------------------------------- 1 | // 2 | // Utils.h 3 | // Utils 4 | // 5 | // Created by Alexander Kravchenkov on 09.08.2018. 6 | // Copyright © 2018 Surf. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for Utils. 12 | FOUNDATION_EXPORT double UtilsVersionNumber; 13 | 14 | //! Project version string for Utils. 15 | FOUNDATION_EXPORT const unsigned char UtilsVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Utils/VibrationFeedbackManager/UIDevice+feedbackType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIDevice+feedbackType.swift 3 | // Utils 4 | // 5 | // Created by Pavel Marinchenko on 9/11/18. 6 | // Copyright © 2018 Surf. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | extension UIDevice { 13 | enum FeedbackType: Int { 14 | case base = 0 15 | case taptic 16 | case haptic 17 | } 18 | 19 | var feedbackType: FeedbackType { 20 | if let fsl = UIDevice.current.value(forKey: "_feedbackSupportLevel") as? Int, 21 | let feedbackSupportLevel = FeedbackType(rawValue: Int(fsl)) { 22 | return feedbackSupportLevel 23 | } 24 | return .base 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Utils/VibrationFeedbackManager/UIDevice+hasHapticFeedback.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIDevice+hasHapticFeedback.swift 3 | // Utils 4 | // 5 | // Created by Pavel Marinchenko on 9/10/18. 6 | // Copyright © 2018 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIDevice { 12 | // haptic feedback support guarantees that device supports taptic engine too. 13 | var hasHapticFeedback: Bool { 14 | return feedbackType == .haptic 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Utils/VibrationFeedbackManager/UIDevice+hasTapticEngine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIDevice+hasTapticEngine.swift 3 | // Utils 4 | // 5 | // Created by Pavel Marinchenko on 9/10/18. 6 | // Copyright © 2018 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIDevice { 12 | var hasTapticEngine: Bool { 13 | return feedbackType == .taptic || hasHapticFeedback 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Utils/WordDeclinationSelector/WordDeclinationSelector.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WordDeclinationConstructor.swift 3 | // Utils 4 | // 5 | // Created by Александр Чаусов on 02/01/2019. 6 | // Copyright © 2019 Surf. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// An instance of this class is used to generate the correct word declination 12 | /// for a number and contains all the available declensions. 13 | public final class WordDeclinations { 14 | let singularNominative: String 15 | let genetiveSingular: String 16 | let genetivePlural: String 17 | 18 | /// Initialization of the class instance. You have to provide three word declensions. 19 | /// - Parameters: 20 | /// - singularNominative: The word nominative case in the singular, for example "День" 21 | /// - genetiveSingular: The word genitive in the singular, for example "Дня" 22 | /// - genetivePlural: The word genitive in the plural, for example "Дней" 23 | public init(_ singularNominative: String, _ genetiveSingular: String, _ genetivePlural: String) { 24 | self.singularNominative = singularNominative 25 | self.genetiveSingular = genetiveSingular 26 | self.genetivePlural = genetivePlural 27 | } 28 | } 29 | 30 | /// Class for select correct word declension for the passed number 31 | public final class WordDeclinationSelector { 32 | 33 | /// This method is used to get correct declension of word for some number 34 | /// 35 | /// Example of usage: 36 | /// ``` 37 | /// let correctForm = WordDeclinationSelector.declineWord(for: 6, from: WordDeclensions("день", "дня", "дней")) 38 | /// ``` 39 | /// 40 | /// - Parameters: 41 | /// - number: The number to which you want to find the declination. 42 | /// - declensions: An instance of the word declinations. 43 | /// - Returns: Returns a word from the word declensions in the right form. 44 | public static func declineWord(for number: Int, from declensions: WordDeclinations) -> String { 45 | let ending = number % 100 46 | if (ending >= 11 && ending <= 19) { 47 | return declensions.genetivePlural 48 | } 49 | switch (ending % 10) { 50 | case 1: 51 | return declensions.singularNominative 52 | case 2, 3, 4: 53 | return declensions.genetiveSingular 54 | default: 55 | return declensions.genetivePlural 56 | } 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /UtilsExample/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | whitelist_rules: 2 | - attributes 3 | - class_delegate_protocol 4 | - closing_brace 5 | - closure_end_indentation 6 | - closure_spacing 7 | - colon 8 | - comma 9 | - cyclomatic_complexity 10 | - empty_count 11 | - empty_parameters 12 | - empty_parentheses_with_trailing_closure 13 | - explicit_init 14 | - explicit_self 15 | - first_where 16 | - force_cast 17 | - force_try 18 | - force_unwrapping 19 | - implicitly_unwrapped_optional 20 | - implicit_getter 21 | - leading_whitespace 22 | - legacy_cggeometry_functions 23 | - legacy_constant 24 | - legacy_constructor 25 | - legacy_nsgeometry_functions 26 | - mark 27 | - line_length 28 | - opening_brace 29 | - operator_usage_whitespace 30 | - redundant_discardable_let 31 | - redundant_nil_coalescing 32 | - redundant_void_return 33 | - return_arrow_whitespace 34 | - shorthand_operator 35 | - statement_position 36 | - syntactic_sugar 37 | - todo 38 | - trailing_comma 39 | - trailing_newline 40 | - trailing_semicolon 41 | - trailing_whitespace 42 | - type_name 43 | - type_body_length 44 | - unused_import 45 | - unused_declaration 46 | - vertical_whitespace 47 | - void_return 48 | - weak_delegate 49 | 50 | 51 | # logic 52 | # disabled_rules: # rule identifiers to exclude from running 53 | # - leading_whitespace 54 | # - line_length 55 | # - variable_name 56 | # - file_length 57 | # - missing_docs 58 | 59 | # # 60 | opt_in_rules: # some rules are only opt-in 61 | # - opening_brace 62 | # - closure_spacing 63 | # - colon 64 | # - comma 65 | # - conditional_returns_on_newline 66 | # - control_statement 67 | # - legacy_cggeometry_functions 68 | # - legacy_constant 69 | # - legacy_constructor 70 | # - mark 71 | # - overridden_super_call 72 | # - return_arrow_whitespace 73 | # - statement_position 74 | # - trailing_newline 75 | # - trailing_semicolon 76 | # - trailing_whitespace 77 | # - vertical_whitespace 78 | excluded: # paths to ignore during linting. Takes precedence over `included`. 79 | - .build 80 | 81 | # configurable rules can be customized from this configuration file 82 | # binary rules can set their severity level 83 | force_cast: warning # implicitly 84 | force_try: 85 | severity: warning # explicitly 86 | # rules that have both warning and error levels, can set just the warning level 87 | # implicitly 88 | line_length: 120 89 | # they can set both implicitly with an array 90 | type_body_length: 91 | - 300 # warning 92 | - 400 # error 93 | # # or they can set both explicitly 94 | # file_length: 95 | # warning: 500 96 | # error: 1200 97 | # # naming rules can set warnings/errors for min_length and max_length 98 | # # additionally they can set excluded names 99 | type_name: 100 | min_length: 4 # only warning 101 | max_length: 50 102 | excluded: # excluded via string array 103 | - Id 104 | - iPhone 105 | 106 | reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit) 107 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CryptoSwift", 6 | "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift", 7 | "state": { 8 | "branch": null, 9 | "revision": "039f56c5d7960f277087a0be51f5eb04ed0ec073", 10 | "version": "1.5.1" 11 | } 12 | }, 13 | { 14 | "package": "SurfPlaybook", 15 | "repositoryURL": "https://github.com/surfstudio/SurfPlaybook.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "f477093718acacd1264dc69625088e2376618104", 19 | "version": "1.3.0" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // UtilsExample 4 | // 5 | // Created by chausov on 09.06.2022. 6 | // 7 | 8 | import UIKit 9 | import SurfPlaybook 10 | 11 | @UIApplicationMain 12 | final class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | // MARK: - Properties 15 | 16 | var window: UIWindow? 17 | 18 | // MARK: - UIApplicationDelegate 19 | 20 | func application(_ application: UIApplication, 21 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 22 | window = UIWindow(frame: UIScreen.main.bounds) 23 | window?.rootViewController = UIViewController() 24 | window?.makeKeyAndVisible() 25 | 26 | Playbook.shared 27 | .add(flowCoordinator: BrightSideCoordinator()) 28 | .add(flowCoordinator: CustomSwitchCoordinator()) 29 | .add(flowCoordinator: GeolocationServiceCoordinator()) 30 | .add(flowCoordinator: KeyboardPresentableCoordinator()) 31 | .add(flowCoordinator: MoneyModelCoordinator()) 32 | .add(flowCoordinator: QueryStringBuilderCoordinator()) 33 | .add(flowCoordinator: RouteMeasurerCoordinator()) 34 | .add(flowCoordinator: SettingsRouterCoordinator()) 35 | .add(flowCoordinator: SkeletonViewCoordinator()) 36 | .add(flowCoordinator: StringAttributesCoordinator()) 37 | .add(flowCoordinator: UIDeviceCoordinator()) 38 | .add(flowCoordinator: WordDeclinationSelectorCoordinator()) 39 | .add(uiKitPage: MainPage()) 40 | .start(from: window) 41 | 42 | return true 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSupportsIndirectInputEvents 6 | 7 | NSLocationWhenInUseUsageDescription 8 | Приложение хочет получить доступ к вашей геопозиции для демонстрации работы утилит 9 | LSRequiresIPhoneOS 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Coordinators/BrightSideCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrightSideCoordinator.swift 3 | // UtilsExample 4 | // 5 | // Created by Евгений Васильев on 24.06.2022. 6 | // 7 | 8 | import SurfPlaybook 9 | 10 | final class BrightSideCoordinator: PlaybookFlowCoordinator { 11 | 12 | // MARK: - Private Properties 13 | 14 | private let router = MainRouter() 15 | 16 | // MARK: - PlaybookFlowCoordinator 17 | 18 | var id: String { 19 | return "BrightSideCoordinator" 20 | } 21 | 22 | var name: String { 23 | return "BrightSideCoordinator" 24 | } 25 | 26 | var type: FlowCoordinatorType { 27 | return .coordinator { [weak self] in 28 | let (view, _) = BrightSideModuleConfigurator().configure() 29 | self?.router.present(view) 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Coordinators/CustomSwitchCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomSwitchCoordinator.swift 3 | // UtilsExample 4 | // 5 | // Created by Евгений Васильев on 23.06.2022. 6 | // 7 | 8 | import SurfPlaybook 9 | 10 | final class CustomSwitchCoordinator: PlaybookFlowCoordinator { 11 | 12 | // MARK: - Private Properties 13 | 14 | private let router = MainRouter() 15 | 16 | // MARK: - PlaybookFlowCoordinator 17 | 18 | var id: String { 19 | return "CustomSwitchCoordinator" 20 | } 21 | 22 | var name: String { 23 | return "CustomSwitchCoordinator" 24 | } 25 | 26 | var type: FlowCoordinatorType { 27 | return .coordinator { [weak self] in 28 | let (view, _) = CustomSwitchModuleConfigurator().configure() 29 | self?.router.present(view) 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Coordinators/GeolocationServiceCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeolocationServiceCoordinator.swift 3 | // UtilsExample 4 | // 5 | // Created by Евгений Васильев on 24.06.2022. 6 | // 7 | 8 | import SurfPlaybook 9 | 10 | final class GeolocationServiceCoordinator: PlaybookFlowCoordinator { 11 | 12 | // MARK: - Private Properties 13 | 14 | private let router = MainRouter() 15 | 16 | // MARK: - PlaybookFlowCoordinator 17 | 18 | var id: String { 19 | return "GeolocationServiceCoordinator" 20 | } 21 | 22 | var name: String { 23 | return "GeolocationServiceCoordinator" 24 | } 25 | 26 | var type: FlowCoordinatorType { 27 | return .coordinator { [weak self] in 28 | let (view, _) = GeolocationServiceModuleConfigurator().configure() 29 | self?.router.present(view) 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Coordinators/KeyboardPresentableCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardPresentableCoordinator.swift 3 | // UtilsExample 4 | // 5 | // Created by Евгений Васильев on 24.06.2022. 6 | // 7 | 8 | import SurfPlaybook 9 | 10 | final class KeyboardPresentableCoordinator: PlaybookFlowCoordinator { 11 | 12 | // MARK: - Private Properties 13 | 14 | private let router = MainRouter() 15 | 16 | // MARK: - PlaybookFlowCoordinator 17 | 18 | var id: String { 19 | return "KeyboardPresentableCoordinator" 20 | } 21 | 22 | var name: String { 23 | return "KeyboardPresentableCoordinator" 24 | } 25 | 26 | var type: FlowCoordinatorType { 27 | return .coordinator { [weak self] in 28 | let (view, _) = KeyboardPresentableModuleConfigurator().configure() 29 | self?.router.present(view) 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Coordinators/MoneyModelCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoneyModelCoordinator.swift 3 | // UtilsExample 4 | // 5 | // Created by Евгений Васильев on 24.06.2022. 6 | // 7 | 8 | import SurfPlaybook 9 | 10 | final class MoneyModelCoordinator: PlaybookFlowCoordinator { 11 | 12 | // MARK: - Private Properties 13 | 14 | private let router = MainRouter() 15 | 16 | // MARK: - PlaybookFlowCoordinator 17 | 18 | var id: String { 19 | return "MoneyModelCoordinator" 20 | } 21 | 22 | var name: String { 23 | return "MoneyModelCoordinator" 24 | } 25 | 26 | var type: FlowCoordinatorType { 27 | return .coordinator { [weak self] in 28 | let (view, _) = MoneyModelModuleConfigurator().configure() 29 | self?.router.present(view) 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Coordinators/QueryStringBuilderCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QueryStringBuilderCoordinator.swift 3 | // UtilsExample 4 | // 5 | // Created by Евгений Васильев on 23.06.2022. 6 | // 7 | 8 | import SurfPlaybook 9 | 10 | final class QueryStringBuilderCoordinator: PlaybookFlowCoordinator { 11 | 12 | // MARK: - Private Properties 13 | 14 | private let router = MainRouter() 15 | 16 | // MARK: - PlaybookFlowCoordinator 17 | 18 | var id: String { 19 | return "QueryStringBuilderCoordinator" 20 | } 21 | 22 | var name: String { 23 | return "QueryStringBuilderCoordinator" 24 | } 25 | 26 | var type: FlowCoordinatorType { 27 | return .coordinator { [weak self] in 28 | let (view, _) = QueryStringBuilderModuleConfigurator().configure() 29 | self?.router.present(view) 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Coordinators/RouteMeasurerCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouteMeasurerCoordinator.swift 3 | // UtilsExample 4 | // 5 | // Created by Евгений Васильев on 24.06.2022. 6 | // 7 | 8 | import SurfPlaybook 9 | 10 | final class RouteMeasurerCoordinator: PlaybookFlowCoordinator { 11 | 12 | // MARK: - Private Properties 13 | 14 | private let router = MainRouter() 15 | 16 | // MARK: - PlaybookFlowCoordinator 17 | 18 | var id: String { 19 | return "RouteMeasurerCoordinator" 20 | } 21 | 22 | var name: String { 23 | return "RouteMeasurerCoordinator" 24 | } 25 | 26 | var type: FlowCoordinatorType { 27 | return .coordinator { [weak self] in 28 | let (view, _) = RouteMeasurerModuleConfigurator().configure() 29 | self?.router.present(view) 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Coordinators/SettingsRouterCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsRouterCoordinator.swift 3 | // UtilsExample 4 | // 5 | // Created by Евгений Васильев on 24.06.2022. 6 | // 7 | 8 | import SurfPlaybook 9 | 10 | final class SettingsRouterCoordinator: PlaybookFlowCoordinator { 11 | 12 | // MARK: - Private Properties 13 | 14 | private let router = MainRouter() 15 | 16 | // MARK: - PlaybookFlowCoordinator 17 | 18 | var id: String { 19 | return "SettingsRouterCoordinator" 20 | } 21 | 22 | var name: String { 23 | return "SettingsRouterCoordinator" 24 | } 25 | 26 | var type: FlowCoordinatorType { 27 | return .coordinator { [weak self] in 28 | let (view, _) = SettingsRouterModuleConfigurator().configure() 29 | self?.router.present(view) 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Coordinators/SkeletonViewCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SkeletonViewCoordinator.swift 3 | // UtilsExample 4 | // 5 | // Created by Евгений Васильев on 24.06.2022. 6 | // 7 | 8 | import SurfPlaybook 9 | 10 | final class SkeletonViewCoordinator: PlaybookFlowCoordinator { 11 | 12 | // MARK: - Private Properties 13 | 14 | private let router = MainRouter() 15 | 16 | // MARK: - PlaybookFlowCoordinator 17 | 18 | var id: String { 19 | return "SkeletonViewCoordinator" 20 | } 21 | 22 | var name: String { 23 | return "SkeletonViewCoordinator" 24 | } 25 | 26 | var type: FlowCoordinatorType { 27 | return .coordinator { [weak self] in 28 | let (view, _) = SkeletonViewModuleConfigurator().configure() 29 | self?.router.present(view) 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Coordinators/StringAttributesCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringAttributes.swift 3 | // 4 | // 5 | // Created by Евгений Васильев on 23.06.2022. 6 | // 7 | 8 | import SurfPlaybook 9 | 10 | final class StringAttributesCoordinator: PlaybookFlowCoordinator { 11 | 12 | // MARK: - Private Properties 13 | 14 | private let router = MainRouter() 15 | 16 | // MARK: - PlaybookFlowCoordinator 17 | 18 | var id: String { 19 | return "StringAttributesCoordinator" 20 | } 21 | 22 | var name: String { 23 | return "StringAttributesCoordinator" 24 | } 25 | 26 | var type: FlowCoordinatorType { 27 | return .coordinator { [weak self] in 28 | let (view, _) = StringAttributesModuleConfigurator().configure() 29 | self?.router.present(view) 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Coordinators/UIDeviceCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIDeviceCoordinator.swift 3 | // UtilsExample 4 | // 5 | // Created by Евгений Васильев on 24.06.2022. 6 | // 7 | 8 | import SurfPlaybook 9 | 10 | final class UIDeviceCoordinator: PlaybookFlowCoordinator { 11 | 12 | // MARK: - Private Properties 13 | 14 | private let router = MainRouter() 15 | 16 | // MARK: - PlaybookFlowCoordinator 17 | 18 | var id: String { 19 | return "UIDeviceCoordinator" 20 | } 21 | 22 | var name: String { 23 | return "UIDeviceCoordinator" 24 | } 25 | 26 | var type: FlowCoordinatorType { 27 | return .coordinator { [weak self] in 28 | let (view, _) = UIDeviceModuleConfigurator().configure() 29 | self?.router.present(view) 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Coordinators/WordDeclinationSelectorCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WordDeclinationSelectorCoordinator.swift 3 | // UtilsExample 4 | // 5 | // Created by Евгений Васильев on 24.06.2022. 6 | // 7 | 8 | import SurfPlaybook 9 | 10 | final class WordDeclinationSelectorCoordinator: PlaybookFlowCoordinator { 11 | 12 | // MARK: - Private Properties 13 | 14 | private let router = MainRouter() 15 | 16 | // MARK: - PlaybookFlowCoordinator 17 | 18 | var id: String { 19 | return "WordDeclinationSelectorCoordinator" 20 | } 21 | 22 | var name: String { 23 | return "WordDeclinationSelectorCoordinator" 24 | } 25 | 26 | var type: FlowCoordinatorType { 27 | return .coordinator { [weak self] in 28 | let (view, _) = WordDeclinationSelectorModuleConfigurator().configure() 29 | self?.router.present(view) 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/BrightSide/Configurator/BrightSideModuleConfigurator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrightSideModuleConfigurator.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class BrightSideModuleConfigurator { 12 | 13 | func configure() -> (UIViewController, BrightSideModuleOutput) { 14 | let view = BrightSideViewController() 15 | let presenter = BrightSidePresenter() 16 | 17 | presenter.view = view 18 | view.output = presenter 19 | 20 | return (view, presenter) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/BrightSide/Presenter/BrightSideModuleInput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrightSideModuleInput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol BrightSideModuleInput: AnyObject { 10 | } 11 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/BrightSide/Presenter/BrightSideModuleOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrightSideModuleOutput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol BrightSideModuleOutput: AnyObject { 10 | } 11 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/BrightSide/Presenter/BrightSidePresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrightSidePresenter.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | final class BrightSidePresenter: BrightSideModuleOutput { 10 | 11 | // MARK: - BrightSideModuleOutput 12 | 13 | // MARK: - Properties 14 | 15 | weak var view: BrightSideViewInput? 16 | 17 | } 18 | 19 | // MARK: - BrightSideModuleInput 20 | 21 | extension BrightSidePresenter: BrightSideModuleInput { 22 | } 23 | 24 | // MARK: - BrightSideViewOutput 25 | 26 | extension BrightSidePresenter: BrightSideViewOutput { 27 | 28 | func viewLoaded() { 29 | view?.setupInitialState() 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/BrightSide/View/BrightSideViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrightSideViewController.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Utils 11 | 12 | final class BrightSideViewController: UIViewController { 13 | 14 | // MARK: - IBOutlets 15 | 16 | @IBOutlet private weak var subTitle: UILabel! 17 | 18 | // MARK: - Properties 19 | 20 | var output: BrightSideViewOutput? 21 | 22 | // MARK: - UIViewController 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | output?.viewLoaded() 27 | } 28 | 29 | } 30 | 31 | // MARK: - BrightSideViewInput 32 | 33 | extension BrightSideViewController: BrightSideViewInput { 34 | 35 | func setupInitialState() { 36 | configureLabel() 37 | } 38 | 39 | } 40 | 41 | // MARK: - Private Methods 42 | 43 | private extension BrightSideViewController { 44 | 45 | func configureLabel() { 46 | let text: String 47 | if BrightSide.isBright() { 48 | text = "Девайс чист как белый лист" 49 | } else { 50 | text = "На девайсе получен root доступ" 51 | } 52 | subTitle.text = text 53 | subTitle.numberOfLines = 0 54 | subTitle.textAlignment = .center 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/BrightSide/View/BrightSideViewInput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrightSideViewInput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol BrightSideViewInput: AnyObject { 10 | /// Method for setup initial state of view 11 | func setupInitialState() 12 | } 13 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/BrightSide/View/BrightSideViewOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrightSideViewOutput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol BrightSideViewOutput { 10 | /// Notify presenter that view is ready 11 | func viewLoaded() 12 | } 13 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/CustomSwitch/Configurator/CustomSwitchModuleConfigurator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomSwitchModuleConfigurator.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 23/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class CustomSwitchModuleConfigurator { 12 | 13 | func configure() -> (UIViewController, CustomSwitchModuleOutput) { 14 | let view = CustomSwitchViewController() 15 | let presenter = CustomSwitchPresenter() 16 | 17 | presenter.view = view 18 | view.output = presenter 19 | 20 | return (view, presenter) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/CustomSwitch/Presenter/CustomSwitchModuleInput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomSwitchModuleInput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 23/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol CustomSwitchModuleInput: AnyObject { } 10 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/CustomSwitch/Presenter/CustomSwitchModuleOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomSwitchModuleOutput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 23/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol CustomSwitchModuleOutput: AnyObject { 10 | } 11 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/CustomSwitch/Presenter/CustomSwitchPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomSwitchPresenter.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 23/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | final class CustomSwitchPresenter: CustomSwitchModuleOutput { 10 | 11 | // MARK: - CustomSwitchModuleOutput 12 | 13 | // MARK: - Properties 14 | 15 | weak var view: CustomSwitchViewInput? 16 | 17 | } 18 | 19 | // MARK: - CustomSwitchModuleInput 20 | 21 | extension CustomSwitchPresenter: CustomSwitchModuleInput { 22 | } 23 | 24 | // MARK: - CustomSwitchViewOutput 25 | 26 | extension CustomSwitchPresenter: CustomSwitchViewOutput { 27 | 28 | func viewLoaded() { 29 | view?.setupInitialState() 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/CustomSwitch/View/CustomSwitchViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomSwitchViewController.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 23/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Utils 11 | 12 | final class CustomSwitchViewController: UIViewController { 13 | 14 | // MARK: - IBOutlets 15 | 16 | @IBOutlet private weak var switchContainer: UIView! 17 | 18 | // MARK: - Properties 19 | 20 | var output: CustomSwitchViewOutput? 21 | 22 | // MARK: - UIViewController 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | output?.viewLoaded() 27 | } 28 | 29 | } 30 | 31 | // MARK: - CustomSwitchViewInput 32 | 33 | extension CustomSwitchViewController: CustomSwitchViewInput { 34 | 35 | func setupInitialState() { 36 | configureSwitch() 37 | } 38 | 39 | } 40 | 41 | // MARK: - Private Methods 42 | 43 | private extension CustomSwitchViewController { 44 | 45 | func configureSwitch() { 46 | let customSwitch = CustomSwitch(frame: CGRect(x: 0, y: 0, width: 100, height: 50)) 47 | customSwitch.layoutConfiguration = .init(padding: 1, spacing: 3, cornerRatio: 0.5) 48 | customSwitch.colorsConfiguration = .init( 49 | offColorConfiguraion: CSSimpleColorConfiguration(color: .gray), 50 | onColorConfiguraion: CSSimpleColorConfiguration(color: .green), 51 | thumbColorConfiguraion: CSGradientColorConfiguration( 52 | colors: [.lightGray, .yellow], 53 | locations: [0, 1] 54 | ) 55 | ) 56 | customSwitch.thumbConfiguration = .init( 57 | cornerRatio: 0.5, 58 | shadowConfiguration: .init( 59 | color: .black, offset: CGSize(), 60 | radius: 5, oppacity: 0.1 61 | ) 62 | ) 63 | customSwitch.animationsConfiguration = .init(duration: 0.3, usingSpringWithDamping: 0.7) 64 | 65 | customSwitch.setOn(true, animated: false) 66 | switchContainer.addSubview(customSwitch) 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/CustomSwitch/View/CustomSwitchViewInput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomSwitchViewInput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 23/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol CustomSwitchViewInput: AnyObject { 10 | /// Method for setup initial state of view 11 | func setupInitialState() 12 | } 13 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/CustomSwitch/View/CustomSwitchViewOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomSwitchViewOutput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 23/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol CustomSwitchViewOutput { 10 | /// Notify presenter that view is ready 11 | func viewLoaded() 12 | } 13 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/GeolocationService/Configurator/GeolocationServiceModuleConfigurator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeolocationServiceModuleConfigurator.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class GeolocationServiceModuleConfigurator { 12 | 13 | func configure() -> (UIViewController, GeolocationServiceModuleOutput) { 14 | let view = GeolocationServiceViewController() 15 | let presenter = GeolocationServicePresenter() 16 | 17 | presenter.view = view 18 | view.output = presenter 19 | 20 | return (view, presenter) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/GeolocationService/Presenter/GeolocationServiceModuleInput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeolocationServiceModuleInput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol GeolocationServiceModuleInput: AnyObject { 10 | } 11 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/GeolocationService/Presenter/GeolocationServiceModuleOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeolocationServiceModuleOutput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol GeolocationServiceModuleOutput: AnyObject { 10 | } 11 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/GeolocationService/Presenter/GeolocationServicePresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeolocationServicePresenter.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | final class GeolocationServicePresenter: GeolocationServiceModuleOutput { 10 | 11 | // MARK: - GeolocationServiceModuleOutput 12 | 13 | // MARK: - Properties 14 | 15 | weak var view: GeolocationServiceViewInput? 16 | 17 | } 18 | 19 | // MARK: - GeolocationServiceModuleInput 20 | 21 | extension GeolocationServicePresenter: GeolocationServiceModuleInput { 22 | } 23 | 24 | // MARK: - GeolocationServiceViewOutput 25 | 26 | extension GeolocationServicePresenter: GeolocationServiceViewOutput { 27 | 28 | func viewLoaded() { 29 | view?.setupInitialState() 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/GeolocationService/View/GeolocationServiceViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeolocationServiceViewController.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Utils 11 | 12 | final class GeolocationServiceViewController: UIViewController { 13 | 14 | // MARK: - IBOutlets 15 | 16 | @IBOutlet private weak var subTitle: UILabel! 17 | 18 | // MARK: - Private Properties 19 | 20 | private let service = GeolocationService() 21 | 22 | // MARK: - Properties 23 | 24 | var output: GeolocationServiceViewOutput? 25 | 26 | // MARK: - UIViewController 27 | 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | output?.viewLoaded() 31 | } 32 | 33 | } 34 | 35 | // MARK: - GeolocationServiceViewInput 36 | 37 | extension GeolocationServiceViewController: GeolocationServiceViewInput { 38 | 39 | func setupInitialState() { 40 | configureLabel() 41 | requestAuthorization() 42 | } 43 | 44 | } 45 | 46 | // MARK: - Private Methods 47 | 48 | private extension GeolocationServiceViewController { 49 | 50 | func configureLabel() { 51 | subTitle.textAlignment = .center 52 | subTitle.numberOfLines = 0 53 | subTitle.text = "Waiting..." 54 | } 55 | 56 | func requestAuthorization() { 57 | service.requestAuthorization { [weak self] result in 58 | switch result { 59 | case .success: 60 | // access is allowed 61 | self?.requestLocation() 62 | case .denied: 63 | // user denied access to geolocation 64 | self?.subTitle.text = "Denied..." 65 | case .failure: 66 | // user doesn't gave permission on his geolocation in the system dialog 67 | self?.subTitle.text = "Failure..." 68 | case .requesting: 69 | // system dialog is currently displayed 70 | self?.subTitle.text = "Requesting..." 71 | } 72 | } 73 | 74 | } 75 | 76 | func requestLocation() { 77 | service.getCurrentLocation { [weak self] result in 78 | switch result { 79 | case .success(let location): 80 | // do something usefull with user location 81 | self?.subTitle.text = "Current location is \(location)" 82 | case .denied: 83 | self?.subTitle.text = "User denied access to geolocation" 84 | case .error: 85 | self?.subTitle.text = "Some error ocured" 86 | } 87 | } 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/GeolocationService/View/GeolocationServiceViewInput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeolocationServiceViewInput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol GeolocationServiceViewInput: AnyObject { 10 | /// Method for setup initial state of view 11 | func setupInitialState() 12 | } 13 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/GeolocationService/View/GeolocationServiceViewOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeolocationServiceViewOutput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol GeolocationServiceViewOutput { 10 | /// Notify presenter that view is ready 11 | func viewLoaded() 12 | } 13 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/KeyboardPresentable/Configurator/KeyboardPresentableModuleConfigurator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardPresentableModuleConfigurator.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class KeyboardPresentableModuleConfigurator { 12 | 13 | func configure() -> (UIViewController, KeyboardPresentableModuleOutput) { 14 | let view = KeyboardPresentableViewController() 15 | let presenter = KeyboardPresentablePresenter() 16 | 17 | presenter.view = view 18 | view.output = presenter 19 | 20 | return (view, presenter) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/KeyboardPresentable/Presenter/KeyboardPresentableModuleInput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardPresentableModuleInput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol KeyboardPresentableModuleInput: AnyObject { 10 | } 11 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/KeyboardPresentable/Presenter/KeyboardPresentableModuleOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardPresentableModuleOutput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol KeyboardPresentableModuleOutput: AnyObject { 10 | } 11 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/KeyboardPresentable/Presenter/KeyboardPresentablePresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardPresentablePresenter.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | final class KeyboardPresentablePresenter: KeyboardPresentableModuleOutput { 10 | 11 | // MARK: - KeyboardPresentableModuleOutput 12 | 13 | // MARK: - Properties 14 | 15 | weak var view: KeyboardPresentableViewInput? 16 | 17 | } 18 | 19 | // MARK: - KeyboardPresentableModuleInput 20 | 21 | extension KeyboardPresentablePresenter: KeyboardPresentableModuleInput { 22 | } 23 | 24 | // MARK: - KeyboardPresentableViewOutput 25 | 26 | extension KeyboardPresentablePresenter: KeyboardPresentableViewOutput { 27 | 28 | func viewLoaded() { 29 | view?.setupInitialState() 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/KeyboardPresentable/View/KeyboardPresentableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardPresentableViewController.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Utils 11 | 12 | final class KeyboardPresentableViewController: UIViewController, KeyboardObservable { 13 | 14 | // MARK: - IBOutlets 15 | 16 | @IBOutlet private weak var subTitle: UILabel! 17 | @IBOutlet private weak var someField: UITextField! 18 | 19 | // MARK: - Properties 20 | 21 | var output: KeyboardPresentableViewOutput? 22 | 23 | // MARK: - UIViewController 24 | 25 | override func viewDidLoad() { 26 | super.viewDidLoad() 27 | // Для подписки на нотификации появления/сокрытия клавиатуры необходимо вызывать: 28 | subscribeOnKeyboardNotifications() 29 | configureGesture() 30 | output?.viewLoaded() 31 | } 32 | 33 | override func viewDidDisappear(_ animated: Bool) { 34 | super.viewDidDisappear(animated) 35 | // Для отписывания от нотификаций появления/сокрытия клавиатуры необходимо вызывать: 36 | unsubscribeFromKeyboardNotifications() 37 | } 38 | 39 | } 40 | 41 | // MARK: - KeyboardPresentableViewInput 42 | 43 | extension KeyboardPresentableViewController: KeyboardPresentableViewInput { 44 | 45 | func setupInitialState() { 46 | subTitle.text = "Keyboard сокрыт" 47 | } 48 | 49 | } 50 | 51 | // MARK: - CommonKeyboardPresentable 52 | 53 | extension KeyboardPresentableViewController: CommonKeyboardPresentable { 54 | 55 | func keyboardWillBeShown(keyboardHeight: CGFloat, duration: TimeInterval) { 56 | subTitle.text = "Keyboard показан" 57 | } 58 | 59 | func keyboardWillBeHidden(duration: TimeInterval) { 60 | subTitle.text = "Keyboard сокрыт" 61 | } 62 | 63 | } 64 | 65 | // MARK: - Private methods 66 | 67 | private extension KeyboardPresentableViewController { 68 | 69 | func configureGesture() { 70 | let tap = UITapGestureRecognizer(target: self, 71 | action: #selector(UIInputViewController.dismissKeyboard)) 72 | view.addGestureRecognizer(tap) 73 | } 74 | 75 | @objc 76 | func dismissKeyboard() { 77 | view.endEditing(true) 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/KeyboardPresentable/View/KeyboardPresentableViewInput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardPresentableViewInput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol KeyboardPresentableViewInput: AnyObject { 10 | /// Method for setup initial state of view 11 | func setupInitialState() 12 | } 13 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/KeyboardPresentable/View/KeyboardPresentableViewOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardPresentableViewOutput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol KeyboardPresentableViewOutput { 10 | /// Notify presenter that view is ready 11 | func viewLoaded() 12 | } 13 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/MoneyModel/Configurator/MoneyModelModuleConfigurator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoneyModelModuleConfigurator.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class MoneyModelModuleConfigurator { 12 | 13 | func configure() -> (UIViewController, MoneyModelModuleOutput) { 14 | let view = MoneyModelViewController() 15 | let presenter = MoneyModelPresenter() 16 | 17 | presenter.view = view 18 | view.output = presenter 19 | 20 | return (view, presenter) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/MoneyModel/Presenter/MoneyModelModuleInput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoneyModelModuleInput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol MoneyModelModuleInput: AnyObject { 10 | } 11 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/MoneyModel/Presenter/MoneyModelModuleOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoneyModelModuleOutput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol MoneyModelModuleOutput: AnyObject { 10 | } 11 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/MoneyModel/Presenter/MoneyModelPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoneyModelPresenter.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | final class MoneyModelPresenter: MoneyModelModuleOutput { 10 | 11 | // MARK: - MoneyModelModuleOutput 12 | 13 | // MARK: - Properties 14 | 15 | weak var view: MoneyModelViewInput? 16 | 17 | } 18 | 19 | // MARK: - MoneyModelModuleInput 20 | 21 | extension MoneyModelPresenter: MoneyModelModuleInput { 22 | } 23 | 24 | // MARK: - MoneyModelViewOutput 25 | 26 | extension MoneyModelPresenter: MoneyModelViewOutput { 27 | 28 | func viewLoaded() { 29 | view?.setupInitialState() 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/MoneyModel/View/MoneyModelViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoneyModelViewController.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Utils 11 | 12 | final class MoneyModelViewController: UIViewController { 13 | 14 | // MARK: - IBOutlets 15 | 16 | @IBOutlet weak var subTitle: UILabel! 17 | 18 | // MARK: - Properties 19 | 20 | var output: MoneyModelViewOutput? 21 | 22 | // MARK: - UIViewController 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | output?.viewLoaded() 27 | } 28 | 29 | } 30 | 31 | // MARK: - MoneyModelViewInput 32 | 33 | extension MoneyModelViewController: MoneyModelViewInput { 34 | 35 | func setupInitialState() { 36 | configureLabel() 37 | } 38 | 39 | } 40 | 41 | // MARK: - Private Methods 42 | 43 | private extension MoneyModelViewController { 44 | 45 | func configureLabel() { 46 | let text = [ 47 | MoneyModel(decimal: 10, digit: 0).asString(), 48 | "\n", 49 | MoneyModel(decimal: 10, digit: 9).asString(), 50 | "\n", 51 | MoneyModel(decimal: 10, digit: 99).asString() 52 | ] 53 | subTitle.text = text.reduce("", +) 54 | subTitle.numberOfLines = 0 55 | subTitle.textAlignment = .center 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/MoneyModel/View/MoneyModelViewInput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoneyModelViewInput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol MoneyModelViewInput: AnyObject { 10 | /// Method for setup initial state of view 11 | func setupInitialState() 12 | } 13 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/MoneyModel/View/MoneyModelViewOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoneyModelViewOutput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol MoneyModelViewOutput { 10 | /// Notify presenter that view is ready 11 | func viewLoaded() 12 | } 13 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/QueryStringBuilder/Configurator/QueryStringBuilderModuleConfigurator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QueryStringBuilderModuleConfigurator.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 23/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class QueryStringBuilderModuleConfigurator { 12 | 13 | func configure() -> (UIViewController, QueryStringBuilderModuleOutput) { 14 | let view = QueryStringBuilderViewController() 15 | let presenter = QueryStringBuilderPresenter() 16 | 17 | presenter.view = view 18 | view.output = presenter 19 | 20 | return (view, presenter) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/QueryStringBuilder/Presenter/QueryStringBuilderModuleInput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QueryStringBuilderModuleInput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 23/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol QueryStringBuilderModuleInput: AnyObject { 10 | } 11 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/QueryStringBuilder/Presenter/QueryStringBuilderModuleOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QueryStringBuilderModuleOutput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 23/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol QueryStringBuilderModuleOutput: AnyObject { 10 | } 11 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/QueryStringBuilder/Presenter/QueryStringBuilderPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QueryStringBuilderPresenter.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 23/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | final class QueryStringBuilderPresenter: QueryStringBuilderModuleOutput { 10 | 11 | // MARK: - QueryStringBuilderModuleOutput 12 | 13 | // MARK: - Properties 14 | 15 | weak var view: QueryStringBuilderViewInput? 16 | 17 | } 18 | 19 | // MARK: - QueryStringBuilderModuleInput 20 | 21 | extension QueryStringBuilderPresenter: QueryStringBuilderModuleInput { 22 | } 23 | 24 | // MARK: - QueryStringBuilderViewOutput 25 | 26 | extension QueryStringBuilderPresenter: QueryStringBuilderViewOutput { 27 | 28 | func viewLoaded() { 29 | view?.setupInitialState() 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/QueryStringBuilder/View/QueryStringBuilderViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QueryStringBuilderViewController.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 23/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Utils 11 | 12 | final class QueryStringBuilderViewController: UIViewController { 13 | 14 | // MARK: - IBOutlets 15 | 16 | @IBOutlet private weak var subTitle: UILabel! 17 | 18 | // MARK: - Properties 19 | 20 | var output: QueryStringBuilderViewOutput? 21 | 22 | // MARK: - UIViewController 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | output?.viewLoaded() 27 | } 28 | 29 | } 30 | 31 | // MARK: - QueryStringBuilderViewInput 32 | 33 | extension QueryStringBuilderViewController: QueryStringBuilderViewInput { 34 | 35 | func setupInitialState() { 36 | configureLabel() 37 | } 38 | 39 | } 40 | 41 | // MARK: - Private Methods 42 | 43 | private extension QueryStringBuilderViewController { 44 | 45 | func configureLabel() { 46 | let dict: [String: Any] = ["key1": "value1", "key2": 2.15, "key3": true] 47 | let queryString = dict.toQueryString() 48 | subTitle.text = queryString 49 | subTitle.numberOfLines = 0 50 | subTitle.textAlignment = .center 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/QueryStringBuilder/View/QueryStringBuilderViewInput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QueryStringBuilderViewInput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 23/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol QueryStringBuilderViewInput: AnyObject { 10 | /// Method for setup initial state of view 11 | func setupInitialState() 12 | } 13 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/QueryStringBuilder/View/QueryStringBuilderViewOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QueryStringBuilderViewOutput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 23/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol QueryStringBuilderViewOutput { 10 | /// Notify presenter that view is ready 11 | func viewLoaded() 12 | } 13 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/RouteMeasurer/Configurator/RouteMeasurerModuleConfigurator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouteMeasurerModuleConfigurator.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class RouteMeasurerModuleConfigurator { 12 | 13 | func configure() -> (UIViewController, RouteMeasurerModuleOutput) { 14 | let view = RouteMeasurerViewController() 15 | let presenter = RouteMeasurerPresenter() 16 | 17 | presenter.view = view 18 | view.output = presenter 19 | 20 | return (view, presenter) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/RouteMeasurer/Presenter/RouteMeasurerModuleInput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouteMeasurerModuleInput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol RouteMeasurerModuleInput: AnyObject { 10 | } 11 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/RouteMeasurer/Presenter/RouteMeasurerModuleOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouteMeasurerModuleOutput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol RouteMeasurerModuleOutput: AnyObject { 10 | } 11 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/RouteMeasurer/Presenter/RouteMeasurerPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouteMeasurerPresenter.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | final class RouteMeasurerPresenter: RouteMeasurerModuleOutput { 10 | 11 | // MARK: - RouteMeasurerModuleOutput 12 | 13 | // MARK: - Properties 14 | 15 | weak var view: RouteMeasurerViewInput? 16 | 17 | } 18 | 19 | // MARK: - RouteMeasurerModuleInput 20 | 21 | extension RouteMeasurerPresenter: RouteMeasurerModuleInput { 22 | } 23 | 24 | // MARK: - RouteMeasurerViewOutput 25 | 26 | extension RouteMeasurerPresenter: RouteMeasurerViewOutput { 27 | 28 | func viewLoaded() { 29 | view?.setupInitialState() 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/RouteMeasurer/View/RouteMeasurerViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouteMeasurerViewController.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Utils 11 | import CoreLocation 12 | 13 | final class RouteMeasurerViewController: UIViewController { 14 | 15 | // MARK: - IBOutlets 16 | 17 | @IBOutlet private weak var subTitle: UILabel! 18 | 19 | // MARK: - Properties 20 | 21 | var output: RouteMeasurerViewOutput? 22 | 23 | // MARK: - UIViewController 24 | 25 | override func viewDidLoad() { 26 | super.viewDidLoad() 27 | output?.viewLoaded() 28 | } 29 | 30 | } 31 | 32 | // MARK: - RouteMeasurerViewInput 33 | 34 | extension RouteMeasurerViewController: RouteMeasurerViewInput { 35 | 36 | func setupInitialState() { 37 | configureLabel() 38 | } 39 | 40 | } 41 | 42 | // MARK: - Private Methods 43 | 44 | private extension RouteMeasurerViewController { 45 | 46 | func configureLabel() { 47 | subTitle.text = "Расстояние вычисляется..." 48 | subTitle.numberOfLines = 0 49 | subTitle.textAlignment = .center 50 | 51 | let firstCoordinate = CLLocationCoordinate2D(latitude: 55.751244, longitude: 37.618423) 52 | let secondCoordinate = CLLocationCoordinate2D(latitude: 51.509865, longitude: -0.118092) 53 | RouteMeasurer.calculateDistance(between: firstCoordinate, 54 | and: secondCoordinate) { [weak self] (distance) in 55 | guard let distance = distance else { 56 | return 57 | } 58 | self?.subTitle.text = RouteMeasurer.formatDistance(distance, 59 | meterPattern: "м", 60 | kilometrPatter: "км") 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/RouteMeasurer/View/RouteMeasurerViewInput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouteMeasurerViewInput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol RouteMeasurerViewInput: AnyObject { 10 | /// Method for setup initial state of view 11 | func setupInitialState() 12 | } 13 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/RouteMeasurer/View/RouteMeasurerViewOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouteMeasurerViewOutput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol RouteMeasurerViewOutput { 10 | /// Notify presenter that view is ready 11 | func viewLoaded() 12 | } 13 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/SettingsRouter/Configurator/SettingsRouterModuleConfigurator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsRouterModuleConfigurator.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class SettingsRouterModuleConfigurator { 12 | 13 | func configure() -> (UIViewController, SettingsRouterModuleOutput) { 14 | let view = SettingsRouterViewController() 15 | let presenter = SettingsRouterPresenter() 16 | 17 | presenter.view = view 18 | view.output = presenter 19 | 20 | return (view, presenter) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/SettingsRouter/Presenter/SettingsRouterModuleInput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsRouterModuleInput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol SettingsRouterModuleInput: AnyObject { 10 | } 11 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/SettingsRouter/Presenter/SettingsRouterModuleOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsRouterModuleOutput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol SettingsRouterModuleOutput: AnyObject { 10 | } 11 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/SettingsRouter/Presenter/SettingsRouterPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsRouterPresenter.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | final class SettingsRouterPresenter: SettingsRouterModuleOutput { 10 | 11 | // MARK: - SettingsRouterModuleOutput 12 | 13 | // MARK: - Properties 14 | 15 | weak var view: SettingsRouterViewInput? 16 | 17 | } 18 | 19 | // MARK: - SettingsRouterModuleInput 20 | 21 | extension SettingsRouterPresenter: SettingsRouterModuleInput { 22 | } 23 | 24 | // MARK: - SettingsRouterViewOutput 25 | 26 | extension SettingsRouterPresenter: SettingsRouterViewOutput { 27 | 28 | func viewLoaded() { 29 | view?.setupInitialState() 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/SettingsRouter/View/SettingsRouterViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsRouterViewController.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Utils 11 | 12 | final class SettingsRouterViewController: UIViewController { 13 | 14 | // MARK: - IBOutlets 15 | 16 | @IBOutlet private weak var button: UIButton! 17 | 18 | // MARK: - Properties 19 | 20 | var output: SettingsRouterViewOutput? 21 | 22 | // MARK: - UIViewController 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | output?.viewLoaded() 27 | } 28 | 29 | } 30 | 31 | // MARK: - SettingsRouterViewInput 32 | 33 | extension SettingsRouterViewController: SettingsRouterViewInput { 34 | 35 | func setupInitialState() { 36 | configureButton() 37 | } 38 | 39 | } 40 | 41 | // MARK: - Private Methods 42 | 43 | private extension SettingsRouterViewController { 44 | 45 | func configureButton() { 46 | button.layer.cornerRadius = 10 47 | button.addTarget(self, 48 | action: #selector(self.didPressButton), 49 | for: .touchUpInside) 50 | } 51 | 52 | @objc 53 | func didPressButton() { 54 | SettingsRouter.openAppSettings() 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/SettingsRouter/View/SettingsRouterViewInput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsRouterViewInput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol SettingsRouterViewInput: AnyObject { 10 | /// Method for setup initial state of view 11 | func setupInitialState() 12 | } 13 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/SettingsRouter/View/SettingsRouterViewOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsRouterViewOutput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol SettingsRouterViewOutput { 10 | /// Notify presenter that view is ready 11 | func viewLoaded() 12 | } 13 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/SkeletonView/Configurator/SkeletonViewModuleConfigurator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SkeletonViewModuleConfigurator.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class SkeletonViewModuleConfigurator { 12 | 13 | func configure() -> (UIViewController, SkeletonViewModuleOutput) { 14 | let view = SkeletonViewViewController() 15 | let presenter = SkeletonViewPresenter() 16 | 17 | presenter.view = view 18 | view.output = presenter 19 | 20 | return (view, presenter) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/SkeletonView/Presenter/SkeletonViewModuleInput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SkeletonViewModuleInput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol SkeletonViewModuleInput: AnyObject { 10 | } 11 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/SkeletonView/Presenter/SkeletonViewModuleOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SkeletonViewModuleOutput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol SkeletonViewModuleOutput: AnyObject { 10 | } 11 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/SkeletonView/Presenter/SkeletonViewPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SkeletonViewPresenter.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | final class SkeletonViewPresenter: SkeletonViewModuleOutput { 10 | 11 | // MARK: - SkeletonViewModuleOutput 12 | 13 | // MARK: - Properties 14 | 15 | weak var view: SkeletonViewViewInput? 16 | 17 | } 18 | 19 | // MARK: - SkeletonViewModuleInput 20 | 21 | extension SkeletonViewPresenter: SkeletonViewModuleInput { 22 | } 23 | 24 | // MARK: - SkeletonViewViewOutput 25 | 26 | extension SkeletonViewPresenter: SkeletonViewViewOutput { 27 | 28 | func viewLoaded() { 29 | view?.setupInitialState() 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/SkeletonView/View/SkeletonViewViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SkeletonViewViewController.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Utils 11 | 12 | final class SkeletonViewViewController: UIViewController { 13 | 14 | // MARK: - IBOutlets 15 | 16 | @IBOutlet private weak var skeletonView: SkeletonView! 17 | @IBOutlet private weak var view1: UIView! 18 | @IBOutlet private weak var view2: UIView! 19 | @IBOutlet private weak var view3: UIView! 20 | @IBOutlet private weak var view4: UIView! 21 | 22 | // MARK: - Properties 23 | 24 | var output: SkeletonViewViewOutput? 25 | 26 | // MARK: - UIViewController 27 | 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | output?.viewLoaded() 31 | } 32 | 33 | } 34 | 35 | // MARK: - SkeletonViewViewInput 36 | 37 | extension SkeletonViewViewController: SkeletonViewViewInput { 38 | 39 | func setupInitialState() { 40 | configureSkeletonView() 41 | } 42 | 43 | } 44 | 45 | // MARK: - Private Methods 46 | 47 | private extension SkeletonViewViewController { 48 | 49 | func configureSkeletonView() { 50 | let views = [view1, view2, view3, view4] 51 | // Устанавливаем какие view(разрешены только subview данного skeletonView) 52 | // должны участвовать в анимации(по умолчанию все subviews) 53 | skeletonView.maskingViews = views.compactMap { view in 54 | view?.layer.cornerRadius = 10 55 | return view 56 | } 57 | 58 | // Направление в котором бегает шиммер(по умолчанию - вправо) 59 | skeletonView.direction = .left 60 | 61 | // Цвет, которым закрашиваются эти самые maskingViews 62 | skeletonView.gradientBackgroundColor = UIColor.lightGray 63 | 64 | // Цвет бегающего по ним шиммера 65 | skeletonView.gradientMovingColor = UIColor.green 66 | 67 | // Отношение ширины шиммера к ширине view. Допустимы значения в диапазоне [0.0, 1.0] 68 | skeletonView.shimmerRatio = 0.7 69 | 70 | // Длительность одного пробега шиммера в секундах 71 | skeletonView.movingAnimationDuration = 1.0 72 | 73 | // Длительность задержки между шагами анимации в секундах 74 | skeletonView.delayBetweenAnimationLoops = 1.0 75 | 76 | // Запускаем анимацию на всех вьюхах включенных в SkeletonView 77 | skeletonView.shimmering = true 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/SkeletonView/View/SkeletonViewViewInput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SkeletonViewViewInput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol SkeletonViewViewInput: AnyObject { 10 | /// Method for setup initial state of view 11 | func setupInitialState() 12 | } 13 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/SkeletonView/View/SkeletonViewViewOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SkeletonViewViewOutput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol SkeletonViewViewOutput { 10 | /// Notify presenter that view is ready 11 | func viewLoaded() 12 | } 13 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/StringAttributes/Configurator/StringAttributesModuleConfigurator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringAttributesModuleConfigurator.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 23/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class StringAttributesModuleConfigurator { 12 | 13 | func configure() -> (UIViewController, StringAttributesModuleOutput) { 14 | let view = StringAttributesViewController() 15 | let presenter = StringAttributesPresenter() 16 | 17 | presenter.view = view 18 | view.output = presenter 19 | 20 | return (view, presenter) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/StringAttributes/Presenter/StringAttributesModuleInput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringAttributesModuleInput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 23/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol StringAttributesModuleInput: AnyObject { } 10 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/StringAttributes/Presenter/StringAttributesModuleOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringAttributesModuleOutput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 23/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol StringAttributesModuleOutput: AnyObject { 10 | } 11 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/StringAttributes/Presenter/StringAttributesPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringAttributesPresenter.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 23/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | final class StringAttributesPresenter: StringAttributesModuleOutput { 10 | 11 | // MARK: - StringAttributesModuleOutput 12 | 13 | // MARK: - Properties 14 | 15 | weak var view: StringAttributesViewInput? 16 | 17 | } 18 | 19 | // MARK: - StringAttributesModuleInput 20 | 21 | extension StringAttributesPresenter: StringAttributesModuleInput { 22 | } 23 | 24 | // MARK: - StringAttributesViewOutput 25 | 26 | extension StringAttributesPresenter: StringAttributesViewOutput { 27 | 28 | func viewLoaded() { 29 | view?.setupInitialState() 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/StringAttributes/View/StringAttributesViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringAttributesViewController.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 23/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Utils 11 | 12 | final class StringAttributesViewController: UIViewController { 13 | 14 | // MARK: - IBOutlets 15 | 16 | @IBOutlet private weak var subTitle: UILabel! 17 | 18 | // MARK: - Properties 19 | 20 | var output: StringAttributesViewOutput? 21 | 22 | // MARK: - UIViewController 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | output?.viewLoaded() 27 | } 28 | 29 | } 30 | 31 | // MARK: - StringAttributesViewInput 32 | 33 | extension StringAttributesViewController: StringAttributesViewInput { 34 | 35 | func setupInitialState() { 36 | configureLabel() 37 | } 38 | 39 | } 40 | 41 | // MARK: - Private Methods 42 | 43 | private extension StringAttributesViewController { 44 | 45 | func configureLabel() { 46 | let globalSttributes: [StringAttribute] = [ 47 | .font(.systemFont(ofSize: 14)), 48 | .foregroundColor(.black) 49 | ] 50 | let attributedString = StringBuilder(globalAttributes: globalSttributes) 51 | .add(.string("Title")) 52 | .add(.delimeterWithString(repeatedDelimeter: .init(type: .space), 53 | string: "blue"), 54 | with: [.foregroundColor(.blue)]) 55 | .add(.delimeterWithString(repeatedDelimeter: .init(type: .lineBreak), 56 | string: "Base style on new line")) 57 | .add(.delimeterWithString(repeatedDelimeter: .init(type: .space), 58 | string: "last word with it's own style"), 59 | with: [.font(.boldSystemFont(ofSize: 16)), .foregroundColor(.red)]) 60 | .value 61 | subTitle.attributedText = attributedString 62 | subTitle.numberOfLines = 0 63 | subTitle.textAlignment = .center 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/StringAttributes/View/StringAttributesViewInput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringAttributesViewInput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 23/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol StringAttributesViewInput: AnyObject { 10 | /// Method for setup initial state of view 11 | func setupInitialState() 12 | } 13 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/StringAttributes/View/StringAttributesViewOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringAttributesViewOutput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 23/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol StringAttributesViewOutput { 10 | /// Notify presenter that view is ready 11 | func viewLoaded() 12 | } 13 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/UIDevice/Configurator/UIDeviceModuleConfigurator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIDeviceModuleConfigurator.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 23/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class UIDeviceModuleConfigurator { 12 | 13 | func configure() -> (UIViewController, UIDeviceModuleOutput) { 14 | let view = UIDeviceViewController() 15 | let presenter = UIDevicePresenter() 16 | 17 | presenter.view = view 18 | view.output = presenter 19 | 20 | return (view, presenter) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/UIDevice/Presenter/UIDeviceModuleInput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIDeviceModuleInput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 23/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol UIDeviceModuleInput: AnyObject { 10 | } 11 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/UIDevice/Presenter/UIDeviceModuleOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIDeviceModuleOutput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 23/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol UIDeviceModuleOutput: AnyObject { 10 | } 11 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/UIDevice/Presenter/UIDevicePresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIDevicePresenter.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 23/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | final class UIDevicePresenter: UIDeviceModuleOutput { 10 | 11 | // MARK: - UIDeviceModuleOutput 12 | 13 | // MARK: - Properties 14 | 15 | weak var view: UIDeviceViewInput? 16 | 17 | } 18 | 19 | // MARK: - UIDeviceModuleInput 20 | 21 | extension UIDevicePresenter: UIDeviceModuleInput { 22 | } 23 | 24 | // MARK: - UIDeviceViewOutput 25 | 26 | extension UIDevicePresenter: UIDeviceViewOutput { 27 | 28 | func viewLoaded() { 29 | view?.setupInitialState() 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/UIDevice/View/UIDeviceViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIDeviceViewController.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 23/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Utils 11 | 12 | final class UIDeviceViewController: UIViewController { 13 | 14 | // MARK: - IBOutlets 15 | 16 | @IBOutlet private weak var subTitle: UILabel! 17 | 18 | // MARK: - Properties 19 | 20 | var output: UIDeviceViewOutput? 21 | 22 | // MARK: - UIViewController 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | output?.viewLoaded() 27 | } 28 | 29 | } 30 | 31 | // MARK: - UIDeviceViewInput 32 | 33 | extension UIDeviceViewController: UIDeviceViewInput { 34 | 35 | func setupInitialState() { 36 | configureLabe() 37 | } 38 | 39 | } 40 | 41 | // MARK: - Private Methods 42 | 43 | private extension UIDeviceViewController { 44 | 45 | func configureLabe() { 46 | let text = [ 47 | "SmallPhone: \(UIDevice.isSmallPhone)", 48 | "\nNormalPhone: \(UIDevice.isNormalPhone)", 49 | "\nXPhone: \(UIDevice.isXPhone)", 50 | "\nPad: \(UIDevice.isPad)", 51 | "\nDevice version: \(Device.version())", 52 | "\nIs simulator: \(Device.isSimulator())" 53 | ] 54 | subTitle.text = text.reduce("", +) 55 | subTitle.numberOfLines = 0 56 | subTitle.textAlignment = .center 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/UIDevice/View/UIDeviceViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/UIDevice/View/UIDeviceViewInput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIDeviceViewInput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 23/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol UIDeviceViewInput: AnyObject { 10 | /// Method for setup initial state of view 11 | func setupInitialState() 12 | } 13 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/UIDevice/View/UIDeviceViewOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIDeviceViewOutput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 23/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol UIDeviceViewOutput { 10 | /// Notify presenter that view is ready 11 | func viewLoaded() 12 | } 13 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/WordDeclinationSelector/Configurator/WordDeclinationSelectorModuleConfigurator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WordDeclinationSelectorModuleConfigurator.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class WordDeclinationSelectorModuleConfigurator { 12 | 13 | func configure() -> (UIViewController, WordDeclinationSelectorModuleOutput) { 14 | let view = WordDeclinationSelectorViewController() 15 | let presenter = WordDeclinationSelectorPresenter() 16 | 17 | presenter.view = view 18 | view.output = presenter 19 | 20 | return (view, presenter) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/WordDeclinationSelector/Presenter/WordDeclinationSelectorModuleInput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WordDeclinationSelectorModuleInput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol WordDeclinationSelectorModuleInput: AnyObject { 10 | } 11 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/WordDeclinationSelector/Presenter/WordDeclinationSelectorModuleOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WordDeclinationSelectorModuleOutput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol WordDeclinationSelectorModuleOutput: AnyObject { 10 | } 11 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/WordDeclinationSelector/Presenter/WordDeclinationSelectorPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WordDeclinationSelectorPresenter.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | final class WordDeclinationSelectorPresenter: WordDeclinationSelectorModuleOutput { 10 | 11 | // MARK: - WordDeclinationSelectorModuleOutput 12 | 13 | // MARK: - Properties 14 | 15 | weak var view: WordDeclinationSelectorViewInput? 16 | 17 | } 18 | 19 | // MARK: - WordDeclinationSelectorModuleInput 20 | 21 | extension WordDeclinationSelectorPresenter: WordDeclinationSelectorModuleInput { 22 | } 23 | 24 | // MARK: - WordDeclinationSelectorViewOutput 25 | 26 | extension WordDeclinationSelectorPresenter: WordDeclinationSelectorViewOutput { 27 | 28 | func viewLoaded() { 29 | view?.setupInitialState() 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/WordDeclinationSelector/View/WordDeclinationSelectorViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WordDeclinationSelectorViewController.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Utils 11 | 12 | final class WordDeclinationSelectorViewController: UIViewController { 13 | 14 | // MARK: - IBOutlets 15 | 16 | @IBOutlet private weak var subTitle: UILabel! 17 | 18 | // MARK: - Properties 19 | 20 | var output: WordDeclinationSelectorViewOutput? 21 | 22 | // MARK: - UIViewController 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | output?.viewLoaded() 27 | } 28 | 29 | } 30 | 31 | // MARK: - WordDeclinationSelectorViewInput 32 | 33 | extension WordDeclinationSelectorViewController: WordDeclinationSelectorViewInput { 34 | 35 | func setupInitialState() { 36 | configureLabel() 37 | } 38 | 39 | } 40 | 41 | // MARK: - Private Methods 42 | 43 | private extension WordDeclinationSelectorViewController { 44 | 45 | func configureLabel() { 46 | let declinations = WordDeclinations("день", "дня", "дней") 47 | let example = [1, 2, 6] 48 | .map { number -> String in 49 | let form = WordDeclinationSelector.declineWord(for: number, from: declinations) 50 | return "\(number) \(form)" 51 | }.joined(separator: "\n") 52 | subTitle.text = example 53 | subTitle.numberOfLines = 0 54 | subTitle.textAlignment = .center 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/WordDeclinationSelector/View/WordDeclinationSelectorViewInput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WordDeclinationSelectorViewInput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol WordDeclinationSelectorViewInput: AnyObject { 10 | /// Method for setup initial state of view 11 | func setupInitialState() 12 | } 13 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Flows/WordDeclinationSelector/View/WordDeclinationSelectorViewOutput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WordDeclinationSelectorViewOutput.swift 3 | // UtilsExample 4 | // 5 | // Created by Evgeny Vasilev on 24/06/2022. 6 | // Copyright © 2022 Surf. All rights reserved. 7 | // 8 | 9 | protocol WordDeclinationSelectorViewOutput { 10 | /// Notify presenter that view is ready 11 | func viewLoaded() 12 | } 13 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Library/Extensions/Presentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Presentable.swift 3 | // UtilsExample 4 | // 5 | // Created by Евгений Васильев on 23.06.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | /// Describes object that can be presented in view hierarchy 11 | protocol Presentable { 12 | func toPresent() -> UIViewController? 13 | } 14 | 15 | extension UIViewController: Presentable { 16 | 17 | func toPresent() -> UIViewController? { 18 | return self 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Library/Extensions/UIApplication.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIApplication.swift 3 | // UtilsExample 4 | // 5 | // Created by Евгений Васильев on 23.06.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIApplication { 11 | 12 | // MARK: - Static Properties 13 | 14 | static var rootView: UIViewController? { 15 | return UIApplication 16 | .shared 17 | .connectedScenes 18 | .flatMap { ($0 as? UIWindowScene)?.windows ?? [] } 19 | .first { $0.isKeyWindow }? 20 | .rootViewController 21 | } 22 | 23 | static var firstKeyWindow: UIWindow? { 24 | return UIApplication 25 | .shared 26 | .connectedScenes 27 | .flatMap { ($0 as? UIWindowScene)?.windows ?? [] } 28 | .first { $0.isKeyWindow } 29 | } 30 | 31 | // MARK: - Static Methods 32 | 33 | static func topViewController( 34 | _ controller: UIViewController? = rootView 35 | ) -> UIViewController? { 36 | if let navigationController = controller as? UINavigationController { 37 | return topViewController(navigationController.visibleViewController) 38 | } 39 | if let tabController = controller as? UITabBarController { 40 | if let selected = tabController.selectedViewController { 41 | return topViewController(selected) 42 | } 43 | } 44 | if let presented = controller?.presentedViewController { 45 | return topViewController(presented) 46 | } 47 | return controller 48 | } 49 | 50 | static func appVersion() -> String? { 51 | let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String 52 | return appVersion 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/Library/Router/Router.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Router.swift 3 | // UtilsExample 4 | // 5 | // Created by Евгений Васильев on 23.06.2022. 6 | // 7 | 8 | /// Describes object that handles all navigation operations 9 | protocol Router { 10 | func present(_ module: Presentable?) 11 | func present(_ module: Presentable?, animated: Bool, completion: (() -> Void)?) 12 | 13 | func push(_ module: Presentable?) 14 | func push(_ module: Presentable?, animated: Bool) 15 | 16 | func popModule() 17 | func popModule(animated: Bool) 18 | func popPreviousView() 19 | func popToRoot(animated: Bool) 20 | 21 | func dismissModule() 22 | func dismissModule(from module: Presentable?) 23 | func dismissModule(animated: Bool, completion: (() -> Void)?) 24 | func dismissAll(animated: Bool, completion: (() -> Void)?) 25 | 26 | func setNavigationControllerRootModule(_ module: Presentable?, animated: Bool, hideBar: Bool) 27 | func setRootModule(_ module: Presentable?) 28 | 29 | func setTab(_ index: Int) 30 | } 31 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExample/Playbook/UIKitPages/MainPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestUIKitPage.swift 3 | // UtilsExample 4 | // 5 | // Created by Евгений Васильев on 21.06.2022. 6 | // 7 | 8 | import UIKit 9 | import SurfPlaybook 10 | 11 | final class MainPage: PlaybookUIKitPage { 12 | 13 | var id: String { 14 | return "MainPage" 15 | } 16 | 17 | var name: String { 18 | return "Main page" 19 | } 20 | 21 | var viewModel: UIKitPageViewModel { 22 | return TestUIKitPageViewModel() 23 | } 24 | 25 | } 26 | 27 | final class TestUIKitPageViewModel: NSObject, UIKitPageViewModel { 28 | 29 | func setup(with tableView: UITableView) { 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExampleTests/UtilsExampleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UtilsExampleTests.swift 3 | // UtilsExampleTests 4 | // 5 | // Created by chausov on 09.06.2022. 6 | // 7 | 8 | import XCTest 9 | @testable import UtilsExample 10 | 11 | class UtilsExampleTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { } 14 | 15 | override func tearDownWithError() throws { } 16 | 17 | func testExample() throws { } 18 | 19 | func testPerformanceExample() throws { 20 | self.measure { 21 | } 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExampleUITests/UtilsExampleUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UtilsExampleUITests.swift 3 | // UtilsExampleUITests 4 | // 5 | // Created by chausov on 09.06.2022. 6 | // 7 | 8 | import XCTest 9 | 10 | class UtilsExampleUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | continueAfterFailure = false 14 | } 15 | 16 | override func tearDownWithError() throws { } 17 | 18 | func testExample() throws { 19 | let app = XCUIApplication() 20 | app.launch() 21 | } 22 | 23 | func testLaunchPerformance() throws { 24 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 25 | measure(metrics: [XCTApplicationLaunchMetric()]) { 26 | XCUIApplication().launch() 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /UtilsExample/UtilsExampleUITests/UtilsExampleUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UtilsExampleUITestsLaunchTests.swift 3 | // UtilsExampleUITests 4 | // 5 | // Created by chausov on 09.06.2022. 6 | // 7 | 8 | import XCTest 9 | 10 | class UtilsExampleUITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | func testLaunch() throws { 21 | let app = XCUIApplication() 22 | app.launch() 23 | 24 | // Insert steps here to perform after app launch but before taking a screenshot, 25 | // such as logging into a test account or navigating somewhere in the app 26 | 27 | let attachment = XCTAttachment(screenshot: app.screenshot()) 28 | attachment.name = "Launch Screen" 29 | attachment.lifetime = .keepAlways 30 | add(attachment) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /UtilsTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /UtilsTests/LocalStorageTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalStorageTests.swift 3 | // UtilsTests 4 | // 5 | // Created by Anton Dryakhlykh on 11.11.2019. 6 | // Copyright © 2019 Surf. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Utils 11 | 12 | struct User: Codable, Equatable { 13 | let id: Int 14 | let name: String 15 | } 16 | 17 | struct Time: Codable { 18 | let seconds: Double 19 | } 20 | 21 | final class LocalStorageTests: XCTestCase { 22 | 23 | // MARK: - Test methods 24 | 25 | func testStoring() { 26 | 27 | // given 28 | 29 | let user = User(id: 3, name: "Jesus") 30 | let filename = "testStoring" 31 | 32 | // when 33 | 34 | try? LocalStorage.store(object: user, as: filename) 35 | 36 | // then 37 | 38 | XCTAssertNotNil(try LocalStorage.load(fileName: filename, as: User.self)) 39 | } 40 | 41 | func testLoading() { 42 | 43 | // given 44 | 45 | let user = User(id: 3, name: "Jesus") 46 | let filename = "testLoading" 47 | 48 | // when 49 | 50 | try? LocalStorage.store(object: user, as: filename) 51 | let loadedUser = try? LocalStorage.load(fileName: filename, as: User.self) 52 | 53 | // then 54 | 55 | XCTAssertEqual(loadedUser, user) 56 | } 57 | 58 | func testRemoving() { 59 | 60 | // given 61 | 62 | let user = User(id: 3, name: "Jesus") 63 | let filename = "testRemoving" 64 | var error: Error? 65 | 66 | // when 67 | 68 | try? LocalStorage.store(object: user, as: filename) 69 | try? LocalStorage.remove(fileName: filename) 70 | 71 | // then 72 | 73 | XCTAssertThrowsError(try LocalStorage.load(fileName: filename, as: User.self)) { 74 | error = $0 75 | } 76 | 77 | XCTAssertTrue(error is LocalStorage.Error.Load) 78 | XCTAssertEqual(error as? LocalStorage.Error.Load, .fileNotExist) 79 | } 80 | 81 | // MARK: - Test error catching 82 | 83 | func testStoringErrorCannotEncode() { 84 | 85 | // given 86 | 87 | let time = Time(seconds: .infinity) 88 | let filename = "testStoringErrorCannotEncode" 89 | var error: Error? 90 | 91 | // when 92 | 93 | XCTAssertThrowsError(try LocalStorage.store(object: time, as: filename)) { 94 | error = $0 95 | } 96 | 97 | // then 98 | 99 | XCTAssertTrue(error is LocalStorage.Error.Store) 100 | XCTAssertEqual(error as? LocalStorage.Error.Store, .cannotEncode) 101 | } 102 | 103 | func testLoadingErrorCannotDecode() { 104 | 105 | // given 106 | 107 | let user = User(id: 3, name: "Jesus") 108 | let filename = "testLoadingErrorCannotDecode" 109 | var error: Error? 110 | 111 | // when 112 | 113 | try? LocalStorage.store(object: user, as: filename) 114 | 115 | XCTAssertThrowsError(try LocalStorage.load(fileName: filename, as: Time.self)) { 116 | error = $0 117 | } 118 | 119 | // then 120 | 121 | XCTAssertTrue(error is LocalStorage.Error.Load) 122 | XCTAssertEqual(error as? LocalStorage.Error.Load, .cannotDecode) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /UtilsTests/RouteMeasurerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouteMeasurerTests.swift 3 | // UtilsTests 4 | // 5 | // Created by Александр Чаусов on 05/01/2019. 6 | // Copyright © 2019 Surf. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Utils 11 | 12 | final class RouteMeasurerTests: XCTestCase { 13 | 14 | // MARK: - Constants 15 | 16 | private let meterPattern = "м" 17 | private let kilometrPattern = "км" 18 | 19 | // MARK: - Tests 20 | 21 | func testNegativeDistanceFormat() { 22 | let result = RouteMeasurer.formatDistance(-5, meterPattern: meterPattern, kilometrPatter: kilometrPattern) 23 | XCTAssertEqual(result, "0 " + meterPattern) 24 | } 25 | 26 | func testZeroDistanceFormat() { 27 | let result = RouteMeasurer.formatDistance(0, meterPattern: meterPattern, kilometrPatter: kilometrPattern) 28 | XCTAssertEqual(result, "0 " + meterPattern) 29 | } 30 | 31 | func testSmallDistanceFormat() { 32 | let result = RouteMeasurer.formatDistance(123, meterPattern: meterPattern, kilometrPatter: kilometrPattern) 33 | XCTAssertEqual(result, "123 " + meterPattern) 34 | } 35 | 36 | func testBigDistanceFormat() { 37 | let result = RouteMeasurer.formatDistance(12310, meterPattern: meterPattern, kilometrPatter: kilometrPattern) 38 | XCTAssertEqual(result, "12.3 " + kilometrPattern) 39 | } 40 | 41 | func testHugeDistanceFormat() { 42 | let result = RouteMeasurer.formatDistance(104123, meterPattern: meterPattern, kilometrPatter: kilometrPattern) 43 | XCTAssertEqual(result, "104 " + kilometrPattern) 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /UtilsTests/SecurityService/KeyChainSecureStoreTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyChainSecureStoreTests.swift 3 | // UtilsTests 4 | // 5 | // Created by Никита Гагаринов on 03.11.2020. 6 | // Copyright © 2020 Surf. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Foundation 11 | 12 | @testable import Utils 13 | 14 | final class KeyChainSecureStoreTests: XCTestCase { 15 | 16 | var store = KeyChainSecureStore(secureStoreQueryable: GenericPasswordQueryable(service: "")) 17 | 18 | override func tearDown() { 19 | try? self.store.removeAll() 20 | super.tearDown() 21 | } 22 | 23 | func testSaveGenericPasswordNotThrow() { 24 | do { 25 | try self.store.save(data: "pwd_1234", by: "testSaveGenericPasswordNotThrow") 26 | } catch { 27 | XCTFail("\(error)") 28 | } 29 | } 30 | 31 | func testReadGenericPassword() { 32 | 33 | // Arrange 34 | 35 | let data = "123" 36 | let key = "testReadGenericPassword" 37 | 38 | // Act-Assert 39 | 40 | do { 41 | try self.store.save(data: data, by: key) 42 | let readed = try self.store.load(by: key) 43 | XCTAssertEqual(data, readed) 44 | } catch { 45 | XCTFail("\(error)") 46 | } 47 | } 48 | 49 | func testUpdateGenericPassword() { 50 | 51 | // Arrange 52 | 53 | let data1 = "123" 54 | let data2 = "asd" 55 | let key = "testUpdateGenericPassword" 56 | 57 | // Act-Assert 58 | 59 | do { 60 | try self.store.save(data: data1, by: key) 61 | try self.store.save(data: data2, by: key) 62 | let password = try self.store.load(by: key) 63 | XCTAssertEqual(data2, password) 64 | } catch { 65 | XCTFail("\(error)") 66 | } 67 | } 68 | 69 | func testRemoveGenericPassword() { 70 | 71 | // Arrange 72 | 73 | let data = "123" 74 | let key = "testRemoveGenericPassword" 75 | 76 | // Act-Assert 77 | 78 | do { 79 | try self.store.save(data: data, by: key) 80 | try self.store.remove(by: key) 81 | XCTAssertThrowsError(try self.store.load(by: key)) 82 | } catch { 83 | XCTFail("\(error)") 84 | } 85 | } 86 | 87 | func testRemoveAllGenericPasswords() { 88 | 89 | // Arrange 90 | 91 | let data = "123" 92 | let key1 = "testRemoveAllGenericPasswords1" 93 | let key2 = "testRemoveAllGenericPasswords2" 94 | 95 | // Act-Assert 96 | 97 | do { 98 | try self.store.save(data: data, by: key1) 99 | try self.store.save(data: data, by: key2) 100 | try self.store.removeAll() 101 | XCTAssertThrowsError(try self.store.load(by: key1)) 102 | XCTAssertThrowsError(try self.store.load(by: key2)) 103 | } catch { 104 | XCTFail("\(error)") 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /UtilsTests/SecurityService/PinCryptoBoxTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PinCryptoBoxTests.swift 3 | // UtilsTests 4 | // 5 | // Created by Никита Гагаринов on 03.11.2020. 6 | // Copyright © 2020 Surf. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import CryptoSwift 11 | import Foundation 12 | 13 | @testable import Utils 14 | 15 | final class PinCryptoBoxTests: XCTestCase { 16 | 17 | private let cryptoBox = PinCryptoBox(secureStore: { KeyChainSecureStore( 18 | secureStoreQueryable: GenericPasswordQueryable(service: "")) }, 19 | hashProvider: SHA3(variant: .sha224), 20 | cryptoService: BlowfishCryptoService(), 21 | ivKey: "ivKey", 22 | dataKey: "dataKey", 23 | saltKey: "saltKey", 24 | hashKey: "hashKey") 25 | 26 | func testCryptoBoxCanEncryptWithouErrors() { 27 | 28 | // Arrange 29 | 30 | let data = "123" 31 | let key = "123" 32 | 33 | // Act - Assert 34 | 35 | XCTAssertNoThrow(try cryptoBox.encrypt(data: data, auth: key)) 36 | } 37 | 38 | func testCryptoBoxCanDecryptWithouErrors() { 39 | 40 | // Arrange 41 | 42 | let data = "123" 43 | let key = "123" 44 | 45 | // Act - Assert 46 | 47 | XCTAssertNoThrow(try cryptoBox.encrypt(data: data, auth: key)) 48 | XCTAssertNoThrow(try cryptoBox.decrypt(auth: key)) 49 | } 50 | 51 | func testCryptoBoxCanEnryptRight() { 52 | 53 | // Arrange 54 | 55 | let data = "123" 56 | let key = "123" 57 | 58 | // Act 59 | 60 | try? cryptoBox.encrypt(data: data, auth: key) 61 | 62 | // Assert 63 | 64 | XCTAssertEqual(data, try? cryptoBox.decrypt(auth: key)) 65 | } 66 | 67 | func testCryptoBoxThrowsErrorForEmptyStore() { 68 | 69 | // Arrange 70 | 71 | let key = "123" 72 | 73 | // Act 74 | 75 | try? KeyChainSecureStore(secureStoreQueryable: GenericPasswordQueryable(service: "")).removeAll() 76 | 77 | // Assert 78 | 79 | XCTAssertThrowsError(try cryptoBox.decrypt(auth: key)) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /UtilsTests/SecurityService/PinHackCryptoBoxTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PinHackCryptoBoxTests.swift 3 | // UtilsTests 4 | // 5 | // Created by Никита Гагаринов on 03.11.2020. 6 | // Copyright © 2020 Surf. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import CryptoSwift 11 | import Foundation 12 | 13 | @testable import Utils 14 | 15 | final class PinHackCryptoBoxTests: XCTestCase { 16 | 17 | private let cryptoBox = PinCryptoBox(secureStore: { KeyChainSecureStore( 18 | secureStoreQueryable: GenericPasswordQueryable(service: "")) }, 19 | hashProvider: SHA3(variant: .sha224), 20 | cryptoService: BlowfishCryptoService(), 21 | ivKey: "ivKey", 22 | dataKey: "dataKey", 23 | saltKey: "saltKey", 24 | hashKey: "hashKey") 25 | private let hackBox = PinCryptoBox(secureStore: { KeyChainSecureStore( 26 | secureStoreQueryable: GenericPasswordQueryable(service: "")) }, 27 | hashProvider: SHA3(variant: .sha224), 28 | cryptoService: BlowfishCryptoService(), 29 | ivKey: "ivKey", 30 | dataKey: "dataKey", 31 | saltKey: "saltKey", 32 | hashKey: "hashKey").hack() 33 | 34 | private let data = "123" 35 | private let newData = "asd" 36 | private let key = "123" 37 | 38 | override func setUp() { 39 | super.setUp() 40 | try? self.cryptoBox.encrypt(data: self.data, auth: self.key) 41 | } 42 | 43 | func testCryptoBoxCanDecryptWithouErrors() { 44 | XCTAssertNoThrow(try hackBox.decrypt()) 45 | } 46 | 47 | func testCryptoBoxCanDecryptRight() { 48 | XCTAssertEqual(self.data, try? self.hackBox.decrypt()) 49 | } 50 | 51 | func testCryptoBoxCanEncryptRight() { 52 | try? hackBox.encrypt(data: self.newData) 53 | XCTAssertEqual(self.newData, try? self.cryptoBox.decrypt(auth: self.key)) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /UtilsTests/WordDeclinationSelectorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WordDeclinationSelectorTests.swift 3 | // UtilsTests 4 | // 5 | // Created by Александр Чаусов on 05/01/2019. 6 | // Copyright © 2019 Surf. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Utils 11 | 12 | final class WordDeclinationSelectorTests: XCTestCase { 13 | 14 | // MARK: - Constants 15 | 16 | private let form1 = "книга" 17 | private let form2 = "книги" 18 | private let form3 = "книг" 19 | 20 | // MARK: - Tests 21 | 22 | func testDeclinationFor11_19() { 23 | testWordDeclinationSelector(sequence: [11, 12, 13, 14, 15, 16, 17, 18, 19, 115, 1018, 10912], 24 | correctForm: form3) 25 | } 26 | 27 | func testDeclinationForEndsIn1() { 28 | testWordDeclinationSelector(sequence: [1, 31, 141, 1941, 1081], 29 | correctForm: form1) 30 | } 31 | 32 | func testDeclinationForEndsIn2_4() { 33 | testWordDeclinationSelector(sequence: [2, 23, 54, 142, 5683, 49654], 34 | correctForm: form2) 35 | } 36 | 37 | func testDeclinationForEndsIn5_0() { 38 | testWordDeclinationSelector(sequence: [0, 5, 6, 7, 8, 9, 20, 50, 75, 106, 4267, 1088, 55439, 100000], 39 | correctForm: form3) 40 | } 41 | 42 | } 43 | 44 | private extension WordDeclinationSelectorTests { 45 | 46 | func testWordDeclinationSelector(sequence: S, correctForm: String) { 47 | for number in sequence.compactMap({ $0 as? Int }) { 48 | let result = WordDeclinationSelector.declineWord(for: number, from: WordDeclinations(form1, form2, form3)) 49 | XCTAssertEqual(result, correctForm) 50 | } 51 | } 52 | 53 | } 54 | --------------------------------------------------------------------------------