├── .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 |
37 |
38 |
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 | [](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 |
--------------------------------------------------------------------------------