├── .gitignore ├── .travis.yml ├── LICENSE ├── Project-iOS ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cartfile.private ├── Gemfile ├── Gemfile.lock └── XLProjectName │ ├── .swift-version │ ├── .swiftlint.yml │ ├── Podfile │ ├── Tests │ ├── Info.plist │ └── Tests.swift │ ├── UITests │ ├── Info.plist │ └── UITests.swift │ ├── XLProjectName.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── XLProjectName.xcscheme │ ├── XLProjectName.xcworkspace │ └── contents.xcworkspacedata │ └── XLProjectName │ ├── AppDelegate.swift │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ ├── Main.storyboard │ └── SearchRepository.storyboard │ ├── Controllers │ ├── .gitkeep │ ├── LoginController.swift │ ├── RepositoryController.swift │ └── SearchRepositoryController.swift │ ├── Helpers │ ├── .gitkeep │ ├── Constants.swift │ ├── Controllers │ │ ├── XLTableViewController.swift │ │ └── XLViewController.swift │ ├── Extensions │ │ ├── AppDelegate+XLProjectName.swift │ │ ├── Date.swift │ │ ├── KeyedDecodingContainer.swift │ │ ├── UIApplication.swift │ │ ├── UIDevice.swift │ │ ├── UIImageView.swift │ │ └── UIViewController.swift │ ├── Helpers.swift │ ├── Opera │ │ ├── Error.swift │ │ ├── Helpers-iOS.swift │ │ ├── Helpers │ │ │ └── OperaHelpers.swift │ │ ├── Pagination │ │ │ ├── FilterType.swift │ │ │ ├── PaginationRequest.swift │ │ │ ├── PaginationRequestType+Rx.swift │ │ │ ├── PaginationRequestType.swift │ │ │ ├── PaginationRequestTypeSettings.swift │ │ │ ├── PaginationResponse.swift │ │ │ ├── PaginationResponseType.swift │ │ │ ├── PaginationViewModel.swift │ │ │ ├── WebLinking.swift │ │ │ └── WebLinkingSettings.swift │ │ ├── RouteType.swift │ │ └── RxManager.swift │ ├── R-Swift │ │ └── .gitignore │ └── WebLinking.swift │ ├── Models │ ├── .gitkeep │ ├── RepositoryModel.swift │ └── UserModel.swift │ ├── Networking │ ├── Alamofire │ │ ├── LoggerEventMonitor.swift │ │ └── RequestAdapter.swift │ ├── Manager.swift │ ├── Routes │ │ ├── DeviceRoute.swift │ │ ├── RepositoryRoute.swift │ │ └── UserRoute.swift │ ├── SessionController.swift │ └── URLRequestSetup.swift │ ├── Supporting Files │ ├── Info-Staging.plist │ └── Info.plist │ └── Views │ └── LoadingIndicator │ ├── LoadingIndicator.swift │ └── LoadingIndicatorManager.swift ├── README.md ├── readme-image.png └── shell.swift /.gitignore: -------------------------------------------------------------------------------- 1 | ## OS X Finder 2 | .DS_Store 3 | 4 | ## Build generated 5 | build/ 6 | DerivedData 7 | 8 | ## Various settings 9 | *.pbxuser 10 | !default.pbxuser 11 | *.mode1v3 12 | !default.mode1v3 13 | *.mode2v3 14 | !default.mode2v3 15 | *.perspectivev3 16 | !default.perspectivev3 17 | xcuserdata 18 | 19 | ## Other 20 | *.xccheckout 21 | *.moved-aside 22 | *.xcuserstate 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | 29 | ## Playgrounds 30 | timeline.xctimeline 31 | playground.xcworkspace 32 | 33 | # Swift Package Manager 34 | # 35 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 36 | # Packages/ 37 | .build/ 38 | 39 | # CocoaPods 40 | # 41 | # We recommend against adding the Pods directory to your .gitignore. However 42 | # you should judge for yourself, the pros and cons are mentioned at: 43 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 44 | # 45 | Project-iOS/Pods 46 | 47 | # Carthage 48 | # 49 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 50 | # Carthage/Checkouts 51 | 52 | Project-iOS/Cartfile.resolved 53 | Project-iOS/Carthage 54 | 55 | Carthage/Build 56 | Project-iOS/XLProjectName/Podfile.lock 57 | 58 | # Remove R.swift generated file 59 | **/R.generated.swift 60 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode11.3 3 | 4 | before_install: 5 | - brew update 6 | - brew outdated carthage || brew upgrade carthage 7 | - if brew outdated | grep -qx xctool; then brew upgrade xctool; fi 8 | - gem install xcpretty --no-document --quiet 9 | - cd Project-iOS/XLProjectName && bundle install && pod repo update && pod install 10 | 11 | script: 12 | - xctool clean build -workspace XLProjectName.xcworkspace -scheme XLProjectName -sdk iphonesimulator | xcpretty 13 | - xcodebuild test -workspace XLProjectName.xcworkspace -scheme XLProjectName -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 6S,OS=9.3' | xcpretty 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 xmartlabs 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 | -------------------------------------------------------------------------------- /Project-iOS/.gitignore: -------------------------------------------------------------------------------- 1 | ## OS X Finder 2 | .DS_Store 3 | 4 | ## Build generated 5 | build/ 6 | DerivedData 7 | 8 | ## Various settings 9 | *.pbxuser 10 | !default.pbxuser 11 | *.mode1v3 12 | !default.mode1v3 13 | *.mode2v3 14 | !default.mode2v3 15 | *.perspectivev3 16 | !default.perspectivev3 17 | xcuserdata 18 | 19 | ## Other 20 | *.xccheckout 21 | *.moved-aside 22 | *.xcuserstate 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | 29 | ## Playgrounds 30 | timeline.xctimeline 31 | playground.xcworkspace 32 | 33 | # Swift Package Manager 34 | # 35 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 36 | # Packages/ 37 | .build/ 38 | 39 | # CocoaPods 40 | # 41 | # We recommend against adding the Pods directory to your .gitignore. However 42 | # you should judge for yourself, the pros and cons are mentioned at: 43 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 44 | # 45 | Pods/ 46 | 47 | # Carthage 48 | # 49 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 50 | # Carthage/Checkouts 51 | 52 | Carthage/Build 53 | 54 | # Remove R.swift generated file 55 | **/R.generated.swift 56 | -------------------------------------------------------------------------------- /Project-iOS/.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode11.3 3 | env: 4 | - DESTINATION="OS=13.3,name=iPhone 11" SCHEME="XLProjectName" SDK=iphonesimulator13.2 5 | 6 | before_install: 7 | - brew update 8 | - brew outdated carthage || brew upgrade carthage 9 | - carthage update --platform iOS 10 | - gem install xcpretty --no-document --quiet 11 | 12 | script: 13 | - xcodebuild clean build -workspace XLProjectName/XLProjectName.xcworkspace -scheme "$SCHEME" -sdk "$SDK" 14 | - xcodebuild test -workspace XLProjectName/XLProjectName.xcworkspace -scheme "$SCHEME" -sdk "$SDK" -destination "$DESTINATION" | xcpretty -c 15 | -------------------------------------------------------------------------------- /Project-iOS/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to XLProductName will be documented in this file. 3 | 4 | ### [1.0.0](https://github.com/XLUserName/XLProductName/releases/tag/1.0.0) 5 | 6 | 7 | * This is the initial version. 8 | 9 | [xmartlabs]: https://xmartlabs.com 10 | -------------------------------------------------------------------------------- /Project-iOS/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing Guidelines 2 | -------------------------------------------------- 3 | 4 | This document provides general guidelines about how to contribute to the project. Keep in mind these important things before you start contributing. 5 | 6 | ### Reporting issues 7 | 8 | * Use [github issues](https://github.com/XLUserName/XLProductName/issues) to report a bug. 9 | * Before creating a new issue: 10 | * Make sure you are working with the [latest release](https://github.com/XLUserName/XLProductName/releases). 11 | * Check if the issue was [already reported or fixed](https://github.com/XLUserName/XLProductName/issues?utf8=%E2%9C%93&q=is%3Aissue). Notice that it may not be released yet. 12 | * If you found a match add a brief comment "I have the same problem" or "+1". This helps prioritize the issues addressing the most common and critical first. If possible add additional information to help us reproduce and fix the issue. Please use your best judgement. 13 | * Reporting issues: 14 | * Please include the following information to help maintainers to fix the problem faster: 15 | * Device model. 16 | * iOS version you are targeting. 17 | * Full Xcode console output of stack trace or code compilation error. 18 | * Any other additional detail you think it would be useful to understand and solve the problem. 19 | 20 | 21 | ### Pull requests 22 | 23 | * Add test coverage to the feature or fix. We only accept new feature pull requests that have related test coverage. This allows us to keep the the project stable as we move forward. 24 | * Remember to document the new feature. We do not accept new feature pull requests without its associated documentation. 25 | * Please only one fix or feature per pull request. This will increase the chances your feature will be merged. 26 | 27 | 28 | ###### Suggested git workflow to contribute 29 | 30 | 1. Clone your forked project into your developer machine: `git clone git@github.com:XLUserName/XLProductName.git` 31 | 2. Before starting a new feature make sure you have the latest master branch changes. `git checkout master` and then `git pull origin master`. 32 | 3. Create a new branch. Note that the starting point is the upstream master branch HEAD. `git checkout -b my-feature-name` 33 | 4. Stage all your changes `git add .` and commit them `git commit -m "Your commit message"` 34 | 5. Make sure your branch is up to date with upstream master, `git pull --rebase origin master`, resolve conflicts if necessary. This will move your commit to the top of git stack. 35 | 6. Squash your commits into one commit. `git rebase -i HEAD~6` considering you did 6 commits. 36 | 7. Push your branch into origin remote repository. 37 | 8. Create a new pull request adding any useful comment. 38 | 39 | 40 | ###### Code style and conventions 41 | 42 | We try to follow our [swift style guide](https://github.com/XLUserName/Swift-Style-Guide). 43 | 44 | 45 | ### Feature proposal 46 | 47 | We would love to hear your ideas and make a discussions about it. 48 | 49 | * Use github issues to make feature proposals. 50 | * We use `type: feature request` label to mark all [feature request issues](https://github.com/XLUserName/XLProductName/labels/type%3A%20feature%20request). 51 | * Before submitting your proposal make sure there is no similar feature request. If you found a match feel free to join the discussion or just add a brief "+1" if you think the feature is worth implementing. 52 | * Be as specific as possible providing a precise explanation of feature request so anyone can understand the problem and the benefits of solving it. 53 | -------------------------------------------------------------------------------- /Project-iOS/Cartfile.private: -------------------------------------------------------------------------------- 1 | github "Quick/Quick" 2 | github "Quick/Nimble" 3 | -------------------------------------------------------------------------------- /Project-iOS/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'cocoapods' 4 | gem 'xcpretty' 5 | -------------------------------------------------------------------------------- /Project-iOS/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (2.3.5) 5 | activesupport (4.2.9) 6 | i18n (~> 0.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | claide (1.0.2) 11 | cocoapods (1.2.1) 12 | activesupport (>= 4.0.2, < 5) 13 | claide (>= 1.0.1, < 2.0) 14 | cocoapods-core (= 1.2.1) 15 | cocoapods-deintegrate (>= 1.0.1, < 2.0) 16 | cocoapods-downloader (>= 1.1.3, < 2.0) 17 | cocoapods-plugins (>= 1.0.0, < 2.0) 18 | cocoapods-search (>= 1.0.0, < 2.0) 19 | cocoapods-stats (>= 1.0.0, < 2.0) 20 | cocoapods-trunk (>= 1.2.0, < 2.0) 21 | cocoapods-try (>= 1.1.0, < 2.0) 22 | colored2 (~> 3.1) 23 | escape (~> 0.0.4) 24 | fourflusher (~> 2.0.1) 25 | gh_inspector (~> 1.0) 26 | molinillo (~> 0.5.7) 27 | nap (~> 1.0) 28 | ruby-macho (~> 1.1) 29 | xcodeproj (>= 1.4.4, < 2.0) 30 | cocoapods-core (1.2.1) 31 | activesupport (>= 4.0.2, < 5) 32 | fuzzy_match (~> 2.0.4) 33 | nap (~> 1.0) 34 | cocoapods-deintegrate (1.0.1) 35 | cocoapods-downloader (1.1.3) 36 | cocoapods-plugins (1.0.0) 37 | nap 38 | cocoapods-search (1.0.0) 39 | cocoapods-stats (1.0.0) 40 | cocoapods-trunk (1.2.0) 41 | nap (>= 0.8, < 2.0) 42 | netrc (= 0.7.8) 43 | cocoapods-try (1.1.0) 44 | colored2 (3.1.2) 45 | escape (0.0.4) 46 | fourflusher (2.0.1) 47 | fuzzy_match (2.0.4) 48 | gh_inspector (1.0.3) 49 | i18n (0.8.6) 50 | minitest (5.10.2) 51 | molinillo (0.5.7) 52 | nanaimo (0.2.3) 53 | nap (1.1.0) 54 | netrc (0.7.8) 55 | rouge (2.0.7) 56 | ruby-macho (1.1.0) 57 | thread_safe (0.3.6) 58 | tzinfo (1.2.3) 59 | thread_safe (~> 0.1) 60 | xcodeproj (1.5.0) 61 | CFPropertyList (~> 2.3.3) 62 | claide (>= 1.0.2, < 2.0) 63 | colored2 (~> 3.1) 64 | nanaimo (~> 0.2.3) 65 | xcpretty (0.2.8) 66 | rouge (~> 2.0.7) 67 | 68 | PLATFORMS 69 | ruby 70 | 71 | DEPENDENCIES 72 | cocoapods 73 | xcpretty 74 | 75 | BUNDLED WITH 76 | 1.15.2 77 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/.swift-version: -------------------------------------------------------------------------------- 1 | 4.2 2 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: # rule identifiers to exclude from running 2 | # - colon 3 | # - comma 4 | # - control_statement 5 | # - file_length 6 | # - force_cast 7 | - force_try 8 | # - function_body_length 9 | # - leading_whitespace 10 | # - line_length 11 | # - nesting 12 | # - operator_whitespace 13 | # - opening_brace 14 | # - return_arrow_whitespace 15 | # - statement_position 16 | # - todo 17 | # - trailing_newline 18 | # - trailing_semicolon 19 | - trailing_whitespace 20 | # - type_body_length 21 | # - type_name 22 | - valid_docs 23 | - variable_name 24 | - variable_name_min_length 25 | # - variable_name_max_length 26 | excluded: # paths to ignore during linting. overridden by `included`. 27 | - Carthage 28 | - Pods 29 | - fastlane 30 | - XLProjectName/Helpers/R-Swift/R.generated.swift 31 | # parameterized rules can be customized from this configuration file 32 | file_length: 33 | - 700 34 | - 1000 35 | line_length: 36 | - 230 37 | - 250 38 | # parameterized rules are first parameterized as a warning level, then error level. 39 | function_body_length: 40 | - 150 # warning 41 | - 200 # error 42 | type_body_length: 43 | - 300 # warning 44 | - 400 # error 45 | variable_name_max_length: 46 | - 30 # warning 47 | - 40 # error 48 | variable_name_min_length: 49 | - 2 # warning 50 | - 1 # error 51 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '12.0' 2 | use_frameworks! 3 | inhibit_all_warnings! 4 | 5 | # Add Application pods here 6 | def app_pods 7 | pod 'Alamofire', '~> 5.0' 8 | pod 'AlamofireImage', '~> 4.0' 9 | pod 'AsyncSwift', '~> 2.0' 10 | pod 'Device', '~> 3.0' 11 | pod 'DynamicColor', '~> 4.0' 12 | pod 'Eureka', '~> 5.0' 13 | pod 'Fabric', '~> 1.10.2' 14 | pod 'Crashlytics', '~> 3.14.0' 15 | # (Recommended) Pod for Google Analytics 16 | pod 'Firebase/Analytics' 17 | pod 'KeychainAccess', '~> 4.0' 18 | pod 'NVActivityIndicatorView', '~> 4.0' 19 | pod 'R.swift', '~> 5.0.2' 20 | pod 'RxCocoa', '~> 5.0' 21 | pod 'RxSwift', '~> 5.0' 22 | pod 'SwiftDate', '~> 6.0' 23 | pod 'SwiftyUserDefaults', '~> 5.0' 24 | pod 'Action', '~> 4.0' 25 | end 26 | 27 | def testing_pods 28 | pod 'Quick', '~> 2.0' 29 | pod 'Nimble', '~> 8.0' 30 | end 31 | 32 | target 'XLProjectName' do 33 | app_pods 34 | end 35 | 36 | target 'Tests' do 37 | testing_pods 38 | end 39 | 40 | target 'UITests' do 41 | testing_pods 42 | end 43 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 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 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/Tests/Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tests.swift 3 | // Tests 4 | // 5 | // Created by Xmartlabs SRL ( http://xmartlabs.com ) 6 | // Copyright © 2020 XLOrganizationName. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class Tests: XCTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | super.tearDown() 21 | } 22 | 23 | func testExample() { 24 | // This is an example of a functional test case. 25 | // Use XCTAssert and related functions to verify your tests produce the correct results. 26 | } 27 | 28 | func testPerformanceExample() { 29 | // This is an example of a performance test case. 30 | self.measure { 31 | // Put the code you want to measure the time of here. 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/UITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 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 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/UITests/UITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITests.swift 3 | // UITests 4 | // 5 | // Created by Xmartlabs SRL ( http://xmartlabs.com ) 6 | // Copyright © 2020 XLOrganizationName. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Nimble 11 | 12 | class UITests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | 17 | // Put setup code here. This method is called before the invocation of each test method in the class. 18 | 19 | // In UI tests it is usually best to stop immediately when a failure occurs. 20 | continueAfterFailure = false 21 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 22 | XCUIApplication().launch() 23 | 24 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 25 | } 26 | 27 | override func tearDown() { 28 | // Put teardown code here. This method is called after the invocation of each test method in the class. 29 | super.tearDown() 30 | } 31 | 32 | func testNothing() { 33 | 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName.xcodeproj/xcshareddata/xcschemes/XLProjectName.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 52 | 53 | 54 | 55 | 57 | 63 | 64 | 65 | 66 | 67 | 77 | 79 | 85 | 86 | 87 | 88 | 94 | 96 | 102 | 103 | 104 | 105 | 107 | 108 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // XLProjectName 4 | // 5 | // Created by XLAuthorName ( XLAuthorWebsite ) 6 | // Copyright © 2020 XLOrganizationName. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Crashlytics 11 | import RxSwift 12 | 13 | @UIApplicationMain 14 | class AppDelegate: UIResponder, UIApplicationDelegate { 15 | 16 | var window: UIWindow? 17 | private var disposeBag = DisposeBag() 18 | 19 | /// true if app was able to get pushn notification token 20 | static var didRegisteredPush = false 21 | 22 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 23 | // Override point for customization after application launch. 24 | setupCrashlytics() 25 | setupNetworking() 26 | stylizeEurekaRows() 27 | 28 | // Register the supported push notifications interaction types. 29 | // Shows alert view askying for allowed push notification types 30 | // you can move this line to a more suitable point in the app. 31 | UIApplication.requestPermissionToShowPushNotification() 32 | // Register for remote notifications. Must be called after registering for supported push notifications interaction types. 33 | UIApplication.shared.registerForRemoteNotifications() 34 | 35 | return true 36 | } 37 | 38 | func applicationWillResignActive(_ application: UIApplication) { 39 | // Sent when the application is about to move from active to inactive state. 40 | // This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) 41 | // or when the user quits the application and it begins the transition to the background state. 42 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 43 | } 44 | 45 | func applicationDidEnterBackground(_ application: UIApplication) { 46 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 47 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 48 | } 49 | 50 | func applicationWillEnterForeground(_ application: UIApplication) { 51 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 52 | } 53 | 54 | func applicationDidBecomeActive(_ application: UIApplication) { 55 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 56 | } 57 | 58 | func applicationWillTerminate(_ application: UIApplication) { 59 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 60 | } 61 | 62 | } 63 | 64 | extension AppDelegate { 65 | 66 | // MARK: Requesting A Device Token 67 | 68 | func application(_ application: UIApplication, didRegister notificationSettings: UNNotificationSettings) { 69 | // notificationSettings.types 70 | // notificationSettings.categories 71 | } 72 | 73 | /** 74 | Receives the device token needed to deliver remote notifications. Device tokens can change, so your app needs to 75 | reregister every time it is launched and pass the received token back to your server. 76 | Device tokens always change when the user restores backup data to a new device or computer 77 | or reinstalls the operating system. 78 | */ 79 | func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { 80 | // pass device token to Intercom 81 | //Intercom.setDeviceToken(deviceToken) 82 | let deviceTokenStr = "\(deviceToken)" 83 | NetworkManager.singleton.deviceUpdate(token: deviceTokenStr) 84 | .subscribe() 85 | .disposed(by: self.disposeBag) 86 | } 87 | 88 | func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { 89 | /** 90 | you should process the error object appropriately and disable any features related to remote notifications. 91 | Because notifications are not going to be arriving anyway, it is usually better to degrade gracefully and 92 | avoid any unnecessary work needed to process or display those notifications. 93 | */ 94 | Crashlytics.sharedInstance().recordError(error) 95 | } 96 | 97 | func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any]) { 98 | 99 | if application.applicationState == .active { 100 | // AppDelegate.showNotificationInForeground(userInfo) 101 | // IntercomHelper.sharedInstance.fetchUnreadConversationsCount() 102 | } else if application.applicationState == .background || application.applicationState == .inactive { 103 | // if let url = userInfo["navigationUrl"] as? String { 104 | // AppDelegate.pendingNotificationURL = NSURL(string: url) 105 | // AppDelegate.router.handleURL(AppDelegate.pendingNotificationURL, withCompletion: nil) 106 | // } 107 | } 108 | // otherwise do nothing, it should be managed by didFinishLaunchingWithOptions. 109 | } 110 | 111 | func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { 112 | 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "size" : "1024x1024", 46 | "scale" : "1x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Base.lproj/SearchRepository.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 61 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xmartlabs/Swift-Project-Template/a9b64fffd57bffee27d3319425817a383d0df953/Project-iOS/XLProjectName/XLProjectName/Controllers/.gitkeep -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Controllers/LoginController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainViewController.swift 3 | // XLProjectName 4 | // 5 | // Created by XLAuthorName ( XLAuthorWebsite ) 6 | // Copyright © 2020 XLOrganizationName. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import Eureka 12 | 13 | class LoginController: FormViewController { 14 | 15 | fileprivate struct RowTags { 16 | static let LogInUsername = "log in username" 17 | static let LogInPassword = "log in password" 18 | } 19 | 20 | let disposeBag = DisposeBag() 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | setUpSections() 25 | } 26 | 27 | fileprivate func setUpSections() { 28 | form +++ Section(header: "Advanced usage", footer: "Please enter your credentials for advanced usage") 29 | <<< NameRow(RowTags.LogInUsername) { 30 | $0.title = "Username:" 31 | $0.placeholder = "insert username here.." 32 | } 33 | <<< PasswordRow(RowTags.LogInPassword) { 34 | $0.title = "Password:" 35 | $0.placeholder = "insert password here.." 36 | } 37 | <<< ButtonRow() { 38 | $0.title = "Log in" 39 | } 40 | .onCellSelection { [weak self] _, _ in 41 | self?.loginTapped() 42 | } 43 | 44 | +++ Section() 45 | <<< ButtonRow() { 46 | $0.title = "Search Repositories" 47 | $0.presentationMode = PresentationMode.segueName(segueName: R.segue.loginController.pushToSearchRepositoriesController.identifier, onDismiss: nil) 48 | } 49 | } 50 | 51 | fileprivate func getTextFromRow(_ tag: String) -> String? { 52 | let textRow: NameRow = form.rowBy(tag: tag)! 53 | let textEntered = textRow.cell.textField.text 54 | return textEntered 55 | } 56 | 57 | fileprivate func getPasswordFromRow(_ tag: String) -> String? { 58 | let textRow: PasswordRow = form.rowBy(tag: tag)! 59 | let textEntered = textRow.cell.textField.text 60 | return textEntered 61 | } 62 | 63 | // MARK: - Actions 64 | 65 | func loginTapped() { 66 | let writtenUsername = getTextFromRow(RowTags.LogInUsername) 67 | let writtenPassword = getPasswordFromRow(RowTags.LogInPassword) 68 | guard let username = writtenUsername, let password = writtenPassword, !username.isEmpty && !password.isEmpty else { 69 | showError("Please enter the username and password") 70 | return 71 | } 72 | 73 | LoadingIndicator.show() 74 | NetworkManager.singleton.login(route: Route.User.Login(username: username, password: password)) 75 | .do(onError: { [weak self] (error: Error) in 76 | LoadingIndicator.hide() 77 | self?.showError("Error", message: (error as? XLProjectNameError)?.localizedDescription ?? "Sorry user login does not work correctly") 78 | }) 79 | .flatMap() { data -> Single in 80 | return NetworkManager.singleton.getInfo(route: Route.User.GetInfo(username: username)) 81 | } 82 | .subscribe { (user) in 83 | LoadingIndicator.hide() 84 | self.showError("Great", message: "You have been successfully logged in") 85 | } 86 | .disposed(by: disposeBag) 87 | } 88 | 89 | 90 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 91 | super.prepare(for: segue, sender: sender) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Controllers/RepositoryController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepositoryController.swift 3 | // XLProjectName 4 | // 5 | // Created by XLAuthorName ( XLAuthorWebsite ) 6 | // Copyright © 2020 XLOrganizationName. All rights reserved. 7 | // 8 | 9 | 10 | import UIKit 11 | 12 | public class RepositoryController: UIViewController { 13 | 14 | @IBOutlet weak var titleLabel: UILabel! 15 | @IBOutlet weak var descriptionLabel: UILabel! 16 | 17 | @IBOutlet weak var starLabel: UILabel! 18 | @IBOutlet weak var starImage: UIImageView! 19 | @IBOutlet weak var forkLabel: UILabel! 20 | @IBOutlet weak var forkImage: UIImageView! 21 | @IBOutlet weak var issueLabel: UILabel! 22 | @IBOutlet weak var issueImage: UIImageView! 23 | @IBOutlet weak var bottomLabel: UILabel! 24 | var repository: Repository! 25 | 26 | public override func viewDidLoad() { 27 | super.viewDidLoad() 28 | } 29 | 30 | public override func viewWillAppear(_ animated: Bool) { 31 | super.viewWillAppear(animated) 32 | titleLabel.text = repository.name 33 | descriptionLabel.text = repository.desc 34 | descriptionLabel.numberOfLines = 0 35 | descriptionLabel.textAlignment = .center 36 | starImage.setImageWithURL("https://assets-cdn.github.com/images/icons/emoji/unicode/2b50.png?v5") 37 | forkImage.setImageWithURL("https://assets-cdn.github.com/images/icons/emoji/unicode/1f374.png?v5") 38 | issueImage.setImageWithURL("https://assets-cdn.github.com/images/icons/emoji/unicode/1f41b.png?v5") 39 | starLabel.text = String(repository.stargazersCount) 40 | issueLabel.text = String(repository.openIssues) 41 | forkLabel.text = String(repository.forksCount) 42 | bottomLabel.text = "written in \(repository.language ?? "an unknown language"), by \(repository.company ?? "an unknown company")" 43 | 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Controllers/SearchRepositoryController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchRepositoryController.swift 3 | // XLProjectName 4 | // 5 | // Created by XLAuthorName ( XLAuthorWebsite ) 6 | // Copyright © 2020 'XLOrganizationName'. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import RxSwift 12 | import RxCocoa 13 | 14 | class SearchRepositoriesController: XLTableViewController { 15 | 16 | private lazy var emptyStateLabel: UILabel = { 17 | let emptyStateLabel = UILabel() 18 | emptyStateLabel.text = ControllerConstants.NoTextMessage 19 | emptyStateLabel.textAlignment = .center 20 | return emptyStateLabel 21 | }() 22 | 23 | lazy var viewModel: PaginationViewModel> = { [unowned self] in 24 | return PaginationViewModel>(paginationRequest: PaginationRequest(route: Route.Repository.Search)) 25 | }() 26 | 27 | override func viewDidLoad() { 28 | super.viewDidLoad() 29 | tableView.backgroundView = emptyStateLabel 30 | tableView.keyboardDismissMode = .onDrag 31 | 32 | tableView.rx.reachedBottom 33 | .bind(to: viewModel.loadNextPageTrigger) 34 | .disposed(by: disposeBag) 35 | 36 | viewModel.loading.asDriver() 37 | .drive(activityIndicatorView.rx.isAnimating) 38 | .disposed(by: disposeBag) 39 | 40 | 41 | Driver.combineLatest(viewModel.elements.asDriver(), viewModel.firstPageLoading, searchBar.rx.text.asDriver()) { elements, loading, searchText in 42 | return loading || searchText?.isEmpty ?? true ? [] : elements 43 | } 44 | .drive(tableView.rx.items(cellIdentifier: "Cell")) { _, repository, cell in 45 | cell.textLabel?.text = repository.name 46 | cell.detailTextLabel?.text = "🌟\(repository.stargazersCount)" 47 | } 48 | .disposed(by: disposeBag) 49 | 50 | searchBar.rx.text.asDriver() 51 | .map{ $0 ?? ""} 52 | .filter { !$0.isEmpty } 53 | .debounce(RxTimeInterval.milliseconds(500)) 54 | .drive(viewModel.queryTrigger) 55 | .disposed(by: disposeBag) 56 | 57 | let refreshControl = UIRefreshControl() 58 | refreshControl.rx.valueChanged 59 | .filter { refreshControl.isRefreshing } 60 | .bind(to: viewModel.refreshTrigger) 61 | .disposed(by: disposeBag) 62 | tableView.addSubview(refreshControl) 63 | 64 | viewModel.firstPageLoading 65 | .filter { $0 == false && refreshControl.isRefreshing } 66 | .drive(onNext: { _ in refreshControl.endRefreshing() }) 67 | .disposed(by: disposeBag) 68 | 69 | Driver.combineLatest(viewModel.emptyState, searchBar.rx.text.asDriver()) { $0 || $1?.isEmpty ?? true } 70 | .drive(onNext: { [weak self] state in 71 | self?.emptyStateLabel.isHidden = !state 72 | self?.emptyStateLabel.text = (self?.searchBar.text?.isEmpty ?? true) ? ControllerConstants.NoTextMessage : ControllerConstants.NoRepositoriesMessage 73 | }) 74 | .disposed(by: disposeBag) 75 | } 76 | } 77 | 78 | extension SearchRepositoriesController { 79 | 80 | fileprivate struct ControllerConstants { 81 | static let NoTextMessage = "Enter text to search repositories" 82 | static let NoRepositoriesMessage = "No repositories found" 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xmartlabs/Swift-Project-Template/a9b64fffd57bffee27d3319425817a383d0df953/Project-iOS/XLProjectName/XLProjectName/Helpers/.gitkeep -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Helpers/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // XLProjectName 4 | // 5 | // Created by XLAuthorName ( XLAuthorWebsite ) 6 | // Copyright © 2020 XLOrganizationName. All rights reserved. 7 | // 8 | 9 | 10 | import Foundation 11 | import UIKit 12 | 13 | struct Constants { 14 | 15 | struct Network { 16 | #if DEBUG 17 | static let BASE_URL = URL(string: "https://api.github.com")! 18 | #else 19 | static let BASE_URL = URL(string: "https://api.github.com")! 20 | #endif 21 | static let AuthTokenName = "Authorization" 22 | static let SuccessCode = 200 23 | static let successRange = 200..<300 24 | static let Unauthorized = 401 25 | static let NotFoundCode = 404 26 | static let ServerError = 500 27 | } 28 | 29 | struct Keychain { 30 | static let SERVICE_IDENTIFIER = UIApplication.applicationVersionNumber 31 | static let SESSION_TOKEN = "session_token" 32 | static let DEVICE_TOKEN = "device_token" 33 | } 34 | 35 | struct Formatters { 36 | 37 | static let debugConsoleDateFormatter: DateFormatter = { 38 | let formatter = DateFormatter() 39 | formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS" 40 | formatter.timeZone = TimeZone(identifier: "UTC")! 41 | return formatter 42 | }() 43 | 44 | } 45 | 46 | struct Debug { 47 | #if DEBUG 48 | static let crashlytics = true 49 | static let jsonResponse = true 50 | #else 51 | static let crashlytics = false 52 | static let jsonResponse = false 53 | #endif 54 | } 55 | 56 | // MARK: - Text Styles 57 | enum TextStyle { 58 | 59 | case body(alignment: NSTextAlignment) 60 | case title 61 | 62 | func getFont() -> UIFont { 63 | switch self { 64 | case .body: 65 | return UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body), size: 22) 66 | case .title: 67 | return UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: .title1), size: 16) 68 | } 69 | } 70 | 71 | var textAttributes: [NSAttributedString.Key: Any]? { 72 | switch self { 73 | case let .body(alignment): 74 | let font = getFont() 75 | let paragraphStyle = NSMutableParagraphStyle() 76 | paragraphStyle.lineSpacing = 20 - font.lineHeight 77 | paragraphStyle.alignment = alignment 78 | return [NSAttributedString.Key.paragraphStyle: paragraphStyle] 79 | case .title: 80 | return [NSAttributedString.Key.kern: -0.53] 81 | } 82 | } 83 | } 84 | 85 | } 86 | 87 | // MARK: - Colors 88 | extension UIColor { 89 | static var primaryColor: UIColor { 90 | return UIColor(red: 0, green: 0, blue: 0, alpha: 1) 91 | } 92 | } 93 | 94 | // MARK: - Errors 95 | enum BaseError: Error { 96 | case networkError(error: NSError) 97 | } 98 | 99 | extension BaseError { 100 | 101 | var errorDescription: String { 102 | switch self { 103 | case .networkError(_): 104 | return "No internet connection" 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Helpers/Controllers/XLTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XLTableViewController.swift 3 | // XLProjectName 4 | // 5 | // Created by XLAuthorName ( XLAuthorWebsite ) 6 | // Copyright © 2020 XLOrganizationName. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import RxCocoa 12 | import RxSwift 13 | 14 | class XLTableViewController: UIViewController { 15 | 16 | @IBOutlet weak var tableView: UITableView! 17 | @IBOutlet weak var activityIndicatorView: UIActivityIndicatorView! 18 | @IBOutlet weak var searchBar: UISearchBar! 19 | 20 | var disposeBag = DisposeBag() 21 | } 22 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Helpers/Controllers/XLViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XLViewController.swift 3 | // XLProjectName 4 | // 5 | // Created by XLAuthorName ( XLAuthorWebsite ) 6 | // Copyright © 2020 XLOrganizationName. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import RxCocoa 12 | import RxSwift 13 | 14 | class XLViewController: UIViewController { 15 | let disposeBag = DisposeBag() 16 | 17 | } 18 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Helpers/Extensions/AppDelegate+XLProjectName.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate+XLProjectName.swift 3 | // XLProjectName 4 | // 5 | // Created by XLAuthorName ( XLAuthorWebsite ) 6 | // Copyright © 2016 'XLOrganizationName'. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Fabric 11 | import Alamofire 12 | import Eureka 13 | import Crashlytics 14 | 15 | extension AppDelegate { 16 | 17 | func setupCrashlytics() { 18 | Fabric.with([Crashlytics.self]) 19 | Fabric.sharedSDK().debug = Constants.Debug.crashlytics 20 | } 21 | 22 | // MARK: Alamofire notifications 23 | func setupNetworking() { 24 | NotificationCenter.default.addObserver( 25 | self, 26 | selector: #selector(AppDelegate.requestDidComplete(_:)), 27 | name: Alamofire.Request.didCompleteTaskNotification, 28 | object: nil) 29 | } 30 | 31 | @objc func requestDidComplete(_ notification: Notification) { 32 | guard let request = notification.request, let response = request.response else { 33 | DEBUGLog("Request object not a task") 34 | return 35 | } 36 | if Constants.Network.successRange ~= response.statusCode { 37 | if let token = response.allHeaderFields["Set-Cookie"] as? String { 38 | SessionController.sharedInstance.token = token 39 | } 40 | } else if response.statusCode == Constants.Network.Unauthorized && SessionController.sharedInstance.isLoggedIn() { 41 | SessionController.sharedInstance.clearSession() 42 | // here you should implement AutoLogout: Transition to login screen and show an appropiate message 43 | } 44 | } 45 | 46 | /** 47 | Set up your Eureka default row customization here 48 | */ 49 | func stylizeEurekaRows() { 50 | 51 | let genericHorizontalMargin = CGFloat(50) 52 | BaseRow.estimatedRowHeight = 58 53 | 54 | EmailRow.defaultRowInitializer = { 55 | $0.placeholder = NSLocalizedString("E-mail Address", comment: "") 56 | $0.placeholderColor = .gray 57 | } 58 | 59 | EmailRow.defaultCellSetup = { cell, _ in 60 | cell.layoutMargins = .zero 61 | cell.contentView.layoutMargins.left = genericHorizontalMargin 62 | cell.height = { 58 } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Helpers/Extensions/Date.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Date.swift 3 | // XLProjectName 4 | // 5 | // Created by XLAuthorName ( XLAuthorWebsite ) 6 | // Copyright © 2020 XLOrganizationName. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Date { 12 | 13 | func dblog() -> String { 14 | return Constants.Formatters.debugConsoleDateFormatter.string(from: self) 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Helpers/Extensions/KeyedDecodingContainer.swift: -------------------------------------------------------------------------------- 1 | // KeyedDecodingContainer.swift 2 | // Example-iOS 3 | // 4 | // Copyright (c) 2019 Xmartlabs SRL ( http://xmartlabs.com ) 5 | // 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | import Foundation 26 | 27 | extension KeyedDecodingContainer { 28 | 29 | public func decode(_ key: Key, as type: T.Type = T.self) throws -> T { 30 | return try self.decode(T.self, forKey: key) 31 | } 32 | 33 | public func decodeIfPresent(_ key: Key) throws -> T? { 34 | return try decodeIfPresent(T.self, forKey: key) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Helpers/Extensions/UIApplication.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIApplication.swift 3 | // XLProjectName 4 | // 5 | // Created by XLAuthorName ( XLAuthorWebsite ) 6 | // Copyright © 2020 XLOrganizationName. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import UserNotifications 12 | 13 | extension UIApplication { 14 | 15 | static var applicationVersionNumber: String { 16 | return Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String 17 | } 18 | 19 | static var applicationBuildNumber: String { 20 | return Bundle.main.infoDictionary!["CFBundleVersion"] as! String 21 | } 22 | 23 | static var bundleIdentifier: String { 24 | return Bundle.main.bundleIdentifier! 25 | } 26 | 27 | static func requestPermissionToShowPushNotification() { 28 | 29 | let app = UIApplication.shared 30 | 31 | 32 | 33 | 34 | 35 | /* The first time your app makes this authorization request, the system prompts the user to grant or deny the request and records the user’s response. Subsequent authorization requests don't prompt the user. */ 36 | let unCenter = UNUserNotificationCenter.current() 37 | unCenter.requestAuthorization(options: [.alert, .sound, .badge]) { (granted, error) in 38 | //Parse errors and track state 39 | } 40 | 41 | 42 | 43 | /** 44 | * The first time an app calls the registerUserNotificationSettings: method, iOS prompts the user to 45 | allow the specified interactions. On subsequent launches, calling this method does not prompt the user. 46 | After you call the method, iOS reports the results asynchronously to the 47 | application:didRegisterUserNotificationSettings: method of your app delegate. 48 | The first time you register your settings, iOS waits for the user’s response before calling this method, 49 | but on subsequent calls it returns the existing user settings. 50 | */ 51 | 52 | /** 53 | * The user can change the notification settings for your app at any time using the Settings app. 54 | Because settings can change, always call the registerUserNotificationSettings: at launch time 55 | and use the application:didRegisterUserNotificationSettings: method to get the response. 56 | If the user disallows specific notification types, avoid using those types when configuring local and 57 | remote notifications for your app. 58 | */ 59 | 60 | //app.registerUserNotificationSettings(settings) 61 | app.registerForRemoteNotifications() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Helpers/Extensions/UIDevice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIDevice.swift 3 | // XLProjectName 4 | // 5 | // Created by XLAuthorName ( XLAuthorWebsite ) 6 | // Copyright © 2020 XLOrganizationName. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import KeychainAccess 12 | 13 | extension UIDevice { 14 | 15 | fileprivate static let keychainKey = "device_id" 16 | fileprivate static let keychain = Keychain(service: UIApplication.bundleIdentifier) 17 | 18 | static var uniqueId: String { 19 | if try! keychain.contains(keychainKey) { 20 | return try! keychain.get(keychainKey)! 21 | } 22 | let newDeviceId = UIDevice.current.identifierForVendor!.uuidString 23 | try! keychain.set(newDeviceId, key: keychainKey) 24 | return newDeviceId 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Helpers/Extensions/UIImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImageView.swift 3 | // XLProjectName 4 | // 5 | // Created by XLAuthorName ( XLAuthorWebsite ) 6 | // Copyright © 2020 'XLOrganizationName'. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | import AlamofireImage 12 | 13 | extension UIImageView { 14 | public func setImageWithURL(_ url: String, filter: ImageFilter? = nil, placeholder: UIImage? = nil, completion: ((AFIDataResponse) -> Void)? = nil) { 15 | 16 | self.af.setImage(withURL: URL(string: url)!, cacheKey: nil, placeholderImage: placeholder, serializer: nil, filter: filter, progress: nil, progressQueue: DispatchQueue.main, imageTransition: .crossDissolve(0.3), runImageTransitionIfCached: false) { (response: AFIDataResponse) in 17 | response.error.map { print($0.localizedDescription) } 18 | completion?(response) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Helpers/Extensions/UIViewController.swift: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // File.swift 4 | // XLProjectName 5 | // 6 | // Created by XLAuthorName ( XLAuthorWebsite ) 7 | // Copyright © 2020 'XLOrganizationName'. All rights reserved. 8 | // 9 | 10 | import Foundation 11 | import UIKit 12 | import RxCocoa 13 | import RxSwift 14 | 15 | extension UIViewController { 16 | 17 | /// shows an UIAlertController alert with error title and message 18 | public func showError(_ title: String, message: String? = nil) { 19 | if !Thread.current.isMainThread { 20 | DispatchQueue.main.async { [weak self] in 21 | self?.showError(title, message: message) 22 | } 23 | return 24 | } 25 | 26 | let controller = UIAlertController(title: title, message: message, preferredStyle: .alert) 27 | controller.view.tintColor = UIWindow.appearance().tintColor 28 | controller.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .cancel, handler: nil)) 29 | present(controller, animated: true, completion: nil) 30 | } 31 | } 32 | 33 | 34 | extension Reactive where Base: UIViewController { 35 | private func controlEvent(for selector: Selector) -> ControlEvent { 36 | return ControlEvent(events: sentMessage(selector).map { _ in }) 37 | } 38 | 39 | var viewWillAppear: ControlEvent { 40 | return controlEvent(for: #selector(UIViewController.viewWillAppear)) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Helpers/Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Helpers.swift 3 | // XLProjectName 4 | // 5 | // Created by XLAuthorName ( XLAuthorWebsite ) 6 | // Copyright © 2016 'XLOrganizationName'. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | func DEBUGLog(_ message: String, file: String = #file, line: Int = #line, function: String = #function) { 12 | #if DEBUG 13 | let fileURL = NSURL(fileURLWithPath: file) 14 | let fileName = fileURL.deletingPathExtension?.lastPathComponent ?? "" 15 | print("\(Date().dblog()) \(fileName)::\(function)[L:\(line)] \(message)") 16 | #endif 17 | // Nothing to do if not debugging 18 | } 19 | 20 | func DEBUGJson(_ value: AnyObject) { 21 | #if DEBUG 22 | if Constants.Debug.jsonResponse { 23 | // print(JSONStringify(value)) 24 | } 25 | #endif 26 | } 27 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Helpers/Opera/Error.swift: -------------------------------------------------------------------------------- 1 | // Error.swift 2 | // Opera ( https://github.com/xmartlabs/Opera ) 3 | // 4 | // Copyright (c) 2019 Xmartlabs SRL ( http://xmartlabs.com ) 5 | // 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | import Foundation 26 | import Alamofire 27 | 28 | 29 | public enum XLProjectNameError: Error { 30 | case afError(error: AFError) 31 | case unknownError 32 | 33 | 34 | var afError: AFError? { 35 | if case XLProjectNameError.afError(let afError) = self { 36 | return afError 37 | } 38 | return nil 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Helpers/Opera/Helpers-iOS.swift: -------------------------------------------------------------------------------- 1 | // Helpers-iOS.swift 2 | // Opera ( https://github.com/xmartlabs/Opera ) 3 | // 4 | // Copyright (c) 2019 Xmartlabs SRL ( http://xmartlabs.com ) 5 | // 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | import RxSwift 26 | import RxCocoa 27 | import Foundation 28 | import UIKit 29 | 30 | extension UIControl { 31 | 32 | public var rx: Reactive { 33 | return Reactive(self) 34 | } 35 | 36 | } 37 | 38 | extension Reactive where Base: UIControl { 39 | /// Reactive wrapper for UIControlEvents.ValueChanged target action pattern. 40 | public var valueChanged: ControlEvent { 41 | return base.rx.controlEvent(.valueChanged) 42 | } 43 | } 44 | 45 | extension Reactive where Base: UIScrollView { 46 | 47 | public var reachedBottom: Observable { 48 | return didEndDecelerating.flatMap { (_) -> Observable in 49 | return self.base.isTableViewScrolledToBottom() ? 50 | Observable.just(()) : Observable.empty() 51 | } 52 | } 53 | } 54 | 55 | extension UIScrollView { 56 | 57 | public func isTableViewScrolledToBottom() -> Bool { 58 | let visibleHeight = frame.height - contentInset.top - contentInset.bottom 59 | let y = contentOffset.y + contentInset.top 60 | let threshold = max(0.0, contentSize.height - visibleHeight) 61 | return y >= threshold 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Helpers/Opera/Helpers/OperaHelpers.swift: -------------------------------------------------------------------------------- 1 | // NSHTTPURLResponse.swift 2 | // Opera ( https://github.com/xmartlabs/Opera ) 3 | // 4 | // Copyright (c) 2019 Xmartlabs SRL ( http://xmartlabs.com ) 5 | // 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | import Foundation 26 | import RxSwift 27 | 28 | extension HTTPURLResponse { 29 | 30 | /** 31 | Get page parameter value from a particular link relation 32 | 33 | - parameter relation: relation name. 34 | - parameter pageParameterName: url page parameter name. 35 | 36 | - returns: The page parameter value. 37 | */ 38 | func linkPagePrameter(_ relation: String, pageParameterName: String) -> String? { 39 | guard let uri = self.findLink(relation: relation)?.uri else { return nil } 40 | let components = URLComponents(string: uri) 41 | return components?.queryItems?.filter { $0.name == pageParameterName }.first?.value 42 | } 43 | } 44 | 45 | public func JSONStringify(_ value: Any, prettyPrinted: Bool = true) -> String { 46 | let options: JSONSerialization.WritingOptions = prettyPrinted ? .prettyPrinted : [] 47 | if JSONSerialization.isValidJSONObject(value) { 48 | if let data = try? JSONSerialization.data(withJSONObject: value, options: options), let string = NSString(data: data, encoding: String.Encoding.utf8.rawValue) { 49 | return string as String 50 | } 51 | } 52 | return "" 53 | } 54 | 55 | public func JSONFrom(data: Data?) -> Any? { 56 | guard 57 | let jsonData = data, 58 | let json = try? JSONSerialization.jsonObject( 59 | with: jsonData, 60 | options: JSONSerialization.ReadingOptions.mutableContainers 61 | ) 62 | else { 63 | return nil 64 | } 65 | return json 66 | } 67 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Helpers/Opera/Pagination/FilterType.swift: -------------------------------------------------------------------------------- 1 | // FilterType.swift 2 | // Opera ( https://github.com/xmartlabs/Opera ) 3 | // 4 | // Copyright (c) 2019 Xmartlabs SRL ( http://xmartlabs.com ) 5 | // 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | import Foundation 26 | 27 | /** 28 | * Types conforming to FilterType can be used to create requst parameters. 29 | Notice that PaginationViewModel relies on this protocol to generate filter parameters. 30 | Normally a view controller passes a FilterType instance to PaginationViewModel 31 | instance in order to make a new request that considers FilterType parameters. 32 | */ 33 | public protocol FilterType { 34 | var parameters: [String: AnyObject]? { get } 35 | } 36 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Helpers/Opera/Pagination/PaginationRequest.swift: -------------------------------------------------------------------------------- 1 | // PaginationRequest.swift 2 | // Opera ( https://github.com/xmartlabs/Opera ) 3 | // 4 | // Copyright (c) 2019 Xmartlabs SRL ( http://xmartlabs.com ) 5 | // 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | import Foundation 26 | 27 | /** 28 | * A Generic type that adopts PaginationRequestType 29 | */ 30 | public struct PaginationRequest: PaginationRequestType { 31 | 32 | 33 | public typealias Response = PaginationResponse 34 | 35 | public var route: RouteType 36 | public var page: String = "" 37 | public var query: String? 38 | public var filter: FilterType? 39 | 40 | public init(route: RouteType, page: String? = nil, query: String? = nil, filter: FilterType? = nil) { 41 | self.route = route 42 | self.page = page ?? (self as? PaginationRequestTypeSettings)? 43 | .firstPageParameterValue ?? Default.firstPageParameterValue 44 | self.query = query 45 | self.filter = filter 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Helpers/Opera/Pagination/PaginationRequestType+Rx.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PaginationRequestType+Rx.swift 3 | // Opera 4 | // 5 | // Copyright (c) 2019 Xmartlabs SRL ( http://xmartlabs.com ) 6 | // 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | 26 | import Foundation 27 | import RxSwift 28 | import Alamofire 29 | 30 | extension Reactive where Base: PaginationRequestType { 31 | 32 | /** 33 | Returns a `Observable` of [Response] for the PaginationRequestType instance. 34 | If something goes wrong a Opera.Error error is propagated through the result sequence. 35 | 36 | - returns: An instance of `Observable` 37 | */ 38 | var collection: Single { 39 | let myPage = base.page 40 | return Single.create { single in 41 | let req = NetworkManager.singleton.response(route: self.base) { (response: DataResponse) in 42 | switch response.result { 43 | case .failure(let error): 44 | single(.error(error)) 45 | case .success(let value): 46 | let previousPage = response.response?.linkPagePrameter((self as? WebLinkingSettings)?.prevRelationName ?? Default.prevRelationName, 47 | pageParameterName: (self as? WebLinkingSettings)? 48 | .relationPageParamName ?? Default.relationPageParamName) 49 | let nextPage = response.response?.linkPagePrameter((self as? WebLinkingSettings)?.nextRelationName ?? Default.nextRelationName, 50 | pageParameterName: (self as? WebLinkingSettings)? 51 | .relationPageParamName ?? Default.relationPageParamName) 52 | let baseResponse = Base.Response(elements: value.elements, previousPage: previousPage, nextPage: nextPage, page: myPage) 53 | single(.success(baseResponse)) 54 | } 55 | } 56 | return Disposables.create { 57 | req.cancel() 58 | } 59 | } 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Helpers/Opera/Pagination/PaginationRequestType.swift: -------------------------------------------------------------------------------- 1 | // PaginationRequestType.swift 2 | // Opera ( https://github.com/xmartlabs/Opera ) 3 | // 4 | // Copyright (c) 2019 Xmartlabs SRL ( http://xmartlabs.com ) 5 | // 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | import Alamofire 26 | import Foundation 27 | import RxSwift 28 | 29 | struct Default { 30 | static let firstPageParameterValue = "1" 31 | static let queryParameterName = "q" 32 | static let pageParamName = "page" 33 | static let prevRelationName = "prev" 34 | static let nextRelationName = "next" 35 | static let relationPageParamName = "page" 36 | 37 | } 38 | 39 | public protocol BasePaginationRequestType: URLRequestConvertible { 40 | /// Route 41 | var route: RouteType { get } 42 | /// Page, represent request page parameter 43 | var page: String { get } 44 | /// Query string 45 | var query: String? { get } 46 | /// Filters 47 | var filter: FilterType? { get } 48 | } 49 | 50 | /** 51 | * A type that adopts PaginationRequestType encapsulates information required 52 | to fetch and filter a collection of items from a server endpoint. 53 | It can be used to create a pagination request. 54 | The request will take into account page, query, route, filter properties. 55 | */ 56 | public protocol PaginationRequestType: BasePaginationRequestType { 57 | 58 | associatedtype Response: PaginationResponseType, Decodable 59 | 60 | /** 61 | Creates a new PaginationRequestType equals to self but updating the page value. 62 | 63 | - parameter page: new page value 64 | 65 | - returns: PaginationRequestType instance identically 66 | to self and having its page value updated to `page` 67 | */ 68 | func routeWithPage(_ page: String) -> Self 69 | 70 | /** 71 | Creates a new PaginationRequestType equals to self but updating 72 | query strng and setting its page to first page value. 73 | 74 | - parameter query: query string 75 | 76 | - returns: PaginationRequestType instance identically to self and 77 | having its query value updated to `query` and its page to firs page value. 78 | */ 79 | func routeWithQuery(_ query: String) -> Self 80 | 81 | /** 82 | Creates a new PaginationRequestType equals to self 83 | but updating its filter. Page value is set to first page. 84 | 85 | - parameter filter: filters 86 | 87 | - returns: PaginationRequestType instance identically to self 88 | and having its filter value updated to `filter` and its page to firs page value. 89 | */ 90 | func routeWithFilter(_ filter: FilterType) -> Self 91 | 92 | /** 93 | Instantiate a new `PaginationRequestType` 94 | 95 | - parameter route: route info 96 | - parameter page: page 97 | - parameter query: query string 98 | - parameter filter: filters 99 | to get array of items to parse using the JSON parsing library. 100 | 101 | - returns: The new PaginationRequestType instance. 102 | */ 103 | init( 104 | route: RouteType, 105 | page: String?, 106 | query: String?, 107 | filter: FilterType? 108 | ) 109 | } 110 | 111 | extension BasePaginationRequestType { 112 | 113 | // MARK: URLRequestConvertible conformance 114 | 115 | public func asURLRequest() throws -> URLRequest { 116 | let url = try route.baseURL.asURL() 117 | var urlRequest = URLRequest(url: url.appendingPathComponent(route.path)) 118 | urlRequest.httpMethod = route.method.rawValue 119 | 120 | var params = (self.route as? URLRequestParametersSetup)? 121 | .urlRequestParametersSetup(urlRequest, parameters: parameters) ?? parameters 122 | params = (self as? URLRequestParametersSetup)? 123 | .urlRequestParametersSetup(urlRequest, parameters: params) ?? params 124 | urlRequest = try route.encoding.encode(urlRequest, with: params) 125 | 126 | return urlRequest 127 | } 128 | 129 | /// Pagination request parameters 130 | var parameters: [String: Any]? { 131 | var result = route.parameters ?? [:] 132 | result[(self as? PaginationRequestTypeSettings)? 133 | .pageParameterName ?? Default.pageParamName] = page as AnyObject? 134 | if let q = query, q != "" { 135 | result[(self as? PaginationRequestTypeSettings)? 136 | .queryParameterName ?? Default.queryParameterName] = query as AnyObject? 137 | } 138 | for (k, v) in filter?.parameters ?? [:] { 139 | result.updateValue(v, forKey: k) 140 | } 141 | return result 142 | } 143 | } 144 | 145 | extension PaginationRequestType { 146 | 147 | public var rx: Reactive { 148 | return Reactive(self) 149 | } 150 | 151 | public func routeWithPage(_ page: String) -> Self { 152 | return Self.init( 153 | route: route, 154 | page: page, 155 | query: query, 156 | filter: filter 157 | ) 158 | } 159 | 160 | public func routeWithQuery(_ query: String) -> Self { 161 | return Self.init( 162 | route: route, 163 | page: (self as? PaginationRequestTypeSettings)? 164 | .firstPageParameterValue ?? Default.firstPageParameterValue, 165 | query: query, 166 | filter: filter 167 | ) 168 | } 169 | 170 | public func routeWithFilter(_ filter: FilterType) -> Self { 171 | return Self.init( 172 | route: route, 173 | page: (self as? PaginationRequestTypeSettings)? 174 | .firstPageParameterValue ?? Default.firstPageParameterValue, 175 | query: query, 176 | filter: filter 177 | ) 178 | } 179 | 180 | } 181 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Helpers/Opera/Pagination/PaginationRequestTypeSettings.swift: -------------------------------------------------------------------------------- 1 | // PaginationRequestTypeSettings.swift 2 | // Opera ( https://github.com/xmartlabs/Opera ) 3 | // 4 | // Copyright (c) 2019 Xmartlabs SRL ( http://xmartlabs.com ) 5 | // 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | import Foundation 26 | 27 | public protocol PaginationRequestTypeSettings { 28 | 29 | var queryParameterName: String { get } 30 | var pageParameterName: String { get } 31 | var firstPageParameterValue: String { get } 32 | } 33 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Helpers/Opera/Pagination/PaginationResponse.swift: -------------------------------------------------------------------------------- 1 | // PaginationResponse.swift 2 | // Opera ( https://github.com/xmartlabs/Opera ) 3 | // 4 | // Copyright (c) 2019 Xmartlabs SRL ( http://xmartlabs.com ) 5 | // 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | import Foundation 26 | 27 | /** 28 | * PaginationResponse is a generic type that adopts PaginationResponseType 29 | */ 30 | public struct PaginationResponse: PaginationResponseType, Decodable { 31 | 32 | public let elements: [E] 33 | public var previousPage: String? = nil 34 | public var nextPage: String? = nil 35 | public var page: String? = nil 36 | 37 | enum CodingKeys: String, CodingKey { 38 | case elements = "items" 39 | } 40 | 41 | 42 | 43 | public init(elements: [E], previousPage: String?, nextPage: String?, page: String?) { 44 | self.elements = elements 45 | self.previousPage = previousPage 46 | self.nextPage = nextPage 47 | self.page = page 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Helpers/Opera/Pagination/PaginationResponseType.swift: -------------------------------------------------------------------------------- 1 | // PaginationResponseType.swift 2 | // Opera ( https://github.com/xmartlabs/Opera ) 3 | // 4 | // Copyright (c) 2019 Xmartlabs SRL ( http://xmartlabs.com ) 5 | // 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | import Foundation 26 | 27 | /** 28 | * PaginationResponseType represent the relevant data within a 29 | PaginationRequestType response such as its serialized elements, 30 | previous and next page information, 31 | and the page used as parameter to fetch the elements. 32 | */ 33 | public protocol PaginationResponseType { 34 | 35 | associatedtype Element 36 | 37 | /// Elements fetched by PaginationRequestType 38 | var elements: [Element] { get } 39 | 40 | /// previous page info gotten from prev relation Link. 41 | var previousPage: String? { get } 42 | /// next page gotten from next relation Link. 43 | var nextPage: String? { get } 44 | /// current page, value passed as a parameter in the related PaginationRequestType request 45 | var page: String? { get } 46 | 47 | /** 48 | PaginationResponseType initializer 49 | 50 | - parameter elements: elements 51 | - parameter previousPage: previous page 52 | - parameter nextPage: next page 53 | - parameter page: current page 54 | 55 | - returns: PaginationResponseType instance 56 | */ 57 | init(elements: [Element], previousPage: String?, nextPage: String?, page: String?) 58 | } 59 | 60 | extension PaginationResponseType { 61 | 62 | /// indicates if there are any items in a previous page. 63 | public var hasPreviousPage: Bool { 64 | return previousPage != nil 65 | } 66 | 67 | /// indicates if the server has more items that can be fetched using the `nextPage` value. 68 | public var hasNextPage: Bool { 69 | return nextPage != nil 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Helpers/Opera/Pagination/PaginationViewModel.swift: -------------------------------------------------------------------------------- 1 | // PaginationViewModel.swift 2 | // Opera ( https://github.com/xmartlabs/Opera ) 3 | // 4 | // Copyright (c) 2019 Xmartlabs SRL ( http://xmartlabs.com ) 5 | // 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | import Alamofire 26 | import Foundation 27 | import RxSwift 28 | import RxCocoa 29 | import Action 30 | 31 | /// Reactive View Model helper to load list of Decodable items. 32 | open class PaginationViewModel 33 | where PaginationRequest.Response.Element: Decodable { 34 | 35 | 36 | private enum LoadActionInput { 37 | case page(page: String) 38 | case query(query: String) 39 | case filter(filter: FilterType) 40 | } 41 | 42 | /// pagination request 43 | var paginationRequest: PaginationRequest 44 | public typealias LoadingType = (Bool, String) 45 | 46 | /// trigger a refresh, if emited item is true it will cancel pending 47 | // request and make a new one. if false it will 48 | // not refresh if there is a request in progress. 49 | public let refreshTrigger = PublishSubject() 50 | /// trigger a next page load, it makes a new request for 51 | // the nextPage value provided by lastest request sent to server. 52 | public let loadNextPageTrigger = PublishSubject() 53 | /// Cancel any in progress request and start a new one using the query string provided. 54 | public let queryTrigger = PublishSubject() 55 | /// Cancel any in progress request and start a new one using the filter parameters provided. 56 | public let filterTrigger = PublishSubject() 57 | 58 | /// Allows subscribers to get notified about networking errors 59 | public let errors: Driver 60 | /// Indicates if there is a next page to load. 61 | // hasNextPage value is the result of getting next link relation from latest response. 62 | public let hasNextPage = BehaviorRelay(value: false) 63 | /// Indicates is there is a request in progress and what is the request page. 64 | public let fullloading = BehaviorRelay(value: (false, Default.firstPageParameterValue)) 65 | 66 | public let loading = BehaviorRelay(value: false) 67 | 68 | /// Elements array from first page up to latest fetched page. 69 | public let elements = BehaviorRelay<[PaginationRequest.Response.Element]>(value: []) 70 | 71 | private let loadAction: Action 72 | 73 | fileprivate var disposeBag = DisposeBag() 74 | 75 | /** 76 | Initialize a new PaginationViewModel instance. 77 | 78 | - parameter paginationRequest: pagination request. 79 | 80 | - returns: A PaginationViewModel instance. 81 | */ 82 | public init(paginationRequest: PaginationRequest) { 83 | self.paginationRequest = paginationRequest 84 | 85 | var _paginationRequest = paginationRequest 86 | self.loadAction = Action { input in 87 | switch input { 88 | case .page(let page): 89 | _paginationRequest = _paginationRequest.routeWithPage(page) 90 | case .query(let query): 91 | _paginationRequest = _paginationRequest.routeWithQuery(query) 92 | case .filter(let filter): 93 | _paginationRequest = _paginationRequest.routeWithFilter(filter) 94 | } 95 | return _paginationRequest.rx.collection.asObservable() 96 | } 97 | 98 | self.errors = loadAction.errors 99 | .asDriver(onErrorDriveWith: .empty()) 100 | .flatMap { error -> Driver in 101 | switch error { 102 | case .underlyingError(let error): 103 | return Driver.just(error) 104 | case .notEnabled: 105 | return Driver.empty() 106 | } 107 | } 108 | 109 | let fistPageValue = (self.paginationRequest as? PaginationRequestTypeSettings)?.firstPageParameterValue ?? Default.firstPageParameterValue 110 | loadAction 111 | .elements 112 | .asDriver(onErrorDriveWith: .empty()) 113 | .scan([]) { 114 | $1.page == fistPageValue ? $1.elements : $0 + $1.elements 115 | } 116 | .startWith([]) 117 | .drive(self.elements) 118 | .disposed(by: disposeBag) 119 | 120 | loadAction.executing 121 | .asDriver(onErrorJustReturn: false) 122 | .drive(self.loading) 123 | .disposed(by: disposeBag) 124 | 125 | loadAction.elements.map { $0.hasNextPage } 126 | .asDriver(onErrorJustReturn: self.hasNextPage.value) 127 | .drive(self.hasNextPage) 128 | .disposed(by: disposeBag) 129 | 130 | 131 | let refreshTrigger = self.refreshTrigger.map { _ in 132 | return LoadActionInput.page(page: fistPageValue) 133 | } 134 | 135 | 136 | let queryObservable = self.queryTrigger.map { 137 | LoadActionInput.query(query: $0) 138 | } 139 | 140 | let filterObservable = self.filterTrigger.map { 141 | LoadActionInput.filter(filter: $0) 142 | } 143 | 144 | let loadNextPageObservable = self.loadNextPageTrigger.withLatestFrom(loadAction.elements).flatMap { $0.nextPage.map { return Observable.of(LoadActionInput.page(page: $0)) } ?? Observable.empty() } 145 | 146 | let newInputTrigger = Observable.merge(queryObservable, filterObservable, loadNextPageObservable, refreshTrigger) 147 | newInputTrigger 148 | .bind(to: self.loadAction.inputs) 149 | .disposed(by: disposeBag) 150 | 151 | 152 | 153 | Driver.combineLatest(loadAction.executing.asDriver(onErrorDriveWith: .empty()).distinctUntilChanged(), newInputTrigger.asDriver(onErrorJustReturn: LoadActionInput.page(page: fistPageValue)).map { 154 | switch $0 { 155 | case .page(let page): 156 | return page 157 | case .query(_), .filter(_): 158 | return fistPageValue 159 | } 160 | }).drive(self.fullloading) 161 | .disposed(by: disposeBag) 162 | 163 | } 164 | 165 | } 166 | 167 | extension PaginationViewModel { 168 | 169 | /// Emits items indicating when first page request starts and completes. 170 | public var firstPageLoading: Driver { 171 | let fistPageValue = (self.paginationRequest as? PaginationRequestTypeSettings)?.firstPageParameterValue ?? Default.firstPageParameterValue 172 | return fullloading.asDriver().map { return $0.1 == fistPageValue ? $0.0 : false } 173 | } 174 | 175 | /// Emits items to show/hide a empty state view 176 | public var emptyState: Driver { 177 | return Driver.combineLatest(self.loading.asDriver(onErrorJustReturn: false), self.elements.asDriver()) { (isLoading, elements) -> Bool in 178 | return !isLoading && elements.isEmpty 179 | } 180 | .distinctUntilChanged() 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Helpers/Opera/Pagination/WebLinking.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A structure representing a RFC 5988 link. 4 | public struct Link: Equatable, Hashable { 5 | /// The URI for the link 6 | public let uri: String 7 | 8 | /// The parameters for the link 9 | public let parameters: [String: String] 10 | 11 | /// Initialize a Link with a given uri and parameters 12 | public init(uri: String, parameters: [String: String]? = nil) { 13 | self.uri = uri 14 | self.parameters = parameters ?? [:] 15 | } 16 | 17 | /// Returns the hash value 18 | public var hashValue: Int { 19 | return uri.hashValue 20 | } 21 | 22 | /// Relation type of the Link. 23 | public var relationType: String? { 24 | return parameters["rel"] 25 | } 26 | 27 | /// Reverse relation of the Link. 28 | public var reverseRelationType: String? { 29 | return parameters["rev"] 30 | } 31 | 32 | /// A hint of what the content type for the link may be. 33 | public var type: String? { 34 | return parameters["type"] 35 | } 36 | } 37 | 38 | /// Returns whether two Link's are equivalent 39 | public func == (lhs: Link, rhs: Link) -> Bool { 40 | return lhs.uri == rhs.uri && lhs.parameters == rhs.parameters 41 | } 42 | 43 | // MARK: HTML Element Conversion 44 | 45 | /// An extension to Link to provide conversion to a HTML element 46 | extension Link { 47 | /// Encode the link into a HTML element 48 | public var html: String { 49 | let components = parameters.map { arg in 50 | let (key, value) = arg 51 | return "\(key)=\"\(value)\"" 52 | } + ["href=\"\(uri)\""] 53 | let elements = components.joined(separator: " ") 54 | return "" 55 | } 56 | } 57 | 58 | // MARK: Header link conversion 59 | 60 | /// An extension to Link to provide conversion to and from a HTTP "Link" header 61 | extension Link { 62 | /// Encode the link into a header 63 | public var header: String { 64 | let components = ["<\(uri)>"] + parameters.map { arg in 65 | let (key, value) = arg 66 | return "\(key)=\"\(value)\"" 67 | } 68 | return components.joined(separator: "; ") 69 | } 70 | 71 | /*** Initialize a Link with a HTTP Link header 72 | - parameter header: A HTTP Link Header 73 | */ 74 | public init(header: String) { 75 | let (uri, parametersString) = takeFirst(separateBy(";", header)) 76 | let parameters = parametersString.map({ split("=", $0) }).map { parameter in 77 | [parameter.0: trim("\"", "\"", parameter.1)] 78 | } 79 | 80 | self.uri = trim("<", ">", uri) 81 | self.parameters = parameters.reduce([:], +) 82 | } 83 | } 84 | 85 | /*** Parses a Web Linking (RFC5988) header into an array of Links 86 | - parameter header: RFC5988 link header. 87 | For example `; rel=\"next\", ; rel=\"prev\"` 88 | :return: An array of Links 89 | */ 90 | public func parseLinkHeader(_ header: String) -> [Link] { 91 | return separateBy(",", header).map { string in 92 | return Link(header: string) 93 | } 94 | } 95 | 96 | /// An extension to NSHTTPURLResponse adding a links property 97 | extension HTTPURLResponse { 98 | /// Parses the links on the response `Link` header 99 | public var links: [Link] { 100 | if let linkHeader = allHeaderFields["Link"] as? String { 101 | return parseLinkHeader(linkHeader).map { link in 102 | var uri = link.uri 103 | 104 | /// Handle relative URIs 105 | if let baseURL = self.url, let URL = URL(string: uri, relativeTo: baseURL) { 106 | uri = URL.absoluteString 107 | } 108 | 109 | return Link(uri: uri, parameters: link.parameters) 110 | } 111 | } 112 | 113 | return [] 114 | } 115 | 116 | /// Finds a link which has matching parameters 117 | public func findLink(parameters: [String: String]) -> Link? { 118 | return links.first { $0.parameters ~= parameters } 119 | } 120 | 121 | /// Find a link for the relation 122 | public func findLink(relation: String) -> Link? { 123 | return findLink(parameters: ["rel": relation]) 124 | } 125 | } 126 | 127 | /// MARK: Private methods (used by link header conversion) 128 | 129 | /// Merge two dictionaries together 130 | func +(lhs: [K:V], rhs: [K:V]) -> [K:V] { 131 | var dictionary = [K: V]() 132 | 133 | for (key, value) in rhs { 134 | dictionary[key] = value 135 | } 136 | 137 | for (key, value) in lhs { 138 | dictionary[key] = value 139 | } 140 | 141 | return dictionary 142 | } 143 | 144 | /// LHS contains all the keys and values from RHS 145 | func ~= (lhs: [String: String], rhs: [String: String]) -> Bool { 146 | for (key, value) in rhs { 147 | if lhs[key] != value { 148 | return false 149 | } 150 | } 151 | 152 | return true 153 | } 154 | 155 | /// Separate a trim a string by a separator 156 | func separateBy(_ separator: String, _ input: String) -> [String] { 157 | return input.components(separatedBy: separator).map { 158 | $0.trimmingCharacters(in: CharacterSet.whitespaces) 159 | } 160 | } 161 | 162 | /// Split a string by a separator into two components 163 | func split(_ separator: String, _ input: String) -> (String, String) { 164 | let range = input.range( 165 | of: separator, 166 | options: NSString.CompareOptions(rawValue: 0), 167 | range: nil, 168 | locale: nil 169 | ) 170 | 171 | if let range = range { 172 | let lhs = String(input[.. (String, ArraySlice) { 182 | if let first = input.first { 183 | let items = input[input.indices.suffix(from: (input.startIndex + 1))] 184 | return (first, items) 185 | } 186 | 187 | return ("", []) 188 | } 189 | 190 | /// Trim a prefix and suffix from a string 191 | func trim(_ lhs: Character, _ rhs: Character, _ input: String) -> String { 192 | if input.hasPrefix("\(lhs)") && input.hasSuffix("\(rhs)") { 193 | return String(input[ 194 | input.index(after: input.startIndex).. [String: Any]? 63 | } 64 | 65 | protocol CustomUrlRequestSetup { 66 | func urlRequestSetup(_ request: inout URLRequest) 67 | } 68 | 69 | 70 | extension RouteType { 71 | 72 | /// The URL request. 73 | public func asURLRequest() throws -> URLRequest { 74 | let url = try baseURL.asURL() 75 | var urlRequest = URLRequest(url: url.appendingPathComponent(path)) 76 | urlRequest.httpMethod = method.rawValue 77 | 78 | let params = (self as? URLRequestParametersSetup)? 79 | .urlRequestParametersSetup(urlRequest, parameters: parameters) ?? parameters 80 | urlRequest = try encoding.encode(urlRequest, with: params) 81 | 82 | return urlRequest 83 | } 84 | 85 | public var encoding: ParameterEncoding { 86 | switch method { 87 | case .post, .put, .patch: 88 | return JSONEncoding.default 89 | default: 90 | return URLEncoding.default 91 | } 92 | } 93 | 94 | public var parameters: [String: Any]? { 95 | return nil 96 | } 97 | 98 | } 99 | 100 | 101 | extension RouteType { 102 | 103 | var baseURL: URL { return Constants.Network.BASE_URL } 104 | } 105 | 106 | extension URLRequestParametersSetup { 107 | public func urlRequestParametersSetup(_ urlRequest: NSMutableURLRequest, parameters: [String: AnyObject]?) -> [String: AnyObject]? { 108 | var params = parameters ?? [:] 109 | if let token = SessionController.sharedInstance.token { 110 | params[Constants.Network.AuthTokenName] = token as AnyObject? 111 | } 112 | return params 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Helpers/Opera/RxManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkManager.swift 3 | // Opera ( https://github.com/xmartlabs/Opera ) 4 | // 5 | // Copyright (c) 2019 Xmartlabs SRL ( http://xmartlabs.com ) 6 | // 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | 26 | import Foundation 27 | import Alamofire 28 | import RxSwift 29 | import RxCocoa 30 | 31 | public protocol ManagerType: class { 32 | 33 | var session: Session { get } 34 | } 35 | 36 | open class OperaManager: ManagerType { 37 | open var session: Session 38 | 39 | let decoder: JSONDecoder = { 40 | let decoder = JSONDecoder(); 41 | let dateFormatter = DateFormatter() 42 | dateFormatter.calendar = Calendar.current 43 | dateFormatter.timeZone = TimeZone.current 44 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" 45 | decoder.dateDecodingStrategy = .formatted(dateFormatter) 46 | return decoder 47 | }() 48 | 49 | public init(session: Session) { 50 | self.session = session 51 | } 52 | } 53 | 54 | 55 | open class RxManager: OperaManager { 56 | 57 | private let disposeBag = DisposeBag() 58 | 59 | public override init(session: Session) { 60 | super.init(session: session) 61 | } 62 | 63 | public func response(route: URLRequestConvertible, completionHandler: @escaping (OperaDataResponse) -> Void) -> DataRequest { 64 | let request = self.session.request(route) 65 | request.responseDecodable(decoder: self.decoder) { (response: DataResponse) in 66 | completionHandler(OperaDataResponse(request: response.request, response: response.response, data: response.data, metrics: response.metrics, serializationDuration: response.serializationDuration, result: response.result.mapError { .afError(error: $0) })) 67 | } 68 | return request 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Helpers/R-Swift/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Helpers/WebLinking.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A structure representing a RFC 5988 link. 4 | public struct Link: Equatable, Hashable { 5 | /// The URI for the link 6 | public let uri: String 7 | 8 | /// The parameters for the link 9 | public let parameters: [String: String] 10 | 11 | /// Initialize a Link with a given uri and parameters 12 | public init(uri: String, parameters: [String: String]? = nil) { 13 | self.uri = uri 14 | self.parameters = parameters ?? [:] 15 | } 16 | 17 | /// Returns the hash value 18 | public var hashValue: Int { 19 | return uri.hashValue 20 | } 21 | 22 | /// Relation type of the Link. 23 | public var relationType: String? { 24 | return parameters["rel"] 25 | } 26 | 27 | /// Reverse relation of the Link. 28 | public var reverseRelationType: String? { 29 | return parameters["rev"] 30 | } 31 | 32 | /// A hint of what the content type for the link may be. 33 | public var type: String? { 34 | return parameters["type"] 35 | } 36 | } 37 | 38 | /// Returns whether two Link's are equivalent 39 | public func == (lhs: Link, rhs: Link) -> Bool { 40 | return lhs.uri == rhs.uri && lhs.parameters == rhs.parameters 41 | } 42 | 43 | // MARK: HTML Element Conversion 44 | 45 | /// An extension to Link to provide conversion to a HTML element 46 | extension Link { 47 | /// Encode the link into a HTML element 48 | public var html: String { 49 | let components = parameters.map { key, value in 50 | "\(key)=\"\(value)\"" 51 | } + ["href=\"\(uri)\""] 52 | let elements = components.joined(separator: " ") 53 | return "" 54 | } 55 | } 56 | 57 | // MARK: Header link conversion 58 | 59 | /// An extension to Link to provide conversion to and from a HTTP "Link" header 60 | extension Link { 61 | /// Encode the link into a header 62 | public var header: String { 63 | let components = ["<\(uri)>"] + parameters.map { key, value in 64 | "\(key)=\"\(value)\"" 65 | } 66 | return components.joined(separator: "; ") 67 | } 68 | 69 | /*** Initialize a Link with a HTTP Link header 70 | - parameter header: A HTTP Link Header 71 | */ 72 | public init(header: String) { 73 | let (uri, parametersString) = takeFirst(separateBy(";")(header)) 74 | let parameters = parametersString.map(split("=")).map { parameter in 75 | [parameter.0: trim("\"", "\"")(parameter.1)] 76 | } 77 | 78 | self.uri = trim("<", ">")(uri) 79 | self.parameters = parameters.reduce([:], +) 80 | } 81 | } 82 | 83 | /*** Parses a Web Linking (RFC5988) header into an array of Links 84 | - parameter header: RFC5988 link header. For example `; rel=\"next\", ; rel=\"prev\"` 85 | :return: An array of Links 86 | */ 87 | public func parseLink(header: String) -> [Link] { 88 | return separateBy(",")(header).map { string in 89 | return Link(header: string) 90 | } 91 | } 92 | 93 | /// An extension to NSHTTPURLResponse adding a links property 94 | extension HTTPURLResponse { 95 | /// Parses the links on the response `Link` header 96 | public var links: [Link] { 97 | if let linkHeader = allHeaderFields["Link"] as? String { 98 | return parseLink(header: linkHeader).map { link in 99 | var uri = link.uri 100 | 101 | /// Handle relative URIs 102 | if let baseURL = self.url, let relativeURI = URL(string: uri, relativeTo: baseURL)?.absoluteString { 103 | uri = relativeURI 104 | } 105 | 106 | return Link(uri: uri, parameters: link.parameters) 107 | } 108 | } 109 | 110 | return [] 111 | } 112 | 113 | /// Finds a link which has matching parameters 114 | public func findLink(_ parameters: [String: String]) -> Link? { 115 | for link in links { 116 | if link.parameters ~= parameters { 117 | return link 118 | } 119 | } 120 | 121 | return nil 122 | } 123 | 124 | /// Find a link for the relation 125 | public func findLink(relation: String) -> Link? { 126 | return findLink(["rel": relation]) 127 | } 128 | } 129 | 130 | /// MARK: Private methods (used by link header conversion) 131 | 132 | /// Merge two dictionaries together 133 | func +(lhs: [K:V], rhs: [K:V]) -> [K:V] { 134 | var dictionary = [K:V]() 135 | 136 | for (key, value) in rhs { 137 | dictionary[key] = value 138 | } 139 | 140 | for (key, value) in lhs { 141 | dictionary[key] = value 142 | } 143 | 144 | return dictionary 145 | } 146 | 147 | /// LHS contains all the keys and values from RHS 148 | func ~=(lhs: [String: String], rhs: [String: String]) -> Bool { 149 | for (key, value) in rhs { 150 | if lhs[key] != value { 151 | return false 152 | } 153 | } 154 | 155 | return true 156 | } 157 | 158 | /// Separate a trim a string by a separator 159 | func separateBy(_ separator: String) -> (String) -> [String] { 160 | return { input in 161 | return input.components(separatedBy: separator).map { 162 | $0.trimmingCharacters(in: CharacterSet.whitespaces) 163 | } 164 | } 165 | } 166 | 167 | /// Split a string by a separator into two components 168 | func split(_ separator: String) -> (String) -> (String, String) { 169 | return { input in 170 | let range = input.range(of: separator, options: NSString.CompareOptions(rawValue: 0), range: nil, locale: nil) 171 | 172 | if let range = range { 173 | let lhs = String(input[.. (String, ArraySlice) { 183 | if let first = input.first { 184 | let items = input[input.indices.suffix(from: (input.startIndex + 1))] 185 | return (first, items) 186 | } 187 | 188 | return ("", []) 189 | } 190 | 191 | /// Trim a prefix and suffix from a string 192 | func trim(_ lhs: Character, _ rhs: Character) -> (String) -> String { 193 | return { input in 194 | if input.hasPrefix("\(lhs)") && input.hasSuffix("\(rhs)") { 195 | let range = input.index(after: input.startIndex)..) -> Void) { 15 | var urlRequest = urlRequest 16 | if let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix(Constants.Network.BASE_URL.absoluteString), let token = SessionController.sharedInstance.token { 17 | urlRequest.setValue("Bearer \(token)", forHTTPHeaderField: Constants.Network.AuthTokenName) 18 | } 19 | completion(Result.success(urlRequest)) 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Networking/Manager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Manager.swift 3 | // XLProjectName 4 | // 5 | // Created by XLAuthorName ( XLAuthorWebsite ) 6 | // Copyright © 2020 XLOrganizationName. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | import KeychainAccess 12 | import RxSwift 13 | 14 | 15 | 16 | class NetworkManager: RxManager { 17 | 18 | static let singleton: NetworkManager = { 19 | var requestInterceptor = Interceptor(adapters: [], retriers: [RetryPolicy()]) 20 | return NetworkManager(session: Session(interceptor: requestInterceptor, eventMonitors: [LoggerEventMonitor()])) 21 | }() 22 | 23 | override init(session: Alamofire.Session) { 24 | super.init(session: session) 25 | } 26 | 27 | func deviceUpdate(token: String) -> Completable{ 28 | return rx.completable(route: Route.Device.Update(token: token)) 29 | } 30 | 31 | func login(route: Route.User.Login) -> Single { 32 | return rx.any(route: route) 33 | } 34 | 35 | func getInfo(route: Route.User.GetInfo) -> Single { 36 | return rx.any(route: route) 37 | } 38 | } 39 | 40 | 41 | public typealias OperaDataResponse = DataResponse 42 | 43 | extension Reactive where Base: NetworkManager { 44 | 45 | /** 46 | Returns a `Single` of T for the current request. Notice that T conforms to OperaDecodable. If something goes wrong an `OperaSwift.Error` error is propagated through the result sequence. 47 | 48 | - parameter route: the route indicates the networking call that will be performed by including all the needed information like parameters, URL and HTTP method. 49 | - parameter keyPath: keyPath to look up json object to serialize. Ignore parameter or pass nil when json object is the json root item. 50 | 51 | - returns: An instance of `Single` 52 | */ 53 | func object(route: URLRequestConvertible) -> Single { 54 | return Single.create { subscriber in 55 | let req = self.base.session.request(route) 56 | req.responseDecodable(decoder: self.base.decoder) { (dataResponse: AFDataResponse) in 57 | switch dataResponse.result { 58 | case .failure(let error): 59 | subscriber(.error(XLProjectNameError.afError(error: error))) 60 | case .success(let decodable): 61 | subscriber(.success(decodable)) 62 | } 63 | } 64 | return Disposables.create { 65 | req.cancel() 66 | } 67 | } 68 | } 69 | 70 | func any(route: URLRequestConvertible) -> Single { 71 | return Single.create { single in 72 | let req = self.base.session.request(route) 73 | req.responseJSON(completionHandler: { (dataResponse: AFDataResponse) in 74 | switch dataResponse.result { 75 | case .failure(let error): 76 | single(.error(XLProjectNameError.afError(error: error))) 77 | case .success(let any): 78 | single(.success(any)) 79 | } 80 | }) 81 | return Disposables.create { 82 | req.cancel() 83 | } 84 | } 85 | } 86 | 87 | func completable(route: URLRequestConvertible) -> Completable { 88 | return Completable.create { completable in 89 | let req = self.base.session.request(route) 90 | req.response(completionHandler: { (dataResponse: AFDataResponse) in 91 | switch dataResponse.result { 92 | case .failure(let error): 93 | completable(.error(XLProjectNameError.afError(error: error))) 94 | case .success(_): 95 | completable(.completed) 96 | } 97 | }) 98 | return Disposables.create { 99 | req.cancel() 100 | } 101 | } 102 | } 103 | } 104 | 105 | 106 | 107 | 108 | extension NetworkManager { 109 | var rx: Reactive { 110 | return Reactive(self) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Networking/Routes/DeviceRoute.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Device.swift 3 | // XLProjectName 4 | // 5 | // Created by XLAuthorName ( XLAuthorWebsite ) 6 | // Copyright © 2020 XLOrganizationName. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | 13 | extension Route { 14 | 15 | enum Device: RouteType { 16 | case Update(token: String) 17 | case Remove 18 | case Get 19 | 20 | var method: Alamofire.HTTPMethod { 21 | switch self { 22 | case .Remove: 23 | return .delete 24 | case .Get: 25 | return .get 26 | case .Update: 27 | return .post 28 | } 29 | } 30 | 31 | var path: String { 32 | switch self { 33 | case .Remove: 34 | return "devices/\(UIDevice.uniqueId)" 35 | case .Get: 36 | return "devices/\(UIDevice.uniqueId)" 37 | case .Update: 38 | return "devices/\(UIDevice.uniqueId)" 39 | } 40 | } 41 | 42 | var parameters: [String: Any]? { 43 | switch self { 44 | case .Update(let token): 45 | var parameters = [String: Any]() 46 | let device = UIDevice.current 47 | parameters["deviceToken"] = token 48 | parameters["device_type"] = "iOS" 49 | parameters["device_name"] = device.systemName 50 | parameters["device_model"] = device.model 51 | parameters["device_description"] = "\(device.systemName) \(device.systemVersion)" 52 | parameters["app_version"] = UIApplication.applicationVersionNumber 53 | parameters["location"] = "" 54 | return parameters 55 | default: 56 | return nil 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Networking/Routes/RepositoryRoute.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepositoryRoute.swift 3 | // XLProjectName 4 | // 5 | // Created by XLAuthorName ( XLAuthorWebsite ) 6 | // Copyright © 2020 XLOrganizationName. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | extension Route { 13 | 14 | enum Repository: RouteType { 15 | 16 | case GetInfo(owner: String, repo: String) 17 | case Search 18 | 19 | var method: Alamofire.HTTPMethod { 20 | switch self { 21 | case .GetInfo, .Search: 22 | return .get 23 | } 24 | } 25 | 26 | var path: String { 27 | switch self { 28 | case let .GetInfo(owner, repo): 29 | return "repos/\(owner)/\(repo)" 30 | case .Search: 31 | return "search/repositories" 32 | } 33 | } 34 | 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Networking/Routes/UserRoute.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkUser.swift 3 | // XLProjectName 4 | // 5 | // Created by XLAuthorName ( XLAuthorWebsite ) 6 | // Copyright © 2020 XLOrganizationName. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | struct Route {} 13 | 14 | extension Route { 15 | struct User { 16 | 17 | struct Login: RouteType, CustomUrlRequestSetup { 18 | let username: String 19 | let password: String 20 | 21 | let method = Alamofire.HTTPMethod.get 22 | let path = "" 23 | 24 | // MARK: - CustomUrlRequestSetup 25 | func urlRequestSetup(_ request: inout URLRequest) { 26 | let utf8 = "\(username):\(password)".data(using: String.Encoding.utf8) 27 | let base64 = utf8?.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0)) 28 | 29 | guard let encodedString = base64 else { 30 | return 31 | } 32 | 33 | request.setValue("Basic \(encodedString)", forHTTPHeaderField: "Authorization") 34 | } 35 | } 36 | 37 | struct GetInfo: RouteType { 38 | let username: String 39 | 40 | let method = Alamofire.HTTPMethod.get 41 | var path: String { return "users/\(username)" } 42 | } 43 | 44 | struct Followers: RouteType { 45 | let username: String 46 | 47 | let method = Alamofire.HTTPMethod.get 48 | var path: String { return "users/\(username)/followers" } 49 | } 50 | 51 | struct Repositories: RouteType { 52 | let username: String 53 | 54 | let method = Alamofire.HTTPMethod.get 55 | var path: String { return "users/\(username)/repos" } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Networking/SessionController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionController.swift 3 | // XLProjectName 4 | // 5 | // Created by XLAuthorName ( XLAuthorWebsite ) 6 | // Copyright © 2016 'XLOrganizationName'. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | import Crashlytics 12 | import KeychainAccess 13 | import RxSwift 14 | 15 | class SessionController { 16 | static let sharedInstance = SessionController() 17 | fileprivate let keychain = Keychain(service: Constants.Keychain.SERVICE_IDENTIFIER) 18 | fileprivate init() { } 19 | 20 | var user: User? 21 | 22 | // MARK: - Session variables 23 | var token: String? { 24 | get { return keychain[Constants.Keychain.SESSION_TOKEN] } 25 | set { keychain[Constants.Keychain.SESSION_TOKEN] = newValue } 26 | } 27 | 28 | // MARK: - Session handling 29 | func logOut() { 30 | clearSession() 31 | //TODO: Logout: App should transition to login / onboarding screen 32 | } 33 | 34 | func isLoggedIn() -> Bool { 35 | invalidateIfNeeded() 36 | return token != nil 37 | } 38 | 39 | func invalidateIfNeeded() { 40 | if token != nil && user == nil { 41 | clearSession() 42 | } 43 | } 44 | 45 | // MARK: - Auxiliary functions 46 | func clearSession() { 47 | token = nil 48 | 49 | // Analytics.reset() 50 | // Analytics.registerUnidentifiedUser() 51 | Crashlytics.sharedInstance().setUserEmail(nil) 52 | Crashlytics.sharedInstance().setUserIdentifier(nil) 53 | Crashlytics.sharedInstance().setUserName(nil) 54 | } 55 | 56 | deinit { 57 | 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Networking/URLRequestSetup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLRequestSetup.swift 3 | // XLProjectName 4 | // 5 | // Created by XLAuthorName ( XLAuthorWebsite ) 6 | // Copyright © 2020 'XLOrganizationName'. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | * By adopting URLRequestSetup a RouteType or PaginationRequstType is able to customize it right before sending it to the server. 13 | */ 14 | public protocol URLRequestSetup { 15 | func urlRequestSetup(urlRequest: NSMutableURLRequest) 16 | } 17 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Supporting Files/Info-Staging.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIBackgroundModes 6 | 7 | remote-notification 8 | 9 | CFBundleDevelopmentRegion 10 | en 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | $(PRODUCT_NAME) 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | 1.0 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | 1 27 | Fabric 28 | 29 | APIKey 30 | XLFabricAPIKey 31 | Kits 32 | 33 | 34 | KitInfo 35 | 36 | customerKey 37 | XLFabricAPIKey 38 | customerSecret 39 | XLCrashlyticsCustomerSecret 40 | 41 | KitName 42 | Crashlytics 43 | 44 | 45 | 46 | LSApplicationCategoryType 47 | 48 | LSRequiresIPhoneOS 49 | 50 | UILaunchStoryboardName 51 | LaunchScreen 52 | UIMainStoryboardFile 53 | Main 54 | UIRequiredDeviceCapabilities 55 | 56 | armv7 57 | 58 | UISupportedInterfaceOrientations 59 | 60 | UIInterfaceOrientationPortrait 61 | UIInterfaceOrientationLandscapeLeft 62 | UIInterfaceOrientationLandscapeRight 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Supporting Files/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIBackgroundModes 6 | 7 | remote-notification 8 | 9 | Fabric 10 | 11 | Kits 12 | 13 | 14 | KitInfo 15 | 16 | customerSecret 17 | XLCrashlyticsCustomerSecret 18 | customerKey 19 | XLFabricAPIKey 20 | 21 | KitName 22 | Crashlytics 23 | 24 | 25 | APIKey 26 | XLFabricAPIKey 27 | 28 | CFBundleDevelopmentRegion 29 | en 30 | LSApplicationCategoryType 31 | 32 | CFBundleExecutable 33 | $(EXECUTABLE_NAME) 34 | CFBundleIdentifier 35 | $(PRODUCT_BUNDLE_IDENTIFIER) 36 | CFBundleInfoDictionaryVersion 37 | 6.0 38 | CFBundleName 39 | $(PRODUCT_NAME) 40 | CFBundlePackageType 41 | APPL 42 | CFBundleShortVersionString 43 | 1.0 44 | CFBundleSignature 45 | ???? 46 | CFBundleVersion 47 | 1 48 | LSRequiresIPhoneOS 49 | 50 | UILaunchStoryboardName 51 | LaunchScreen 52 | UIMainStoryboardFile 53 | Main 54 | UIRequiredDeviceCapabilities 55 | 56 | armv7 57 | 58 | UISupportedInterfaceOrientations 59 | 60 | UIInterfaceOrientationPortrait 61 | UIInterfaceOrientationLandscapeLeft 62 | UIInterfaceOrientationLandscapeRight 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Views/LoadingIndicator/LoadingIndicator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingIndicator.swift 3 | // XLProjectName 4 | // 5 | // Created by Diego Ernst on 9/2/16. 6 | // Copyright © 2016 'XLOrganizationName'. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import NVActivityIndicatorView 11 | import UIKit 12 | 13 | class LoadingIndicator { 14 | 15 | static let size = CGSize(width: 30, height: 30) 16 | static let type = NVActivityIndicatorType.ballPulse 17 | static let color = UIColor.white 18 | static let minimumVisibleTime = TimeInterval(0.2) 19 | static let displayTimeThreshold = TimeInterval(0.1) 20 | 21 | static func show( 22 | message: String? = nil, 23 | minimumVisibleTime: TimeInterval = LoadingIndicator.minimumVisibleTime, 24 | displayTimeThreshold: TimeInterval = LoadingIndicator.displayTimeThreshold) { 25 | 26 | LoadingIndicatorManager.sharedInstance.show(message: message, minimumVisibleTime: minimumVisibleTime, displayTimeThreshold: displayTimeThreshold) 27 | } 28 | 29 | static func hide() { 30 | LoadingIndicatorManager.sharedInstance.hide() 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Project-iOS/XLProjectName/XLProjectName/Views/LoadingIndicator/LoadingIndicatorManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingIndicatorManager.swift 3 | // XLProjectName 4 | // 5 | // Created by Diego Ernst on 9/2/16. 6 | // Copyright © 2016 'XLOrganizationName'. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import NVActivityIndicatorView 11 | import UIKit 12 | 13 | class ActivityData { 14 | 15 | let message: String? 16 | let minimumVisibleTime: TimeInterval 17 | let displayTimeThreshold: TimeInterval 18 | 19 | init(message: String?, minimumVisibleTime: TimeInterval, displayTimeThreshold: TimeInterval) { 20 | self.message = message 21 | self.minimumVisibleTime = minimumVisibleTime 22 | self.displayTimeThreshold = displayTimeThreshold 23 | } 24 | 25 | } 26 | 27 | class PresenterViewController: UIViewController, NVActivityIndicatorViewable { } 28 | 29 | class LoadingIndicatorManager { 30 | 31 | static let sharedInstance = LoadingIndicatorManager() 32 | 33 | fileprivate let presenter = PresenterViewController() 34 | fileprivate var showActivityTimer: Timer? 35 | fileprivate var hideActivityTimer: Timer? 36 | fileprivate var userWantsToStopActivity = false 37 | 38 | fileprivate init() { } 39 | 40 | func show(message: String? = nil, minimumVisibleTime: TimeInterval, displayTimeThreshold: TimeInterval) { 41 | let data = ActivityData(message: message, minimumVisibleTime: minimumVisibleTime, displayTimeThreshold: displayTimeThreshold) 42 | guard showActivityTimer == nil else { return } 43 | userWantsToStopActivity = false 44 | showActivityTimer = scheduleTimer(data.displayTimeThreshold, selector: #selector(LoadingIndicatorManager.showActivityTimerFired(_:)), data: data) 45 | } 46 | 47 | func hide() { 48 | userWantsToStopActivity = true 49 | guard hideActivityTimer == nil else { return } 50 | hideActivity() 51 | } 52 | 53 | // MARK: - Timer events 54 | 55 | @objc func hideActivityTimerFired(_ timer: Timer) { 56 | hideActivityTimer?.invalidate() 57 | hideActivityTimer = nil 58 | if userWantsToStopActivity { 59 | hideActivity() 60 | } 61 | } 62 | 63 | @objc func showActivityTimerFired(_ timer: Timer) { 64 | guard let activityData = timer.userInfo as? ActivityData else { return } 65 | showActivity(activityData) 66 | } 67 | 68 | // MARK: - Helpers 69 | 70 | fileprivate func showActivity(_ data: ActivityData) { 71 | presenter.startAnimating(LoadingIndicator.size, message: data.message, type: LoadingIndicator.type, color: LoadingIndicator.color, padding: nil) 72 | hideActivityTimer = scheduleTimer(data.minimumVisibleTime, selector: #selector(LoadingIndicatorManager.hideActivityTimerFired(_:)), data: nil) 73 | } 74 | 75 | fileprivate func hideActivity() { 76 | presenter.stopAnimating() 77 | showActivityTimer?.invalidate() 78 | showActivityTimer = nil 79 | } 80 | 81 | fileprivate func scheduleTimer(_ timeInterval: TimeInterval, selector: Selector, data: ActivityData?) -> Timer { 82 | return Timer.scheduledTimer(timeInterval: timeInterval, target: self, selector: selector, userInfo: data, repeats: false) 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift-Project-Template 2 | 3 | [![Build Status](https://travis-ci.org/xmartlabs/Swift-Project-Template.svg?branch=master)](https://travis-ci.org/xmartlabs/Swift-Project-Template) 4 | 5 | Create your iOS Base project in just a few seconds by executing a script and answering some project questions. This is what we use to get started with a new iOS project from scratch! 6 | 7 | Swift Project Template provides us with a base iOS project template along with and a swift script to make naming customizations on it. 8 | 9 | Currently you can find the project template under the [master](/tree/master) branch and a more complete example project (including more example files) under the [ExampleProject](/tree/ExampleProject) branch. 10 | 11 | iOS project has the following configuration: 12 | 13 | * Targets 14 | * Test: Unit tests working with Quick and Nimble. 15 | * UITest: Functional tests working with Nimble matcher. 16 | * App Production Target. 17 | * App Staging Target. Same app source code with a different bundle id, it points to a different Restful API (staging one). 18 | 19 | * Project Configuration 20 | * R-Swift integration. 21 | * Warnings for TODO and FIXME comments. 22 | * Swift Lint integration. 23 | * Crashlytics integration. 24 | * `travis.yml` file. 25 | * `podfile` containing most used libraries by us. 26 | - Realm, Decodable, Alamofire, RxSwift, Eureka, OperaSwift and many others. 27 | 28 | * Networking 29 | * `Alamofire` networking library. 30 | * `OperaSwift` network abstraction layer integrated along with some examples. 31 | 32 | 33 | ##### Usage 34 | 35 | Clone the repository: 36 | 37 | ```shell 38 | git clone git@github.com:xmartlabs/Swift-Project-Template.git 39 | ``` 40 | Run `shell.swift` script from there: 41 | 42 | ```swift 43 | swift -target x86_64-apple-macosx10.11 Swift-Project-Template/shell.swift 44 | ``` 45 | 46 | Answer some questions: 47 | 48 | 49 | 50 | We are done! Now start coding your app! 🍻🍻 51 | -------------------------------------------------------------------------------- /readme-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xmartlabs/Swift-Project-Template/a9b64fffd57bffee27d3319425817a383d0df953/readme-image.png -------------------------------------------------------------------------------- /shell.swift: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env xcrun swift 2 | 3 | import Foundation 4 | 5 | let templateProjectName = "XLProjectName" 6 | let templateBundleDomain = "XLOrganizationIdentifier" 7 | let templateAuthor = "XLAuthorName" 8 | let templateAuthorWebsite = "XLAuthorWebsite" 9 | let templateUserName = "XLUserName" 10 | let templateOrganizationName = "XLOrganizationName" 11 | 12 | var projectName = "MyProject" 13 | var bundleDomain = "com.xmartlabs" 14 | var author = "Xmartlabs SRL" 15 | var authorWebsite = "https://xmartlabs.com" 16 | var userName = "xmartlabs" 17 | var organizationName = "Xmartlabs SRL" 18 | 19 | let fileManager = FileManager.default 20 | 21 | let runScriptPathURL = NSURL(fileURLWithPath: fileManager.currentDirectoryPath, isDirectory: true) 22 | let currentScriptPathURL = NSURL(fileURLWithPath: NSURL(fileURLWithPath: CommandLine.arguments[0], relativeTo: runScriptPathURL as URL).deletingLastPathComponent!.path, isDirectory: true) 23 | let iOSProjectTemplateForlderURL = NSURL(fileURLWithPath: "Project-iOS", relativeTo: currentScriptPathURL as URL) 24 | var newProjectFolderPath = "" 25 | let ignoredFiles = [".DS_Store", "UserInterfaceState.xcuserstate"] 26 | 27 | extension NSURL { 28 | var fileName: String { 29 | var fileName: AnyObject? 30 | try! getResourceValue(&fileName, forKey: URLResourceKey.nameKey) 31 | return fileName as! String 32 | } 33 | 34 | var isDirectory: Bool { 35 | var isDirectory: AnyObject? 36 | try! getResourceValue(&isDirectory, forKey: URLResourceKey.isDirectoryKey) 37 | return isDirectory as! Bool 38 | } 39 | 40 | func renameIfNeeded() { 41 | if let _ = fileName.range(of: "XLProjectName") { 42 | let renamedFileName = fileName.replacingOccurrences(of: "XLProjectName", with: projectName) 43 | try! FileManager.default.moveItem(at: self as URL, to: NSURL(fileURLWithPath: renamedFileName, relativeTo: deletingLastPathComponent) as URL) 44 | } 45 | } 46 | 47 | func updateContent() { 48 | guard let path = path, let content = try? String(contentsOfFile: path, encoding: String.Encoding.utf8) else { 49 | print("ERROR READING: \(self)") 50 | return 51 | } 52 | var newContent = content.replacingOccurrences(of: templateProjectName, with: projectName) 53 | newContent = newContent.replacingOccurrences(of: templateBundleDomain, with: bundleDomain) 54 | newContent = newContent.replacingOccurrences(of: templateAuthor, with: author) 55 | newContent = newContent.replacingOccurrences(of: templateUserName, with: userName) 56 | newContent = newContent.replacingOccurrences(of: templateAuthorWebsite, with: authorWebsite) 57 | newContent = newContent.replacingOccurrences(of: templateOrganizationName, with: organizationName) 58 | try! newContent.write(to: self as URL, atomically: true, encoding: String.Encoding.utf8) 59 | } 60 | } 61 | 62 | func printInfo(message: T) { 63 | print("\n-------------------Info:-------------------------") 64 | print("\(message)") 65 | print("--------------------------------------------------\n") 66 | } 67 | 68 | func printErrorAndExit(message: T) { 69 | print("\n-------------------Error:-------------------------") 70 | print("\(message)") 71 | print("--------------------------------------------------\n") 72 | exit(1) 73 | } 74 | 75 | func checkThatProjectForlderCanBeCreated(projectURL: NSURL){ 76 | var isDirectory: ObjCBool = true 77 | if fileManager.fileExists(atPath: projectURL.path!, isDirectory: &isDirectory){ 78 | printErrorAndExit(message: "\(projectName) \(isDirectory.boolValue ? "folder already" : "file") exists in \(runScriptPathURL.path) directory, please delete it and try again") 79 | } 80 | } 81 | 82 | func shell(args: String...) -> (output: String, exitCode: Int32) { 83 | let task = Process() 84 | task.launchPath = "/usr/bin/env" 85 | task.arguments = args 86 | task.currentDirectoryPath = newProjectFolderPath 87 | let pipe = Pipe() 88 | task.standardOutput = pipe 89 | task.launch() 90 | task.waitUntilExit() 91 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 92 | let output = NSString(data: data, encoding: String.Encoding.utf8.rawValue) as? String ?? "" 93 | return (output, task.terminationStatus) 94 | } 95 | 96 | func prompt(message: String, defaultValue: String) -> String { 97 | print("\n> \(message) (or press Enter to use \(defaultValue))") 98 | let line = readLine() 99 | return line == nil || line == "" ? defaultValue : line! 100 | } 101 | 102 | print("\nLet's go over some question to create your base project code!") 103 | 104 | projectName = prompt(message: "Project name", defaultValue: projectName) 105 | print(projectName) 106 | 107 | // Check if folder already exists 108 | let newProjectFolderURL = NSURL(fileURLWithPath: projectName, relativeTo: runScriptPathURL as URL) 109 | newProjectFolderPath = newProjectFolderURL.path! 110 | checkThatProjectForlderCanBeCreated(projectURL: newProjectFolderURL) 111 | 112 | bundleDomain = prompt(message: "Bundle domain", defaultValue: bundleDomain) 113 | author = prompt(message: "Author", defaultValue: author) 114 | authorWebsite = prompt(message: "Author Website", defaultValue: authorWebsite) 115 | userName = prompt(message: "Github username", defaultValue: userName) 116 | organizationName = prompt(message: "Organization Name", defaultValue: organizationName) 117 | 118 | // Copy template folder to a new folder inside run script url called projectName 119 | do { 120 | try fileManager.copyItem(at: iOSProjectTemplateForlderURL as URL, to: newProjectFolderURL as URL) 121 | } catch let error as NSError { 122 | printErrorAndExit(message: error.localizedDescription) 123 | } 124 | 125 | // rename files and update content 126 | let enumerator = fileManager.enumerator(at: newProjectFolderURL as URL, includingPropertiesForKeys: [.nameKey, .isDirectoryKey], options: [], errorHandler: nil)! 127 | var directories = [NSURL]() 128 | print("\nCreating \(projectName) ...") 129 | while let fileURL = enumerator.nextObject() as? NSURL { 130 | guard !ignoredFiles.contains(fileURL.fileName) else { continue } 131 | if fileURL.isDirectory { 132 | directories.append(fileURL) 133 | } 134 | else { 135 | fileURL.updateContent() 136 | fileURL.renameIfNeeded() 137 | } 138 | } 139 | for fileURL in directories.reversed() { 140 | fileURL.renameIfNeeded() 141 | } 142 | 143 | print("git init\n") 144 | print(shell(args: "git", "init").output) 145 | print("git add .\n") 146 | print(shell(args: "git", "add", ".").output) 147 | print("git commit -m 'Initial commit'\n") 148 | print(shell(args: "git", "commit", "-m", "'Initial commit'").output) 149 | print("git remote add origin git@github.com:\(userName)/\(projectName).git\n") 150 | print(shell(args: "git", "remote", "add", "origin", "git@github.com:\(userName)/\(projectName).git").output) 151 | print("pod install --project-directory=\(projectName)\n") 152 | print(shell(args: "pod", "install", "--project-directory=\(projectName)").output) 153 | print("open \(projectName)/\(projectName).xcworkspace\n") 154 | print(shell(args: "open", "\(projectName)/\(projectName).xcworkspace").output) 155 | --------------------------------------------------------------------------------