Compare commits

..

31 Commits

Author SHA1 Message Date
Ivan Smolin de0d556bae simplify TotalCountCursor constraints 2018-03-06 15:22:44 +03:00
Ivan Smolin 7ab28068ad Merge branch 'master' into feature/general_loading_and_pagination
# Conflicts:
#	LeadKit.xcodeproj/project.pbxproj
2018-01-18 16:03:05 +03:00
Ivan Smolin 38eb97d60f image rotation 2017-12-20 16:09:26 +03:00
Ivan Smolin 99ef62dfea Rx DataSourceProtocol conformance 2017-12-06 14:17:26 +03:00
Ivan Smolin b5f36019ae fix renderTemplate for support UIImage+Extensions 2017-12-05 19:49:43 +03:00
Ivan Smolin 2c59c0ff90 proper indent 2017-12-05 19:48:45 +03:00
Ivan Smolin bd5fc5bfb1 Merge branch 'master' into feature/general_loading_and_pagination
# Conflicts:
#	Sources/Extensions/Alamofire/AlamofireManager+Extensions.swift
2017-12-05 16:56:16 +03:00
Ivan Smolin 0677931dd3 Ability to map primitive response types (`String`, `Int`, `[String]`, etc.). 2017-12-05 16:04:49 +03:00
Ivan Smolin 85b1a119e2 Merge branch 'master' into feature/general_loading_and_pagination
# Conflicts:
#	Sources/Extensions/Alamofire/AlamofireManager+Extensions.swift
2017-12-01 17:21:19 +03:00
Ivan Smolin b3f963a524 Revert "fix crash when NetworkService creates lazy in background thread (globalinit_*_func0)"
This reverts commit 149467cd4d.
2017-12-01 15:52:32 +03:00
Ivan Smolin 91b6cc8f3c add acceptable status codes to network service constructor 2017-12-01 15:28:53 +03:00
Ivan Smolin 149467cd4d fix crash when NetworkService creates lazy in background thread (globalinit_*_func0) 2017-12-01 15:11:47 +03:00
Ivan Smolin 5c66ad9c69 pass acceptable status codes 2017-12-01 14:59:44 +03:00
Ivan Smolin 5f001c75f0 Merge branch 'master' into feature/general_loading_and_pagination
# Conflicts:
#	Sources/Classes/Views/SeparatorCellTableCell/SeparatorTableCell.swift
2017-12-01 14:59:23 +03:00
Ivan Smolin f685b8555d separator cell 2017-11-14 17:25:04 +03:00
Ivan Smolin 8b17fd5d17 fix double calling load more 2017-11-13 16:16:50 +03:00
Ivan Smolin e76f557d2e fix total count cursor 2017-11-12 22:38:42 +03:00
Ivan Smolin 9a47fff654 fix total count cursor result sharing 2017-11-12 22:07:46 +03:00
Ivan Smolin b0e3463581 fix safe index 2017-11-10 12:57:30 +03:00
Ivan Smolin 3cdae4b398 small fix 2017-11-09 13:17:32 +03:00
Ivan Smolin 5a6a8e77a2 Merge branch 'master' into feature/general_loading_and_pagination 2017-11-09 13:15:26 +03:00
Ivan Smolin a9e3ded0c2 pagination wrapper fixes 2017-11-09 13:14:46 +03:00
Ivan Smolin dd170e0873 naming fix 2017-11-08 17:57:17 +03:00
Ivan Smolin ba94c670b3 pass xib view to configure function 2017-11-08 14:22:37 +03:00
Ivan Smolin 03cc92553c pagination fixes 2017-11-08 14:22:23 +03:00
Ivan Smolin 334105c7af change parameter naming 2017-11-03 20:44:04 +03:00
Ivan Smolin 54b3b22d72 total count cursor fixes 2017-11-03 13:33:55 +03:00
Ivan Smolin 80c416f247 pass datasource to pagination results 2017-11-03 05:02:19 +03:00
Ivan Smolin cf6f8e9268 fix wrong error state 2017-11-02 22:28:05 +03:00
Ivan Smolin 02dfc709c3 temporary change podspec version 2017-11-02 20:58:10 +03:00
Ivan Smolin d33a865b3e PaginationWrapper now works with UICollectionView 2017-11-02 20:45:29 +03:00
1044 changed files with 4228 additions and 68653 deletions

View File

@ -1,2 +0,0 @@
---
BUNDLE_PATH: ".gem"

View File

@ -1,96 +0,0 @@
#!/usr/bin/env python3
import sys, re, os
from subprocess import check_output
from sys import getdefaultencoding
getdefaultencoding() # utf-8
valid_commit_style = '^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style)(\(\S+\))?\!?: .+'
merge_commit_style = '^(m|M)erge .+'
success_title = 'SUCCESS'
success_color = '92m'
error_title = 'ERROR'
error_message = 'Incorrect commit message style!\nThe commit pattern:'
error_commit_pattern = ' type(scope): message | type: message \n'
error_color = '91m'
breaking_changes_message = 'If commit include Breaking changes use ! after type or scope:'
colored_breaking_changes_message = 'If commit include \033[91mBreaking changes\033[00m use \033[91m!\033[00m after type or scope:'
breaking_changes_commit_pattern = ' type(scope)!: message | type!: message \n'
available_types_message = 'Available commit types:'
available_commit_types = ['build: Changes that affect the build system or external dependencies',
'ci: Changes to our CI configuration files and scripts',
'docs: Documentation only changes',
'feat: A new feature. Correlates with MINOR in SemVer',
'fix: A bug fix. Correlates with PATCH in SemVer',
'perf: A code change that improves performance',
'refactor: A code change that neither fixes',
'revert: A revert to previous commit',
'style: Changes that do not affect the meaning of the code (white-space, formatting, etc)']
is_GUI_client = False
def print_result_header(result_title, color):
if not is_GUI_client:
print("[\033[96mcommit lint\033[00m] [\033[{}{}\033[00m]\n".format(color, result_title))
def print_pattern(pattern):
if is_GUI_client:
print(pattern)
else:
print("\033[96m{}\033[00m".format(pattern))
def print_error_message():
print_result_header(error_title, error_color)
print(error_message)
print_pattern(error_commit_pattern)
if is_GUI_client:
print(breaking_changes_message)
else:
print(colored_breaking_changes_message)
print_pattern(breaking_changes_commit_pattern)
print_available_commit_types()
def print_available_commit_types():
print(available_types_message)
for commit_type in available_commit_types:
print(" - %s" %commit_type)
def write_commit_message(fh, commit_msg):
fh.seek(0, 0)
fh.write(commit_msg)
def lint_commit_message(fh, commit_msg):
is_merge_commit = re.findall(merge_commit_style, commit_msg)
is_valid_commit = re.findall(valid_commit_style, commit_msg)
if is_valid_commit or is_merge_commit:
print_result_header(success_title, success_color)
write_commit_message(fh, commit_msg)
sys.exit(os.EX_OK)
else:
print_error_message()
sys.exit(os.EX_DATAERR)
def run_script():
commit_msg_filepath = sys.argv[1]
with open(commit_msg_filepath, 'r+') as fh:
commit_msg = fh.read()
lint_commit_message(fh, commit_msg)
try:
sys.stdin = open("/dev/tty", "r")
is_GUI_client = False
except:
is_GUI_client = True
run_script()

81
.gitignore vendored
View File

@ -1,18 +1,16 @@
# ================
# Swift.gitignore
# ================
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## User settings
xcuserdata/
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
*.xcscmblueprint
*.xccheckout
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
## Build generated
build/
DerivedData/
*.moved-aside
DerivedData
## Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
@ -21,14 +19,17 @@ DerivedData/
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
## Other
*.xccheckout
*.moved-aside
*.xcuserstate
*.xcscmblueprint
## Obj-C/Swift specific
*.hmap
## App packaging
*.ipa
*.dSYM.zip
*.dSYM
## Playgrounds
timeline.xctimeline
@ -38,14 +39,6 @@ playground.xcworkspace
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
# Package.pins
# Package.resolved
# *.xcodeproj
#
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
.swiftpm
.build/
# CocoaPods
@ -55,56 +48,30 @@ playground.xcworkspace
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
Pods/
#
# Add this line if you want to avoid checking in source code from the Xcode workspace
# *.xcworkspace
# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
Carthage/Checkouts
Carthage/Build/
# Accio dependency management
Dependencies/
.accio/
Carthage/Build
# fastlane
#
# It is recommended to not store the screenshots in the git repo.
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control
# https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output
fastlane/screenshots
# Code Injection
#
# After new code Injection tools there's a generated folder /iOSInjectionProject
# https://github.com/johnno1962/injectionforxcode
iOSInjectionProject/
# AppCode
# https://intellij-support.jetbrains.com/hc/en-us/articles/206544839-How-to-manage-projects-under-Version-Control-Systems
# homebrew-bundle
Brewfile.lock.json
.idea/workspace.xml
.idea/tasks.xml
# Node.js
# Dependency directories
node_modules/
# Touch Instinct custom
Downloads/
fastlane/README.md
Templates/
cpd-output.xml
*.swp
*IDEWorkspaceChecks.plist
# Gem
.gem/
.DS_Store

6
.gitmodules vendored
View File

@ -1,3 +1,3 @@
[submodule "build-scripts"]
path = build-scripts
url = https://git.svc.touchin.ru/TouchInstinct/BuildScripts.git
[submodule "code-quality"]
path = code-quality
url = https://github.com/TouchInstinct/code-quality-ios

1
.swift-version Normal file
View File

@ -0,0 +1 @@
4.0

1
.swiftlint.yml Symbolic link
View File

@ -0,0 +1 @@
code-quality/.swiftlint.yml

1
.tailor.yml Symbolic link
View File

@ -0,0 +1 @@
code-quality/.tailor.yml

View File

@ -1,850 +1,5 @@
# Changelog
### 1.56.0
- **Update**: `ViewSkeletonsConfiguration`. It's possible to enable or disable animation for specific skeletons now.
- **Added**: `HolderViewSkeletonsConfiguration` for skeleton root view configuration
- **Added**: `DashedBoundsLayer` can now be applied to `CALayer`
### 1.55.1
- **Update**: revert `TextSkeletonsConfiguration` line height calculation
### 1.55.0
- **Update**: use TouchInstinct `TableKit` fork instead of original one
- **Update**: remove default value from `BoolValueDefaultsStorage`
### 1.54.6
- **Added**: `xcprivacy` files
- **Update**: Correctly detect app reinstall in `AppInstallLifetimeSingleValueStorage`
- **Update**: use `xHeight` instead of `pointSize` for default skeleton line height calculation
- **Update**: update `linkTextAttributes` in `UITextView` when setting interactive url parts
### 1.54.5
- **Update**: Сhange `StatefulButton` event propogation avoidance method.
### 1.54.4
- **Update**: Fix `StatefulButton` state configuration for iOS 15+.
### 1.54.3
- **Update**: Set reasonable defaults for `SkeletonConfiguration`.
### 1.54.2
- **Update**: Changed access level from internal to public of title and subtitle view in `BaseTitleSubtitleView`.
### 1.54.1
- **Added**: `BaseTitleSubtitleView` which can be inherited for fine-tuning skeletons and other behavior.
- **Update**: Changed lines number calculation method in `TextSkeletonsConfiguration`.
### 1.54.0
- **Added**: `maxWidth` parameter to `BaseViewSkeletonsConfiguration`.
- **Added**: custom `SkeletonConfigurations` for nested `SkeletonPresenters`.
- **Update**: Many fixes and improvenments to `TextSkeletonsConfiguration`.
### 1.53.3
- **Update**: `Skeletonable` can now control custom geometry change notification.
- **Update**: Filter hidden views from skeletonable views by default.
### 1.53.2
- **Update**: `DefaultTitleSubtitleView` support for separated configuration of title and subtitle labels layout.
- **Update**: `BaseListItemView` fixed trailing insets when trailing view is hidden.
### 1.53.1
- **Update**: Insets layout heuristics for `WrappedViewHodler` implementations
### 1.53.0
- **Added**: Custom string attributes to `BaseTextAttributes`
- **Added**: Customizeable `UIViewBackground` and `UIViewBorder` for `UIView.Appearance`
- **Added**: Keychain single value storage for codable models -`CodableSingleValueKeychainStorage`
- **Update**: Renamed methods `startAnimation` and `stopAnimation` of `SkeletonPresenter`, so it won't conflict with `Animatable` protocol anymore
### 1.52.0
- **Added**: `TIApplication` module with core dependencies of main application and its extension targets
- **Added**: `DefaultHomogeneousItemsCollectionView` default collection view implementation with configurable identical-type cells
- **Update**: Changed implementation of `AppInstallLifetimeSingleValueStorage`. Now it uses `SingleValueStorage<Bool>` to be able to migrate stored UserDefaults values
- **Added**: `UserLocationFetcher.OnLocationFetchFailureCallback` and `ItemDistanceTo` in `TIMapUtils`
- **Added**: Tap handler closure to `DefaultConfigurableStatefulButton.ViewModel`
- **Added**: Universal DSL
### 1.51.0
- **Added**: `BaseModalViewController` implementing `PanModalPresentable` with additional functionality
- **Added**: `BaseModalWrapperViewController` for wrapping `UIViewController`s with `BaseModalViewController` functionality
### 1.50.0
- **Updated**: Fix activity indicator positioning for `StatefulButton` on iOS 15+ and disabled state touch handling
- **Added**: iOS 15+ activity indicator placement support in `StatefulButton`
- **Added**: `TICoreGraphicsUtils` module for drawing operations and other CoreGraphics related functionality
- **Update**: `MarkerIconFactory` can now return optional `UIImage`. In this case MapManagers will show the default marker icon.
### 1.49.0
- **Added**: `BaseMigratingSingleValueKeychainStorage` and `BaseMigratingSingleValueDefaultsStorage` implementations for migrating keys from one storage to another.
### 1.48.0
- **Added**: `BaseStackView` with configurable items appearance
- **Fixed**: `CollectionTableViewCell` self-sizing
- **Added**: `ViewAppearance.WrappedViewLayout` support for all `WrappedViewHolders`
- **Added**: `ViewCallbacks` support for all `BaseInitializeableViews`
### 1.47.0
- **Added**: `flatMap` operator for `AsyncOperation`
- **Update**: `CodableKeyValueStorage` now returns `Swift.Result` with typed errors.
- **Added**: `SingleValueExpirationStorage` for time aware entries (expirable api tokens, etc.)
- **Added**: `AsyncOperation` variants of process methods in NetworkServices.
### 1.46.0
- **Added**: `AsyncSingleValueStorage` and `SingleValueStorageAsyncWrapper<SingleValueStorage>` for async access to SingleValue storages wtih swift concurrency support
- **Added**: `BaseMapUISettings` used to configure map view of different providers + user location icon rendering for yandex maps
- **Added**: `UserLocationFetcher` helper that requests authorization and subscribes to user location updates
- **Update**: add `DEVELOPMENT_INSTALL` support for all podspecs and fix playground compilation issues
### 1.45.0
- **Added**: `SingleValueStorage` implementations + `AppInstallLifetimeSingleValueStorage` for automatically removing keychain items on app reinstall.
- **Added**: `TILogging` with error logging types
- **Update**: `DefaultRecoverableJsonNetworkService` supports iOS 12.
- **Update**: `DefaultFingerprintsProvider` now uses `SingleValueStorage`
### 1.44.0
- **Added**: HTTP status codes to `EndpointErrorResult.apiError` responses
- **Added**: SwiftLint pre-build SPM step to TINetworking module
### 1.43.1
- **Fixed**: build scripts submodule url
### 1.43.0
- **Added**: `TITextProcessing` for regex and text formatting added
### 1.42.1
- **Fixed**: Podspecs source and homepage urls
### 1.42.0
- **Added**: TIDeeplink to support deeplink API
### 1.41.0
- **Update**: added callbacks for views while skeletons change status to presented or hidden
### 1.40.0
- **Added**: `PlaceholderFactory` for creating `DefaultPlaceholderView` views
- **Added**: `DefaultPlaceholderImageView`
### 1.39.0
- **Added**: UIButton Appearance model
- **Added**: `SpacedWrappedViewLayout` for spacing configurations
- **Update**: UIView appearance model with border configurations
### 1.38.0
- **Added**: Placemarks states for icon updating
- **Added**: Selecting / deselecting markers through cluster manager
### 1.37.0
- **Added**: API for converting view hierarchy to skeletons
### 1.36.1
- **Update**: `YandexMapsMobile` version updated
- **Fix**: Map manager memory leak removed
### 1.36.0
- **Removed**: `TILogger`module
- **Updated**: moved `LoggingPresenter` to `TIDeveloperUtils` module.
### 1.35.1
- **Added**: Auto documentation generation for `TIFoundationUtils` playground and compile checks for playground before release
- **Updated**: `AsyncOperation` fixed ordering of chain operations execution
### 1.35.0
- **Added**: `TIDeveloperUtils` framework, that contains different utils for development
- **Added**: `UIView` and `UIViewController` extensions for showing SwiftUI previews
- **Added**: `DashedBoundsLayer` for debugging views' frames visually
### 1.34.0
- **Added**: `BaseListItemView` for displaying three views horizontally
- **Added**: `DefaultTitleSubtitleView` for displaying one or two labels vertically
- **Update**: `StatefulButton` now can be configured with `ViewAppearance` model for each state
### 1.33.0
- **Added**: `ViewAppearance` and `ViewLayout` models for setting up Views' appearance and layout
- **Added**: `TableKit.Row` extension for configuration inner View's appearance and layout
- **Added**: `WrappableView` with typealiases for creating wrapped in the container views
- **Added**: `CollectionTableViewCell` and `ContainerView`
- **Update**: Separator appearance configureation for table views
### 1.32.0
- **Added**: `BaseInitializableWebView` with navigation and error handling api.
### 1.31.0
- **Added**: `URLInteractiveTextView` for terms and conditions hints in login flow
### 1.30.0
- **Added**: Base classes for encryption and decryption user token with pin code or biometry
- **Added**: Pin code validators
### 1.29.1
- **Updated**: `BaseTextAttributes` correct detection of the necessity of using attributed string
### 1.29.0
- **Added**: `BaseTextAttributes`can now measure text size and provides paragraph style configuration API.
- **Removed**: `ViewText`. Was fully replaced with `BaseTextAttributes`
- **Fixed**: `Operation.flattenDependencies` used in `Operation.add(to:waitUntilFinished:)` now works correctly.
- **Added**: Now it's possible to add dependent operation to start of the queue.
### 1.28.0
- **Add**: `LoggingPresenter`to present list of logs with ability of sharing it
- **Add**: `TILogger` wrapper object to log events.
### 1.27.1
- **Fix**: Weak target reference in `RefreshControl`
### 1.27.0
- **Add**: Tag like filter collection view
- **Add**: List like filter table view
- **Add**: Range like filter view
### 1.26.3
- **Update**: Add @escaping in `RequestExecutor.ExecutionClosure`
### 1.26.2
- **Update**: Add failureCompletion in `RequestExecutor`
### 1.26.1
- **Fix**: Use OperationQueue instead of NSLock in `DefaultTokenInterceptor`
- **Update**: AsyncOperation refactoring
### 1.26.0
- **Add**: `TIEcommerce` module with Cart, products, promocodes, bonuses and other related actions.
### 1.25.0
- **Update**: `RequestError` cases now contain additional url assotiated value
- **Update**: Network requests error catching now throws `RequestError` with url
### 1.24.0
- **Add**: `AlertFactory` for presenting alerts in SwiftUI and UIKit.
### 1.23.0
- **Update**: `UITextView` now support configuration with `BaseTextAttributes`
- **Add**: `ReconfigurableView` & `ChangeableViewModel` for non-destructing state update
- **Add**: `WrappedViewHolder` protocol with table/collection view cell implementations
- **Add**: `UIViewPresenter` and `ReusableUIViewPresenter` protocols with default implementation for proper handling view/cells reuse
### 1.22.0
- **Update**: Asynchronous request preprocessing
### 1.21.0
- **Update**: `AsyncEventHandler` was replaced with `EndpointRequestRetrier`
- **Add**: `FingerprintsTrustEvaluator` and `FingerprintsProvider` for fingerprint-based host trust evaluation
- **Add**: `DefaultTokenInterceptor` for queue-based token refresh across all requests of single api interactor (network service).
- **Update**: `DefaultRecoverableJsonNetworkService` now returns collection of errors in result
- **Update**: `CancellableTask` was renamed to `Cancellable`. Cancellable implementations has been moved from `TIMoyaNetworking` to `TIFoundationUtils`.
- **Add**: `ApiInteractor` protocol with basic request/response methods
### 1.20.0
- **Add**: OpenAPI security schemes support for EndpointRequest's.
- **Update**: Replace `AdditionalHeadersPlugin` with `SecuritySchemePreprocessor` and `EndpointRequestPreprocessor` (with default implementations)
### 1.19.0
- **Add**: Add presenter protocols to `TISwiftUICore` and `TIUIKitCore` modules
- **Add**: `CodeConfirmPresenter` protocol and `DefaultCodeConfirmPresenter` implementation in `TIAuth` module
### 1.18.0
- **Add**: add MapManagers for routine maps configuration
### 1.17.0
- **Add**: add smooth CameraUpdate actions for supported maps
### 1.16.2
- **Update**: `DefaultRecoverableJsonNetworkService` now supports error forwarding from its error handlers to initial requests.
### 1.16.1
- **Update**: `DateFormattersReusePool` and `ISO8601DateFormattersReusePool` are now thread safe.
### 1.16.0
- **Add**: `TIMapUtils`, `TIAppleMapUtils`, `TIGoogleMapUtils` and `TIYandexMapUtils` modules for map items clustering and interacting with them.
### 1.15.0
- **Update**: Network services in TIMoyaNetworking now passes MoyaError in result of EnpointRequest execution.
- **Add**: `TINetworkingCache` module - caching results of EndpointRequests.
- **Important Note**: `TINetworkingCache` added via SPM may require you to add `DISABLE_DIAMOND_PROBLEM_DIAGNOSTIC=YES` flag to build settings of project target (see [probably related problem](https://forums.swift.org/t/adding-a-package-to-two-targets-in-one-projects-results-in-an-error/35007/18))
### 1.14.3
- **Fix**: Creating headerView and footerView when initializing a section with rows in `TITableKitUtils`.
- **Add**: Empty table section initialization method in `TITableKitUtils`.
### 1.14.2
- **Update**: DateFormatters properties preset in reuse pools
### 1.14.1
- **Fix**: Array encoding for `QueryStringParameterEncoding`
### 1.14.0
- **Add**: [Date] coding methods
### 1.13.0
- **Update**: Change access modifiers in `DefaultJsonNetworkService` from `public` to `open`, added additional Moya plugins processing
- **Add**: `DisplayDecodingErrorPlugin` for showing developer-frendly decoding error messages
- **Add**: Gemfile for cocoapods versioning
### 1.12.3
- **Fix**: Try parse date in ISO8601 format appending `.withFractionalSeconds` if `.withInternetDateTime` fails
### 1.12.2
- **Fix**: HeaderParameterEncoding encodes array correctly
### 1.12.1
- **Update**: DefaultRecoverableNetworkService `request` parameter was renamed to prevent ambgious reference
### 1.12.0
- **Update**: EndpointRequest Body can take a nil value
- **Update**: Parameter value can be nil as well
- **Update**: observe operator of AsyncOperation now accepts callback queue parameter
### 1.11.1
- **Fix**: `timeoutIntervalForRequest` parameter for `URLSessionConfiguration` in `NetworkServiceConfiguration` added.
### 1.11.0
- **Breaking changes**: many method signatures was changes in `TIMoyaNetworking`.
- **Add**: `ISO8601DateFormattersReusePool` and codable helpers for ISO8601 date (de)coding.
- **Add**: Moya plugin protocol for adding HTTP headers with default implementation.
### 1.10.0
- **Add**: `DefaultRecoverableJsonNetworkService` with error handling chain.
### 1.9.0
- **Add**: `TIMoyaNetworking` target - Moya + Swagger network service.
- **Update**: `TISwiftUtils` - added async closure typealiases.
- **Update**: `TIFoundationUtils` - added date formatting & decoding helpers.
### 1.8.0
- **Add**: `TIFoundationUtils.AsyncOperation` - generic subclass of Operation with chaining and result observation support
### 1.7.0
- **Add**: `TINetworking` - Swagger-frendly networking layer helpers
### 1.6.0
- **Add**: the pretty timer - TITimer.
### 1.5.0
- **Add**: `HeaderTransitionDelegate` - Helper for transition of TableView header and navigationBar title view
### 1.4.0
- **Update**: update minor dependencies.
- **Fix**: project's scripts.
### 1.3.0
- **Add**: `TIPaginator` - realisation of paginating items from a data source.
### 1.2.0
- **Add**: `TIKeychainUtils` - Set of helpers for Keychain classes.
### 1.1.1
- **Fix**: `StatefullButton` propagation
### 1.1.0
- **Add**: `BaseInitializeableViewController`, `BaseCustomViewController` and `BaseViewController` to TIUIKitCore.
- **Add**: `TableKitTableView` and `TableDirectorHolder` to TITableKitUtils.
### 1.0.0
- **API BreakingChanges**: up swift version to 5.1.
- **Update**: build scripts.
- **Update**: code with new swiftlint rules.
- **Update**: RxSwift to 6.0.0.
### 0.13.1
- **Fix**: LeadKit.podspec file.
### 0.13.0
- **Add**: Githook `prepare-commit-msg` to check commit's style.
- **Add**: Setup script.
### 0.12.0
- **Add**: StatefulButton & RoundedStatefulButton to TIUIElements.
- **Add**: added CACornerMask rounding extension to TIUIElements.
- **Add**: UIControl.State dictionary extensions to TIUIKitCore.
- **Add**: UIFont and CTFont extensions to TIUIKitCore.
- **Breaking change**: reworked BaseTextAttributes & ViewText. Removed ViewTextConfigurable protocol & conformances.
### 0.11.0
- **Add**: Cocoapods support for TI-family libraries.
- **Add**: `SeparatorConfigurable` and all helper types for separator configuration.
- **Add**: `BaseSeparatorCell` - `BaseInitializeableCell` subclass with separators support.
- **Add**: `TITableKitUtils` - set of helpers for TableKit classes.
- **Add**: `BaseTextAttributes` and `ViewText` implementation form LeadKit.
- **Update**: `BaseInitializableView` and `BaseInitializableControl` are moved to `TIUIElements` from `TIUIKitCore`.
### 0.10.9
- **Fix**: `change presentedOrTopViewController to open`.
### 0.10.8
- **Fix**: `Add presentedOrTopViewController`.
### 0.10.7
- **Fix**: `Add BaseOrientationController`.
- **Fix**: `Add videoOrientation extension`.
### 0.10.6
- **Fix**: `Add tvos exclude files`.
### 0.10.5
- **Add**: `OrientationNavigationController` .
- **Add**: `Forced Interface Orientation logic to BaseConfigurableController` .
- **Fix**: `Exclude files to watchos and tvos`.
### 0.10.4
- **Fix**: `noConnection` error.
### 0.10.3
- **Fix**: `mappingQueue` of `SessionManager`.
### 0.10.2
- **Add**: `RefreshControl` - a basic UIRefreshControl with fixed refresh action.
### 0.10.1
- **Update**: Third party dependencies: `Alamofire` 5.2.2, `RxAlamofire` 5.6.1
### 0.10.0
- **Update**: Third party dependencies: `RxSwift` (and all sub-dependencies) to 5.1.0, `Alamofire` 5.0, `SnapKit` 5.0
- **Refactored**: NetworkManager to use new Alamofire API
- **API BreakingChanges**: NetworkServiceConfiguration no longer accepts `ServerTrustPolicy`, it is now replaced by an instance of a `ServerTrustEvaluating` protocol. Full description and default implementations can be found at Alamofire [sources](https://github.com/Alamofire/Alamofire/blob/master/Source/ServerTrustEvaluation.swift). Since new evaluation is used, evaluation against self-signed certificates will now throw an AfError and abort any outcoming request. To support self-signed certificates use `DisabledTrustEvaluator` for specified host in configuration.
- **Removed**: UIImage+SupportExtensions, UIScrollView+Support
### 0.9.44
- **Add**: `TIFoundationUtils` - set of helpers for Foundation framework classes.
#### TISwiftUtils
- **Add**: `BackingStore` - a property wrapper that wraps storage and defines getter and setter for accessing value from it.
#### TIFoundationUtils
- **Add**: `CodableKeyValueStorage` - storage that can get and set codable objects by the key.
### 0.9.43
- **Fix**: `OTPSwiftView`'s dependencies.
### 0.9.42
- **Fix**: Logic bugs of `PaginationWrapper`.
### 0.9.41
- **Add**: `OTPSwiftView` - a fully customizable OTP view.
- **Add**: `BaseInitializableControl` UIControl conformance to InitializableView.
- **Add**: `TISwiftUtils` a bunch of useful helpers for development.
### 0.9.40
- **Fix**: Load more request repetion in `PaginationWrapper`.
### 0.9.39
- **Add**: `Animatable` protocol to TIUIKitCore.
- **Add**: `ActivityIndicator` protocol to TIUIKitCore.
- **Add**: `ActivityIndicatorHolder` protocol to TIUIKitCore.
- **Add**: `TIUIElements` for ui elements.
### 0.9.38
- **Add**: `BaseRxTableViewCell` is subclass of `UITableViewCell` class with support `InitializableView` and `DisposeBagHolder` protocols.
- **Add**: `ContainerTableCell` is container class that provides wrapping any `UIView` into `UITableViewCell`.
- **Add**: `BaseTappableViewModel` is simplifies interaction between view and viewModel for events of tapping.
- **Add**: `VoidTappableViewModel` is subclass of `BaseTappableViewModel` class with void payload type.
### 0.9.37
- **Fix**: ScrollView content offset of `PaginationWrapper` for iOS 13.
- **Fix**: Load more request crash of `PaginationWrapper`.
### 0.9.36
- **Add**: SPM Package.swift.
- **Add**: TITransitions via SPM.
- **Add**: TIUIKitCore via SPM.
- **Update**: Readme.
### 0.9.35
- **Add**: Selector `refreshAction()` for refresh control of `PaginationWrapper`.
### 0.9.34
- **Add**: `ButtonHolder` - protocol that contains button property.
- **Add**: `ButtonHolderView` - view which contains button.
- **Add**: Conformance `UIButton` to `ButtonHolder`.
- **Add**: Conformance `BasePlaceholderView` to `ButtonHolderView`.
- **[Breaking change]**: Replace functions `footerRetryButton() -> UIButton?` to `footerRetryView() -> ButtonHolderView?` and `footerRetryButtonHeight() -> CGFloat` to `footerRetryViewHeight() -> CGFloat` for `PaginationWrapperUIDelegate`.
- **[Breaking change]**: Replace functions `footerRetryButtonWillAppear()` to `footerRetryViewWillAppear()` and `footerRetryButtonWillDisappear()` to `footerRetryViewWillDisappear()` for `PaginationWrapperUIDelegate`.
### 0.9.33
- **Fix**: `CustomizableButtonView` container class that provides great customization.
- **Fix**: `CustomizableButtonViewModel` viewModel class for `CustomizableButtonView` configuration.
### 0.9.32
- **Fix**: `CustomizableButtonView` container class that provides great customization.
### 0.9.31
- **Add**: `@discardableResult` to function - `replace(with:at:with:manualBeginEndUpdates)` in `TableDirector`.
### 0.9.30
- **Add**: character `*` into a valid set of characters in the extension `telpromptURL` of String.
### 0.9.29
- **Update**: remove Carthage binary dependencies, update build scripts.
### 0.9.28
- **Add**: method `presentFullScreen(_ viewController:presentationStyle:animated:completion:)` for `UIViewController` that present any `viewController` modally in full screen mode by default (avoid problems with *iOS13* default presentation mode changed to `.automatic` stork)
### 0.9.27
- **Add**: method `date(from string:formats:parsedIn:)` method for `DateFormattingService` that parses date from string in one of the given formats with current region.
### 0.9.26
- **Add**: method `processResultFromConfigurationSingle` for `TotalCountCursor` that allows to get server response.
- **Add**: possibility to inherit from `TotalCountCursor`.
### 0.9.25
- **Add**: `queryItems` parameter for `ApiRequestParameters`.
- **Add**: `asQueryItems` method for `Encodable` that converts model to query items array.
### 0.9.24
- **Fix**: Make `ApiRequestParameters` properties public.
### 0.9.23
- **Add**: Rounding for `Decimal`.
- **Add**: `doubleValue` property for `Decimal`.
- **Add**: `intValue` property for `Decimal`.
- **Fix**: Rounding for `Double`.
### 0.9.22
- **Fix**: Make `Initializable` protocol public.
### 0.9.21
- **Add**: `Initializable` - common protocol for object types that can be initialized without params.
- **Add**: `instantiateArray(count:)` function in `Initializable` extension to initialize an array of instances.
### 0.9.20
- **Fix**: `bindBottomInsetBinding(from bottomInsetDriver:)` in `BaseScrollContentController` works correctly now.
### 0.9.19
- **Add**: `hexString` property for `UIColor` that returns hex representation of color as string.
### 0.9.18
- **Add**: `CustomizableButtonView` container class that provides great customization.
- **Add**: `CustomizableButtonViewModel` viewModel class for `CustomizableButtonView` configuration.
- **Add**: `CustomizableButton` class that is a `CustomizableButtonView` subview and gives it a button functionality.
### 0.9.17
- **Fix**: SpinnerView infinity animation.
### 0.9.16
- **Add**: `LabelTableViewCell` moved from `LeadKitAdditions`.
- **Add**: `SnapKit` dependency.
### 0.9.15
- **Add**: `BaseSearchViewController` class that allows to enter text for search and then displays search results in table view.
- **Add**: `BaseSearchViewModel` class that loads data from a given data source and performs search among the results.
- **Add**: `SearchResultsController` protocol that represent a controller able to display search results.
- **Add**: `SearchResultsControllerState` enum that represents `SearchResultsController` state.
### 0.9.14
- **Update**: SwiftDate dependency (~> 6).
### 0.9.13
- **Add**: `ApiUploadRequestParameters` struct that defines upload data request parameters.
- **Add**: `rxUploadRequest` method to `NetworkService` class that performs reactive request to upload data.
- **Add**: `uploadResponseModel` method to `SessionManager` extension that executes upload request and serializes response.
- **Add**: `handleMappingError` method to `Error` extension that tries to serialize response from a mapping request error to a model.
- **Add**: `handleMappingError` method to `ObservableType`, `Single`, `Completable` extensions that handles a mapping error and serialize response to a model.
- **Add**: `validate` method to `DataRequest` observable extension that validates status codes and catch network errors.
- **Add**: `dataApiResponse` method to `DataRequest` reactive extension that serializes response into data.
- **Update**: `validStatusCodes` parameter in network methods renamed to `additionalValidStatusCodes`.
### 0.9.12
- **Update**: Swift 5 support
### 0.9.11
- **[Breaking change]**: Renamed `NumberFormat`'s `allOptions` to `allCases`
- **Fix**: Closure syntax fix. New closure naming.
- **Fix**: Added missing `BasePlaceholderView` protocol function.
### 0.9.10
- **Remove**: Removed unused scheme & target
- **Remove**: Cocoapods deintegrated
- **Update**: New closure typealiases
### 0.9.9
- **Add**: `validStatusCodes` parameter to request methods in `NetworkService` class, that expands valid status codes for request.
- **Add**: `validStatusCodes` parameter to response methods in `SessionManager` extension, that expands valid status codes for request.
### 0.9.8
- **Add**: `rxDataRequest` method to `NetworkService` class, that performs reactive request to get data and http response.
- **Add**: `responseData` method to `SessionManager` extension, that executes request and returns data.
### 0.9.7
- **Add**: Carthage support.
### 0.9.6
- **Add**: Add new `configureSeparators` method to `SeparatorRowBox` array.
### 0.9.5
- **Add**: `TitleType` enum, that defines `UIViewController`'s title type.
- **Add**: `UINavigationItem.largeTitleDisplayMode` property, that defines `UINavigationItem`'s large title display mode.
- **Add**: `UIViewController.updateNavigationItemTitle` method, that takes `TitleType` as a parameter and updates `UIViewController`'s title.
### 0.9.4
- **Add**: initialization of `ApiRequestParameters`, that takes an array as a request parameter.
- **Add**: `NetworkServiceConfiguration.apiRequestParameters` method, that creates `ApiRequestParameters` with array request parameter.
- **Add**: `SessionManager.request` method, that takes an array as a request parameter.
- **Add**: `RequestUsageError` error, that represents wrong usage of requset parameters.
### 0.9.3
- **Add**: `Insert`/`Remove` section with animation functions to `TableKit`. Also make new function `Replace` that uses new `Insert`/`Remove` to animate section replace.
### 0.9.2
- **Update**: Add response to `RequestError`.
- **Fix**: Update `SessionManager+Extensions` to catch network connection error.
### 0.9.1
- **Update**: `DataRequest+Extensions` time out as network error
### 0.9.0
- **Update**: version update.
### 0.8.13
- **Add**: `configureLayout` method to `InitializeableView` protocol and all implementations.
- **Update**: `GeneralDataLoadingViewModel` now can handle state changes and result of data source. Previously it was possible only in view controller.
- **Add**: `GeneralDataLoadingHandler` protocol, that defines methods for common data loading states handling.
- **Add**: `resultObservable` and `resultDriver` properties to `GeneralDataLoadingViewModel`.
- **Add**: `hidesWhenStopped` option to `SpinnerView`, so you can stop animation without hiding image inside it.
- **Update**: Migrate to Swift 4.2 & Xcode 10. Update dependencies.
### 0.8.12
- **Add**: `UserDefaults+Codable` is back. Now with generic subscript support.
### 0.8.11
- **Change**: `NumberFormattingService.computedFormatters` computed var reverted to static.
### 0.8.10
- **[Breaking change]**: `NumberFormattingService` methods is not static anymore.
- **Add**: `NSNumberConvertible` protocol for `NumberFormattingService` use cases.
- **Add**: `TableDirector` methods for rows insertion and removal without reload a whole table.
- **Add**: `UIImageView` binder for disclosure indicator rotation.
- **Add**: `UIView.addSubviews(:)` methods with variable number of arguments and array of views.
- **Add**: `PlaceholderConfigurable` that defines attributes and methods for view with placeholder and regular state.
- **Add**: `ContentLoadingViewModel` enum that describes possible `PlaceholderConfigurable` view states.
### 0.8.9
- **Add**: Methods `replace(with:)`, `asVoid()`, `asOptional()` to `ObservableType`, `SharedSequence` (aka `Driver`) and `Single`.
- **Add**: `Completable.deferredJust(:)` static method.
- **Add**: `ViewTextConfigurable` protocol. Conform `UILabel`, `UITextField` and `UIButton` to this protocol.
- **Add**: `BaseTextAttributes` with base text appearance attributes.
- **Update**: `ViewText.string` now uses `BaseTextAttributes` instead of separate properties.
- **Add**: `BasePlaceholderView` and `BasePlaceholderViewModel` classes used to create your own placeholder.
- **Add**: `TableKitViewModel` protocol that adds convenient extensions to cell view models that implements it.
### 0.8.8
- **Update**: Update `DateFormat` protocol. Add `dateToStringFormat` and `stringToDateFormat` according to SwiftDate 5.0.
- **Update**: Replace `String` with `DateFormat` in `DataFormattingService` date parsing methods.
- **Update**: Replace `DateInRegion` with `DateRepresentable` in `DataFormattingService` string formatting methods.
- **Add**: `parsedIn` optional parameter to date parsing method in `DataFormattingService`.
### 0.8.7
- **Add**: Base configurable controllers hierarchy with generic custom view argument (`BaseConfigurableController`, `BaseCustomViewController`, `BaseScrollContentController`, `BaseTableContentController` and `BaseCollectionContentController`).
- **Add**: `ScrollViewHolder`, `TableViewHolder` and `CollectionViewHolder` protocols.
- **Update**: Update dependencies.
- **[Breaking change]**: Update `SwiftDate` to 5.0.x.
- **[Breaking change]**: Update `DateFormattingService`. Change `format` argument from `DateFormatType` to `String`.
- **Update**: Add compile time debug messages. Improve compile time for some pieces of code.
### 0.8.6
- **Fix**: Add `trustPolicies` param to `NetworkServiceConfiguration` initialization.
- **Fix**: Update `serverTrustPolicies` to save host instead of the whole URL as a key.
- **Add**: String extension that extracts host.
### 0.8.5
- **Add**: `replaceDataSource` method to `RxNetworkOperationModel`.
- **Add**: `customErrorHandler` constructor parameter to `RxNetworkOperationModel` and it heirs.
### 0.8.4
- **Fix**: Add `SeparatorCell` to `Core-iOS-Extension`.
- **Fix**: `UIApplication` extensions path for `Core-iOS-Extension` exclusions.
### 0.8.3
- **Fix**: `SpinnerView` animation freezing
### 0.8.2
- **Add**: `acceptableStatusCodes` property to `NetworkServiceConfiguration`
### 0.8.1
- **Add**: Support for `localizedComponent` for `FixedWidthInteger`
### 0.8.0
- **Add**: tests for `NetworkService`
- **Add**: `toJSON(with encoder: JSONEncoder)` method to `Encodable`
- **Add**: `failedToDecode` error case to `LeadKitError`
- **Add**: `SessionManager` class
- **Remove**: occurrences `ObjectMapper` pod and its occurrences in code
- **Update**: replace `ObjectMapper` mapping with `Decodable`
### 0.7.19
- **Fix**: `PaginationWrapper` retry button showing.
### 0.7.18
- **Update**: default implementation of `PaginationWrapperUIDelegate`.
### 0.7.17
- **Add**: `RxNetworkOperationModel` base class, `NetworkOperationState` and `NetworkOperationStateType` protocols.
### 0.7.16
- **[Breaking Change]**: Remove `ModuleConfigurator`, change type of `ConfigurableController.viewModel` property from `IUO` to plain `ViewModelT`.
- **Add**: `InitializableView` protocol with default implementation.
- **Update**: `ConfigurableController` protocol now inherit `InitializableView`.
- **[Breaking Change]**: `setAppearance` of `ConfigurableController` replaced with `configureAppearance` of `InitializableView`.
### 0.7.15
- **Fix**: `Double.roundValue(withPrecision:)` rounding issue
- **Add**: `Double+Rounding` test case
### 0.7.14
- **[Breaking Change]**: `PaginationWrapper` separating state views from data loading.
### 0.7.13
- **Update**: Migrate from `Variable` to `BehaviorRelay`.
- **Fix**: `PaginationWrapper` retry load more after fail.
- **Fix**: `safeClear` method of `TableDirector` now creates section without header and footer.
- **Add**: `TableSection` convenience initializer for section without footer and header.
### 0.7.12
- **Add**: `UniversalMappable` protocol to have ability generate generic mapping models
### 0.7.11
- **Fix**: `addHeaderBackground` cells overlapping.
### 0.7.10
- **Fix**: `wtihInsets` renamed to `with insets`
### 0.7.9
- **Fix**: timeoutInterval is set to another URLSessionConfiguration property in NetworkServiceConfiguration
### 0.7.8
- **Remove**: `App`, `Log` and `LogFormatter`.
- **Remove**: `CocoaLumberjack` dependency.
- **Add**: Rotate operation for image drawing.
- **Add**: `mapViewEvents` overload with closure that returns array of disposables.
- **Update**: Update `ObjectMapper` to 3.1.
- **Add**: `apiRequestParameters` method to `NetworkServiceConfiguration` extension.
- **Update**: Rename setToCenter(withInsets:) to pintToSuperview(withInsets:excluding:)
- **Update**: Added parameter "edges" with label "excluding" to aforementioned method
### 0.7.7
- **Fix**: Fix doubling separator line issue
### 0.7.6
- **Add**: `NetworkServiceConfiguration` to configure NetworkService instance
- **Remove**: `ConfigurableNetworkSevice` protocol
- **Update**: Acceptable status codes in SessionManager become `Set<Int>`
### 0.7.5
- **Add**: `topConfiguration` and `bottomConfiguration` properties, methods to configure top and bottom separators in `CellSeparatorType` extension.
- **Add**: `totalHeight` property in `SeparatorConfiguration` extension.
### 0.7.4
- **Update**: Exclude UIApplication extensions from iOS-Extension subspec.
### 0.7.3
- **Update**: Xcode 9.3 migration.
- **Remove**: Default initializer for Network service that conforms to `ConfigurableNetworkService` protocol.
- **[Breaking Change]**: `DateFormattingService` class replaced with protocol.
- **Add**: `SwiftDate` dependency for `DateFormattingService`.
- **Add**: `ViewBackground` enum that describes possible view backgrounds.
- **Add**: `ViewText` enum that describes text with appearance options.
- **Removed**: `String+SizeCalculation` extension.
### 0.7.2
- **Fixed**: Change root controller for window
### 0.7.1
- **Add**: Extension for comparing optional arrays (`[T]?`) with `Equatable` elements.
- **Add**: `additionalHttpHeaders` static field in `ConfigurableNetworkService` protocol.
- **Add**: Default initializer for Network service that conforms to `ConfigurableNetworkService` protocol.
## 0.7.0
- **Add**: `TotalCountCursor` for total count based pagination and related stuff.
- **[Breaking Change]**: `PaginationTableViewWrapper` and `PaginationTableViewWrapperDelegate` was renamed to `PaginationWrapper` and `PaginationWrapperDelegate `. Also there is significant changes in api
- **Add**: `GeneralDataLoadingModel` and `PaginationDataLoadingModel` for regular and paginated data loading with state handling.
- **Add**: `GeneralDataLoadingViewModel` and `GeneralDataLoadingController` for regular data loading and state handling in UI.
- **Add**: `ConfigurableNetworkService` - replacement of `DefaultNetworkService` from LeadKitAdditions.
- **Add**: `NumberFormattingService` and `NumberFormat` protocols with default implementation for creating per-project number formatters.
- **Add**: Very flexible in configuration `TextFieldViewModel` with build-in two-side data model binding.
- **Add**: `SingleLoadCursorConfiguration` as a replacement of `SingleLoadCursor`.
- **Add**: `UIApplication` extensions for making phone calls.
- **Add**: `NSAttributedString` extensions for appending attributed strings using `+` operator.
- **Change**: Lots of fixes and enhancements.
- **Update**: Update dependecies versions.
### 0.6.7
- **Add**: UITableView extension to add colored background for tableview bounce area.
@ -861,13 +16,13 @@
- **Fix**: SpinnerView bug(no animation) in Swift 4.
### 0.6.3
## 0.6.3
- **Fix**: SeparatorCell updates constraints after setting separator insets
### 0.6.2
## 0.6.2
- **Fix**: AlamofireManager extension no longer performs requests with default manager
### 0.6.1
## 0.6.1
- **New**: `RequestError`. Represents general api request errors
- **Change**: All api methods now throws `RequestError` when fails.
@ -883,59 +38,62 @@
- **Remove**: `Observable` creation for `ImmutableMappable`
- **Remove**: `UIView` and `UsedDefaults` extensions, `EstimatedViewHeightProtocol`, `StaticEstimatedViewHeightProtocol`, `StoryboardIdentifierProtocol`
### 0.5.18
## 0.5.18
- **Fix**: EmptyCell first appearance setup fix
### 0.5.17
## 0.5.17
- **Fix**: EmptyCell reusing appearance fix
- **Fix**: SeparatorCell reusing separators fix
### 0.5.16
## 0.5.16
- **Change**: Rename `AppearanceProtocol` to `AppearanceConfigurable`
- **Add**: `subscript(safe:)` subscript to `Array` extension for safe access to element by index
### 0.5.15
## 0.5.15
- **Add**: `AppearanceProtocol` which ensures that specific type can apply appearance to itself
- **Add**: `with(appearance:)`, `set(appearance:)` methods to TableRow extension
- **Add**: `Appearance` to `EmptyCell`
- **Remove**: `SeparatorCellViewModel`.
### 0.5.13
## 0.5.13
- **Change**: Remove type erasure behavior from `AnyBaseTableRow`
- **Change**: Rename `AnyBaseTableRow` class to `SeparatorRowBox`
- **Change**: Move `anyRow` property from `EmptyCellRow` to `TableRow` extension and rename it to `separatorRowBox`.
- **Change**: Move `configure(extreme: middle:)` method from `TableDirector` extension to `Array` extension and rename it to `configureSeparators(extreme: middle:)`
### 0.5.12
## 0.5.12
- **Fix**: Update type of `viewModel` in `ConfigurableController` to `ImplicitlyUwrappedOptional<ViewModelT>` instead of `ViewModelT`
### 0.5.11
## 0.5.11
- **[Breaking Change]**: rename initializer from `init(initialFrom:)` to `init(resetFrom:)` in `ResettableType`
- **Add**: `SeparatorCell` with `SeparatorCellViewModel`
- **Add**: `AnyBaseTableRow` for type-erasure
- **Add**: `EmptyCellRow` for empty cell with static height
### 0.5.10
## 0.5.10
- **Fix**: `Public` modifier for `SpinnerView`
- **Fix**: `Public` modifier for `SpinnerView`
### 0.5.9
## 0.5.9
- **Fix**: One-two-many fixed for values more than 99
### 0.5.8
## 0.5.8
- **Fix**: Synchronization over `NSRecursiveLock` for request count tracker in NetworkService
### 0.5.7
## 0.5.7
- **Add**: String extension `localizedComponent(value:stringOne:stringTwo:stringMany:)`
### 0.5.6
## 0.5.6
- **Fix**: Clear tableview if placeholder is shown

View File

@ -1,7 +0,0 @@
github "malcommac/SwiftDate"
github "Alamofire/Alamofire"
github "RxSwiftCommunity/RxAlamofire" ~> 6.1
github "TouchInstinct/TableKit"
github "ReactiveX/RxSwift" ~> 6.2
github "pronebird/UIScrollView-InfiniteScroll" "1.1.0"
github "SnapKit/SnapKit" ~> 5.0

View File

@ -1,7 +0,0 @@
github "Alamofire/Alamofire" "5.4.3"
github "ReactiveX/RxSwift" "6.2.0"
github "RxSwiftCommunity/RxAlamofire" "v6.1.2"
github "SnapKit/SnapKit" "5.0.1"
github "TouchInstinct/TableKit" "2.10008.1"
github "malcommac/SwiftDate" "6.3.1"
github "pronebird/UIScrollView-InfiniteScroll" "1.1.0"

View File

@ -1,5 +0,0 @@
# frozen_string_literal: true
source "https://rubygems.org"
gem "cocoapods", "~> 1.11"

View File

@ -1,98 +0,0 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.5)
rexml
activesupport (6.1.5)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
atomos (0.1.3)
claide (1.1.0)
cocoapods (1.11.3)
addressable (~> 2.8)
claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.11.3)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 1.4.0, < 2.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
cocoapods-search (>= 1.0.0, < 2.0)
cocoapods-trunk (>= 1.4.0, < 2.0)
cocoapods-try (>= 1.1.0, < 2.0)
colored2 (~> 3.1)
escape (~> 0.0.4)
fourflusher (>= 2.3.0, < 3.0)
gh_inspector (~> 1.0)
molinillo (~> 0.8.0)
nap (~> 1.0)
ruby-macho (>= 1.0, < 3.0)
xcodeproj (>= 1.21.0, < 2.0)
cocoapods-core (1.11.3)
activesupport (>= 5.0, < 7)
addressable (~> 2.8)
algoliasearch (~> 1.0)
concurrent-ruby (~> 1.1)
fuzzy_match (~> 2.0.4)
nap (~> 1.0)
netrc (~> 0.11)
public_suffix (~> 4.0)
typhoeus (~> 1.0)
cocoapods-deintegrate (1.0.5)
cocoapods-downloader (1.6.2)
cocoapods-plugins (1.0.0)
nap
cocoapods-search (1.0.1)
cocoapods-trunk (1.6.0)
nap (>= 0.8, < 2.0)
netrc (~> 0.11)
cocoapods-try (1.2.0)
colored2 (3.1.2)
concurrent-ruby (1.1.10)
escape (0.0.4)
ethon (0.15.0)
ffi (>= 1.15.0)
ffi (1.15.5)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
httpclient (2.8.3)
i18n (1.10.0)
concurrent-ruby (~> 1.0)
json (2.6.1)
minitest (5.15.0)
molinillo (0.8.0)
nanaimo (0.3.0)
nap (1.1.0)
netrc (0.11.0)
public_suffix (4.0.6)
rexml (3.2.5)
ruby-macho (2.5.1)
typhoeus (1.4.0)
ethon (>= 0.9.0)
tzinfo (2.0.4)
concurrent-ruby (~> 1.0)
xcodeproj (1.21.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
zeitwerk (2.5.4)
PLATFORMS
x86_64-darwin-20
x86_64-darwin-21
DEPENDENCIES
cocoapods (~> 1.11)
BUNDLED WITH
2.3.26

View File

@ -1,120 +1,117 @@
Pod::Spec.new do |s|
s.name = "LeadKit"
s.version = "1.35.0"
s.version = "0.6.7"
s.summary = "iOS framework with a bunch of tools for rapid development"
s.homepage = "https://git.svc.touchin.ru/TouchInstinct/LeadKit"
s.homepage = "https://github.com/TouchInstinct/LeadKit"
s.license = "Apache License, Version 2.0"
s.author = "Touch Instinct"
s.source = { :git => "https://git.svc.touchin.ru/TouchInstinct/LeadKit.git", :tag => s.version }
s.platform = :ios, '10.0'
s.swift_versions = ['5.1']
s.source = { :git => "https://github.com/TouchInstinct/LeadKit.git", :tag => s.version }
s.platform = :ios, '9.0'
s.subspec 'UIColorHex' do |ss|
ss.ios.deployment_target = '10.0'
ss.tvos.deployment_target = '10.0'
ss.watchos.deployment_target = '3.0'
ss.ios.deployment_target = '8.0'
ss.tvos.deployment_target = '9.0'
ss.watchos.deployment_target = '2.0'
ss.source_files = "Sources/Extensions/UIColor/UIColor+Hex.swift"
end
s.subspec 'Drawing' do |ss|
# ss.ios.deployment_target = '8.0' # can't get it work: DrawingOperation.swift:29:17: note: did you mean 'DrawingOperation'?
# ss.tvos.deployment_target = '9.0' # can't get it work: DrawingOperation.swift:29:17: note: did you mean 'DrawingOperation'?
ss.watchos.deployment_target = '2.0'
ss.source_files = [
"Sources/Enums/ResizeMode.swift",
"Sources/Extensions/{CGContext,CGImage,CGSize,UIImage}/*",
"Sources/Protocols/{DrawingOperation,SupportProtocol}.swift",
"Sources/Structures/Drawing/*",
]
ss.watchos.exclude_files = [
"Sources/Structures/Drawing/CALayerDrawingOperation.swift",
"Sources/Extensions/UIImage/*",
]
end
s.subspec 'Core' do |ss|
ss.ios.deployment_target = '10.0'
ss.tvos.deployment_target = '10.0'
ss.watchos.deployment_target = '3.0'
ss.ios.deployment_target = '9.0'
ss.tvos.deployment_target = '9.0'
ss.watchos.deployment_target = '2.0'
ss.source_files = "Sources/**/*.swift"
ss.watchos.exclude_files = [
"Sources/Classes/Controllers/**/*",
"Sources/Classes/Pagination/PaginationTableViewWrapper.swift",
"Sources/Classes/Views/SeparatorRowBox/*",
"Sources/Classes/Views/BaseRxTableViewCell/*",
"Sources/Classes/Views/ContainerTableCell/*",
"Sources/Classes/Views/SeparatorCell/*",
"Sources/Classes/Views/EmptyCell/*",
"Sources/Classes/Views/LabelTableViewCell/*",
"Sources/Classes/DataLoading/PaginationDataLoading/PaginationWrapper.swift",
"Sources/Classes/Views/XibView/*",
"Sources/Classes/Views/SpinnerView/*",
"Sources/Classes/Views/DefaultPlaceholders/*",
"Sources/Classes/Views/CollectionViewWrapperView/*",
"Sources/Classes/Views/TableViewWrapperView/*",
"Sources/Classes/Views/BasePlaceholderView/*",
"Sources/Classes/Views/CustomizableButton/*",
"Sources/Classes/Search/*",
"Sources/Enums/Search/*",
"Sources/Extensions/CABasicAnimation/*",
"Sources/Extensions/CGFloat/CGFloat+Pixels.swift",
"Sources/Extensions/NetworkService/NetworkService+ActivityIndicator.swift",
"Sources/Extensions/NetworkService/NetworkService+RxLoadImage.swift",
"Sources/Extensions/DataLoading/GeneralDataLoading/GeneralDataLoadingController+DefaultImplementation.swift",
"Sources/Extensions/DataLoading/PaginationDataLoading/*",
"Sources/Extensions/Support/UINavigationItem+Support.swift",
"Sources/Extensions/TableKit/**/*.swift",
"Sources/Extensions/PaginationTableViewWrapperDelegate/PaginationTableViewWrapperDelegate+DefaultImplementation.swift",
"Sources/Extensions/Support/UIScrollView+Support.swift",
"Sources/Extensions/TableDirector/*",
"Sources/Extensions/Array/Array+SeparatorRowBoxExtensions.swift",
"Sources/Extensions/Array/Array+RowExtensions.swift",
"Sources/Extensions/Drawing/UIImage/*",
"Sources/Extensions/UIKit/**/*.swift",
"Sources/Extensions/Views/ViewBackground/*",
"Sources/Extensions/Views/SeparatorCell/*",
"Sources/Extensions/Views/ConfigurableView/*",
"Sources/Extensions/Views/PlaceholderConfigurable/*",
"Sources/Protocols/UIKit/**/*.swift",
"Sources/Extensions/UIActivityIndicatorView/*",
"Sources/Extensions/UIAlertcontroller/*",
"Sources/Extensions/UICollectionView/*",
"Sources/Extensions/UIDevice/*",
"Sources/Extensions/UIImage/*",
"Sources/Extensions/UITableView/*",
"Sources/Extensions/UIView/*",
"Sources/Extensions/UIViewController/*",
"Sources/Extensions/UIWindow/*",
"Sources/Protocols/LoadingIndicator.swift",
"Sources/Protocols/DataLoading/PaginationDataLoading/PaginationWrappable.swift",
"Sources/Protocols/DataLoading/GeneralDataLoading/GeneralDataLoadingController.swift",
"Sources/Protocols/Views/SeparatorCell/*",
"Sources/Protocols/Views/PlaceholderConfigurable/*",
"Sources/Protocols/TableKit/**/*",
"Sources/Protocols/Controllers/SearchResultsViewController.swift",
"Sources/Structures/Views/AnyLoadingIndicator.swift",
"Sources/Structures/DrawingOperations/CALayerDrawingOperation.swift",
"Sources/Structures/DrawingOperations/RoundDrawingOperation.swift",
"Sources/Structures/DrawingOperations/BorderDrawingOperation.swift",
"Sources/Structures/DataLoading/PaginationDataLoading/*",
"Sources/Extensions/UIInterfaceOrientation/*"
]
ss.tvos.exclude_files = [
"Sources/Classes/Controllers/BaseConfigurableController.swift",
"Sources/Classes/Controllers/BaseCollectionContentController.swift",
"Sources/Classes/Views/TableViewWrapperView/TableViewWrapperView.swift",
"Sources/Classes/Views/CollectionViewWrapperView/CollectionViewWrapperView.swift",
"Sources/Classes/Controllers/BaseScrollContentController.swift",
"Sources/Classes/Controllers/BaseCustomViewController.swift",
"Sources/Classes/Controllers/BaseOrientationNavigationController.swift",
"Sources/Extensions/UIKit/UIDevice/UIDevice+ScreenOrientation.swift",
"Sources/Classes/Controllers/BaseTableContentController.swift",
"Sources/Classes/Views/BaseRxTableViewCell/*",
"Sources/Classes/Views/ContainerTableCell/*",
"Sources/Classes/Views/SeparatorRowBox/*",
"Sources/Classes/Views/SeparatorCell/*",
"Sources/Classes/Views/EmptyCell/*",
"Sources/Classes/Views/LabelTableViewCell/*",
"Sources/Classes/Views/CustomizableButton/*",
"Sources/Classes/DataLoading/PaginationDataLoading/PaginationWrapper.swift",
"Sources/Classes/Search/*",
"Sources/Classes/Pagination/PaginationTableViewWrapper.swift",
"Sources/Structures/Drawing/CALayerDrawingOperation.swift",
"Sources/Enums/Search/*",
"Sources/Extensions/DataLoading/PaginationDataLoading/*",
"Sources/Extensions/Support/UINavigationItem+Support.swift",
"Sources/Extensions/TableKit/**/*.swift",
"Sources/Extensions/Array/Array+SeparatorRowBoxExtensions.swift",
"Sources/Extensions/Array/Array+RowExtensions.swift",
"Sources/Extensions/Views/SeparatorCell/*",
"Sources/Protocols/DataLoading/PaginationDataLoading/PaginationWrappable.swift",
"Sources/Protocols/Views/SeparatorCell/*",
"Sources/Protocols/TableKit/**/*",
"Sources/Protocols/Controllers/SearchResultsViewController.swift",
"Sources/Structures/DataLoading/PaginationDataLoading/*",
"Sources/Extensions/UIInterfaceOrientation/*",
"Sources/Classes/Controllers/BaseOrientationController.swift"
"Sources/Extensions/NetworkService/NetworkService+ActivityIndicator.swift",
"Sources/Extensions/PaginationTableViewWrapperDelegate/PaginationTableViewWrapperDelegate+DefaultImplementation.swift",
"Sources/Extensions/Support/UIScrollView+Support.swift",
"Sources/Extensions/TableDirector/*",
"Sources/Extensions/Array/Array+SeparatorRowBoxExtensions.swift"
]
ss.dependency "RxSwift", '~> 6.2'
ss.dependency "RxCocoa", '~> 6.2'
ss.dependency "RxAlamofire", '~> 6.1'
ss.dependency "SwiftDate", '~> 6'
ss.dependency "CocoaLumberjack/Swift", '~> 3.3.0'
ss.dependency "RxSwift", '4.0.0'
ss.dependency "RxCocoa", '4.0.0'
ss.dependency "RxAlamofire", '4.0.0'
ss.dependency "ObjectMapper", '~> 3.0.0'
ss.ios.dependency "TableKit", '~> 2.11'
ss.ios.dependency "SnapKit", '~> 5.0.1'
ss.ios.dependency "UIScrollView-InfiniteScroll", '~> 1.1.0'
ss.ios.dependency "TableKit", '~> 2.5.0'
ss.ios.dependency "UIScrollView-InfiniteScroll", '~> 1.0.0'
end
s.subspec 'Core-iOS-Extension' do |ss|
ss.platform = :ios, '9.0'
ss.source_files = "Sources/**/*.swift"
ss.exclude_files = [
"Sources/Classes/Views/SeparatorRowBox/*",
"Sources/Classes/Views/SeparatorCell/*",
"Sources/Classes/Views/EmptyCell/*",
"Sources/Classes/Pagination/PaginationWrapper.swift",
"Sources/Extensions/NetworkService/NetworkService+ActivityIndicator.swift",
"Sources/Extensions/TableDirector/*",
"Sources/Extensions/Array/Array+SeparatorRowBoxExtensions.swift"
]
ss.dependency "CocoaLumberjack/Swift", '~> 3.3.0'
ss.dependency "RxSwift", '4.0.0'
ss.dependency "RxCocoa", '4.0.0'
ss.dependency "RxAlamofire", '4.0.0'
ss.dependency "ObjectMapper", '~> 3.0.0'
end
s.default_subspec = 'Core'

File diff suppressed because it is too large Load Diff

View File

@ -1,95 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1230"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "67186B271EB248F100CFAFFB"
BuildableName = "LeadKit.framework"
BlueprintName = "LeadKit iOS"
ReferencedContainer = "container:LeadKit.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "67186B271EB248F100CFAFFB"
BuildableName = "LeadKit.framework"
BlueprintName = "LeadKit iOS"
ReferencedContainer = "container:LeadKit.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "67186B2F1EB248F100CFAFFB"
BuildableName = "LeadKit iOSTests.xctest"
BlueprintName = "LeadKit iOSTests"
ReferencedContainer = "container:LeadKit.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "67186B271EB248F100CFAFFB"
BuildableName = "LeadKit.framework"
BlueprintName = "LeadKit iOS"
ReferencedContainer = "container:LeadKit.xcodeproj">
</BuildableReference>
</MacroExpansion>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "67186B271EB248F100CFAFFB"
BuildableName = "LeadKit.framework"
BlueprintName = "LeadKit iOS"
ReferencedContainer = "container:LeadKit.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -1,95 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1230"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "6782BB9F1EB31D590086E0B8"
BuildableName = "LeadKit.framework"
BlueprintName = "LeadKit tvOS"
ReferencedContainer = "container:LeadKit.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "6782BB9F1EB31D590086E0B8"
BuildableName = "LeadKit.framework"
BlueprintName = "LeadKit tvOS"
ReferencedContainer = "container:LeadKit.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "6782BBA71EB31D5A0086E0B8"
BuildableName = "LeadKit tvOSTests.xctest"
BlueprintName = "LeadKit tvOSTests"
ReferencedContainer = "container:LeadKit.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "6782BB9F1EB31D590086E0B8"
BuildableName = "LeadKit.framework"
BlueprintName = "LeadKit tvOS"
ReferencedContainer = "container:LeadKit.xcodeproj">
</BuildableReference>
</MacroExpansion>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "6782BB9F1EB31D590086E0B8"
BuildableName = "LeadKit.framework"
BlueprintName = "LeadKit tvOS"
ReferencedContainer = "container:LeadKit.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -1,9 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef location = "group:TIUIElements.playground"></FileRef>
<FileRef
location = "group:TIUIElements.xcodeproj">
<FileRef
location = "group:LeadKit.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">

107
Makefile
View File

@ -1,107 +0,0 @@
export SRCROOT := $(shell pwd)
push_to_podspecs: TISwiftUtils.target TIFoundationUtils.target TICoreGraphicsUtils.target TIKeychainUtils.target TIUIKitCore.target TIUIElements.target TIWebView.target TIBottomSheet.target TISwiftUICore.target TITableKitUtils.target TIDeeplink.target TIDeveloperUtils.target TILogging.target TINetworking.target TIMoyaNetworking.target TINetworkingCache.target TIMapUtils.target TIAppleMapUtils.target TIGoogleMapUtils.target TIPagination.target TIAuth.target TIEcommerce.target TITextProcessing.target TIApplication.target
$(call clean)
TISwiftUtils.target:
MODULE_NAME="TISwiftUtils" ./project-scripts/push_to_podspecs.sh
touch TISwiftUtils.target
TIFoundationUtils.target: TISwiftUtils.target TILogging.target
MODULE_NAME="TIFoundationUtils" ./project-scripts/push_to_podspecs.sh
touch TIFoundationUtils.target
TICoreGraphicsUtils.target:
MODULE_NAME="TICoreGraphicsUtils" ./project-scripts/push_to_podspecs.sh
touch TICoreGraphicsUtils.target
TIKeychainUtils.target: TIFoundationUtils.target
MODULE_NAME="TIKeychainUtils" ./project-scripts/push_to_podspecs.sh
touch TIKeychainUtils.target
TIUIKitCore.target: TISwiftUtils.target
MODULE_NAME="TIUIKitCore" ./project-scripts/push_to_podspecs.sh
touch TIUIKitCore.target
TIUIElements.target: TIUIKitCore.target TILogging.target
MODULE_NAME="TIUIElements" ./project-scripts/push_to_podspecs.sh
touch TIUIElements.target
TIWebView.target: TIUIKitCore.target
MODULE_NAME="TIWebView" ./project-scripts/push_to_podspecs.sh
touch TIWebView.target
TIBottomSheet.target: TIUIElements.target
MODULE_NAME="TIBottomSheet" ./project-scripts/push_to_podspecs.sh
touch TIBottomSheet.target
TISwiftUICore.target: TIUIKitCore.target
MODULE_NAME="TISwiftUICore" ./project-scripts/push_to_podspecs.sh
touch TISwiftUICore.target
TITableKitUtils.target: TIUIElements.target
MODULE_NAME="TITableKitUtils" ./project-scripts/push_to_podspecs.sh
touch TITableKitUtils.target
TIDeeplink.target: TIFoundationUtils.target
MODULE_NAME="TIDeeplink" ./project-scripts/push_to_podspecs.sh
touch TIDeeplink.target
TIDeveloperUtils.target: TIUIElements.target
MODULE_NAME="TIDeveloperUtils" ./project-scripts/push_to_podspecs.sh
touch TIDeveloperUtils.target
TINetworking.target: TIFoundationUtils.target
MODULE_NAME="TINetworking" ./project-scripts/push_to_podspecs.sh
touch TINetworking.target
TILogging.target:
MODULE_NAME="TILogging" ./project-scripts/push_to_podspecs.sh
touch TILogging.target
TIMoyaNetworking.target: TINetworking.target
MODULE_NAME="TIMoyaNetworking" ./project-scripts/push_to_podspecs.sh
touch TIMoyaNetworking.target
TINetworkingCache.target: TINetworking.target
MODULE_NAME="TINetworkingCache" ./project-scripts/push_to_podspecs.sh
touch TINetworkingCache.target
TIMapUtils.target: TILogging TICoreGraphicsUtils.target
MODULE_NAME="TIMapUtils" ./project-scripts/push_to_podspecs.sh
touch TIMapUtils.target
TIAppleMapUtils.target: TIMapUtils.target
MODULE_NAME="TIAppleMapUtils" ./project-scripts/push_to_podspecs.sh
touch TIAppleMapUtils.target
TIGoogleMapUtils.target: TIMapUtils.target
MODULE_NAME="TIGoogleMapUtils" ./project-scripts/push_to_podspecs.sh
touch TIGoogleMapUtils.target
TIYandexMapUtils.target: TIMapUtils.target
MODULE_NAME="TIYandexMapUtils" ./project-scripts/push_to_podspecs.sh
touch TIYandexMapUtils.target
TIPagination.target: TISwiftUtils.target
MODULE_NAME="TIPagination" ./project-scripts/push_to_podspecs.sh
touch TIPagination.target
TIAuth.target: TIUIKitCore.target TIKeychainUtils.target
MODULE_NAME="TIAuth" ./project-scripts/push_to_podspecs.sh
touch TIAuth.target
TIEcommerce.target: TINetworking.target TIUIElements.target
MODULE_NAME="TIEcommerce" ./project-scripts/push_to_podspecs.sh
touch TIEcommerce.target
TITextProcessing.target:
MODULE_NAME="TITextProcessing" ./project-scripts/push_to_podspecs.sh
touch TITextProcessing.target
TIApplication.target: TIFoundationUtils.target TILogging.target
MODULE_NAME="TIApplication" ./project-scripts/push_to_podspecs.sh
touch TIApplication.target
clean:
rm *.target

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

View File

@ -1,151 +0,0 @@
# OTPSwiftView
![Platform](https://img.shields.io/badge/platform-iOS-green)
A fully customizable OTP view.
<p align="left">
<img src="Assets/preview.gif" width=300 height=533>
</p>
# Usage
```swift
class ViewController: UIViewController {
let otpView = CustomOTPSwiftView() // Custom OTP view
let config = OTPCodeConfig(codeSymbolsCount: 6, // Base configuration of OTP view
spacing: 6,
customSpacing: [2: 20])
override func viewDidLoad() {
super.viewDidLoad()
/*
Add your codeView and set layout
*/
/* Configure OTP view */
otpView.configure(with: config)
/* Bind events */
otpView.onTextEnter = { code in
// Get code from codeView
}
/* Update text */
otpView.code = "234435"
/* Update focus */
otpView.beginFirstResponder() // show keyboard
otpView.resignFirstResponder() // hide keyboard
}
}
```
# Customization
## Single OTP View
*OTPView* is a base class that describes a single OTP textfield.
To customize the appearance and layout, you must inherit from the OTPView.
*Don't forget to add UIGestureRecognizer to call closure `onTap?()`. Use UITapGestureRecognizer to avoid bugs.*
```swift
import OTPSwiftView
class CustomOTPView: OTPView {
override func addViews() {
super.addViews()
// Adding additional views to current view. The OTP textfield has already been added.
}
override func configureLayout() {
super.configureLayout()
// Confgiure layout of subviews
}
override func bindViews() {
super.bindViews()
// Binding to data or user actions
let gesture = UITapGestureRecognizer(target: self, action: #selector(onTapAction))
addGestureRecognizer(gesture)
}
private func onTapAction() {
onTap?()
}
override func configureAppearance() {
super.configureAppearance()
// Appearance configuration method
}
}
```
*If needed to set validation for input use `validationClosure: ValidationClosure<String>?`*. For example, only numbers validation:
```swift
import OTPSwiftView
class CustomOTPView: OTPView {
override func bindViews() {
super.bindViews()
codeTextField.validationClosure = { input in
input.allSatisfy { $0.isNumber }
}
}
}
```
## OTPSwiftView
*OTPSwiftView* is a base class that is responsible for the layout of single OTP views.
As with OTPView, you should create an heir class to configure your full OTP view.
```swift
import OTPSwiftView
final class CustomOTPSwiftView: OTPSwiftView<CustomOTPView> {
override func addViews() {
super.addViews()
// Adding additional views to current code view. The single OTP views has already been added.
}
override func configureLayout() {
super.configureLayout()
// Confgiure layout of subviews
}
override func bindViews() {
super.bindViews()
// Binding to data or user actions
}
override func configureAppearance() {
super.configureAppearance()
// Appearance configuration method
}
override func configure(with config: OTPCodeConfig) {
super.configure(with: config)
// Configure you code view with configuration
}
}
```
# Installation via SPM
You can install this framework as a target of LeadKit.

View File

@ -1,38 +0,0 @@
//
// Copyright (c) 2020 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import UIKit
/// Base configuration for OTPSwiftView
open class OTPCodeConfig {
public typealias Spacing = [Int: CGFloat]
public let codeSymbolsCount: Int
public let spacing: CGFloat
public let customSpacing: Spacing?
public init(codeSymbolsCount: Int, spacing: CGFloat, customSpacing: Spacing?) {
self.codeSymbolsCount = codeSymbolsCount
self.spacing = spacing
self.customSpacing = customSpacing
}
}

View File

@ -1,149 +0,0 @@
//
// Copyright (c) 2020 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import UIKit
import TIUIElements
import TISwiftUtils
/// Base full OTP View for entering the verification code
open class OTPSwiftView<View: OTPView>: BaseInitializableControl {
private var emptyOTPView: View? {
textFieldsCollection.first { $0.codeTextField.text.orEmpty.isEmpty } ?? textFieldsCollection.last
}
public private(set) var codeStackView = UIStackView()
public private(set) var textFieldsCollection: [View] = []
public var onTextEnter: ParameterClosure<String>?
public var code: String {
get {
textFieldsCollection.compactMap { $0.codeTextField.text }.joined()
}
set {
textFieldsCollection.first?.codeTextField.set(inputText: newValue)
}
}
public override var isFirstResponder: Bool {
!textFieldsCollection.allSatisfy { !$0.codeTextField.isFirstResponder }
}
open override func addViews() {
super.addViews()
addSubview(codeStackView)
}
open override func configureAppearance() {
super.configureAppearance()
codeStackView.contentMode = .center
codeStackView.distribution = .fillEqually
}
open func configure(with config: OTPCodeConfig) {
textFieldsCollection = createTextFields(numberOfFields: config.codeSymbolsCount)
codeStackView.addArrangedSubviews(textFieldsCollection)
codeStackView.spacing = config.spacing
configure(customSpacing: config.customSpacing, for: codeStackView)
bindTextFields(with: config)
}
@discardableResult
open override func becomeFirstResponder() -> Bool {
guard let emptyOTPView = emptyOTPView, !emptyOTPView.isFirstResponder else {
return false
}
return emptyOTPView.codeTextField.becomeFirstResponder()
}
@discardableResult
open override func resignFirstResponder() -> Bool {
guard let emptyOTPView = emptyOTPView, emptyOTPView.isFirstResponder else {
return false
}
return emptyOTPView.codeTextField.resignFirstResponder()
}
}
// MARK: - Configure textfields
private extension OTPSwiftView {
func configure(customSpacing: OTPCodeConfig.Spacing?, for stackView: UIStackView) {
guard let customSpacing = customSpacing else {
return
}
customSpacing.forEach { viewIndex, spacing in
guard viewIndex < stackView.arrangedSubviews.count, viewIndex >= .zero else {
return
}
self.set(spacing: spacing,
after: stackView.arrangedSubviews[viewIndex],
for: stackView)
}
}
func set(spacing: CGFloat, after view: UIView, for stackView: UIStackView) {
stackView.setCustomSpacing(spacing, after: view)
}
func createTextFields(numberOfFields: Int) -> [View] {
var textFieldsCollection: [View] = []
(.zero..<numberOfFields).forEach { _ in
let textField = View()
textField.codeTextField.previousTextField = textFieldsCollection.last?.codeTextField
textFieldsCollection.last?.codeTextField.nextTextField = textField.codeTextField
textFieldsCollection.append(textField)
}
return textFieldsCollection
}
func bindTextFields(with config: OTPCodeConfig) {
let onTextChangedSignal: VoidClosure = { [weak self] in
guard let code = self?.code else {
return
}
let correctedCode = code.prefix(config.codeSymbolsCount).string
self?.onTextEnter?(correctedCode)
}
let onTap: VoidClosure = { [weak self] in
self?.becomeFirstResponder()
}
textFieldsCollection.forEach {
$0.codeTextField.onTextChangedSignal = onTextChangedSignal
$0.onTap = onTap
}
}
}

View File

@ -1,131 +0,0 @@
//
// Copyright (c) 2020 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import UIKit
import TISwiftUtils
/// Base one symbol textfield
open class OTPTextField: UITextField {
private let maxSymbolsCount = 1
public weak var previousTextField: OTPTextField?
public weak var nextTextField: OTPTextField?
public var onTextChangedSignal: VoidClosure?
public var validationClosure: Closure<String, Bool>?
public var caretHeight: CGFloat?
public var lastNotEmpty: OTPTextField {
let isLastNotEmpty = !text.orEmpty.isEmpty && nextTextField?.text.orEmpty.isEmpty ?? true
return isLastNotEmpty ? self : nextTextField?.lastNotEmpty ?? self
}
open override var font: UIFont? {
didSet {
if caretHeight == nil, let font = font {
caretHeight = font.pointSize - font.descender
}
}
}
public override init(frame: CGRect) {
super.init(frame: frame)
delegate = self
}
@available(*, unavailable)
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
open override func deleteBackward() {
guard text.orEmpty.isEmpty else {
return
}
onTextChangedSignal?()
previousTextField?.text = ""
previousTextField?.becomeFirstResponder()
}
public func set(inputText: String) {
text = inputText.prefix(maxSymbolsCount).string
let nextInputText = inputText.count >= maxSymbolsCount
? inputText.suffix(inputText.count - maxSymbolsCount).string
: ""
nextTextField?.set(inputText: nextInputText)
}
open override func caretRect(for position: UITextPosition) -> CGRect {
guard let caretHeight = caretHeight else {
return super.caretRect(for: position)
}
var superRect = super.caretRect(for: position)
superRect.size.height = caretHeight
return superRect
}
open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
return view == self && isFirstResponder ? view : nil
}
}
extension OTPTextField: UITextFieldDelegate {
public func textField(_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool {
guard let textField = textField as? OTPTextField else {
return true
}
let isInputEmpty = textField.text.orEmpty.isEmpty && string.isEmpty
guard isInputEmpty || validationClosure?(string) ?? true else {
return false
}
switch range.length {
case 0: // set text to textfield
textField.set(inputText: string)
let currentTextField = textField.lastNotEmpty.nextTextField ?? textField.lastNotEmpty
currentTextField.becomeFirstResponder()
textField.onTextChangedSignal?()
return false
case 1: // remove character from textfield
textField.text = ""
textField.onTextChangedSignal?()
return false
default:
return true
}
}
}

View File

@ -1,37 +0,0 @@
//
// Copyright (c) 2020 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import TIUIElements
import TISwiftUtils
/// Base OTP view with textfield for entering a one symbol
open class OTPView: BaseInitializableView {
public let codeTextField = OTPTextField()
public var onTap: VoidClosure?
open override func addViews() {
super.addViews()
addSubview(codeTextField)
}
}

View File

@ -1,95 +0,0 @@
{
"pins" : [
{
"identity" : "alamofire",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Alamofire/Alamofire.git",
"state" : {
"revision" : "bc268c28fb170f494de9e9927c371b8342979ece",
"version" : "5.7.1"
}
},
{
"identity" : "antlr4",
"kind" : "remoteSourceControl",
"location" : "https://github.com/antlr/antlr4",
"state" : {
"revision" : "44d87bc1d130c88aa452894aa5f7e2f710f68253",
"version" : "4.10.1"
}
},
{
"identity" : "cache",
"kind" : "remoteSourceControl",
"location" : "https://github.com/hyperoslo/Cache.git",
"state" : {
"revision" : "c7f4d633049c3bd649a353bad36f6c17e9df085f",
"version" : "6.0.0"
}
},
{
"identity" : "cursors",
"kind" : "remoteSourceControl",
"location" : "https://github.com/petropavel13/Cursors",
"state" : {
"revision" : "52f27b82cb1cbbc2b5fd09514c48b9c75e3b0300",
"version" : "0.6.0"
}
},
{
"identity" : "keychainaccess",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kishikawakatsumi/KeychainAccess.git",
"state" : {
"revision" : "84e546727d66f1adc5439debad16270d0fdd04e7",
"version" : "4.2.2"
}
},
{
"identity" : "moya",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Moya/Moya.git",
"state" : {
"revision" : "c263811c1f3dbf002be9bd83107f7cdc38992b26",
"version" : "15.0.3"
}
},
{
"identity" : "panmodal",
"kind" : "remoteSourceControl",
"location" : "https://git.svc.touchin.ru/TouchInstinct/PanModal",
"state" : {
"revision" : "ced7c1703f90746df0224b6e0d33c146d9ae4284",
"version" : "1.3.1"
}
},
{
"identity" : "reactiveswift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ReactiveCocoa/ReactiveSwift.git",
"state" : {
"revision" : "c43bae3dac73fdd3cb906bd5a1914686ca71ed3c",
"version" : "6.7.0"
}
},
{
"identity" : "rxswift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ReactiveX/RxSwift.git",
"state" : {
"revision" : "9dcaa4b333db437b0fbfaf453fad29069044a8b4",
"version" : "6.6.0"
}
},
{
"identity" : "tablekit",
"kind" : "remoteSourceControl",
"location" : "https://git.svc.touchin.ru/TouchInstinct/TableKit.git",
"state" : {
"revision" : "fec9537745799fab55df7477cb3ec2b4ea5c254d",
"version" : "2.12.0"
}
}
],
"version" : 2
}

View File

@ -1,198 +0,0 @@
// swift-tools-version:5.7
#if canImport(PackageDescription)
import PackageDescription
let package = Package(
name: "LeadKit",
platforms: [
.iOS(.v12)
],
products: [
// MARK: - Application
.library(name: "TIApplication", targets: ["TIApplication"]),
// MARK: - UIKit
.library(name: "TIUIKitCore", targets: ["TIUIKitCore"]),
.library(name: "TIUIElements", targets: ["TIUIElements"]),
.library(name: "TIWebView", targets: ["TIWebView"]),
.library(name: "TIBottomSheet", targets: ["TIBottomSheet"]),
// MARK: - SwiftUI
.library(name: "TISwiftUICore", targets: ["TISwiftUICore"]),
// MARK: - Utils
.library(name: "TISwiftUtils", targets: ["TISwiftUtils"]),
.library(name: "TIFoundationUtils", targets: ["TIFoundationUtils"]),
.library(name: "TICoreGraphicsUtils", targets: ["TICoreGraphicsUtils"]),
.library(name: "TIKeychainUtils", targets: ["TIKeychainUtils"]),
.library(name: "TITableKitUtils", targets: ["TITableKitUtils"]),
.library(name: "TIDeeplink", targets: ["TIDeeplink"]),
.library(name: "TIDeveloperUtils", targets: ["TIDeveloperUtils"]),
// MARK: - Networking
.library(name: "TINetworking", targets: ["TINetworking"]),
.library(name: "TIMoyaNetworking", targets: ["TIMoyaNetworking"]),
.library(name: "TINetworkingCache", targets: ["TINetworkingCache"]),
// MARK: - Maps
.library(name: "TIMapUtils", targets: ["TIMapUtils"]),
.library(name: "TIAppleMapUtils", targets: ["TIAppleMapUtils"]),
// MARK: - Elements
.library(name: "OTPSwiftView", targets: ["OTPSwiftView"]),
.library(name: "TITransitions", targets: ["TITransitions"]),
.library(name: "TIPagination", targets: ["TIPagination"]),
.library(name: "TIAuth", targets: ["TIAuth"]),
.library(name: "TIEcommerce", targets: ["TIEcommerce"]),
.library(name: "TITextProcessing", targets: ["TITextProcessing"])
],
dependencies: [
.package(url: "https://git.svc.touchin.ru/TouchInstinct/TableKit.git", .upToNextMinor(from: "2.12.0")),
.package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", .upToNextMajor(from: "4.2.2")),
.package(url: "https://github.com/petropavel13/Cursors", .upToNextMajor(from: "0.5.1")),
.package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.4.0")),
.package(url: "https://github.com/Moya/Moya.git", .upToNextMajor(from: "15.0.0")),
.package(url: "https://github.com/hyperoslo/Cache.git", .upToNextMajor(from: "6.0.0")),
.package(url: "https://github.com/antlr/antlr4", .upToNextMinor(from: "4.10.1")),
.package(url: "https://git.svc.touchin.ru/TouchInstinct/PanModal", .upToNextMinor(from: "1.3.0"))
],
targets: [
// MARK: - Application architecture
.target(name: "TIApplication",
dependencies: ["TILogging", "TIFoundationUtils", "KeychainAccess"],
path: "TIApplication/Sources",
plugins: [.plugin(name: "TISwiftLintPlugin")]),
// MARK: - UIKit
.target(name: "TIUIKitCore", dependencies: ["TISwiftUtils"], path: "TIUIKitCore/Sources"),
.target(name: "TIUIElements",
dependencies: ["TIUIKitCore", "TILogging"],
path: "TIUIElements/Sources",
exclude: ["../TIUIElements.app"],
plugins: [.plugin(name: "TISwiftLintPlugin")]),
.target(name: "TIWebView", dependencies: ["TIUIKitCore", "TISwiftUtils"], path: "TIWebView/Sources"),
.target(name: "TIBottomSheet",
dependencies: ["PanModal", "TIUIElements", "TIUIKitCore", "TISwiftUtils"],
path: "TIBottomSheet/Sources",
exclude: ["../TIBottomSheet.app"],
plugins: [.plugin(name: "TISwiftLintPlugin")]),
// MARK: - SwiftUI
.target(name: "TISwiftUICore",
dependencies: ["TIUIKitCore", "TISwiftUtils"],
path: "TISwiftUICore/Sources"),
// MARK: - Utils
.target(name: "TISwiftUtils",
path: "TISwiftUtils/Sources",
plugins: [.plugin(name: "TISwiftLintPlugin")]),
.target(name: "TIFoundationUtils",
dependencies: ["TISwiftUtils", "TILogging"],
path: "TIFoundationUtils",
exclude: ["TIFoundationUtils.app"],
resources: [
.copy("PrivacyInfo.xcprivacy"),
],
plugins: [.plugin(name: "TISwiftLintPlugin")]),
.target(name: "TICoreGraphicsUtils",
dependencies: [],
path: "TICoreGraphicsUtils/Sources",
exclude: ["../TICoreGraphicsUtils.app"],
plugins: [.plugin(name: "TISwiftLintPlugin")]),
.target(name: "TIKeychainUtils",
dependencies: ["TIFoundationUtils", "KeychainAccess"],
path: "TIKeychainUtils/Sources",
exclude: ["../TIKeychainUtils.app"],
plugins: [.plugin(name: "TISwiftLintPlugin")]),
.target(name: "TITableKitUtils", dependencies: ["TIUIElements", "TableKit"], path: "TITableKitUtils/Sources"),
.target(name: "TIDeeplink", dependencies: ["TIFoundationUtils"], path: "TIDeeplink", exclude: ["TIDeeplink.app"]),
.target(name: "TIDeveloperUtils", dependencies: ["TISwiftUtils", "TIUIKitCore", "TIUIElements"], path: "TIDeveloperUtils/Sources"),
.target(name: "TILogging", path: "TILogging/Sources", plugins: ["TISwiftLintPlugin"]),
// MARK: - Networking
.target(name: "TINetworking",
dependencies: ["TIFoundationUtils", "Alamofire", "TILogging"],
path: "TINetworking/Sources",
plugins: [.plugin(name: "TISwiftLintPlugin")]),
.target(name: "TIMoyaNetworking",
dependencies: ["TINetworking", "Moya"],
path: "TIMoyaNetworking/Sources",
plugins: [.plugin(name: "TISwiftLintPlugin")]),
.target(name: "TINetworkingCache",
dependencies: ["TINetworking", "Cache"],
path: "TINetworkingCache/Sources",
plugins: [.plugin(name: "TISwiftLintPlugin")]),
// MARK: - Maps
.target(name: "TIMapUtils",
dependencies: ["TILogging", "TICoreGraphicsUtils"],
path: "TIMapUtils/Sources",
plugins: [.plugin(name: "TISwiftLintPlugin")]),
.target(name: "TIAppleMapUtils",
dependencies: ["TIMapUtils"],
path: "TIAppleMapUtils/Sources",
plugins: [.plugin(name: "TISwiftLintPlugin")]),
// MARK: - Elements
.target(name: "OTPSwiftView", dependencies: ["TIUIElements"], path: "OTPSwiftView/Sources"),
.target(name: "TITransitions", path: "TITransitions/Sources"),
.target(name: "TIPagination", dependencies: ["Cursors", "TISwiftUtils"], path: "TIPagination/Sources"),
.target(name: "TIAuth", dependencies: ["TIUIKitCore", "TIKeychainUtils"], path: "TIAuth/Sources"),
.target(name: "TIEcommerce", dependencies: ["TIFoundationUtils", "TISwiftUtils", "TINetworking", "TIUIKitCore", "TIUIElements"], path: "TIEcommerce/Sources"),
.target(name: "TITextProcessing",
dependencies: [.product(name: "Antlr4", package: "antlr4")],
path: "TITextProcessing/Sources",
exclude: ["../TITextProcessing.app"]),
.binaryTarget(name: "SwiftLintBinary",
url: "https://github.com/realm/SwiftLint/releases/download/0.52.2/SwiftLintBinary-macos.artifactbundle.zip",
checksum: "89651e1c87fb62faf076ef785a5b1af7f43570b2b74c6773526e0d5114e0578e"),
.plugin(name: "TISwiftLintPlugin",
capability: .buildTool(),
dependencies: ["SwiftLintBinary"]),
// MARK: - Tests
.testTarget(
name: "TITimerTests",
dependencies: ["TIFoundationUtils"],
path: "Tests/TITimerTests"),
.testTarget(
name: "TITextProcessingTests",
dependencies: ["TITextProcessing"],
path: "Tests/TITextProcessingTests"),
.testTarget(
name: "TIFoundationUtilsTests",
dependencies: ["TIFoundationUtils", "TISwiftUtils", "TILogging"],
path: "Tests/TIFoundationUtilsTests")
]
)
#endif

View File

@ -1,57 +0,0 @@
//
// Copyright (c) 2023 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import PackagePlugin
import Foundation
@main
struct SwiftLintPlugin: BuildToolPlugin {
func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
let swiftlintScriptPath = context.package.directory.appending(["build-scripts", "xcode", "build_phases", "swiftlint.sh"])
let swiftlintExecutablePath = try context.tool(named: "swiftlint").path
let srcRoot = context.package.directory.string
let targetDir = target.directory.string
let relativeTargetDir = targetDir.replacingOccurrences(of: srcRoot, with: "")
let clearRelativeTargetDir = relativeTargetDir[relativeTargetDir.index(after: relativeTargetDir.startIndex)...] // trim leading /
return [
.prebuildCommand(displayName: "SwiftLint linting \(target.name)...",
executable: swiftlintScriptPath,
arguments: [
swiftlintExecutablePath,
context.package.directory.appending(subpath: "swiftlint_base.yml")
],
environment: [
"SCRIPT_DIR": swiftlintScriptPath.removingLastComponent().string,
"SRCROOT": srcRoot,
"SCRIPT_INPUT_FILE_COUNT": "1",
"SCRIPT_INPUT_FILE_0": clearRelativeTargetDir,
// "FORCE_LINT": "1", // Lint all files in target (not only modified)
// "AUTOCORRECT": "1"
],
outputFilesDirectory: context.package.directory)
]
}
}

59
Podfile Normal file
View File

@ -0,0 +1,59 @@
abstract_target 'LeadKit' do
pod "CocoaLumberjack/Swift", '~> 3.3.0'
pod "RxSwift", '4.0.0'
pod "RxCocoa", '4.0.0'
pod "RxAlamofire", '4.0.0'
pod "ObjectMapper", '~> 3.0.0'
inhibit_all_warnings!
target 'LeadKit iOS' do
platform :ios, '9.0'
use_frameworks!
pod "TableKit", '~> 2.5.0'
pod "UIScrollView-InfiniteScroll", '~> 1.0.0'
target 'LeadKit iOSTests' do
inherit! :search_paths
# Pods for testing
end
end
target 'LeadKit iOS Extensions' do
platform :ios, '9.0'
use_frameworks!
target 'LeadKit iOS ExtensionsTests' do
inherit! :search_paths
# Pods for testing
end
end
target 'LeadKit watchOS' do
platform :watchos, '2.0'
use_frameworks!
end
target 'LeadKit tvOS' do
platform :tvos, '9.0'
use_frameworks!
target 'LeadKit tvOSTests' do
inherit! :search_paths
# Pods for testing
end
end
end
# If you have slow HDD
ENV['COCOAPODS_DISABLE_STATS'] = "true"

39
Podfile.lock Normal file
View File

@ -0,0 +1,39 @@
PODS:
- Alamofire (4.5.1)
- CocoaLumberjack/Default (3.3.0)
- CocoaLumberjack/Swift (3.3.0):
- CocoaLumberjack/Default
- ObjectMapper (3.0.0)
- RxAlamofire (4.0.0):
- RxAlamofire/Core (= 4.0.0)
- RxAlamofire/Core (4.0.0):
- Alamofire (~> 4.5)
- RxSwift (~> 4.0)
- RxCocoa (4.0.0):
- RxSwift (~> 4.0)
- RxSwift (4.0.0)
- TableKit (2.5.0)
- UIScrollView-InfiniteScroll (1.0.2)
DEPENDENCIES:
- CocoaLumberjack/Swift (~> 3.3.0)
- ObjectMapper (~> 3.0.0)
- RxAlamofire (= 4.0.0)
- RxCocoa (= 4.0.0)
- RxSwift (= 4.0.0)
- TableKit (~> 2.5.0)
- UIScrollView-InfiniteScroll (~> 1.0.0)
SPEC CHECKSUMS:
Alamofire: 2d95912bf4c34f164fdfc335872e8c312acaea4a
CocoaLumberjack: 3c8c74683302f9012bb168e1c4b7ae3c0b558431
ObjectMapper: 92230db59bf8f341a5c3a3cf0b9fbdde3cf0d87f
RxAlamofire: 6ea579ac53bf14cb4bc7049a3866e5a769989b1d
RxCocoa: d62846ca96495d862fa4c59ea7d87e5031d7340e
RxSwift: fd680d75283beb5e2559486f3c0ff852f0d35334
TableKit: 42d4dff2944f273cdeec2ef6352064eb6a9a355b
UIScrollView-InfiniteScroll: c132d6d5851daff229ab4a1060ccf70a05a051c9
PODFILE CHECKSUM: ef8520adc4869bbbf0cf4cf70ab5757b0c95be1f
COCOAPODS: 1.3.1

139
README.md
View File

@ -1,139 +1,2 @@
# LeadKit
LeadKit is the iOS framework with a bunch of tools for rapid app development.
This repository contains the following frameworks:
- [TISwiftUtils](TISwiftUtils) - a bunch of useful helpers for Swift development.
- [TIFoundationUtils](TIFoundationUtils) - set of helpers for Foundation framework classes.
- [TIUIKitCore](TIUIKitCore) - core ui elements and protocols from LeadKit.
- [TISwiftUICore](TISwiftUICore) Core UI elements: protocols, views and helpers.
- [TIUIElements](TIUIElements) - bunch of of useful protocols and views.
- [OTPSwiftView](OTPSwiftView) - a fully customizable OTP view.
- [TITableKitUtils](TITableKitUtils) - set of helpers for TableKit classes.
- [TIKeychainUtils](TIKeychainUtils) - set of helpers for Keychain classes.
- [TIPagination](TIPagination) - realisation of paginating items from a data source.
- [TINetworking](TINetworking) - Swagger-frendly networking layer helpers.
- [TIMoyaNetworking](TIMoyaNetworking) - Moya + Swagger network service.
- [TIAppleMapUtils](TIAppleMapUtils) - set of helpers for map objects clustering and interacting using Apple MapKit.
- [TIGoogleMapUtils](TIGoogleMapUtils) - set of helpers for map objects clustering and interacting using Google Maps SDK.
- [TIYandexMapUtils](TIYandexMapUtils) - set of helpers for map objects clustering and interacting using Yandex Maps SDK.
- [TIAuth](TIAuth) - login, registration, confirmation and other related actions
## Playgrounds
### Create new Playground
```sh
$ cd TIModuleName
$ touch PlaygroundPodfile
$ echo "ENV[\"DEVELOPMENT_INSTALL\"] = \"true\"
target 'TIModuleName' do
platform :ios, IOS_VERSION_NUMBER
use_frameworks!
pod 'TIDependencyModuleName', :path => '../../../../TIDependencyModuleName/TIDependencyModuleName.podspec'
pod 'TIModuleName', :path => '../../../../TIModuleName/TIModuleName.podspec'
end" > PlaygroundPodfile
$ nef playground --name TIModuleName --cocoapods --custom-podfile PlaygroundPodfile
```
See example of `PlaygroundPodfile` in `TIFoundationUtils`
### Rename/add pages to Playground
For every new feature in module create new Playground page with documentation in comments. See [nef markdown documentation](https://github.com/bow-swift/nef#-generating-a-markdown-project).
### Create symlink to nef playground
```sh
$ cd TIModuleName
$ ln -s TIModuleName.app/Contents/MacOS/TIModuleName.playground TIModuleName.playground
```
### Add nef files to TIModuleName.app/.gitignore
```
# gitignore nef files
**/build/
**/nef/
LICENSE
```
### Exclude .app bundles from package sources
#### SPM
```swift
.target(name: "TIModuleName", dependencies: ..., path: ..., exclude: ["TIModuleName.app"]),
```
#### Podspec
```ruby
sources = 'your_sources_expression'
if ENV["DEVELOPMENT_INSTALL"] # installing using :path =>
s.source_files = sources
s.exclude_files = s.name + '.app'
else
s.source_files = s.name + '/' + sources
s.exclude_files = s.name + '/*.app'
end
```
## Docs:
- [TIFoundationUtils](docs/tifoundationutils)
* [AsyncOperation](docs/tifoundationutils/asyncoperation.md)
- [TICoreGraphicsUtils](docs/ticoregraphicsutils)
* [DrawingOperations](docs/ticoregraphicsutils/drawingoperations.md)
- [TIKeychainUtils](docs/tikeychainutils)
* [SingleValueStorage](docs/tikeychainutils/singlevaluestorage.md)
- [TIUIElements](docs/tiuielements)
* [Skeletons](docs/tiuielements/skeletons.md)
* [Placeholders](docs/tiuielements/placeholder.md)
- [TITextProcessing](docs/titextprocessing)
* [TITextProcessing](docs/titextprocessing/titextprocessing.md)
- [TIDeeplink](docs/tideeplink/deeplinks.md)
- [TIBottomSheet](docs/tibottomsheet/tibottomsheet.md)
- [Semantic Commit Messages](docs/semantic-commit-messages.md) - commit message codestyle.
- [Snippets](docs/snippets.md) - useful commands and scripts for development.
## Contributing
- Run following script in framework's folder:
```
./setup
```
- If legacy [Source](https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/master/Sources) folder needed, [build dependencies for LeadKit.xcodeproj](https://git.svc.touchin.ru/TouchInstinct/LeadKit/blob/master/docs/snippets.md#build-dependencies-for-LeadKit.xcodeproj).
- Make sure the commit message codestyle is followed. More about [Semantic Commit Messages](docs/semantic-commit-messages.md).
## Installation
### SPM
```swift
dependencies: [
.package(url: "https://git.svc.touchin.ru/TouchInstinct/LeadKit.git", from: "x.y.z"),
],
```
### Cocoapods
```ruby
source 'https://git.svc.touchin.ru/TouchInstinct/Podspecs.git'
pod 'TISwiftUtils', 'x.y.z'
pod 'TIFoundationUtils', 'x.y.z'
# ...
```
## Legacy
Code located in root `Sources` folder and `LeadKit.podspec` should be treated as legacy and shouldn't be used in newly created projects. Please use TI* modules via SPM or CocoaPods.
LeadKit it's a iOS framework with a bunch of tools for rapid app development

View File

@ -1,36 +0,0 @@
//
// Copyright (c) 2018 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import UIKit
/// Base collection controller configurable with view model and CollectionViewWrapperView as custom view.
open class BaseCollectionContentController<ViewModel>: BaseScrollContentController<ViewModel, CollectionViewWrapperView> {
override open func createView() -> CollectionViewWrapperView {
CollectionViewWrapperView(layout: UICollectionViewFlowLayout())
}
/// Contained UICollectionView instance.
public var collectionView: UICollectionView {
customView.collectionView
}
}

View File

@ -1,69 +0,0 @@
//
// Copyright (c) 2018 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import UIKit.UIViewController
/// Base controller that should be configured with view model.
open class BaseConfigurableController<ViewModel>: BaseOrientationController, ConfigurableController {
/// A view model instance used by this controller.
public let viewModel: ViewModel
/// Initializer with view model parameter.
///
/// - Parameter viewModel: A view model to configure this controller.
public init(viewModel: ViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - ConfigurableController
open func addViews() {
// override in subclass
}
open func configureLayout() {
// override in subclass
}
open func bindViews() {
// override in subclass
}
open func configureAppearance() {
// override in subclass
}
open func localize() {
// override in subclass
}
open func configureBarButtons() {
// override in subclass
}
}

View File

@ -1,54 +0,0 @@
//
// Copyright (c) 2018 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import UIKit.UIView
/// Base controller configurable by view model and custom view.
open class BaseCustomViewController<ViewModel, View: UIView>: BaseConfigurableController<ViewModel> {
/// Contained custom view.
public private(set) lazy var customView = createView()
/// Initializer with view model and custom view parameters.
///
/// - Parameters:
/// - viewModel: A view model to configure this controller.
/// - customView: UIView instance to assign in view property.
public override init(viewModel: ViewModel) {
super.init(viewModel: viewModel)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override open func loadView() {
view = customView
}
/// Creates custom view.
///
/// - Returns: Initialized custom view.
open func createView() -> View {
View()
}
}

View File

@ -1,30 +0,0 @@
import Foundation
open class BaseOrientationController: UIViewController {
/// Ability to set forced screen orientation
open var forcedInterfaceOrientation: UIInterfaceOrientation?
open override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
switch forcedInterfaceOrientation {
case .landscapeLeft:
return .landscapeLeft
case .landscapeRight:
return .landscapeRight
case .portrait:
return .portrait
case .portraitUpsideDown:
return .portraitUpsideDown
default:
return super.supportedInterfaceOrientations
}
}
open override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
forcedInterfaceOrientation ?? super.preferredInterfaceOrientationForPresentation
}
}

View File

@ -1,25 +0,0 @@
import UIKit
open class OrientationNavigationController: UINavigationController {
// MARK: - Public properties
open var presentedOrTopViewController: UIViewController? {
presentedViewController ?? topViewController
}
open override var shouldAutorotate: Bool {
presentedOrTopViewController?.shouldAutorotate
?? super.shouldAutorotate
}
open override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
presentedOrTopViewController?.supportedInterfaceOrientations
?? super.supportedInterfaceOrientations
}
open override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
presentedOrTopViewController?.preferredInterfaceOrientationForPresentation
?? super.preferredInterfaceOrientationForPresentation
}
}

View File

@ -1,79 +0,0 @@
//
// Copyright (c) 2018 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import RxSwift
import RxCocoa
public typealias ScrollViewHolderView = UIView & ScrollViewHolder
/// Base controller configurable with view model and ScrollViewHolder custom view.
open class BaseScrollContentController<ViewModel, View: ScrollViewHolderView>: BaseCustomViewController<ViewModel, View> {
private var bottomInsetDisposable: Disposable?
private let defaultInsetsRelay = BehaviorRelay<UIEdgeInsets>(value: .zero)
/// Bind given driver to bottom inset of scroll view. Takes into account default bottom insets.
///
/// - Parameter bottomInsetDriver: Driver that emits CGFloat bottom inset changes.
public func bindBottomInsetBinding(from bottomInsetDriver: Driver<CGFloat>) {
bottomInsetDisposable = bottomInsetDriver
.withLatestFrom(defaultInsetsRelay.asDriver()) {
$0 + $1.bottom
}
.drive(customView.scrollView.rx.bottomInsetBinder)
}
/// Unbind scroll view from previous binding.
public func unbindBottomInsetBinding() {
bottomInsetDisposable?.dispose()
}
/// Contained UIScrollView instance.
public var scrollView: UIScrollView {
customView.scrollView
}
/// Default insets used for contained scroll view.
public var defaultInsets: UIEdgeInsets {
get {
defaultInsetsRelay.value
}
set {
defaultInsetsRelay.accept(newValue)
customView.scrollView.contentInset = newValue
customView.scrollView.scrollIndicatorInsets = newValue
}
}
}
public extension BaseScrollContentController {
/// On iOS, tvOS 11+ sets contentInsetAdjustmentBehavior to .never.
/// On earlier versions sets automaticallyAdjustsScrollViewInsets to false.
func disableAdjustsScrollViewInsets() {
if #available(iOS 11.0, tvOS 11.0, *) {
customView.scrollView.contentInsetAdjustmentBehavior = .never
} else {
automaticallyAdjustsScrollViewInsets = false
}
}
}

View File

@ -1,52 +0,0 @@
//
// Copyright (c) 2018 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import TableKit
/// Base table controller configurable with view model and TableViewWrapperView as custom view.
open class BaseTableContentController<ViewModel>: BaseScrollContentController<ViewModel, TableViewWrapperView> {
/// TableDirector binded to table view.
public private(set) lazy var tableDirector = createTableDirector()
/// Creates tableDirector for table view.
///
/// - Returns: Initialized TableDirector.
open func createTableDirector() -> TableDirector {
TableDirector(tableView: tableView)
}
override open func createView() -> TableViewWrapperView {
TableViewWrapperView(tableViewStyle: .plain)
}
override open func configureAppearance() {
super.configureAppearance()
tableView.separatorStyle = .none
}
/// Contained UITableView instance.
public var tableView: UITableView {
customView.tableView
}
}

View File

@ -23,9 +23,7 @@
import RxSwift
/// Paging cursor implementation with enclosed cursor for fetching results
public class FixedPageCursor<Cursor: CursorType>: CursorType, RxDataSource {
public typealias ResultType = [Element]
public class FixedPageCursor<Cursor: CursorType>: CursorType {
fileprivate let cursor: Cursor
@ -42,17 +40,17 @@ public class FixedPageCursor<Cursor: CursorType>: CursorType, RxDataSource {
}
public var exhausted: Bool {
cursor.exhausted && cursor.count == count
return cursor.exhausted && cursor.count == count
}
public private(set) var count: Int = 0
public subscript(index: Int) -> Cursor.Element {
cursor[index]
return cursor[index]
}
public func loadNextBatch() -> Single<[Cursor.Element]> {
Single.deferred {
return Single.deferred {
if self.exhausted {
return .error(CursorError.exhausted)
}
@ -69,16 +67,15 @@ public class FixedPageCursor<Cursor: CursorType>: CursorType, RxDataSource {
return self.cursor.loadNextBatch()
.flatMap { _ in
self.loadNextBatch()
}
}
}
}
}
/// FixedPageCursor subclass with implementation of ResettableType
public class ResettableFixedPageCursor<Cursor: ResettableCursorType>: FixedPageCursor<Cursor>, ResettableType {
public typealias ResultType = [Element]
public override init(cursor: Cursor, pageSize: Int) {
super.init(cursor: cursor, pageSize: pageSize)
}
@ -86,4 +83,5 @@ public class ResettableFixedPageCursor<Cursor: ResettableCursorType>: FixedPageC
public required init(resetFrom other: ResettableFixedPageCursor) {
super.init(cursor: other.cursor.reset(), pageSize: other.pageSize)
}
}

View File

@ -29,7 +29,7 @@ public extension CursorType {
/// - Parameter transform: closure to transform elements
/// - Returns: new MapCursor instance
func flatMap<T>(transform: @escaping MapCursor<Self, T>.Transform) -> MapCursor<Self, T> {
MapCursor(cursor: self, transform: transform)
return MapCursor(cursor: self, transform: transform)
}
/// Creates ResettableMapCursor with current cursor
@ -39,14 +39,13 @@ public extension CursorType {
func flatMap<T>(transform: @escaping ResettableMapCursor<Self, T>.Transform)
-> ResettableMapCursor<Self, T> where Self: ResettableCursorType {
ResettableMapCursor(cursor: self, transform: transform)
return ResettableMapCursor(cursor: self, transform: transform)
}
}
/// Map cursor implementation with enclosed cursor for fetching results
public class MapCursor<Cursor: CursorType, T>: CursorType, RxDataSource {
public typealias ResultType = [Element]
public class MapCursor<Cursor: CursorType, T>: CursorType {
public typealias Transform = (Cursor.Element) -> T?
@ -67,32 +66,31 @@ public class MapCursor<Cursor: CursorType, T>: CursorType, RxDataSource {
}
public var exhausted: Bool {
cursor.exhausted
return cursor.exhausted
}
public var count: Int {
elements.count
return elements.count
}
public subscript(index: Int) -> T {
elements[index]
return elements[index]
}
public func loadNextBatch() -> Single<[T]> {
cursor.loadNextBatch().map { newItems in
let transformedNewItems = newItems.compactMap(self.transform)
return cursor.loadNextBatch().map { newItems in
let transformedNewItems = newItems.flatMap(self.transform)
self.elements += transformedNewItems
return transformedNewItems
}
}
}
/// MapCursor subclass with implementation of ResettableType
public class ResettableMapCursor<Cursor: ResettableCursorType, T>: MapCursor<Cursor, T>, ResettableType {
public typealias ResultType = [Cursor.Element]
public override init(cursor: Cursor, transform: @escaping Transform) {
super.init(cursor: cursor, transform: transform)
}
@ -100,4 +98,5 @@ public class ResettableMapCursor<Cursor: ResettableCursorType, T>: MapCursor<Cur
public required init(resetFrom other: ResettableMapCursor) {
super.init(cursor: other.cursor.reset(), transform: other.transform)
}
}

View File

@ -23,9 +23,7 @@
import RxSwift
/// Stub cursor implementation for array content type
public class StaticCursor<Element>: ResettableRxDataSourceCursor {
public typealias ResultType = [Element]
public class StaticCursor<Element>: ResettableCursorType {
private let content: [Element]
@ -45,11 +43,11 @@ public class StaticCursor<Element>: ResettableRxDataSourceCursor {
public private(set) var count = 0
public subscript(index: Int) -> Element {
content[index]
return content[index]
}
public func loadNextBatch() -> Single<[Element]> {
Single.deferred {
return Single.deferred {
if self.exhausted {
return .error(CursorError.exhausted)
}
@ -61,4 +59,5 @@ public class StaticCursor<Element>: ResettableRxDataSourceCursor {
return .just(self.content)
}
}
}

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2018 Touch Instinct
// Copyright (c) 2017 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
@ -23,49 +23,51 @@
import RxSwift
import RxCocoa
open class TotalCountCursor<CursorConfiguration: TotalCountCursorConfiguration>: ResettableRxDataSourceCursor {
public final class TotalCountCursor<CC: TotalCountCursorConfiguration>: ResettableCursorDataSource {
public typealias Element = CursorConfiguration.ResultType.ElementType
public typealias ResultType = [Element]
public typealias Element = CC.ElementType
public typealias ResultType = [CC.ElementType]
private let configuration: CursorConfiguration
private let configuration: CC
private var elements: [Element] = []
public private(set) var totalCount: Int = .max
private let disposeBag = DisposeBag()
public var exhausted: Bool {
count >= totalCount
return count >= totalCount
}
public var count: Int {
elements.count
return elements.count
}
public subscript(index: Int) -> Element {
elements[index]
return elements[index]
}
public init(configuration: CursorConfiguration) {
public init(configuration: CC) {
self.configuration = configuration
}
public required init(resetFrom other: TotalCountCursor) {
configuration = other.configuration.reset()
}
open func processResultFromConfigurationSingle() -> Single<CursorConfiguration.ResultType> {
configuration.resultSingle()
self.configuration = other.configuration.reset()
}
public func loadNextBatch() -> Single<[Element]> {
processResultFromConfigurationSingle()
.do(onSuccess: { [weak self] listingResult in
self?.totalCount = listingResult.totalCount
self?.elements = (self?.elements ?? []) + listingResult.results
return configuration.nextBatchObservable()
.map { [configuration] listing in
configuration.getResult(from: listing)
}
.do(onNext: { listingResult in
self.totalCount = listingResult.totalCount
self.elements += listingResult.results
})
.map {
$0.results
}
}
}

View File

@ -1,93 +0,0 @@
//
// Copyright (c) 2017 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import RxSwift
/// Single load cursor configuration for single load operation
public final class SingleLoadCursorConfiguration<Element>: TotalCountCursorConfiguration {
public typealias ResultType = [Element]
private let loadingSingle: Single<ResultType>
/// Initializer for Single with array result type.
///
/// - Parameter loadingSingle: Single that will emit array of result type.
public init(loadingSingle: Single<ResultType>) {
self.loadingSingle = loadingSingle
}
public func resultSingle() -> Single<ResultType> {
loadingSingle
}
public init(resetFrom other: SingleLoadCursorConfiguration) {
self.loadingSingle = other.loadingSingle
}
}
/// Cursor implementation for single load operation
@available(*, deprecated, message: "Use SingleLoadCursorConfiguration with TotalCountCursor.")
public class SingleLoadCursor<Element>: ResettableCursorType {
private let loadingObservable: Single<[Element]>
private var content: [Element] = []
/// Initializer for array content type
///
/// - Parameter loadingObservable: Single observable with element of [Element] type
public init(loadingObservable: Single<[Element]>) {
self.loadingObservable = loadingObservable
}
public required init(resetFrom other: SingleLoadCursor) {
self.loadingObservable = other.loadingObservable
}
public private(set) var exhausted = false
public var count: Int {
content.count
}
public subscript(index: Int) -> Element {
content[index]
}
public func loadNextBatch() -> Single<[Element]> {
Single.deferred {
if self.exhausted {
return .error(CursorError.exhausted)
}
return self.loadingObservable.do(onSuccess: { [weak self] newItems in
self?.onGot(result: newItems)
})
}
}
private func onGot(result: [Element]) {
content = result
exhausted = true
}
}

View File

@ -1,27 +0,0 @@
//
// Copyright (c) 2018 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import RxSwift
/// Data loading model for GeneralDataLoadingState with Single as data source.
public final class GeneralDataLoadingModel<T>: RxDataLoadingModel<GeneralDataLoadingState<Single<T>>> {
}

View File

@ -1,131 +0,0 @@
//
// Copyright (c) 2018 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import RxSwift
import RxCocoa
/// ViewModel that loads data from given data source with loading state tracking.
open class GeneralDataLoadingViewModel<ResultType>: BaseViewModel, GeneralDataLoadingHandler, DisposeBagHolder {
public typealias LoadingModel = GeneralDataLoadingModel<ResultType>
public typealias DataSourceType = Single<ResultType>
public typealias LoadingState = GeneralDataLoadingState<DataSourceType>
private let loadingModel: LoadingModel
private let loadingStateRelay = BehaviorRelay<LoadingState>(value: .initial)
// MARK: - DisposeBagHolder
public let disposeBag = DisposeBag()
/// Initializer with single result sequence and empty result checker closure.
///
/// - Parameters:
/// - dataSource: A single element sequence.
/// - customErrorHandler: Custom error handler for state update. Pass nil for default error handling.
/// - emptyResultChecker: Closure for checking result on empty state.
public init(dataSource: DataSourceType,
customErrorHandler: LoadingModel.ErrorHandler? = nil,
emptyResultChecker: @escaping LoadingModel.EmptyResultChecker = { _ in false }) {
loadingModel = LoadingModel(dataSource: dataSource,
customErrorHandler: customErrorHandler,
emptyResultChecker: emptyResultChecker)
loadingModel.stateDriver
.drive(loadingStateRelay)
.disposed(by: disposeBag)
bindLoadingState(from: loadingStateDriver)
loadingModel.reload()
}
/// Returns observable that emits current loading state.
open var loadingStateObservable: Observable<LoadingState> {
loadingStateRelay.asObservable()
}
/// Returns driver that emits current loading state.
open var loadingStateDriver: Driver<LoadingState> {
loadingStateRelay.asDriver()
}
/// By default returns true if loading state == .result.
open var hasContent: Bool {
currentLoadingState.hasResult
}
/// Returns current result if it exists.
public var currentResult: ResultType? {
currentLoadingState.result
}
/// Current state of loading process.
private(set) public var currentLoadingState: LoadingState {
get {
loadingStateRelay.value
}
set {
loadingStateRelay.accept(newValue)
}
}
/// Manually update loading state.
/// Should be used only in specific situations on your own risk!
///
/// - Parameter newState: New loading state.
public func updateStateManually(to newState: LoadingState) {
currentLoadingState = newState
}
/// Replaces current data source of loading model with new one.
///
/// - Parameter dataSource: A single element sequence.
public func replaceDataSource(with newDataSource: DataSourceType) {
loadingModel.replaceDataSource(with: newDataSource)
}
/// Reload data.
public func reload() {
loadingModel.reload()
}
// MARK: - GeneralDataLoadingHandler
open func onLoadingState() {
// override in subclass
}
open func onResultsState(result: ResultType) {
// override in subclass
}
open func onEmptyState() {
// override in subclass
}
open func onErrorState(error: Error) {
// override in subclass
}
}

View File

@ -1,65 +0,0 @@
//
// Copyright (c) 2018 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import RxSwift
import RxCocoa
open class RxDataLoadingModel<LoadingStateType: DataLoadingState>: RxNetworkOperationModel<LoadingStateType>
where LoadingStateType.DataSourceType: RxDataSource {
public typealias EmptyResultChecker = (ResultType) -> Bool
let emptyResultChecker: EmptyResultChecker
/// Model initializer with data source, empty result checker and custom error handler.
///
/// - Parameters:
/// - dataSource: Data source for data loading.
/// - customErrorHandler: Custom error handler for state update. Pass nil for default error handling.
/// - emptyResultChecker: Empty result checker closure.
public init(dataSource: DataSourceType,
customErrorHandler: ErrorHandler? = nil,
emptyResultChecker: @escaping EmptyResultChecker) {
self.emptyResultChecker = emptyResultChecker
super.init(dataSource: dataSource, customErrorHandler: customErrorHandler)
}
open func reload() {
execute()
}
override func onGot(result: ResultType, from dataSource: DataSourceType) {
if emptyResultChecker(result) {
state = .emptyState
} else {
super.onGot(result: result, from: dataSource)
}
updateStateAfterResult(from: dataSource)
}
func updateStateAfterResult(from dataSource: DataSourceType) {
// override in subcass if needed
}
}

View File

@ -1,100 +0,0 @@
//
// Copyright (c) 2018 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import RxSwift
import RxCocoa
open class RxNetworkOperationModel<LoadingStateType: NetworkOperationState>: NetworkOperationModel
where LoadingStateType.DataSourceType: RxDataSource {
public typealias DataSourceType = LoadingStateType.DataSourceType
public typealias ResultType = DataSourceType.ResultType
public typealias ErrorHandler = (Error, LoadingStateType) -> LoadingStateType
private let stateRelay = BehaviorRelay<LoadingStateType>(value: .initialState)
var currentRequestDisposable: Disposable?
private(set) var dataSource: DataSourceType
private let errorHandler: ErrorHandler
open var stateDriver: Driver<LoadingStateType> {
stateRelay.asDriver()
}
/// Model initializer with data source and custom error handler.
///
/// - Parameters:
/// - dataSource: Data source for network operation.
/// - customErrorHandler: Custom error handler for state update. Pass nil for default error handling.
public init(dataSource: DataSourceType, customErrorHandler: ErrorHandler? = nil) {
self.errorHandler = customErrorHandler ?? { .errorState(error: $0, after: $1) }
self.dataSource = dataSource
}
/// Performs request to given data source
public func execute() {
currentRequestDisposable?.dispose()
state = .initialLoadingState(after: state)
requestResult(from: dataSource)
}
/// Replaces current data source with new one.
///
/// - Parameter newDataSource: A new data source to use.
public func replaceDataSource(with newDataSource: DataSourceType) {
dataSource = newDataSource
}
func onGot(error: Error) {
state = errorHandler(error, state)
}
func onGot(result: ResultType, from dataSource: DataSourceType) {
state = .resultState(result: result,
from: dataSource,
after: state)
}
func requestResult(from dataSource: DataSourceType) {
currentRequestDisposable = dataSource
.resultSingle()
.observe(on: MainScheduler.instance)
.subscribe(onSuccess: { [weak self] result in
self?.onGot(result: result, from: dataSource)
}, onFailure: { [weak self] error in
self?.onGot(error: error)
})
}
var state: LoadingStateType {
get {
stateRelay.value
}
set {
stateRelay.accept(newValue)
}
}
}

View File

@ -0,0 +1,59 @@
//
// Copyright (c) 2017 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
open class App {
fileprivate static let stringVendorIdentifierKey = "stringIdentifierForVendor"
/// The value of CFBundleName
open static let bundleName = Bundle.main.infoDictionary?["CFBundleName"] as? String ?? ""
/// The value of CFBundleShortVersionString
open static let shortVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
/// The value of CFBundleVersion
open static let bundleVersion = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? ""
/**
Return app's version
- returns: shortBundleVersion.bundleVersion
*/
open static var version: String {
return App.shortVersion + "." + App.bundleVersion
}
/**
Return device identifier
- returns: UUIDString
*/
open static var deviceUniqueIdentifier: String {
if let vendorIdentifier = UserDefaults.standard.string(forKey: App.stringVendorIdentifierKey) {
return vendorIdentifier
}
let vendorIdentifier = UUID().uuidString
UserDefaults.standard.set(vendorIdentifier, forKey: App.stringVendorIdentifierKey)
UserDefaults.standard.synchronize()
return vendorIdentifier
}
}

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2019 Touch Instinct
// Copyright (c) 2017 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
@ -21,16 +21,36 @@
//
import Foundation
import CocoaLumberjack
public extension Decimal {
open class Log {
/// Conver Decimal to Double value
var doubleValue: Double {
NSDecimalNumber(decimal: self).doubleValue
public init() {
DDLog.add(DDFileLogger())
DDLog.add(DDASLLogger.sharedInstance)
DDLog.add(DDTTYLogger.sharedInstance)
let logFormatter = LogFormatter()
DDASLLogger.sharedInstance.logFormatter = logFormatter
DDTTYLogger.sharedInstance.logFormatter = logFormatter
let assertionHandler = NSAssertionHandler()
Thread.current.threadDictionary.setValue(assertionHandler, forKey: NSAssertionHandlerKey)
}
/// Conver Decimal to Int value
var intValue: Int {
NSDecimalNumber(decimal: self).intValue
/**
Add start message for your application
- returns: Return value looks like "AppName 1.0.1 session started on version 9.2 (build 13c75)"
*/
open static var startMessage: String {
let startMessage = App.bundleName + " " + App.shortVersion + "."
+ App.bundleVersion + " session started on "
+ ProcessInfo.processInfo.operatingSystemVersionString.lowercased()
return startMessage
}
}

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2018 Touch Instinct
// Copyright (c) 2017 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
@ -21,17 +21,37 @@
//
import Foundation
import CocoaLumberjack
extension String {
class LogFormatter: NSObject, DDLogFormatter {
fileprivate let dateFormatter: DateFormatter
/// Extracts host from strings that can be converted to URL.
/// Causes assertionFailure if string cannot be converted.
override init() {
dateFormatter = DateFormatter()
dateFormatter.formatterBehavior = .behavior10_4
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss:SSS"
var asHost: String {
guard let host = URL(string: self)?.host else {
assertionFailure("Cannot detect host for base URL")
return ""
}
return host
super.init()
}
func format(message logMessage: DDLogMessage) -> String? {
let level: String
switch logMessage.flag {
case DDLogFlag.error:
level = "ERR"
case DDLogFlag.warning:
level = "WRN"
case DDLogFlag.info:
level = "INF"
case DDLogFlag.debug:
level = "DBG"
default:
level = "VRB"
}
let dateAndTime = dateFormatter.string(from: logMessage.timestamp)
return "\(level) \(dateAndTime) [\(logMessage.fileName):\(logMessage.line)]: \(logMessage.message)"
}
}

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2018 Touch Instinct
// Copyright (c) 2017 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
@ -24,26 +24,21 @@ import RxSwift
import RxCocoa
import UIScrollView_InfiniteScroll
/// Class that connects PaginationDataLoadingModel with UIScrollView. It handles all non-visual and visual states.
final public class PaginationWrapper<Cursor: ResettableRxDataSourceCursor, Delegate: PaginationWrapperDelegate>
/// Class that connects PaginationViewModel with UIScrollView. It handles all non-visual and visual states.
final public class PaginationWrapper<Cursor: ResettableCursorType, Delegate: PaginationWrapperDelegate>
where Cursor == Delegate.DataSourceType, Cursor.ResultType == [Cursor.Element] {
private typealias DataLoadingModel = PaginationDataLoadingModel<Cursor>
private typealias LoadingState = PaginationLoadingViewModel<Cursor>.LoadingStateType
private typealias LoadingState = DataLoadingModel.NetworkOperationStateType
private typealias FinishInfiniteScrollCompletion = ((UIScrollView) -> Void)
private var wrappedView: AnyPaginationWrappable
private let paginationViewModel: DataLoadingModel
private var wrappedView: AnyPaginationWrappableView
private let paginationViewModel: PaginationLoadingViewModel<Cursor>
private weak var delegate: Delegate?
private weak var uiDelegate: PaginationWrapperUIDelegate?
/// Sets the offset between the real end of the scroll view content and the scroll position,
/// so the handler can be triggered before reaching end. Defaults to 0.0;
public var infiniteScrollTriggerOffset: CGFloat {
get {
wrappedView.scrollView.infiniteScrollTriggerOffset
return wrappedView.scrollView.infiniteScrollTriggerOffset
}
set {
wrappedView.scrollView.infiniteScrollTriggerOffset = newValue
@ -60,35 +55,31 @@ final public class PaginationWrapper<Cursor: ResettableRxDataSourceCursor, Deleg
}
}
private var bottom: CGFloat {
wrappedView.scrollView.contentSize.height - wrappedView.scrollView.frame.size.height
}
private let disposeBag = DisposeBag()
private var currentPlaceholderView: UIView?
private var currentPlaceholderViewTopConstraint: NSLayoutConstraint?
private let applicationCurrentyActive = Variable<Bool>(true)
/// Initializer with table view, placeholders container view, cusor and delegate parameters.
///
/// - Parameters:
/// - wrappedView: UIScrollView instance to work with.
/// - cursor: Cursor object that acts as data source.
/// - delegate: Delegate object for data loading events handling.
/// - uiDelegate: Delegate object for UI customization.
public init(wrappedView: AnyPaginationWrappable,
cursor: Cursor,
delegate: Delegate,
uiDelegate: PaginationWrapperUIDelegate? = nil) {
/// - delegate: Delegate object for data loading events handling and UI customization.
public init(wrappedView: AnyPaginationWrappableView, cursor: Cursor, delegate: Delegate) {
self.wrappedView = wrappedView
self.delegate = delegate
self.uiDelegate = uiDelegate
self.paginationViewModel = PaginationDataLoadingModel(dataSource: cursor) { $0.isEmpty }
let configuration = PaginationLoadingViewModelConfiguration(dataSource: cursor)
self.paginationViewModel = PaginationLoadingViewModel(configuration: configuration)
bindViewModelStates()
createRefreshControl()
bindAppStateNotifications()
}
/// Method that reload all data in internal view model.
@ -105,10 +96,10 @@ final public class PaginationWrapper<Cursor: ResettableRxDataSourceCursor, Deleg
///
/// - Parameter scrollObservable: Observable that emits content offset as CGPoint.
public func setScrollObservable(_ scrollObservable: Observable<CGPoint>) {
scrollObservable
.asDriver(onErrorJustReturn: .zero)
.drive(scrollOffsetChanged)
.disposed(by: disposeBag)
scrollObservable.subscribe(onNext: { [weak self] offset in
self?.currentPlaceholderViewTopConstraint?.constant = -offset.y
})
.disposed(by: disposeBag)
}
// MARK: - States handling
@ -121,9 +112,9 @@ final public class PaginationWrapper<Cursor: ResettableRxDataSourceCursor, Deleg
if case .initial = afterState {
wrappedView.scrollView.isUserInteractionEnabled = false
removeAllPlaceholderView()
removeCurrentPlaceholderView()
guard let loadingIndicator = uiDelegate?.initialLoadingIndicator() else {
guard let loadingIndicator = delegate?.initialLoadingIndicator() else {
return
}
@ -144,122 +135,100 @@ final public class PaginationWrapper<Cursor: ResettableRxDataSourceCursor, Deleg
private func onLoadingMoreState(afterState: LoadingState) {
if case .error = afterState { // user tap retry button in table footer
uiDelegate?.footerRetryViewWillDisappear()
delegate?.retryLoadMoreButtonIsAboutToHide()
wrappedView.footerView = nil
addInfiniteScroll(withHandler: false)
wrappedView.scrollView.beginInfiniteScroll(true)
}
}
private func onResultsState(newItems: DataLoadingModel.ResultType,
private func onResultsState(newItems: [Cursor.Element],
from cursor: Cursor,
afterState: LoadingState) {
wrappedView.scrollView.isUserInteractionEnabled = true
if case .initialLoading = afterState {
if case .loading = afterState {
delegate?.paginationWrapper(didReload: newItems, using: cursor)
removeAllPlaceholderView()
removeCurrentPlaceholderView()
wrappedView.scrollView.refreshControl?.endRefreshing()
wrappedView.scrollView.support.refreshControl?.endRefreshing()
addInfiniteScroll(withHandler: true)
} else if case .loadingMore = afterState {
delegate?.paginationWrapper(didLoad: newItems, using: cursor)
removeAllPlaceholderView()
addInfiniteScrollWithHandler()
readdInfiniteScrollWithHandler()
}
}
private func onErrorState(error: Error, afterState: LoadingState) {
if case .initialLoading = afterState {
if case .loading = afterState {
defer {
wrappedView.scrollView.refreshControl?.endRefreshing()
wrappedView.scrollView.support.refreshControl?.endRefreshing()
}
delegate?.clearData()
let customErrorHandling = uiDelegate?.customInitialLoadingErrorHandling(for: error) ?? false
guard !customErrorHandling, let errorView = uiDelegate?.errorPlaceholder(for: error) else {
guard let errorView = delegate?.errorPlaceholder(for: error) else {
return
}
replacePlaceholderViewIfNeeded(with: errorView)
} else {
guard let retryView = uiDelegate?.footerRetryView(),
let retryViewHeight = uiDelegate?.footerRetryViewHeight() else {
removeInfiniteScroll()
delegate?.clearView()
} else if case .loadingMore = afterState {
removeInfiniteScroll()
guard let retryButton = delegate?.retryLoadMoreButton(),
let retryButtonHeigth = delegate?.retryLoadMoreButtonHeight() else {
return
}
retryView.frame = CGRect(x: 0, y: 0, width: wrappedView.scrollView.bounds.width, height: retryViewHeight)
retryView.button.addTarget(self, action: #selector(retryEvent), for: .touchUpInside)
retryButton.frame = CGRect(x: 0, y: 0, width: wrappedView.scrollView.bounds.width, height: retryButtonHeigth)
uiDelegate?.footerRetryViewWillAppear()
removeInfiniteScroll { scrollView in
self.wrappedView.footerView = retryView
let shouldUpdateContentOffset = Int(scrollView.contentOffset.y + retryViewHeight) >= Int(self.bottom)
if shouldUpdateContentOffset {
let newContentOffset = CGPoint(x: 0, y: scrollView.contentOffset.y + retryViewHeight)
scrollView.setContentOffset(newContentOffset, animated: true)
if #available(iOS 13, *) {
scrollView.setContentOffset(newContentOffset, animated: true)
}
retryButton.rx.controlEvent(.touchUpInside)
.bind { [weak self] in
self?.paginationViewModel.loadMore()
}
}
}
}
.disposed(by: disposeBag)
@objc private func retryEvent() {
paginationViewModel.loadMore()
delegate?.retryLoadMoreButtonIsAboutToShow()
wrappedView.footerView = retryButton
}
}
private func onEmptyState() {
defer {
wrappedView.scrollView.refreshControl?.endRefreshing()
wrappedView.scrollView.support.refreshControl?.endRefreshing()
}
delegate?.clearData()
guard let emptyView = uiDelegate?.emptyPlaceholder() else {
guard let emptyView = delegate?.emptyPlaceholder() else {
return
}
replacePlaceholderViewIfNeeded(with: emptyView)
}
private func replacePlaceholderViewIfNeeded(with placeholderView: UIView) {
wrappedView.scrollView.isUserInteractionEnabled = true
removeAllPlaceholderView()
removeCurrentPlaceholderView()
placeholderView.translatesAutoresizingMaskIntoConstraints = false
placeholderView.isHidden = false
// I was unable to add pull-to-refresh placeholder scroll behaviour without this trick
let placeholderWrapperView = UIView()
placeholderWrapperView.addSubview(placeholderView)
let wrapperView = UIView()
wrapperView.addSubview(placeholderView)
let leadingConstraint = placeholderView.leadingAnchor.constraint(equalTo: placeholderWrapperView.leadingAnchor)
let trailingConstraint = placeholderView.trailingAnchor.constraint(equalTo: placeholderWrapperView.trailingAnchor)
let topConstraint = placeholderView.topAnchor.constraint(equalTo: placeholderWrapperView.topAnchor)
let bottomConstraint = placeholderView.bottomAnchor.constraint(equalTo: placeholderWrapperView.bottomAnchor)
let leadingConstraint = placeholderView.leadingAnchor.constraint(equalTo: wrapperView.leadingAnchor)
let trailingConstraint = placeholderView.trailingAnchor.constraint(equalTo: wrapperView.trailingAnchor)
let topConstraint = placeholderView.topAnchor.constraint(equalTo: wrapperView.topAnchor)
let bottomConstraint = placeholderView.bottomAnchor.constraint(equalTo: wrapperView.bottomAnchor)
NSLayoutConstraint.activate([
leadingConstraint,
trailingConstraint,
topConstraint,
bottomConstraint
])
wrapperView.addConstraints([leadingConstraint, trailingConstraint, topConstraint, bottomConstraint])
currentPlaceholderViewTopConstraint = topConstraint
wrappedView.backgroundView = placeholderWrapperView
wrappedView.backgroundView = wrapperView
currentPlaceholderView = placeholderView
}
@ -268,10 +237,9 @@ final public class PaginationWrapper<Cursor: ResettableRxDataSourceCursor, Deleg
private func onExhaustedState() {
removeInfiniteScroll()
removeAllPlaceholderView()
}
private func addInfiniteScrollWithHandler() {
private func readdInfiniteScrollWithHandler() {
removeInfiniteScroll()
addInfiniteScroll(withHandler: true)
}
@ -285,77 +253,85 @@ final public class PaginationWrapper<Cursor: ResettableRxDataSourceCursor, Deleg
wrappedView.scrollView.addInfiniteScroll { _ in }
}
wrappedView.scrollView.infiniteScrollIndicatorView = uiDelegate?.loadingMoreIndicator()?.view
wrappedView.scrollView.infiniteScrollIndicatorView = delegate?.loadingMoreIndicator().view
}
private func removeInfiniteScroll(with completion: FinishInfiniteScrollCompletion? = nil) {
wrappedView.scrollView.finishInfiniteScroll(completion: completion)
private func removeInfiniteScroll() {
wrappedView.scrollView.finishInfiniteScroll()
wrappedView.scrollView.removeInfiniteScroll()
}
private func createRefreshControl() {
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(refreshAction), for: .valueChanged)
refreshControl.rx.controlEvent(.valueChanged)
.bind { [weak self] in
self?.reload()
}
.disposed(by: disposeBag)
wrappedView.scrollView.refreshControl = refreshControl
}
@objc private func refreshAction() {
// it is implemented the combined behavior of `touchUpInside` and `touchUpOutside` using `CFRunLoopPerformBlock`,
// which `UIRefreshControl` does not support
CFRunLoopPerformBlock(CFRunLoopGetMain(), CFRunLoopMode.defaultMode.rawValue) { [weak self] in
self?.reload()
}
wrappedView.scrollView.support.setRefreshControl(refreshControl)
}
private func removeRefreshControl() {
wrappedView.scrollView.refreshControl = nil
wrappedView.scrollView.support.setRefreshControl(nil)
}
private func bindViewModelStates() {
typealias State = PaginationLoadingViewModel<Cursor>.LoadingStateType
paginationViewModel.stateDriver
.drive(stateChanged)
.flatMap { [applicationCurrentyActive] state -> Driver<State> in
if applicationCurrentyActive.value {
return .just(state)
} else {
return applicationCurrentyActive
.asObservable()
.filter { $0 }
.delay(0.5, scheduler: MainScheduler.instance)
.asDriver(onErrorJustReturn: true)
.map { _ in state }
}
}
.drive(onNext: { [weak self] state in
switch state {
case .initial:
self?.onInitialState()
case .loading(let after):
self?.onLoadingState(afterState: after)
case .loadingMore(let after):
self?.onLoadingMoreState(afterState: after)
case .results(let newItems, let from, let after):
self?.onResultsState(newItems: newItems, from: from, afterState: after)
case .error(let error, let after):
self?.onErrorState(error: error, afterState: after)
case .empty:
self?.delegate?.clearView()
self?.onEmptyState()
case .exhausted:
self?.onExhaustedState()
}
})
.disposed(by: disposeBag)
}
private func removeAllPlaceholderView() {
private func removeCurrentPlaceholderView() {
wrappedView.backgroundView = nil
wrappedView.footerView = nil
}
}
private extension PaginationWrapper {
private var stateChanged: Binder<LoadingState> {
Binder(self) { base, value in
switch value {
case .initial:
base.onInitialState()
case let .initialLoading(after):
base.onLoadingState(afterState: after)
case let .loadingMore(after):
base.onLoadingMoreState(afterState: after)
case let .results(newItems, from, after):
base.onResultsState(newItems: newItems, from: from, afterState: after)
case let .error(error, after):
base.onErrorState(error: error, afterState: after)
case .empty:
base.onEmptyState()
case .exhausted:
base.onExhaustedState()
}
}
}
var scrollOffsetChanged: Binder<CGPoint> {
Binder(self) { base, value in
base.currentPlaceholderViewTopConstraint?.constant = -value.y
}
}
private func bindAppStateNotifications() {
let notificationCenter = NotificationCenter.default.rx
notificationCenter.notification(.UIApplicationWillResignActive)
.subscribe(onNext: { [weak self] _ in
self?.applicationCurrentyActive.value = false
})
.disposed(by: disposeBag)
notificationCenter.notification(.UIApplicationDidBecomeActive)
.subscribe(onNext: { [weak self] _ in
self?.applicationCurrentyActive.value = true
})
.disposed(by: disposeBag)
}
}

View File

@ -1,202 +0,0 @@
//
// Copyright (c) 2019 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import TableKit
import RxSwift
import UIKit
public typealias SearchResultsController = UIViewController & SearchResultsViewController
/// Class that allows to enter text for search and then displays search results in table view
open class BaseSearchViewController < Item,
ItemViewModel,
ViewModel,
CustomView: UIView & TableViewHolder>: BaseCustomViewController<ViewModel, CustomView>
where ViewModel: BaseSearchViewModel<Item, ItemViewModel> {
// MARK: - Properties
private let disposeBag = DisposeBag()
private let searchResultsController: SearchResultsController
private let searchController: UISearchController
private var didEnterText = false
// MARK: - Initialization
public init(viewModel: ViewModel, searchResultsController: SearchResultsController) {
self.searchResultsController = searchResultsController
self.searchController = UISearchController(searchResultsController: searchResultsController)
super.init(viewModel: viewModel)
initialLoadView()
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Configurable Controller
open override func bindViews() {
super.bindViews()
viewModel.itemsViewModelsDriver
.drive(with: self) { owner, viewModels in
owner.handle(itemViewModels: viewModels)
}
.disposed(by: disposeBag)
Observable.merge(searchResults, resetResults)
.subscribe(with: self) { owner, state in
owner.handle(searchResultsState: state)
}
.disposed(by: disposeBag)
let searchText = searchController.searchBar.rx.text
.changed
.do(onNext: { [weak self] text in
self?.handle(searchText: text)
})
.map { $0 ?? "" }
viewModel.bind(searchText: searchText)
.disposed(by: disposeBag)
}
open override func addViews() {
super.addViews()
if #available(iOS 11.0, *) {
navigationItem.searchController = searchController
} else {
customView.tableView.tableHeaderView = searchController.searchBar
}
searchController.view.addSubview(statusBarView)
}
open override func configureAppearance() {
super.configureAppearance()
definesPresentationContext = true
configureSearchBarAppearance(searchController.searchBar)
customView.tableView.tableHeaderView?.backgroundColor = searchBarColor
}
open override func localize() {
super.localize()
searchController.searchBar.placeholder = searchBarPlaceholder
}
// MARK: - Search Controller Functionality
open func createRows(from itemsViewModels: [ItemViewModel]) -> [Row] {
assertionFailure("createRows(from:) has not been implemented")
return []
}
open var searchBarPlaceholder: String {
""
}
open var searchBarColor: UIColor {
.gray
}
open var statusBarView: UIView {
let statusBarSize = statusBarFrame().size
let statusBarView = UIView(frame: CGRect(x: 0,
y: 0,
width: statusBarSize.width,
height: statusBarSize.height))
statusBarView.backgroundColor = statusBarColor
return statusBarView
}
open var statusBarColor: UIColor {
.black
}
open func updateContent(with viewModels: [ItemViewModel]) {
// override in subclass
}
open func stateForUpdate(with viewModels: [ItemViewModel]) -> SearchResultsViewControllerState {
let rows = createRows(from: viewModels)
return .rowsContent(rows: rows)
}
open var resetResults: Observable<SearchResultsViewControllerState> {
searchController.rx.willPresent
.map { SearchResultsViewControllerState.initial }
}
open var searchResults: Observable<SearchResultsViewControllerState> {
viewModel.searchResultsDriver
.asObservable()
.compactMap { [weak self] viewModels -> SearchResultsViewControllerState? in
self?.stateForUpdate(with: viewModels)
}
}
// MARK: - Helpers
open func handle(itemViewModels viewModels: [ItemViewModel]) {
updateContent(with: viewModels)
}
open func handle(searchResultsState state: SearchResultsViewControllerState) {
searchResultsController.update(for: state)
}
open func handle(searchText: String?) {
setTableViewInsets()
}
private func setTableViewInsets() {
guard !didEnterText else {
return
}
didEnterText = true
searchResultsController.searchResultsView.tableView.contentInset = tableViewInsets
searchResultsController.searchResultsView.tableView.scrollIndicatorInsets = tableViewInsets
}
open func statusBarFrame() -> CGRect {
/// override in subclass
return .zero
}
open func configureSearchBarAppearance(_ searchBar: UISearchBar) {
// override in subclass
}
}
extension BaseSearchViewController {
open var tableViewInsets: UIEdgeInsets {
let searchBarHeight = searchController.searchBar.frame.height
let statusBarHeight = statusBarFrame().height
return UIEdgeInsets(top: searchBarHeight + statusBarHeight,
left: 0,
bottom: 0,
right: 0)
}
}

View File

@ -1,100 +0,0 @@
//
// Copyright (c) 2019 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import RxSwift
import RxCocoa
/// ViewModel that loads data from a given data source and performs search among results
open class BaseSearchViewModel<Item, ItemViewModel>: GeneralDataLoadingViewModel<[Item]> {
public typealias ItemsList = [Item]
private let searchTextRelay = BehaviorRelay(value: "")
public init(dataSource: Single<ItemsList>) {
super.init(dataSource: dataSource, emptyResultChecker: { $0.isEmpty })
}
open var itemsViewModelsDriver: Driver<[ItemViewModel]> {
loadingResultObservable
.compactMap { [weak self] items in
self?.viewModels(from: items)
}
.flatMap { Observable.from(optional: $0) }
.share(replay: 1, scope: .forever)
.asDriver(onErrorDriveWith: .empty())
}
open var searchDebounceInterval: RxTimeInterval {
.seconds(1)
}
open var searchResultsDriver: Driver<[ItemViewModel]> {
searchTextRelay.debounce(searchDebounceInterval, scheduler: MainScheduler.instance)
.withLatestFrom(loadingResultObservable) { ($0, $1) }
.flatMapLatest { [weak self] searchText, items -> Observable<ItemsList> in
self?.search(by: searchText, from: items).asObservable() ?? .empty()
}
.compactMap { [weak self] items in
self?.viewModels(from: items)
}
.flatMap { Observable.from(optional: $0) }
.share(replay: 1, scope: .forever)
.asDriver(onErrorDriveWith: .empty())
}
open func viewModel(from item: Item) -> ItemViewModel {
fatalError("viewModel(from:) has not been implemented")
}
open func search(by searchString: String, from items: ItemsList) -> Single<ItemsList> {
fatalError("searchEngine(for:) has not been implemented")
}
open func bind(searchText: Observable<String>) -> Disposable {
searchText.bind(to: searchTextRelay)
}
private func viewModels(from items: ItemsList) -> [ItemViewModel] {
items.map { self.viewModel(from: $0) }
}
open var loadingResultObservable: Observable<ResultType> {
loadingStateDriver
.asObservable()
.map { $0.result }
.flatMap { Observable.from(optional: $0) }
}
open var loadingErrorObservable: Observable<Error> {
loadingStateDriver
.asObservable()
.map { $0.error }
.flatMap { Observable.from(optional: $0) }
}
open var firstLoadingResultObservable: Single<ResultType> {
loadingResultObservable
.take(1)
.asSingle()
}
}

View File

@ -0,0 +1,72 @@
//
// Copyright (c) 2017 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
/// Date formatting service. Used for date to string and string to date formatting.
/// Takes into account locale and timezone.
open class DateFormattingService {
private var dateFormatters: [DateFormattingArguments: DateFormatter] = [:]
public init(formattingArguments: [DateFormattingArguments] = []) {
for argument in formattingArguments {
register(arguments: argument)
}
}
/// DateFormatter registration method.
///
/// - Parameter arguments: A formatting arguments structure.
public func register(arguments: DateFormattingArguments) {
dateFormatters[arguments] = arguments.dateFormatter
}
/// String to date convertion method.
///
/// - Parameters:
/// - dateString: The string to parse.
/// - arguments: A formatting arguments structure.
/// - Returns: A date representation of a given string interpreted using given arguments.
public func date(from dateString: String, arguments: DateFormattingArguments) -> Date? {
guard let dateFormatter = dateFormatters[arguments] else {
fatalError("No date formatter registered for given arguments: \(arguments)")
}
return dateFormatter.date(from: dateString)
}
/// Date to string convertion method.
///
/// - Parameters:
/// - date: The date to format.
/// - arguments: A formatting arguments structure.
/// - Returns: A string representation of a given date formatted using given arguments.
public func string(from date: Date, arguments: DateFormattingArguments) -> String? {
guard let dateFormatter = dateFormatters[arguments] else {
fatalError("No date formatter registered for given arguments: \(arguments)")
}
return dateFormatter.string(from: date)
}
}

View File

@ -23,118 +23,72 @@
import RxSwift
import RxCocoa
import Alamofire
import ObjectMapper
import RxAlamofire
/// Base network service implementation build on top of LeadKit extensions for Alamofire.
/// Has an ability to automatically show / hide network activity indicator
open class NetworkService {
/// Enable synchronization for setting behaviour relay from different thread
/// Enable synchronization for setting variable from different thread
private let lock = NSRecursiveLock()
private let requestCountRelay = BehaviorRelay(value: 0)
private let requestCountVariable = Variable<Int>(0)
public let configuration: NetworkServiceConfiguration
public let sessionManager: SessionManager
public let sessionManager: Alamofire.SessionManager
/// Driver that emits true when active requests count != 0 and false otherwise.
public var isActivityIndicatorVisibleDriver: Driver<Bool> {
requestCountRelay.asDriver().map { $0 != 0 }.distinctUntilChanged()
private let acceptableStatusCodes: [Int]
var requestCount: Driver<Int> {
return requestCountVariable.asDriver()
}
/// - Parameter sessionManager: Alamofire.SessionManager to use for requests
/// Creates new instance of NetworkService with given Alamofire session manager
///
/// - Parameters:
/// - configuration: instance of NetworkServiceConfiguration to configure network service.
public init(configuration: NetworkServiceConfiguration) {
/// - Parameter sessionManager: Alamofire.SessionManager to use for requests
public init(sessionManager: Alamofire.SessionManager,
acceptableStatusCodes: [Int] = Alamofire.SessionManager.defaultAcceptableStatusCodes) {
self.configuration = configuration
self.sessionManager = configuration.sessionManager
self.sessionManager = sessionManager
self.acceptableStatusCodes = acceptableStatusCodes
}
/// Perform reactive request to get mapped ObservableMappable model and http response
///
/// - Parameters:
/// - parameters: api parameters to pass to Alamofire
/// - additionalValidStatusCodes: set of additional valid status codes
/// - decoder: json decoder to decode response data
/// - Parameter parameters: api parameters to pass Alamofire
/// - Returns: Observable of tuple containing (HTTPURLResponse, ObservableMappable)
public func rxObservableRequest<T: ObservableMappable>(with parameters: ApiRequestParameters,
additionalValidStatusCodes: Set<Int> = [],
decoder: JSONDecoder = JSONDecoder())
-> Observable<SessionManager.ModelResponse<T>> {
public func rxRequest<T: ObservableMappable>(with parameters: ApiRequestParameters)
-> Observable<(response: HTTPURLResponse, model: T)> where T.ModelType == T {
sessionManager.rx.responseObservableModel(requestParameters: parameters,
additionalValidStatusCodes: additionalValidStatusCodes,
decoder: decoder)
.counterTracking(for: self)
return sessionManager.rx.responseObservableModel(requestParameters: parameters,
acceptableStatusCodes: acceptableStatusCodes)
.counterTracking(for: self)
}
/// Perform reactive request to get mapped ImmutableMappable model and http response
///
/// - Parameters:
/// - parameters: api parameters to pass to Alamofire
/// - additionalValidStatusCodes: set of additional valid status codes
/// - decoder: json decoder to decode response data
/// - Parameter parameters: api parameters to pass Alamofire
/// - Returns: Observable of tuple containing (HTTPURLResponse, ImmutableMappable)
public func rxRequest<T: Decodable>(with parameters: ApiRequestParameters,
additionalValidStatusCodes: Set<Int> = [],
decoder: JSONDecoder = JSONDecoder())
-> Observable<SessionManager.ModelResponse<T>> {
public func rxRequest<T: ImmutableMappable>(with parameters: ApiRequestParameters)
-> Observable<(response: HTTPURLResponse, model: T)> {
sessionManager.rx.responseModel(requestParameters: parameters,
additionalValidStatusCodes: additionalValidStatusCodes,
decoder: decoder)
.counterTracking(for: self)
return sessionManager.rx.responseModel(requestParameters: parameters,
acceptableStatusCodes: acceptableStatusCodes)
.counterTracking(for: self)
}
/// Perform reactive request to get data and http response
///
/// - Parameters:
/// - parameters: api parameters to pass to Alamofire
/// - additionalValidStatusCodes: set of additional valid status codes
/// - Returns: Observable of tuple containing (HTTPURLResponse, Data)
public func rxDataRequest(with parameters: ApiRequestParameters, additionalValidStatusCodes: Set<Int> = [])
-> Observable<SessionManager.DataResponse> {
sessionManager.rx.responseData(requestParameters: parameters,
additionalValidStatusCodes: additionalValidStatusCodes)
.counterTracking(for: self)
}
/// Perform reactive request to upload data and get Observable model and http response
///
/// - Parameters:
/// - parameters: api upload parameters to pass Alamofire
/// - additionalValidStatusCodes: set of additional valid status codes
/// - decoder: json decoder to decode response data
/// - Returns: Observable of model response
public func rxUploadRequest<T: Decodable>(with parameters: ApiUploadRequestParameters,
additionalValidStatusCodes: Set<Int> = [],
decoder: JSONDecoder = JSONDecoder())
-> Observable<SessionManager.ModelResponse<T>> {
sessionManager.rx.uploadResponseModel(requestParameters: parameters,
additionalValidStatusCodes: additionalValidStatusCodes,
decoder: decoder)
.counterTracking(for: self)
}
}
private extension NetworkService {
func increaseRequestCounter() {
fileprivate func increaseRequestCounter() {
lock.lock()
requestCountRelay.accept(requestCountRelay.value + 1)
requestCountVariable.value += 1
lock.unlock()
}
func decreaseRequestCounter() {
fileprivate func decreaseRequestCounter() {
lock.lock()
requestCountRelay.accept(requestCountRelay.value - 1)
requestCountVariable.value -= 1
lock.unlock()
}
}
public extension Observable {
@ -144,11 +98,12 @@ public extension Observable {
///
/// - Parameter networkService: NetworkService to operate on it
/// - Returns: The source sequence with the side-effecting behavior applied.
func counterTracking(for networkService: NetworkService) -> Observable<Observable.Element> {
`do`(onSubscribe: {
func counterTracking(for networkService: NetworkService) -> Observable<Observable.E> {
return `do`(onSubscribe: {
networkService.increaseRequestCounter()
}, onDispose: {
networkService.decreaseRequestCounter()
})
}
}

View File

@ -1,77 +0,0 @@
//
// Copyright (c) 2018 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Alamofire
/// Session Manager stored in NetworkService
open class SessionManager: Alamofire.Session {
/// Response with HTTP URL Response and target object
public typealias ModelResponse<T> = (response: HTTPURLResponse, model: T)
/// Response with HTTP URL Response and data
public typealias DataResponse = (response: HTTPURLResponse, data: Data)
/// Acceptable status codes for validation
public let acceptableStatusCodes: Set<Int>
/// Dispatch Queue on which mapping is performed
public let mappingQueue: DispatchQueue
public init(configuration: URLSessionConfiguration,
serverTrustManager: ServerTrustManager,
acceptableStatusCodes: Set<Int>,
mappingQueue: DispatchQueue) {
self.acceptableStatusCodes = acceptableStatusCodes
self.mappingQueue = mappingQueue
let delegate = SessionDelegate()
let delegateQueue = OperationQueue()
delegateQueue.underlyingQueue = mappingQueue
let session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: delegateQueue)
super.init(session: session,
delegate: delegate,
rootQueue: mappingQueue,
serverTrustManager: serverTrustManager)
}
public init(session: URLSession,
delegate: SessionDelegate,
serverTrustManager: ServerTrustManager,
acceptableStatusCodes: Set<Int>,
mappingQueue: DispatchQueue) {
self.acceptableStatusCodes = acceptableStatusCodes
self.mappingQueue = mappingQueue
session.delegateQueue.underlyingQueue = mappingQueue
super.init(session: session,
delegate: delegate,
rootQueue: mappingQueue,
serverTrustManager: serverTrustManager)
}
}

View File

@ -1,44 +0,0 @@
//
// Copyright (c) 2020 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import RxCocoa
import RxSwift
open class BaseTappableViewModel<PayloadType> {
private let tapRelay = PublishRelay<PayloadType>()
public var tapDriver: Driver<PayloadType> {
tapRelay.asDriver(onErrorDriveWith: .empty())
}
public var tapObservable: Observable<PayloadType> {
tapRelay.asObservable()
}
public func bind(tapObservable: Observable<PayloadType>) -> Disposable {
tapObservable.bind(to: tapRelay)
}
public func tap(payload: PayloadType) {
tapRelay.accept(payload)
}
}

View File

@ -1,27 +0,0 @@
//
// Copyright (c) 2020 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
open class VoidTappableViewModel: BaseTappableViewModel<Void> {
public func tap() {
tap(payload: ())
}
}

View File

@ -1,77 +0,0 @@
//
// Copyright (c) 2018 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import UIKit
/// Placeholder view visual attributes without layout.
open class BasePlaceholderViewModel {
/// Title text with text attributes.
public let title: ViewText
/// Description text with text attributes.
public let description: ViewText?
/// Center image of placeholder.
public let centerImage: UIImage?
/// Button title with text attributes.
public let buttonTitle: ViewText?
/// Placeholder background.
public let background: ViewBackground
/// Memberwise initializer.
///
/// - Parameters:
/// - title: Title text with text attributes.
/// - description: Description text with text attributes.
/// - centerImage: Center image of placeholder.
/// - buttonTitle: Button title with text attributes.
/// - background: Placeholder background.
public init(title: ViewText,
description: ViewText? = nil,
centerImage: UIImage? = nil,
buttonTitle: ViewText? = nil,
background: ViewBackground = .color(.clear)) {
self.title = title
self.description = description
self.centerImage = centerImage
self.buttonTitle = buttonTitle
self.background = background
}
}
public extension BasePlaceholderViewModel {
/// Returns true if description is not nil.
var hasDescription: Bool {
description != nil
}
/// Returns true buttonTitle is not nil.
var hasButton: Bool {
buttonTitle != nil
}
/// Returns true if centerImage is not nil.
var hasCenterImage: Bool {
centerImage != nil
}
}

View File

@ -1,104 +0,0 @@
//
// Copyright (c) 2018 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import UIKit
/// Layoutless placeholder view. This class is used as views holder & configurator.
/// You should inherit it and implement layout.
open class BasePlaceholderView: ButtonHolderView, InitializableView {
/// Title label of placeholder view.
public let titleLabel = UILabel()
/// Description label of placeholder view.
public let descriptionLabel = UILabel()
/// Center image view of placeholder view.
public let centerImageView = UIImageView()
/// Action button of placeholder view.
public private(set) lazy var button = createButton()
/// Background image view of placeholder view.
public let backgroundImageView = UIImageView()
public override init(frame: CGRect) {
super.init(frame: frame)
initializeView()
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Subclass override
/// Override to create your own subclass button.
///
/// - Returns: UIButton (sub)class.
open func createButton() -> UIButton {
UIButton()
}
// MARK: - InitializableView
open func addViews() {
// override in subclass
}
open func bindViews() {
// override in subclass
}
open func configureAppearance() {
// override in subclass
}
open func localize() {
// override in subclass
}
open func configureLayout() {
// override in subclass
}
}
public extension BasePlaceholderView {
/// Method for base configuration BasePlaceholderView instance.
///
/// - Parameter viewModel: Placeholder view visual attributes without layout.
func baseConfigure(with viewModel: BasePlaceholderViewModel) {
titleLabel.configure(with: viewModel.title)
descriptionLabel.isHidden = !viewModel.hasDescription
viewModel.description?.configure(view: descriptionLabel)
centerImageView.isHidden = !viewModel.hasCenterImage
centerImageView.image = viewModel.centerImage
viewModel.background.configure(backgroundView: self,
backgroundImageView: backgroundImageView)
button.isHidden = !viewModel.hasButton
viewModel.buttonTitle?.configure(view: button)
}
}

View File

@ -1,73 +0,0 @@
//
// Copyright (c) 2020 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import RxSwift
open class BaseRxTableViewCell: UITableViewCell, InitializableView, DisposeBagHolder {
// MARK: - Properties
public var disposeBag = DisposeBag()
// MARK: - Initialization
override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: .default, reuseIdentifier: reuseIdentifier)
initializeView()
}
@available(*, unavailable)
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
// MARK: - Override
override open func prepareForReuse() {
super.prepareForReuse()
disposeBag = DisposeBag()
}
// MARK: - InitializableView
open func addViews() {
// overriding
}
open func bindViews() {
// overriding
}
open func configureLayout() {
// overriding
}
open func configureAppearance() {
selectionStyle = .none
}
open func localize() {
// overriding
}
}

View File

@ -1,47 +0,0 @@
//
// Copyright (c) 2018 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import UIKit
/// The main purpose of this class is to fix empty space on top of the screen
/// when view controller view is UICollectionView.
open class CollectionViewWrapperView: ScrollViewHolderView, CollectionViewHolder {
/// Contained collection view.
public let collectionView: UICollectionView
/// Initializer with collection view layout parameter.
///
/// - Parameter layout: UICollectionViewLayout to pass in UICollectionView init.
public init(layout: UICollectionViewLayout) {
self.collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
self.collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
super.init(frame: .zero)
addSubview(collectionView)
}
public required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -1,79 +0,0 @@
//
// Copyright (c) 2020 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import RxSwift
import TableKit
open class ContainerTableCell<TView: UIView>: BaseRxTableViewCell, ConfigurableCell where TView: ConfigurableView {
// MARK: - Properties
private let wrappedView = TView()
open var shouldConfigureDefaultConstraints: Bool {
true
}
open var contentInsets: UIEdgeInsets {
.zero
}
open var contentViewBackgroundColor: UIColor {
.clear
}
// MARK: - ConfigurableCell
open func configure(with viewModel: TView.ViewModelType) {
disposeBag = DisposeBag()
wrappedView.configure(with: viewModel)
}
// MARK: - InitializableView
override open func addViews() {
super.addViews()
contentView.addSubview(wrappedView)
}
override open func configureLayout() {
super.configureLayout()
if shouldConfigureDefaultConstraints {
wrappedView.snp.makeConstraints {
$0.edges.equalToSuperview().inset(contentInsets)
}
} else {
configureCustomConstraints(forWrappedView: wrappedView)
}
}
override open func configureAppearance() {
super.configureAppearance()
contentView.backgroundColor = contentViewBackgroundColor
backgroundColor = contentViewBackgroundColor
}
open func configureCustomConstraints(forWrappedView view: TView) { }
}

View File

@ -1,109 +0,0 @@
//
// Copyright (c) 2019 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import UIKit
import RxCocoa
import RxSwift
/// class that is a CustomizableButtonView subview and gives it a button functionality
open class CustomizableButton: UIButton {
// MARK: - Constants
private let defaultBackgroundColor = UIColor.white
// MARK: - Background
private var backgroundColors: [UIControl.State: UIColor] = [:] {
didSet {
updateBackgroundColor()
}
}
func set(backgroundColors: [UIControl.State: UIColor]) {
backgroundColors.forEach { setBackgroundColor($1, for: $0) }
}
func setBackgroundColor(_ color: UIColor, for state: UIControl.State) {
backgroundColors[state] = color
}
func backgroundColor(for state: UIControl.State) -> UIColor? {
backgroundColors[state]
}
private func updateBackgroundColor() {
if isEnabled {
if isHighlighted {
updateBackgroundColor(to: .highlighted)
} else {
updateBackgroundColor(to: .normal)
}
} else {
updateBackgroundColor(to: .disabled)
}
}
private func updateBackgroundColor(to state: UIControl.State) {
if let stateColor = backgroundColor(for: state) {
backgroundColor = stateColor
} else if state != .normal, let normalStateColor = backgroundColor(for: .normal) {
backgroundColor = normalStateColor
} else {
backgroundColor = defaultBackgroundColor
}
}
// MARK: - Title
func set(titleColors: [UIControl.State: UIColor]) {
titleColors.forEach { setTitleColor($1, for: $0) }
}
func set(titles: [UIControl.State: String]) {
titles.forEach { setTitle($1, for: $0) }
}
func set(attributtedTitles: [UIControl.State: NSAttributedString]) {
attributtedTitles.forEach { setAttributedTitle($1, for: $0) }
}
// MARK: - Images
func set(images: [UIControl.State: UIImage]) {
images.forEach { setImage($1, for: $0) }
}
// MARK: - State
override open var isEnabled: Bool {
didSet {
updateBackgroundColor()
}
}
override open var isHighlighted: Bool {
didSet {
updateBackgroundColor()
}
}
}

View File

@ -1,342 +0,0 @@
//
// Copyright (c) 2019 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import RxSwift
public typealias Spinner = UIView & Animatable
public struct CustomizableButtonState: OptionSet {
// MARK: - OptionSet conformance
public let rawValue: Int
public init(rawValue: Int) {
self.rawValue = rawValue
}
// MARK: - States
public static let highlighted = CustomizableButtonState(rawValue: 1 << 1)
public static let normal = CustomizableButtonState(rawValue: 1 << 2)
public static let enabled = CustomizableButtonState(rawValue: 1 << 3)
public static let disabled = CustomizableButtonState(rawValue: 1 << 4)
public static let loading = CustomizableButtonState(rawValue: 1 << 5)
// MARK: - Properties
public var isLoading: Bool {
contains(.loading)
}
}
/// container class that acts like a button and provides great customization
open class CustomizableButtonView: UIView, InitializableView, ConfigurableView {
// MARK: - Stored Properties
public private(set) var disposeBag = DisposeBag()
private let button = CustomizableButton()
open var tapOnDisabledButton: VoidBlock?
public var shadowView = UIView() {
willSet {
shadowView.removeFromSuperview()
}
didSet {
insertSubview(shadowView, at: 0)
configureShadowViewConstraints()
}
}
public var spinnerView: Spinner? {
willSet {
removeSpinner()
}
didSet {
if spinnerView != nil {
addSpinner()
}
}
}
public var appearance = Appearance() {
didSet {
configureAppearance()
configureConstraints()
}
}
public var buttonTitle: String = "" {
willSet {
button.text = newValue
}
}
public var hidesLabelWhenLoading = false
// MARK: - Computed Properties
public var tapObservable: Observable<Void> {
button.rx.tap.asObservable()
}
override open var forFirstBaselineLayout: UIView {
button.forFirstBaselineLayout
}
override open var forLastBaselineLayout: UIView {
button.forLastBaselineLayout
}
override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if var touchPoint = touches.first?.location(in: self) {
touchPoint = convert(touchPoint, to: self)
if button.frame.contains(touchPoint) && !button.isEnabled {
tapOnDisabledButton?()
}
}
super.touchesBegan(touches, with: event)
}
// MARK: - Initialization
override public init(frame: CGRect) {
super.init(frame: frame)
initializeView()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initializeView()
}
// MARK: - UI
override open func layoutSubviews() {
super.layoutSubviews()
if shadowView.layer.cornerRadius == 0 {
shadowView.layer.shadowPath = UIBezierPath(rect: button.bounds).cgPath
}
}
private func set(active: Bool) {
if hidesLabelWhenLoading {
button.titleLabel?.layer.opacity = active ? 0 : 1
}
spinnerView?.isHidden = !active
if active {
spinnerView?.startAnimating()
} else {
spinnerView?.stopAnimating()
}
}
private func addSpinner() {
if let spinner = spinnerView {
addSubview(spinner)
configureSpinnerConstraints()
spinner.isHidden = true
}
}
private func removeSpinner() {
if spinnerView != nil {
self.spinnerView?.removeFromSuperview()
}
}
// MARK: - Layout
private func configureConstraints() {
button.pinToSuperview(with: appearance.buttonInsets)
configureShadowViewConstraints()
layoutIfNeeded()
}
private func configureSpinnerConstraints() {
guard let spinnerView = spinnerView else {
return
}
spinnerView.translatesAutoresizingMaskIntoConstraints = false
var constraints = [NSLayoutConstraint]()
switch appearance.spinnerPosition {
case .center:
constraints = [
spinnerView.centerXAnchor.constraint(equalTo: button.centerXAnchor),
spinnerView.centerYAnchor.constraint(equalTo: button.centerYAnchor)
]
case .leftToText(let offset):
if let buttonLabel = button.titleLabel {
constraints = [
spinnerView.centerYAnchor.constraint(equalTo: buttonLabel.centerYAnchor),
spinnerView.trailingAnchor.constraint(equalTo: buttonLabel.leadingAnchor, constant: -offset)
]
}
case .rightToText(let offset):
if let buttonLabel = button.titleLabel {
constraints = [
spinnerView.centerYAnchor.constraint(equalTo: buttonLabel.centerYAnchor),
spinnerView.leadingAnchor.constraint(equalTo: buttonLabel.trailingAnchor, constant: offset)
]
}
}
NSLayoutConstraint.activate(constraints)
}
private func configureShadowViewConstraints() {
shadowView.constraintToEdges(of: button, with: .zero)
}
// MARK: - Initializable View
open func addViews() {
addSubviews(shadowView, button)
}
open func configureAppearance() {
button.titleLabel?.numberOfLines = appearance.numberOfLines
button.titleLabel?.font = appearance.buttonFont
button.alpha = appearance.alpha
button.set(attributtedTitles: appearance.buttonStateAttributtedTitles)
button.set(titleColors: appearance.buttonTitleStateColors)
button.set(images: appearance.buttonStateIcons)
button.set(backgroundColors: appearance.buttonBackgroundStateColors)
let offset = appearance.buttonIconOffset
button.imageEdgeInsets = UIEdgeInsets(top: offset.vertical,
left: offset.horizontal,
bottom: -offset.vertical,
right: -offset.horizontal)
if let cornerRadius = appearance.buttonCornerRadius {
button.layer.cornerRadius = cornerRadius
} else {
button.layer.cornerRadius = 0
}
setNeedsDisplay()
}
open func configure(with viewModel: CustomizableButtonViewModel) {
disposeBag = DisposeBag()
viewModel.stateDriver.drive(stateBinder).disposed(by: disposeBag)
viewModel.bind(tapObservable: tapObservable).disposed(by: disposeBag)
button.text = viewModel.buttonTitle
appearance = viewModel.appearance
}
private var stateBinder: Binder<CustomizableButtonState> {
Binder(self) { base, value in
base.configureButton(withState: value)
base.onStateChange(value)
}
}
open func onStateChange(_ state: CustomizableButtonState) {
/// override in subclass
}
open func configureButton(withState state: CustomizableButtonState) {
button.isEnabled = ![.disabled, .loading].contains(state)
isUserInteractionEnabled = button.isEnabled
button.isHighlighted = state.contains(.highlighted) && !state.contains(.normal)
set(active: state.contains(.loading))
setNeedsDisplay()
}
}
private extension UIView {
func constraintToEdges(of view: UIView, with offset: UIEdgeInsets) {
translatesAutoresizingMaskIntoConstraints = false
let constraints = [
leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: offset.left),
trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: offset.right),
topAnchor.constraint(equalTo: view.topAnchor, constant: offset.top),
bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: offset.bottom)
]
NSLayoutConstraint.activate(constraints)
}
}
public extension CustomizableButtonView {
struct Appearance {
public var buttonFont: UIFont
public var buttonStateAttributtedTitles: [UIControl.State: NSAttributedString]
public var buttonTitleStateColors: [UIControl.State: UIColor]
public var buttonBackgroundStateColors: [UIControl.State: UIColor]
public var buttonStateIcons: [UIControl.State: UIImage]
public var buttonIconOffset: UIOffset
public var buttonInsets: UIEdgeInsets
public var buttonCornerRadius: CGFloat?
public var spinnerPosition: SpinnerPosition
public var numberOfLines: Int
public var alpha: CGFloat
public init(buttonFont: UIFont = .systemFont(ofSize: 15),
buttonStateAttributtedTitles: [UIControl.State: NSAttributedString] = [:],
buttonTitleStateColors: [UIControl.State: UIColor] = [:],
buttonBackgroundStateColors: [UIControl.State: UIColor] = [:],
buttonStateIcons: [UIControl.State: UIImage] = [:],
buttonIconOffset: UIOffset = .zero,
buttonInsets: UIEdgeInsets = .zero,
buttonCornerRadius: CGFloat? = nil,
spinnerPosition: SpinnerPosition = .center,
numberOfLines: Int = 0,
alpha: CGFloat = 1) {
self.buttonFont = buttonFont
self.buttonStateAttributtedTitles = buttonStateAttributtedTitles
self.buttonTitleStateColors = buttonTitleStateColors
self.buttonBackgroundStateColors = buttonBackgroundStateColors
self.buttonStateIcons = buttonStateIcons
self.buttonIconOffset = buttonIconOffset
self.buttonInsets = buttonInsets
self.buttonCornerRadius = buttonCornerRadius
self.spinnerPosition = spinnerPosition
self.numberOfLines = numberOfLines
self.alpha = alpha
}
}
enum SpinnerPosition {
case center
case leftToText(offset: CGFloat)
case rightToText(offset: CGFloat)
}
}
extension UIControl.State: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(Int(rawValue))
}
}

View File

@ -1,56 +0,0 @@
//
// Copyright (c) 2019 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import RxCocoa
import RxSwift
/// viewModel class for CustomizableButtonView configuration
open class CustomizableButtonViewModel {
public typealias Appearance = CustomizableButtonView.Appearance
private let stateRelay = BehaviorRelay(value: CustomizableButtonState.enabled)
private let tapRelay = BehaviorRelay(value: ())
public let appearance: Appearance
public let buttonTitle: String
public init(buttonTitle: String, appearance: Appearance) {
self.buttonTitle = buttonTitle
self.appearance = appearance
}
open var stateDriver: Driver<CustomizableButtonState> {
stateRelay.asDriver()
}
func bind(tapObservable: Observable<Void>) -> Disposable {
tapObservable.bind(to: tapRelay)
}
public var tapDriver: Driver<Void> {
tapRelay.asDriver()
}
public func updateState(with newState: CustomizableButtonState) {
stateRelay.accept(newState)
}
}

View File

@ -1,54 +0,0 @@
//
// Copyright (c) 2018 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import UIKit
internal final class TextPlaceholderView: UIView {
enum PlaceholderText: String {
case empty = "There is nothing here"
case error = "An error has occurred"
case loading = "Loading..."
case retry = "Retry"
case retryLoadMore = "Retry load more"
}
init(title: PlaceholderText) {
super.init(frame: .zero)
let label = UILabel()
label.text = title.rawValue
label.translatesAutoresizingMaskIntoConstraints = false
addSubview(label)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: centerXAnchor),
label.centerYAnchor.constraint(equalTo: centerYAnchor)
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -1,61 +0,0 @@
//
// Copyright (c) 2018 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import UIKit
internal final class TextWithButtonPlaceholder: UIView {
typealias TapHandler = () -> Void
private let tapHandler: TapHandler
init(title: TextPlaceholderView.PlaceholderText,
buttonTitle: TextPlaceholderView.PlaceholderText,
tapHandler: @escaping TapHandler) {
self.tapHandler = tapHandler
super.init(frame: .zero)
let textPlaceholder = TextPlaceholderView(title: title)
let button = UIButton(type: .custom)
button.backgroundColor = .lightGray
button.setTitle(buttonTitle.rawValue, for: .normal)
button.addTarget(self, action: #selector(buttonDidTapped(_:)), for: .touchUpInside)
let stackView = UIStackView(arrangedSubviews: [textPlaceholder, button])
stackView.axis = .vertical
addSubview(stackView)
stackView.setToCenter(withSize: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func buttonDidTapped(_ button: UIButton) {
tapHandler()
}
}

View File

@ -25,7 +25,7 @@ import TableKit
/// Empty cell class. Do not use it directly.
/// - see: `EmptyCellRow`
public final class EmptyCell: SeparatorCell, AppearanceConfigurable, ConfigurableCell {
public final class EmptyCell: SeparatorTableCell, AppearanceConfigurable, ConfigurableCell {
public struct Appearance {
let color: UIColor
@ -34,7 +34,7 @@ public final class EmptyCell: SeparatorCell, AppearanceConfigurable, Configurabl
}
}
public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
public override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
resetAppearance()
@ -61,4 +61,5 @@ public final class EmptyCell: SeparatorCell, AppearanceConfigurable, Configurabl
private func resetAppearance() {
configure(appearance: Appearance())
}
}

View File

@ -48,6 +48,7 @@ public final class EmptyCellRow: TableRow<EmptyCell> {
/// Used for set custom height to each cell, not for each cell type
override public var defaultHeight: CGFloat? {
rowHeight
return rowHeight
}
}

View File

@ -1,52 +0,0 @@
//
// Copyright (c) 2019 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
/// Default view model for LabelTableViewCell.
open class LabelCellViewModel {
public let viewText: ViewText
public let contentBackground: ViewBackground
public let contentInsets: UIEdgeInsets
public let labelInsets: UIEdgeInsets
public let separatorType: CellSeparatorType
/// Memberwise initializer.
///
/// - Parameters:
/// - viewText: View text to configure label.
/// - contentBackground: View background to configure background.
/// - contentInsets: Content insets to use for layout whole content.
/// - labelInsets: Label insets to use for layout label.
/// - separatorType: Separator type to use for separators.
public init(viewText: ViewText,
contentBackground: ViewBackground = .color(.clear),
contentInsets: UIEdgeInsets = .zero,
labelInsets: UIEdgeInsets = .zero,
separatorType: CellSeparatorType = .none) {
self.viewText = viewText
self.contentBackground = contentBackground
self.contentInsets = contentInsets
self.labelInsets = labelInsets
self.separatorType = separatorType
}
}

View File

@ -1,143 +0,0 @@
//
// Copyright (c) 2019 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import TableKit
import SnapKit
/// Label cell with separators, includes background image view.
open class LabelTableViewCell: SeparatorCell, InitializableView, ConfigurableCell {
// MARK: - Properties
private let label = UILabel()
private let backgroundImageView = UIImageView()
private let contentContainerView = UIView()
private var viewModel: LabelCellViewModel?
// MARK: - Init
public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
initializeView()
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initializeView()
}
open override func updateConstraints() {
let topSeparatorHeight = viewModel?.separatorType.topConfiguration?.totalHeight ?? 0
let bottomSeparatorHeight = viewModel?.separatorType.bottomConfiguration?.totalHeight ?? 0
contentContainerView.snp.remakeConstraints { make in
make.top.equalToSuperview().inset(contentInsets.top + topSeparatorHeight)
make.leading.equalToSuperview().inset(contentInsets.left)
make.trailing.equalToSuperview().inset(contentInsets.right)
make.bottom.equalToSuperview().inset(contentInsets.bottom + bottomSeparatorHeight)
}
label.snp.remakeConstraints { make in
make.edges.equalToSuperview().inset(labelInsets)
}
backgroundImageView.snp.remakeConstraints { make in
make.edges.equalToSuperview()
}
super.updateConstraints()
}
// MARK: - InitializableView
open func addViews() {
contentView.addSubview(contentContainerView)
contentContainerView.addSubviews(backgroundImageView, label)
}
open func configureAppearance() {
selectionStyle = .none
backgroundColor = .clear
contentView.backgroundColor = .clear
configureAppearance(of: label, backgroundImageView: backgroundImageView)
}
// MARK: - ConfigurableCell
public func configure(with viewModel: LabelCellViewModel) {
configureLabelCell(with: viewModel)
}
// MARK: - Private
private var labelInsets: UIEdgeInsets {
viewModel?.labelInsets ?? .zero
}
private var contentInsets: UIEdgeInsets {
viewModel?.contentInsets ?? .zero
}
// MARK: - Subclass methods to override
/// Callback for label and background image view appearance configuration.
///
/// - Parameters:
/// - label: Internal UILabel instance to configure.
/// - backgroundImageView: Internal UIImageView instance to configure.
open func configureAppearance(of label: UILabel, backgroundImageView: UIImageView) {
label.numberOfLines = 0
}
// MARK: - Configuration methods
/// Convenient method for configuration cell with LabelCellViewModel.
///
/// - Parameter viewModel: LabelCellViewModel instance.
public func configureLabelCell(with viewModel: LabelCellViewModel) {
self.viewModel = viewModel
configureSeparator(with: viewModel.separatorType)
configureLabelText(with: viewModel.viewText)
configureContentBackground(with: viewModel.contentBackground)
setNeedsUpdateConstraints()
}
/// Method for background configuration.
///
/// - Parameter contentBackground: Content background to use as background.
public func configureContentBackground(with contentBackground: ViewBackground) {
contentBackground.configure(backgroundView: contentContainerView,
backgroundImageView: backgroundImageView)
}
/// Method for text configuration.
///
/// - Parameter viewText: View text to use as background.
public func configureLabelText(with viewText: ViewText) {
label.configure(with: viewText)
}
}

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2018 Touch Instinct
// Copyright (c) 2017 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
@ -20,6 +20,8 @@
// THE SOFTWARE.
//
import UIKit
/// Cell self-descriptive separator type
public enum CellSeparatorType {
@ -35,3 +37,27 @@ public enum CellSeparatorType {
/// First configuration for top, second for bottom
case full(SeparatorConfiguration, SeparatorConfiguration)
}
public extension CellSeparatorType {
/// Determine if bottom separator is hidden
var bottomIsHidden: Bool {
switch self {
case .top, .none:
return true
case .bottom, .full:
return false
}
}
/// Determine if top separator is hidden
var topIsHidden: Bool {
switch self {
case .bottom, .none:
return true
case .top, .full:
return false
}
}
}

View File

@ -24,9 +24,8 @@ import UIKit
/// Separator configuration. Supports positioning, color and height per each separator
public struct SeparatorConfiguration {
public let color: UIColor
public let insets: UIEdgeInsets
public let insets: UIEdgeInsets?
public let height: CGFloat
/// Initialize configuration with parameters
@ -34,9 +33,10 @@ public struct SeparatorConfiguration {
/// - parameter insets: Insets for separator. Default is no insets
/// - parameter height: Height for separator. Default is 1 pixel
/// - returns: Ready to use separator configuration
public init(color: UIColor, insets: UIEdgeInsets = .zero, height: CGFloat = CGFloat(pixels: 1)) {
self.color = color
public init(color: UIColor, insets: UIEdgeInsets? = .zero, height: CGFloat = CGFloat(pixels: 1)) {
self.color = color
self.insets = insets
self.height = height
}
}

View File

@ -32,53 +32,12 @@ private enum Constants {
/// - in `configure(with:)` you must call `configureSeparator(with:)`
/// - separators are simple views, that located on `contentView`.
/// - if you hide that with another view that fully hide you can use that method `moveSeparators(to:)`
open class SeparatorCell: UITableViewCell {
// MARK: - Public
/// Configure separator with viewModel
/// - parameter separatorType: type of separators
public func configureSeparator(with separatorType: CellSeparatorType) {
topView.isHidden = separatorType.topIsHidden
bottomView.isHidden = separatorType.bottomIsHidden
switch separatorType {
case .none:
break
case .bottom(let configuration):
updateBottomSeparator(with: configuration)
setNeedsUpdateConstraints()
case .top(let configuration):
updateTopSeparator(with: configuration)
setNeedsUpdateConstraints()
case let .full(topConfiguration, bottomConfiguration):
updateTopSeparator(with: topConfiguration)
updateBottomSeparator(with: bottomConfiguration)
setNeedsUpdateConstraints()
}
}
/// Move separator upward in hierarchy
public func bringSeparatorsToFront() {
contentView.bringSubviewToFront(topView)
contentView.bringSubviewToFront(bottomView)
}
/// Move separator backward in hierarchy
public func sendSeparatorsToBack() {
contentView.sendSubviewToBack(topView)
contentView.sendSubviewToBack(bottomView)
}
open class SeparatorTableCell: UITableViewCell, SeparatorCell {
// MARK: - Private
// swiftlint:disable implicitly_unwrapped_optional
private var topView: UIView!
private var bottomView: UIView!
private(set) public var topView: UIView!
private(set) public var bottomView: UIView!
// top separator constraints
private var topViewLeftConstraint: NSLayoutConstraint!
@ -92,12 +51,10 @@ open class SeparatorCell: UITableViewCell {
private var bottomViewBottomConstraint: NSLayoutConstraint!
private var bottomViewHeightConstraint: NSLayoutConstraint!
// swiftlint:enable implicitly_unwrapped_optional
private var topSeparatorInsets = UIEdgeInsets.zero
private var topSeparatorInsets = UIEdgeInsets.zero
private var bottomSeparatorInsets = UIEdgeInsets.zero
private var topSeparatorHeight = Constants.defaultSeparatorHeight
private var topSeparatorHeight = Constants.defaultSeparatorHeight
private var bottomSeparatorHeight = Constants.defaultSeparatorHeight
// MARK: - Initialization
@ -108,20 +65,20 @@ open class SeparatorCell: UITableViewCell {
configureLineViews()
}
public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
public override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
configureLineViews()
}
override open func updateConstraints() {
topViewTopConstraint.constant = topSeparatorInsets.top
topViewLeftConstraint.constant = topSeparatorInsets.left
topViewRightConstraint.constant = topSeparatorInsets.right
topViewHeightConstraint.constant = topSeparatorHeight
topViewTopConstraint.constant = topSeparatorInsets.top
topViewLeftConstraint.constant = topSeparatorInsets.left
topViewRightConstraint.constant = topSeparatorInsets.right
topViewHeightConstraint.constant = topSeparatorHeight
bottomViewLeftConstraint.constant = bottomSeparatorInsets.left
bottomViewRightConstraint.constant = bottomSeparatorInsets.right
bottomViewLeftConstraint.constant = bottomSeparatorInsets.left
bottomViewRightConstraint.constant = bottomSeparatorInsets.right
bottomViewBottomConstraint.constant = bottomSeparatorInsets.bottom
bottomViewHeightConstraint.constant = bottomSeparatorHeight
@ -146,49 +103,59 @@ open class SeparatorCell: UITableViewCell {
return view
}
private func updateTopSeparator(with configuration: SeparatorConfiguration) {
public func updateTopSeparator(with configuration: SeparatorConfiguration) {
topView.backgroundColor = configuration.color
topSeparatorHeight = configuration.height
topSeparatorInsets = configuration.insets
topSeparatorInsets = configuration.insets ?? .zero
}
private func updateBottomSeparator(with configuration: SeparatorConfiguration) {
public func updateBottomSeparator(with configuration: SeparatorConfiguration) {
bottomView.backgroundColor = configuration.color
bottomSeparatorHeight = configuration.height
bottomSeparatorInsets = configuration.insets
bottomSeparatorHeight = configuration.height
bottomSeparatorInsets = configuration.insets ?? .zero
}
private func createConstraints() {
// height
topViewHeightConstraint = topView.heightAnchor.constraint(equalToConstant: topSeparatorHeight)
topViewHeightConstraint.isActive = true
bottomViewHeightConstraint = bottomView.heightAnchor.constraint(equalToConstant: bottomSeparatorHeight)
bottomViewHeightConstraint.isActive = true
// top separator
topViewTopConstraint = topView.topAnchor.constraint(equalTo: contentView.topAnchor)
topViewTopConstraint.isActive = true
topViewRightConstraint = contentView.rightAnchor.constraint(equalTo: topView.rightAnchor)
topViewRightConstraint.isActive = true
if let topView = topView {
topViewRightConstraint = contentView.rightAnchor.constraint(equalTo: topView.rightAnchor)
}
topViewLeftConstraint = topView.leftAnchor.constraint(equalTo: contentView.leftAnchor)
topViewLeftConstraint.isActive = true
// bottom separator
bottomViewRightConstraint = contentView.rightAnchor.constraint(equalTo: bottomView.rightAnchor)
bottomViewRightConstraint.isActive = true
if let bottomView = bottomView {
bottomViewRightConstraint = contentView.rightAnchor.constraint(equalTo: bottomView.rightAnchor)
}
bottomViewLeftConstraint = bottomView.leftAnchor.constraint(equalTo: contentView.leftAnchor)
bottomViewLeftConstraint.isActive = true
bottomViewBottomConstraint = bottomView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
bottomViewBottomConstraint.isActive = true
let allConstraints = [
topViewHeightConstraint,
bottomViewHeightConstraint,
topViewTopConstraint,
topViewRightConstraint,
topViewLeftConstraint,
bottomViewRightConstraint,
bottomViewLeftConstraint,
bottomViewBottomConstraint
].flatMap { $0 }
NSLayoutConstraint.activate(allConstraints)
}
open override func prepareForReuse() {
super.prepareForReuse()
configureSeparator(with: .none)
}
}

View File

@ -35,8 +35,9 @@ public final class SeparatorRowBox {
/// Initialize AnyBaseTableRow with tableRow
/// - parameter row: TableRow which `cell` conforms to SeparatorCell
public init<T>(row: TableRow<T>) where T: SeparatorCell {
public init<T>(row: TableRow<T>) where T: SeparatorTableCell {
self.row = row
setSeparatorHandler = row.set
}
}

View File

@ -24,27 +24,21 @@ import UIKit
public final class SpinnerView: UIView, Animatable, LoadingIndicator {
private var animating: Bool {
imageView?.layer.animation(forKey: CABasicAnimation.rotationKeyPath) != nil
}
private(set) var animating: Bool = false
private var startTime = CFTimeInterval(0)
private var stopTime = CFTimeInterval(0)
private weak var imageView: UIImageView?
private let hidesWhenStopped: Bool
private let animationDuration: CFTimeInterval
private let animationRepeatCount: Float
private let clockwiseAnimation: Bool
public init(image: UIImage,
hidesWhenStopped: Bool = true,
animationDuration: CFTimeInterval = 1,
animationRepeatCount: Float = Float.infinity,
clockwiseAnimation: Bool = true) {
self.hidesWhenStopped = hidesWhenStopped
self.animationDuration = animationDuration
self.animationRepeatCount = animationRepeatCount
self.clockwiseAnimation = clockwiseAnimation
@ -66,7 +60,7 @@ public final class SpinnerView: UIView, Animatable, LoadingIndicator {
NotificationCenter.default.addObserver(self,
selector: #selector(SpinnerView.restartAnimationIfNeeded),
name: UIApplication.willEnterForegroundNotification,
name: .UIApplicationWillEnterForeground,
object: nil)
}
@ -86,14 +80,6 @@ public final class SpinnerView: UIView, Animatable, LoadingIndicator {
}
}
override public func didMoveToSuperview() {
super.didMoveToSuperview()
if superview != nil {
restartAnimationIfNeeded()
}
}
// MARK: - Animatable
@objc public func startAnimating() {
@ -101,9 +87,9 @@ public final class SpinnerView: UIView, Animatable, LoadingIndicator {
return
}
if hidesWhenStopped {
imageView?.isHidden = false
}
animating = true
imageView?.isHidden = false
addAnimation()
}
@ -113,9 +99,9 @@ public final class SpinnerView: UIView, Animatable, LoadingIndicator {
return
}
if hidesWhenStopped {
imageView?.isHidden = true
}
animating = false
imageView?.isHidden = true
removeAnimation()
}
@ -153,4 +139,5 @@ public final class SpinnerView: UIView, Animatable, LoadingIndicator {
addAnimation()
}
}
}

View File

@ -1,47 +0,0 @@
//
// Copyright (c) 2018 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import UIKit.UITableView
/// The main purpose of this class is to fix empty space on top of the screen
/// when view controller view is UITableView.
open class TableViewWrapperView: ScrollViewHolderView, TableViewHolder {
/// Contained table view.
public let tableView: UITableView
/// Initializer with tableViewStyle parameter.
///
/// - Parameter tableViewStyle: UITableViewStyle to pass in UITableView init.
public init(tableViewStyle: UITableView.Style) {
self.tableView = UITableView(frame: .zero, style: tableViewStyle)
self.tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
super.init(frame: .zero)
addSubview(tableView)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -1,130 +0,0 @@
//
// Copyright (c) 2018 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import RxSwift
import RxCocoa
/// Class that maps data model field to String and vise-versa.
public final class DataModelFieldBinding<T> {
public typealias GetFieldClosure = (T) -> String?
public typealias MergeFieldClosure = (T, String?) -> T
private let modelRelay: BehaviorRelay<T>
private let modelDriver: Driver<T>
private let getFieldClosure: DataModelFieldBinding<T>.GetFieldClosure
private let mergeFieldClosure: DataModelFieldBinding<T>.MergeFieldClosure
/// Memberwise initializer.
///
/// - Parameters:
/// - modelRelay: BehaviourRelay that contains data model.
/// - modelDriver: Driver that emits new data models.
/// - getFieldClosure: Closure for getting field string reprerentation from data model.
/// - mergeFieldClosure: Closure for merging new field value into data model.
public init(modelRelay: BehaviorRelay<T>,
modelDriver: Driver<T>,
getFieldClosure: @escaping GetFieldClosure,
mergeFieldClosure: @escaping MergeFieldClosure) {
self.modelRelay = modelRelay
self.modelDriver = modelDriver
self.getFieldClosure = getFieldClosure
self.mergeFieldClosure = mergeFieldClosure
}
/// Method that merges new field values with data model.
///
/// - Parameter textDriver: Driver that emits new text values.
/// - Returns: Disposable object that can be used to unsubscribe the observer from the behaviour relay.
public func mergeStringToModel(from textDriver: Driver<String?>) -> Disposable {
textDriver.map { [modelRelay, mergeFieldClosure] in
mergeFieldClosure(modelRelay.value, $0)
}
.drive(modelRelay)
}
/// A Driver that will emit current field value.
public var fieldDriver: Driver<String?> {
modelDriver.map(getFieldClosure)
}
}
public extension DataModelFieldBinding {
/// Convenience initializer without modelDriver, which will be obtained from modelRelay.
///
/// - Parameters:
/// - modelRelay: BehaviourRelay that contains data model.
/// - getFieldClosure: Closure for getting field string reprerentation from data model.
/// - mergeFieldClosure: Closure for merging new field value into data model.
convenience init(modelRelay: BehaviorRelay<T>,
getFieldClosure: @escaping GetFieldClosure,
mergeFieldClosure: @escaping MergeFieldClosure) {
self.init(modelRelay: modelRelay,
modelDriver: modelRelay.asDriver(),
getFieldClosure: getFieldClosure,
mergeFieldClosure: mergeFieldClosure)
}
}
public extension DataModelFieldBinding where T == String? {
/// Convenience initializer for data model of string.
///
/// - Parameter modelRelay: BehaviourRelay that contains data model.
convenience init(modelRelay: BehaviorRelay<T>) {
self.init(modelRelay: modelRelay,
modelDriver: modelRelay.asDriver(),
getFieldClosure: { $0 },
mergeFieldClosure: { $1 })
}
}
public extension BehaviorRelay {
/// Creates DataModelFieldBinding configured with given closures and behaviour relay itself.
///
/// - Parameters:
/// - getFieldClosure: Closure for getting field string reprerentation from data model.
/// - mergeFieldClosure: Closure for merging new field value into data model.
/// - Returns: DataModelFieldBinding instance.
func fieldBinding(getFieldClosure: @escaping DataModelFieldBinding<Element>.GetFieldClosure,
mergeFieldClosure: @escaping DataModelFieldBinding<Element>.MergeFieldClosure)
-> DataModelFieldBinding<Element> {
DataModelFieldBinding(modelRelay: self,
getFieldClosure: getFieldClosure,
mergeFieldClosure: mergeFieldClosure)
}
}
public extension BehaviorRelay where Element == String? {
/// Creates DataModelFieldBinding configured with behaviour relay itself.
///
/// - Returns: DataModelFieldBinding instance.
func fieldBinding() -> DataModelFieldBinding<Element> {
DataModelFieldBinding(modelRelay: self)
}
}

View File

@ -1,99 +0,0 @@
//
// Copyright (c) 2018 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import RxSwift
import RxCocoa
/// Class that used for binding text field with upper level view model.
open class TextFieldViewModel < ViewEvents: TextFieldViewEvents,
ViewModelEvents: TextFieldViewModelEvents> {
/// Events that can be emitted by view model.
public let viewModelEvents: ViewModelEvents
private let viewEventsRelay = BehaviorRelay<ViewEvents?>(value: nil)
private(set) public var disposeBag = DisposeBag()
/// Initializer with view model events.
///
/// - Parameter viewModelEvents: Events that can be emitted by view model.
public init(viewModelEvents: ViewModelEvents) {
self.viewModelEvents = viewModelEvents
}
/// View events driver that will emit view events structure
/// when view will bind itself to the view model.
public var viewEventsDriver: Driver<ViewEvents> {
viewEventsRelay
.asDriver()
.flatMap { viewEvents -> Driver<ViewEvents> in
guard let viewEvents = viewEvents else {
return .empty()
}
return .just(viewEvents)
}
}
/// Method that performs binding view events to view model.
///
/// - Parameter viewEvents: View events structure.
public func bind(viewEvents: ViewEvents) {
viewEventsRelay.accept(viewEvents)
}
/// Unbinds view from view model.
public func unbindView() {
disposeBag = DisposeBag()
}
}
public extension TextFieldViewModel {
typealias MapViewEventClosure = (ViewEvents) -> Disposable
/// Convenient method for binding to the current view events structure.
///
/// - Parameter closure: Closure that takes a view events parameter and returns Disposable.
/// - Returns: Disposable object that can be used to unsubscribe the observer from the binding.
func mapViewEvents(_ closure: @escaping MapViewEventClosure) -> Disposable {
mapViewEvents { [closure($0)] }
}
typealias MapViewEventsClosure = (ViewEvents) -> [Disposable]
/// Convenient method for binding to the current view events structure.
///
/// - Parameter closure: Closure that takes a view events parameter and returns [Disposable].
/// - Returns: Disposable object that can be used to unsubscribe the observer from the binding.
func mapViewEvents(_ closure: @escaping MapViewEventsClosure) -> Disposable {
viewEventsDriver
.map { [weak self] in
guard let strongSelf = self else {
return
}
closure($0).forEach { $0.disposed(by: strongSelf.disposeBag) }
}
.drive()
}
}

View File

@ -1,47 +0,0 @@
//
// Copyright (c) 2018 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import UIKit.UIFont
import UIKit.UIColor
/// Base set of attributes to configure appearance of text.
open class BaseTextAttributes {
/// Text font.
public let font: UIFont
/// Text color.
public let color: UIColor
/// Text alignment.
public let alignment: NSTextAlignment
/// Memberwise initializer.
///
/// - Parameters:
/// - font: Text font.
/// - color: Text color.
/// - alignment: Text alignment.
public init(font: UIFont, color: UIColor, alignment: NSTextAlignment = .natural) {
self.font = font
self.color = color
self.alignment = alignment
}
}

View File

@ -28,7 +28,7 @@ open class XibView: UIView {
/// Nib name used to instantiate inner view
/// - NOTE: Be very carefully when you're intending to change this line
open var innerViewNibName: String {
typeName(of: type(of: self))
return typeName(of: type(of: self))
}
public convenience init() {
@ -55,10 +55,14 @@ open class XibView: UIView {
addSubview(view)
configure()
backgroundColor = .clear
configure(xibView: view)
}
/// Provide initial configuration. Called once
open func configure() {
open func configure(xibView: UIView) {
}
}

View File

@ -28,4 +28,5 @@ import Foundation
public enum CursorError: Error {
case exhausted
}

View File

@ -1,97 +0,0 @@
//
// Copyright (c) 2018 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
/// Enum that contains states for general data loading.
///
/// - initial: Initial state. Before something will happen.
/// - loading: Loading state. When data loading is started.
/// - result: Result state from a specific data source with result.
/// - error: Error state with a specific error.
/// - empty: Empty state. When data was requested and empty result was received.
public enum GeneralDataLoadingState<DS: DataSource> {
case initial
case loading
case result(newResult: DS.ResultType, from: DS)
case error(error: Error)
case empty
}
extension GeneralDataLoadingState: DataLoadingState {
public typealias DataSourceType = DS
public static var initialState: GeneralDataLoadingState<DS> {
.initial
}
public static var emptyState: GeneralDataLoadingState<DS> {
.empty
}
public static func initialLoadingState(after: GeneralDataLoadingState<DS>) -> GeneralDataLoadingState<DS> {
.loading
}
public static func resultState(result: DS.ResultType,
from: DS,
after: GeneralDataLoadingState<DS>) -> GeneralDataLoadingState<DS> {
.result(newResult: result, from: from)
}
public static func errorState(error: Error,
after: GeneralDataLoadingState<DS>) -> GeneralDataLoadingState<DS> {
.error(error: error)
}
public var isInitialState: Bool {
switch self {
case .initial:
return true
default:
return false
}
}
public var result: DS.ResultType? {
switch self {
case .result(let newResult, _):
return newResult
default:
return nil
}
}
public var error: Error? {
switch self {
case .error(let error):
return error
default:
return nil
}
}
}

View File

@ -1,101 +0,0 @@
//
// Copyright (c) 2018 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
/// Enum that contains states for paginated data loading.
///
/// - initial: Initial state. Before something will happen.
/// - initialLoading: Initial loading state. When data was requested initially.
/// - loadingMore: Loading more state. When additional data was requested.
/// - results: Result state from a specific data source after a given state.
/// - error: Error state with a specific error after a given state.
/// - empty: Empty state. When data was initially requested and empty result was received.
/// - exhausted: Exhausted state. When no more results can be received.
public indirect enum PaginationDataLoadingState<DS: DataSource> {
case initial
case initialLoading(after: PaginationDataLoadingState)
case loadingMore(after: PaginationDataLoadingState)
case results(newItems: DS.ResultType, from: DS, after: PaginationDataLoadingState)
case error(error: Error, after: PaginationDataLoadingState)
case empty
case exhausted
}
extension PaginationDataLoadingState: DataLoadingState {
public typealias DataSourceType = DS
public static var initialState: PaginationDataLoadingState<DS> {
.initial
}
public static var emptyState: PaginationDataLoadingState<DS> {
.empty
}
public static func initialLoadingState(after: PaginationDataLoadingState<DS>) -> PaginationDataLoadingState<DS> {
.initialLoading(after: after)
}
public static func resultState(result: DS.ResultType,
from: DataSourceType,
after: PaginationDataLoadingState<DS>) -> PaginationDataLoadingState<DS> {
.results(newItems: result, from: from, after: after)
}
public static func errorState(error: Error,
after: PaginationDataLoadingState<DS>) -> PaginationDataLoadingState<DS> {
.error(error: error, after: after)
}
public var isInitialState: Bool {
switch self {
case .initial:
return true
default:
return false
}
}
public var result: DS.ResultType? {
switch self {
case .results(let newItems, _, _):
return newItems
default:
return nil
}
}
public var error: Error? {
switch self {
case .error(let error, _):
return error
default:
return nil
}
}
}

View File

@ -25,11 +25,8 @@ import Foundation
/// Enum that represents common errors in LeadKit framework
///
/// - failedToCastValue: attempt to cast was failed
/// - failedToDecode: attempt to decoding was failed
/// - failedToEncodeQueryItems: attempt to encoding to query items was failed
public enum LeadKitError: Error {
case failedToCastValue(expectedType: Any.Type, givenType: Any.Type)
case failedToDecode(reason: String)
case failedToEncodeQueryItems
}

View File

@ -31,8 +31,9 @@ import Alamofire
/// - mapping: Errors that occurs during mapping json into model.
public enum RequestError: Error {
case noConnection(url: String?)
case network(error: Error, response: Data?, url: String?)
case invalidResponse(error: AFError, response: Data?, url: String?)
case mapping(error: Error, response: Data, url: String?)
case noConnection
case network(error: Error)
case invalidResponse(error: AFError)
case mapping(error: Error, response: Any)
}

View File

@ -1,29 +0,0 @@
//
// Copyright (c) 2019 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import TableKit
public enum SearchResultsViewControllerState {
case initial
case rowsContent(rows: [Row])
case sectionsContent(sections: [TableSection])
}

View File

@ -1,32 +0,0 @@
//
// Copyright (c) 2019 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
/// Title type for UIViewController title.
///
/// - large: large sized title
/// - normal: normal sized title
/// - empty: empty title
public enum TitleType {
case large(title: String)
case normal(title: String)
case empty
}

View File

@ -1,31 +0,0 @@
//
// Copyright (c) 2018 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
/// Enum that describes possible PlaceholderConfigurable view states.
///
/// - placeholder: Placeholder state with placeholder view model.
/// - content: Content state with content view model.
public enum ContentLoadingViewModel<ContentType, PlaceholderType> {
case placeholder(PlaceholderType)
case content(ContentType)
}

View File

@ -1,33 +0,0 @@
//
// Copyright (c) 2018 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import UIKit
/// Enum that describes possible view backgrounds.
///
/// - color: Solid color background.
/// - image: Image background.
public enum ViewBackground {
case color(UIColor)
case image(UIImage)
}

View File

@ -1,33 +0,0 @@
//
// Copyright (c) 2018 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import UIKit
/// Enum that describes text with appearance options.
///
/// - string: Regular string with common and often-used text attributes.
/// - attributedString: Attributed string.
public enum ViewText {
case string(String, textAttributes: BaseTextAttributes)
case attributedString(NSAttributedString)
}

View File

@ -0,0 +1,123 @@
//
// Copyright (c) 2017 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Alamofire
import RxSwift
import RxAlamofire
import ObjectMapper
public extension Alamofire.SessionManager {
/// The default acceptable range 200...299
static let defaultAcceptableStatusCodes = Array(200..<300)
}
public extension Reactive where Base: Alamofire.SessionManager {
/// Method which executes request with given api parameters
///
/// - Parameter requestParameters: api parameters to pass Alamofire
/// - Returns: Observable with request
func apiRequest(requestParameters: ApiRequestParameters,
acceptableStatusCodes: [Int] = Base.defaultAcceptableStatusCodes)
-> Observable<DataRequest> {
return request(requestParameters.method,
requestParameters.url,
parameters: requestParameters.parameters,
encoding: requestParameters.encoding,
headers: requestParameters.headers)
.map { $0.validate(statusCode: acceptableStatusCodes) }
}
/// Method that executes request and serializes response into target object
///
/// - Parameter requestParameters: api parameters to pass Alamofire
/// - Parameter mappingQueue: The dispatch queue to use for mapping
/// - Returns: Observable with HTTP URL Response and target object
func responseModel<T: ImmutableMappable>(requestParameters: ApiRequestParameters,
mappingQueue: DispatchQueue = .global(),
acceptableStatusCodes: [Int] = Base.defaultAcceptableStatusCodes)
-> Observable<(response: HTTPURLResponse, model: T)> {
return apiRequest(requestParameters: requestParameters, acceptableStatusCodes: acceptableStatusCodes)
.flatMap { $0.rx.apiResponse(mappingQueue: mappingQueue) }
}
/// Method that executes request and serializes response into array of target objects
///
/// - Parameter requestParameters: api parameters to pass Alamofire
/// - Parameter mappingQueue: The dispatch queue to use for mapping
/// - Returns: Observable with HTTP URL Response and array of target objects
func responseModel<T: ImmutableMappable>(requestParameters: ApiRequestParameters,
mappingQueue: DispatchQueue = .global(),
acceptableStatusCodes: [Int] = Base.defaultAcceptableStatusCodes)
-> Observable<(response: HTTPURLResponse, models: [T])> {
return apiRequest(requestParameters: requestParameters, acceptableStatusCodes: acceptableStatusCodes)
.flatMap { $0.rx.apiResponse(mappingQueue: mappingQueue) }
}
/// Method that executes request and serializes response into target type
///
/// - Parameter requestParameters: api parameters to pass Alamofire
/// - Parameter mappingQueue: The dispatch queue to use for mapping
/// - Returns: Observable with HTTP URL Response and target object
func responseObject<T>(requestParameters: ApiRequestParameters,
mappingQueue: DispatchQueue = .global(),
acceptableStatusCodes: [Int] = Base.defaultAcceptableStatusCodes)
-> Observable<(response: HTTPURLResponse, object: T)> {
return apiRequest(requestParameters: requestParameters, acceptableStatusCodes: acceptableStatusCodes)
.flatMap { $0.rx.apiResponse(mappingQueue: mappingQueue) }
}
/// Method that executes request and serializes response into target object
///
/// - Parameter requestParameters: api parameters to pass Alamofire
/// - Parameter mappingQueue: The dispatch queue to use for mapping
/// - Returns: Observable with HTTP URL Response and target object
func responseObservableModel<T: ObservableMappable>(requestParameters: ApiRequestParameters,
mappingQueue: DispatchQueue = .global(),
acceptableStatusCodes: [Int] = Base.defaultAcceptableStatusCodes)
-> Observable<(response: HTTPURLResponse, model: T)> where T.ModelType == T {
return apiRequest(requestParameters: requestParameters, acceptableStatusCodes: acceptableStatusCodes)
.flatMap { $0.rx.observableApiResponse(mappingQueue: mappingQueue) }
}
/// Method that executes request and serializes response into array of target objects
///
/// - Parameter requestParameters: api parameters to pass Alamofire
/// - Parameter mappingQueue: The dispatch queue to use for mapping
/// - Returns: Observable with HTTP URL Response and array of target objects
func responseObservableModel<T: ObservableMappable>(requestParameters: ApiRequestParameters,
mappingQueue: DispatchQueue = .global(),
acceptableStatusCodes: [Int] = Base.defaultAcceptableStatusCodes)
-> Observable<(response: HTTPURLResponse, models: [T])> where T.ModelType == T {
return apiRequest(requestParameters: requestParameters, acceptableStatusCodes: acceptableStatusCodes)
.flatMap { $0.rx.observableApiResponse(mappingQueue: mappingQueue) }
}
}

View File

@ -0,0 +1,167 @@
//
// Copyright (c) 2017 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Alamofire
import RxSwift
import ObjectMapper
import RxAlamofire
typealias ServerResponse = (response: HTTPURLResponse, result: Any)
public extension Reactive where Base: DataRequest {
private typealias JSON = [String: Any]
/// Method that serializes response into target object
///
/// - Parameter mappingQueue: The dispatch queue to use for mapping
/// - Returns: Observable with HTTP URL Response and target object
func apiResponse<T: ImmutableMappable>(mappingQueue: DispatchQueue = .global())
-> Observable<(response: HTTPURLResponse, model: T)> {
return responseJSONOnQueue(mappingQueue)
.tryMapResult { resp, value in
let json = try cast(value) as JSON
return (resp, try T(JSON: json))
}
}
/// Method that serializes response into array of target objects
///
/// - Parameter mappingQueue: The dispatch queue to use for mapping
/// - Returns: Observable with HTTP URL Response and array of target objects
func apiResponse<T: ImmutableMappable>(mappingQueue: DispatchQueue = .global())
-> Observable<(response: HTTPURLResponse, models: [T])> {
return responseJSONOnQueue(mappingQueue)
.tryMapResult { resp, value in
let jsonArray = try cast(value) as [JSON]
return (resp, try Mapper<T>().mapArray(JSONArray: jsonArray))
}
}
/// Method that serializes response into target type
///
/// - Parameter mappingQueue: The dispatch queue to use for mapping
/// - Returns: Observable with HTTP URL Response and target object
func apiResponse<T>(mappingQueue: DispatchQueue = .global())
-> Observable<(response: HTTPURLResponse, object: T)> {
return responseJSONOnQueue(mappingQueue)
.tryMapResult { resp, value in
return (resp, try cast(value) as T)
}
}
/// Method that serializes response into target object
///
/// - Parameter mappingQueue: The dispatch queue to use for mapping
/// - Returns: Observable with HTTP URL Response and target object
func observableApiResponse<T: ObservableMappable>(mappingQueue: DispatchQueue = .global())
-> Observable<(response: HTTPURLResponse, model: T)> where T.ModelType == T {
return responseJSONOnQueue(mappingQueue)
.tryMapObservableResult { resp, value in
let json = try cast(value) as JSON
return T.createFrom(map: Map(mappingType: .fromJSON, JSON: json))
.map { (resp, $0) }
}
}
/// Method that serializes response into array of target objects
///
/// - Parameter mappingQueue: The dispatch queue to use for mapping
/// - Returns: Observable with HTTP URL Response and array of target objects
func observableApiResponse<T: ObservableMappable>(mappingQueue: DispatchQueue = .global())
-> Observable<(response: HTTPURLResponse, models: [T])> where T.ModelType == T {
return responseJSONOnQueue(mappingQueue)
.tryMapObservableResult { resp, value in
let jsonArray = try cast(value) as [JSON]
let createFromList = jsonArray.map {
T.createFrom(map: Map(mappingType: .fromJSON, JSON: $0))
}
return Observable.zip(createFromList) { $0 }
.map { (resp, $0) }
}
}
internal func responseJSONOnQueue(_ queue: DispatchQueue) -> Observable<ServerResponse> {
let responseSerializer = DataRequest.jsonResponseSerializer(options: .allowFragments)
return responseResult(queue: queue, responseSerializer: responseSerializer)
.map { ServerResponse(response: $0.0, result: $0.1) }
.catchError {
switch $0 {
case let urlError as URLError:
switch urlError.code {
case .notConnectedToInternet, .timedOut:
throw RequestError.noConnection
default:
throw RequestError.network(error: urlError)
}
case let afError as AFError:
switch afError {
case .responseSerializationFailed, .responseValidationFailed:
throw RequestError.invalidResponse(error: afError)
default:
throw RequestError.network(error: afError)
}
default:
throw RequestError.network(error: $0)
}
}
}
}
private extension ObservableType where E == ServerResponse {
func tryMapResult<R>(_ transform: @escaping (E) throws -> R) -> Observable<R> {
return map {
do {
return try transform($0)
} catch {
throw RequestError.mapping(error: error, response: $0.1)
}
}
}
func tryMapObservableResult<R>(_ transform: @escaping (E) throws -> Observable<R>) -> Observable<R> {
return flatMap { response -> Observable<R> in
do {
return try transform(response)
.catchError {
throw RequestError.mapping(error: $0, response: response.result)
}
} catch {
throw RequestError.mapping(error: error, response: response.result)
}
}
}
}

View File

@ -1,167 +0,0 @@
//
// Copyright (c) 2017 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Alamofire
import RxSwift
import RxAlamofire
typealias ServerResponse = (HTTPURLResponse, Data)
public extension Reactive where Base: DataRequest {
/// Method that serializes response into target object
///
/// - Parameter mappingQueue: The dispatch queue to use for mapping
/// - Parameter decoder: JSONDecoder used to decode a decodable type
/// - Returns: Observable with HTTP URL Response and target object
func apiResponse<T: Decodable>(mappingQueue: DispatchQueue = .global(), decoder: JSONDecoder)
-> Observable<SessionManager.ModelResponse<T>> {
response(onQueue: mappingQueue)
.tryMapResult { response, data in
(response, try decoder.decode(T.self, from: data))
}
.catchAsRequestError(with: self.base)
}
/// Method that serializes response into target object
///
/// - Parameter mappingQueue: The dispatch queue to use for mapping
/// - Returns: Observable with HTTP URL Response and target object
func observableApiResponse<T: ObservableMappable>(mappingQueue: DispatchQueue = .global(), decoder: JSONDecoder)
-> Observable<SessionManager.ModelResponse<T>> {
response(onQueue: mappingQueue)
.tryMapObservableResult { response, value in
let json = try JSONSerialization.jsonObject(with: value, options: [])
return T.create(from: json, with: decoder)
.map { (response, $0) }
}
.catchAsRequestError(with: self.base)
}
/// Method that serializes response into data
///
/// - Parameter mappingQueue: The dispatch queue to use for mapping
/// - Returns: Observable with HTTP URL Response and data
func dataApiResponse(mappingQueue: DispatchQueue) -> Observable<SessionManager.DataResponse> {
response(onQueue: mappingQueue)
.map { $0 as SessionManager.DataResponse }
.catchAsRequestError(with: self.base)
}
private func response(onQueue queue: DispatchQueue) -> Observable<(HTTPURLResponse, Data)> {
responseResult(queue: queue, responseSerializer: DataResponseSerializer())
}
}
public extension ObservableType where Element == DataRequest {
/// Method that validates status codes and catch network errors
///
/// - Parameter statusCodes: set of status codes to validate
/// - Returns: Observable on self
func validate(statusCodes: Set<Int>, url: String? = nil) -> Observable<Element> {
map { $0.validate(statusCode: statusCodes) }
.catchAsRequestError(url: url)
}
}
private extension ObservableType where Element == ServerResponse {
func tryMapResult<R>(_ transform: @escaping (Element) throws -> R) -> Observable<R> {
map {
do {
return try transform($0)
} catch {
throw RequestError.mapping(error: error,
response: $0.1,
url: $0.0.url?.absoluteString)
}
}
}
func tryMapObservableResult<R>(_ transform: @escaping (Element) throws -> Observable<R>) -> Observable<R> {
flatMap { response, result -> Observable<R> in
do {
return try transform((response, result))
.catch {
throw RequestError.mapping(error: $0,
response: result,
url: response.url?.absoluteString)
}
} catch {
throw RequestError.mapping(error: error,
response: result,
url: response.url?.absoluteString)
}
}
}
}
private extension ObservableType {
func catchAsRequestError(with request: DataRequest? = nil,
url: String? = nil) -> Observable<Element> {
self.catch { error in
let resultError: RequestError
let response = request?.data
switch error {
case let requestError as RequestError:
resultError = requestError
case let urlError as URLError:
switch urlError.code {
case .notConnectedToInternet:
resultError = .noConnection(url: url)
default:
resultError = .network(error: urlError, response: response, url: url)
}
case let afError as AFError:
switch afError {
case let .sessionTaskFailed(error):
switch error {
case let urlError as URLError where urlError.code == .notConnectedToInternet:
resultError = .noConnection(url: url)
default:
resultError = .network(error: error, response: response, url: url)
}
case .responseSerializationFailed, .responseValidationFailed:
resultError = .invalidResponse(error: afError, response: response, url: url)
default:
resultError = .network(error: afError, response: response, url: url)
}
default:
resultError = .network(error: error, response: response, url: url)
}
throw resultError
}
}
}

View File

@ -1,202 +0,0 @@
//
// Copyright (c) 2017 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Alamofire
import RxSwift
import RxAlamofire
/// Enum that represents wrong usage of requset parameters
///
/// - getMethodForbidden: invalid usage of get method
/// - urlEncodingForbidden: invalid usage of URLEncoding
enum RequestUsageError: Error {
case getMethodForbidden
case urlEncodingForbidden
case unableToHandleQueryParams
}
public extension Reactive where Base: SessionManager {
/// Creates an observable of the `Request`.
///
/// - Parameters:
/// - method: Alamofire method object
/// - url: An object adopting `URLConvertible`
/// - parameters: An array of JSON objects containing all necessary options
/// - encoding: The kind of encoding used to process parameters
/// - headers: A dictionary containing all the additional headers
/// - Returns: An observable of the `Request`
func request(_ method: Alamofire.HTTPMethod,
_ url: URLConvertible,
parameters: [Any]? = nil,
encoding: JSONEncoding = .default,
headers: HTTPHeaders? = nil)
-> Observable<DataRequest> {
Observable.deferred {
guard method != .get else {
assertionFailure("Unable to pass array in get request")
throw RequestUsageError.getMethodForbidden
}
let urlRequest = try URLRequest(url: try url.asURL(), method: method, headers: headers)
let encodedUrlRequest = try encoding.encode(urlRequest, withJSONObject: parameters)
return self.request(urlRequest: encodedUrlRequest)
}
}
/// Method which executes request with given api parameters
///
/// - Parameters:
/// - requestParameters: api parameters to pass Alamofire
/// - additionalValidStatusCodes: set of additional valid status codes
/// - Returns: Observable with request
func apiRequest(requestParameters: ApiRequestParameters, additionalValidStatusCodes: Set<Int>) -> Observable<DataRequest> {
.deferred {
var url = try requestParameters.url.asURL()
if let queryItems = requestParameters.queryItems {
guard var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
return .error(RequestUsageError.unableToHandleQueryParams)
}
urlComponents.queryItems = queryItems
url = try urlComponents.asURL()
}
let requestObservable: Observable<DataRequest>
switch requestParameters.parameters {
case .dictionary(let parameters)?:
requestObservable = self.request(requestParameters.method,
url,
parameters: parameters,
encoding: requestParameters.encoding,
headers: requestParameters.headers)
case .array(let parameters)?:
guard let encoding = requestParameters.encoding as? JSONEncoding else {
assertionFailure("Invalid encoding type with array parameter")
return .error(RequestUsageError.urlEncodingForbidden)
}
requestObservable = self.request(requestParameters.method,
url,
parameters: parameters,
encoding: encoding,
headers: requestParameters.headers)
case .none:
requestObservable = self.request(requestParameters.method,
url,
parameters: nil as Parameters?,
encoding: requestParameters.encoding,
headers: requestParameters.headers)
}
return requestObservable
.validate(statusCodes: self.base.acceptableStatusCodes.union(additionalValidStatusCodes),
url: url.absoluteString)
}
}
/// Method that executes request and serializes response into target object
///
/// - Parameters:
/// - requestParameters: api parameters to pass Alamofire
/// - additionalValidStatusCodes: set of additional valid status codes
/// - decoder: json decoder to decode response data
/// - Returns: Observable with HTTP URL Response and target object
func responseModel<T: Decodable>(requestParameters: ApiRequestParameters,
additionalValidStatusCodes: Set<Int>,
decoder: JSONDecoder)
-> Observable<SessionManager.ModelResponse<T>> {
apiRequest(requestParameters: requestParameters, additionalValidStatusCodes: additionalValidStatusCodes)
.flatMap {
$0.rx.apiResponse(mappingQueue: self.base.mappingQueue, decoder: decoder)
}
}
/// Method that executes request and serializes response into target object
///
/// - Parameters:
/// - requestParameters: api parameters to pass Alamofire
/// - additionalValidStatusCodes: set of additional valid status codes
/// - decoder: json decoder to decode response data
/// - Returns: Observable with HTTP URL Response and target object
func responseObservableModel<T: ObservableMappable>(requestParameters: ApiRequestParameters,
additionalValidStatusCodes: Set<Int>,
decoder: JSONDecoder)
-> Observable<SessionManager.ModelResponse<T>> {
apiRequest(requestParameters: requestParameters, additionalValidStatusCodes: additionalValidStatusCodes)
.flatMap {
$0.rx.observableApiResponse(mappingQueue: self.base.mappingQueue, decoder: decoder)
}
}
/// Method that executes request and returns data
///
/// - Parameters:
/// - requestParameters: api parameters to pass Alamofire
/// - additionalValidStatusCodes: set of additional valid status codes
/// - Returns: Observable with HTTP URL Response and Data
func responseData(requestParameters: ApiRequestParameters, additionalValidStatusCodes: Set<Int>)
-> Observable<SessionManager.DataResponse> {
apiRequest(requestParameters: requestParameters, additionalValidStatusCodes: additionalValidStatusCodes)
.flatMap {
$0.rx.dataApiResponse(mappingQueue: self.base.mappingQueue)
}
}
/// Method that executes upload request and serializes response into target object
///
/// - Parameters:
/// - requestParameters: api upload parameters to pass Alamofire
/// - additionalValidStatusCodes: set of additional valid status codes
/// - decoder: json decoder to decode response data
/// - Returns: Observable with HTTP URL Response and target object
func uploadResponseModel<T: Decodable>(requestParameters: ApiUploadRequestParameters,
additionalValidStatusCodes: Set<Int>,
decoder: JSONDecoder)
-> Observable<SessionManager.ModelResponse<T>> {
Observable.deferred {
let urlRequest = try URLRequest(url: requestParameters.url, method: .post, headers: requestParameters.headers)
let data = try requestParameters.formData.encode()
return self.upload(data, urlRequest: urlRequest)
.map { $0 as DataRequest }
.validate(statusCodes: self.base.acceptableStatusCodes.union(additionalValidStatusCodes),
url: try? requestParameters.url.asURL().absoluteString)
.flatMap {
$0.rx.apiResponse(mappingQueue: self.base.mappingQueue, decoder: decoder)
}
}
}
}

View File

@ -63,12 +63,14 @@ public extension Array where Element: Equatable {
let allValues = values.flatMap { $0 }
return filter { !allValues.contains($0) }
}
}
public extension Array {
// Subscript for safe access to element by index
subscript(safe index: Index) -> Element? {
(index < count && index >= 0) ? self[index] : nil
return indices.contains(index) ? self[index] : nil
}
}

View File

@ -1,36 +0,0 @@
//
// Copyright (c) 2018 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import TableKit
public extension Array where Element: TableKitViewModel {
/// Creates [Row] array from TableKitViewModels.
var tableRows: [Row] {
map { $0.tableRow }
}
/// Creates TableSection with empty, zero height header and footer.
var onlyRowsSection: TableSection {
TableSection(onlyRows: tableRows)
}
}

View File

@ -26,7 +26,7 @@ public extension Array where Element == SeparatorRowBox {
/// Create rows from SeparatorRowBox array
var rows: [Row] {
map { $0.row }
return map { $0.row }
}
/// Configure separators from SeparatorRowBox array
@ -35,31 +35,18 @@ public extension Array where Element == SeparatorRowBox {
func configureSeparators(extreme extremeSeparatorConfiguration: SeparatorConfiguration,
middle middleSeparatorConfiguration: SeparatorConfiguration) {
configureSeparators(first: extremeSeparatorConfiguration,
middle: middleSeparatorConfiguration,
last: extremeSeparatorConfiguration)
}
/// Configure separators from SeparatorRowBox array
/// - parameter first: Configuration of the top separator of the first row
/// - parameter middle: Configuration of the separators between the rows
/// - parameter last: Configuration of the bottom separator of the last row
func configureSeparators(first firstSeparatorConfiguration: SeparatorConfiguration,
middle middleSeparatorConfiguration: SeparatorConfiguration,
last lastSeparatorConfiguration: SeparatorConfiguration) {
if isEmpty {
return
}
switch count {
case 1:
first?.set(separatorType: .full(firstSeparatorConfiguration, lastSeparatorConfiguration))
first?.set(separatorType: .full(extremeSeparatorConfiguration, extremeSeparatorConfiguration))
default:
forEach { $0.set(separatorType: .bottom(middleSeparatorConfiguration)) }
first?.set(separatorType: .full(firstSeparatorConfiguration, middleSeparatorConfiguration))
last?.set(separatorType: .bottom(lastSeparatorConfiguration))
forEach { $0.set(separatorType: .full(middleSeparatorConfiguration, middleSeparatorConfiguration))}
first?.set(separatorType: .top(extremeSeparatorConfiguration))
last?.set(separatorType: .bottom(extremeSeparatorConfiguration))
}
}
}

View File

@ -1,34 +0,0 @@
//
// Copyright (c) 2018 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
extension Array: TotalCountCursorListingResult {
public typealias ElementType = Element
public var results: [Element] {
self
}
public var totalCount: Int {
count
}
}

View File

@ -27,7 +27,7 @@ extension CABasicAnimation {
static let rotationKeyPath = "transform.rotation.z"
static func zRotationAnimationWith(duration: CFTimeInterval = 1,
repeatCount: Float = .infinity,
repeatCount: Float = Float.infinity,
clockwise: Bool = true) -> CABasicAnimation {
let animation = CABasicAnimation(keyPath: CABasicAnimation.rotationKeyPath)
@ -36,10 +36,8 @@ extension CABasicAnimation {
animation.duration = duration
animation.isCumulative = true
animation.repeatCount = repeatCount
if repeatCount == .infinity {
animation.isRemovedOnCompletion = false
}
return animation
}
}

Some files were not shown because too many files have changed in this diff Show More