Compare commits

..

No commits in common. "master" and "feature/tinetworking_nullable_body" have entirely different histories.

689 changed files with 1342 additions and 48070 deletions

View File

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

3
.gitignore vendored
View File

@ -104,7 +104,4 @@ cpd-output.xml
*.swp
*IDEWorkspaceChecks.plist
# Gem
.gem/
.DS_Store

2
.gitmodules vendored
View File

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

View File

@ -1,372 +1,11 @@
# 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.

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,11 +1,11 @@
Pod::Spec.new do |s|
s.name = "LeadKit"
s.version = "1.35.0"
s.version = "1.12.0"
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.source = { :git => "https://github.com/TouchInstinct/LeadKit.git", :tag => s.version }
s.platform = :ios, '10.0'
s.swift_versions = ['5.1']

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

View File

@ -1,95 +1,70 @@
{
"pins" : [
{
"identity" : "alamofire",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Alamofire/Alamofire.git",
"state" : {
"revision" : "bc268c28fb170f494de9e9927c371b8342979ece",
"version" : "5.7.1"
"object": {
"pins": [
{
"package": "Alamofire",
"repositoryURL": "https://github.com/Alamofire/Alamofire.git",
"state": {
"branch": null,
"revision": "f96b619bcb2383b43d898402283924b80e2c4bae",
"version": "5.4.3"
}
},
{
"package": "Cursors",
"repositoryURL": "https://github.com/petropavel13/Cursors",
"state": {
"branch": null,
"revision": "a1561869135e72832eff3b1e729075c56c2eebf6",
"version": "0.5.1"
}
},
{
"package": "KeychainAccess",
"repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess.git",
"state": {
"branch": null,
"revision": "84e546727d66f1adc5439debad16270d0fdd04e7",
"version": "4.2.2"
}
},
{
"package": "Moya",
"repositoryURL": "https://github.com/Moya/Moya.git",
"state": {
"branch": null,
"revision": "9b906860e3c3c09032879465c471e6375829593f",
"version": "15.0.0"
}
},
{
"package": "ReactiveSwift",
"repositoryURL": "https://github.com/ReactiveCocoa/ReactiveSwift.git",
"state": {
"branch": null,
"revision": "c43bae3dac73fdd3cb906bd5a1914686ca71ed3c",
"version": "6.7.0"
}
},
{
"package": "RxSwift",
"repositoryURL": "https://github.com/ReactiveX/RxSwift.git",
"state": {
"branch": null,
"revision": "b4307ba0b6425c0ba4178e138799946c3da594f8",
"version": "6.5.0"
}
},
{
"package": "TableKit",
"repositoryURL": "https://github.com/maxsokolov/TableKit.git",
"state": {
"branch": null,
"revision": "8bf4840d9d0475a92352f02f368f88b74eced447",
"version": "2.11.0"
}
}
},
{
"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
]
},
"version": 1
}

View File

@ -1,198 +1,65 @@
// swift-tools-version:5.7
#if canImport(PackageDescription)
// swift-tools-version:5.1
import PackageDescription
let package = Package(
name: "LeadKit",
platforms: [
.iOS(.v12)
.iOS(.v11)
],
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/maxsokolov/TableKit.git", .upToNextMajor(from: "2.11.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"),
.target(name: "TIUIKitCore", path: "TIUIKitCore/Sources"),
.target(name: "TIUIElements", dependencies: ["TIUIKitCore", "TISwiftUtils"], path: "TIUIElements/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: "TISwiftUtils", path: "TISwiftUtils/Sources"),
.target(name: "TIFoundationUtils", dependencies: ["TISwiftUtils"], path: "TIFoundationUtils"),
.target(name: "TIKeychainUtils", dependencies: ["TIFoundationUtils", "KeychainAccess"], path: "TIKeychainUtils/Sources"),
.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")]),
.target(name: "TINetworking", dependencies: ["TISwiftUtils", "Alamofire"], path: "TINetworking/Sources"),
.target(name: "TIMoyaNetworking", dependencies: ["TINetworking", "Moya"], path: "TIMoyaNetworking"),
// 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)
]
}
}

110
README.md
View File

@ -1,105 +1,23 @@
# LeadKit
LeadKit is the iOS framework with a bunch of tools for rapid app development.
This repository contains the following frameworks:
## Additional
This repository contains the following additional 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.
- [TITransitions](TITransitions) - set of custom transitions to present controller.
- [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.
- [TISwiftUtils](TISwiftUtils) - a bunch of useful helpers for development.
- [TITableKitUtils](TITableKitUtils) - Set of helpers for TableKit classes.
- [TIFoundationUtils](TIFoundationUtils) - Set of helpers for Foundation framework 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)
Useful docs:
- [Semantic Commit Messages](docs/semantic-commit-messages.md) - commit message codestyle.
- [Snippets](docs/snippets.md) - useful commands and scripts for development.
@ -110,7 +28,7 @@ LICENSE
./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).
- If legacy [Source](https://github.com/TouchInstinct/LeadKit/tree/master/Sources) folder needed, [build dependencies for LeadKit.xcodeproj](https://github.com/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).
@ -120,20 +38,16 @@ LICENSE
```swift
dependencies: [
.package(url: "https://git.svc.touchin.ru/TouchInstinct/LeadKit.git", from: "x.y.z"),
.package(url: "https://github.com/TouchInstinct/LeadKit.git", from: "x.y.z"),
],
```
### Cocoapods
```ruby
source 'https://git.svc.touchin.ru/TouchInstinct/Podspecs.git'
source 'https://github.com/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.

View File

@ -31,8 +31,8 @@ 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, response: Data?)
case invalidResponse(error: AFError, response: Data?)
case mapping(error: Error, response: Data)
}

View File

@ -80,9 +80,9 @@ public extension ObservableType where Element == DataRequest {
///
/// - Parameter statusCodes: set of status codes to validate
/// - Returns: Observable on self
func validate(statusCodes: Set<Int>, url: String? = nil) -> Observable<Element> {
func validate(statusCodes: Set<Int>) -> Observable<Element> {
map { $0.validate(statusCode: statusCodes) }
.catchAsRequestError(url: url)
.catchAsRequestError()
}
}
@ -93,9 +93,7 @@ private extension ObservableType where Element == ServerResponse {
do {
return try transform($0)
} catch {
throw RequestError.mapping(error: error,
response: $0.1,
url: $0.0.url?.absoluteString)
throw RequestError.mapping(error: error, response: $0.1)
}
}
}
@ -105,14 +103,10 @@ private extension ObservableType where Element == ServerResponse {
do {
return try transform((response, result))
.catch {
throw RequestError.mapping(error: $0,
response: result,
url: response.url?.absoluteString)
throw RequestError.mapping(error: $0, response: result)
}
} catch {
throw RequestError.mapping(error: error,
response: result,
url: response.url?.absoluteString)
throw RequestError.mapping(error: error, response: result)
}
}
}
@ -120,8 +114,7 @@ private extension ObservableType where Element == ServerResponse {
private extension ObservableType {
func catchAsRequestError(with request: DataRequest? = nil,
url: String? = nil) -> Observable<Element> {
func catchAsRequestError(with request: DataRequest? = nil) -> Observable<Element> {
self.catch { error in
let resultError: RequestError
let response = request?.data
@ -133,10 +126,10 @@ private extension ObservableType {
case let urlError as URLError:
switch urlError.code {
case .notConnectedToInternet:
resultError = .noConnection(url: url)
resultError = .noConnection
default:
resultError = .network(error: urlError, response: response, url: url)
resultError = .network(error: urlError, response: response)
}
case let afError as AFError:
@ -144,21 +137,21 @@ private extension ObservableType {
case let .sessionTaskFailed(error):
switch error {
case let urlError as URLError where urlError.code == .notConnectedToInternet:
resultError = .noConnection(url: url)
resultError = .noConnection
default:
resultError = .network(error: error, response: response, url: url)
resultError = .network(error: error, response: response)
}
case .responseSerializationFailed, .responseValidationFailed:
resultError = .invalidResponse(error: afError, response: response, url: url)
resultError = .invalidResponse(error: afError, response: response)
default:
resultError = .network(error: afError, response: response, url: url)
resultError = .network(error: afError, response: response)
}
default:
resultError = .network(error: error, response: response, url: url)
resultError = .network(error: error, response: response)
}
throw resultError

View File

@ -117,8 +117,7 @@ public extension Reactive where Base: SessionManager {
}
return requestObservable
.validate(statusCodes: self.base.acceptableStatusCodes.union(additionalValidStatusCodes),
url: url.absoluteString)
.validate(statusCodes: self.base.acceptableStatusCodes.union(additionalValidStatusCodes))
}
}
@ -192,8 +191,7 @@ public extension Reactive where Base: SessionManager {
return self.upload(data, urlRequest: urlRequest)
.map { $0 as DataRequest }
.validate(statusCodes: self.base.acceptableStatusCodes.union(additionalValidStatusCodes),
url: try? requestParameters.url.asURL().absoluteString)
.validate(statusCodes: self.base.acceptableStatusCodes.union(additionalValidStatusCodes))
.flatMap {
$0.rx.apiResponse(mappingQueue: self.base.mappingQueue, decoder: decoder)
}

View File

@ -34,7 +34,7 @@ public extension Error {
/// - Returns: optional target object
/// - Throws: an error during decoding
func handleMappingError<T: Decodable>(with decoder: JSONDecoder = JSONDecoder()) throws -> T? {
guard let self = requestError, case .mapping(_, let response, _) = self else {
guard let self = requestError, case .mapping(_, let response) = self else {
return nil
}

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2022 Touch Instinct
// 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
@ -58,7 +58,6 @@ public struct NetworkServiceConfiguration {
sessionConfiguration = URLSessionConfiguration.default
sessionConfiguration.timeoutIntervalForResource = timeoutInterval
sessionConfiguration.timeoutIntervalForRequest = timeoutInterval
sessionConfiguration.httpAdditionalHeaders = additionalHttpHeaders
serverTrustPolicies = Dictionary(uniqueKeysWithValues: trustPolicies.map { ($0.key.asHost, $0.value) })

View File

@ -1,168 +0,0 @@
//
// Copyright (c) 2022 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 TIMapUtils
import MapKit
import UIKit
open class AppleClusterPlacemarkManager<Model>: BaseClusterPlacemarkManager<MKAnnotationView,
ApplePlacemarkManager<Model>,
MKMapRect>,
MKMapViewDelegate {
public weak var mapViewDelegate: MKMapViewDelegate?
private let mapDelegateSelectors = NSObject.instanceMethodSelectors(of: MKMapViewDelegate.self)
public init<IF: MarkerIconFactory>(placemarkManagers: [ApplePlacemarkManager<Model>],
mapViewDelegate: MKMapViewDelegate? = nil,
iconFactory: IF?,
tapHandler: TapHandlerClosure?) where IF.Model == [Model] {
self.mapViewDelegate = mapViewDelegate
super.init(placemarkPosition: .from(coordinates: placemarkManagers.map(\.placemarkPosition)),
dataModel: placemarkManagers,
iconFactory: iconFactory?.asAnyMarkerIconFactory { $0.map { $0.dataModel } },
tapHandler: tapHandler)
}
open func addMarkers(to map: MKMapView) {
map.delegate = self
map.addAnnotations(dataModel)
}
open func removeMarkers(from map: MKMapView) {
map.removeAnnotations(dataModel)
}
// MARK: - PlacemarkManager
override open func configure(placemark: MKAnnotationView) {
guard let clusterAnnotation = placemark.annotation as? MKClusterAnnotation,
let placemarkManagers = clusterAnnotation.memberAnnotations as? [ApplePlacemarkManager<Model>] else {
return
}
placemark.image = iconFactory?.markerIcon(for: placemarkManagers, state: .default)
}
// MARK: - MKMapViewDelegate
open func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard !(mapViewDelegate?.responds(to: #selector(mapView(_:viewFor:))) ?? false) else {
return mapViewDelegate?.mapView?(mapView, viewFor: annotation)
}
switch annotation {
case is MKClusterAnnotation:
let defaultAnnotationView = iconFactory != nil
? MKAnnotationView(annotation: annotation,
reuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
: MKMarkerAnnotationView(annotation: annotation,
reuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
configure(placemark: defaultAnnotationView)
return defaultAnnotationView
case let placemarkManager as ApplePlacemarkManager<Model>:
let defaultAnnotationView = placemarkManager.iconFactory != nil
? MKAnnotationView(annotation: annotation,
reuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
: MKMarkerAnnotationView(annotation: annotation,
reuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
placemarkManager.configure(placemark: defaultAnnotationView)
return defaultAnnotationView
default:
return nil
}
}
open func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
guard !(mapViewDelegate?.responds(to: #selector(mapView(_:didSelect:))) ?? false) else {
mapViewDelegate?.mapView?(mapView, didSelect: view)
return
}
switch view.annotation {
case let clusterAnnotation as MKClusterAnnotation:
guard let placemarkManagers = clusterAnnotation.memberAnnotations as? [ApplePlacemarkManager<Model>] else {
return
}
_ = tapHandler?(placemarkManagers, .from(coordinates: placemarkManagers.map { $0.coordinate }))
case let placemarkManager as ApplePlacemarkManager<Model>:
let isTapHandled = placemarkManager.tapHandler?(placemarkManager.dataModel, placemarkManager.coordinate) ?? false
if isTapHandled {
placemarkManager.state = .selected
}
default:
return
}
}
open func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) {
guard !(mapViewDelegate?.responds(to: #selector(mapView(_:didDeselect:))) ?? false) else {
mapViewDelegate?.mapView?(mapView, didDeselect: view)
return
}
switch view.annotation {
case let placemarkManager as ApplePlacemarkManager<Model>:
placemarkManager.state = .default
default:
return
}
}
// MARK: - MKMapViewDelegate selectors forwarding
open override func responds(to aSelector: Selector) -> Bool {
let superResponds = super.responds(to: aSelector)
guard !superResponds else {
return superResponds
}
guard mapDelegateSelectors.contains(aSelector) else {
return superResponds
}
return mapViewDelegate?.responds(to: aSelector) ?? false
}
open override func forwardingTarget(for aSelector: Selector) -> Any? {
guard mapDelegateSelectors.contains(aSelector) else {
return nil
}
return mapViewDelegate
}
}

View File

@ -1,97 +0,0 @@
//
// Copyright (c) 2022 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 TIMapUtils
import MapKit
open class AppleMapManager<DataModel>: BaseMapManager<MKMapView,
ApplePlacemarkManager<DataModel>,
AppleClusterPlacemarkManager<DataModel>,
MKCameraUpdate,
AppleMapUISettings> {
public typealias ClusteringIdentifier = String
public init<IF: MarkerIconFactory, CIF: MarkerIconFactory>(map: MKMapView,
positionGetter: @escaping PositionGetter,
clusteringIdentifierGetter: @escaping (DataModel) -> ClusteringIdentifier,
iconFactory: IF?,
clusterIconFactory: CIF?,
mapViewDelegate: MKMapViewDelegate? = nil,
selectPlacemarkHandler: @escaping SelectPlacemarkHandler)
where IF.Model == DataModel, CIF.Model == [DataModel] {
let placemarkManagerCreator: PlacemarkManagerCreator = {
guard let position = positionGetter($0) else {
return nil
}
return ApplePlacemarkManager(map: map,
dataModel: $0,
position: position,
clusteringIdentifier: clusteringIdentifierGetter($0),
iconFactory: iconFactory?.asAnyMarkerIconFactory(),
tapHandler: $1)
}
let clusterPlacemarkManagerCreator: ClusterPlacemarkManagerCreator = {
AppleClusterPlacemarkManager(placemarkManagers: $0,
mapViewDelegate: mapViewDelegate,
iconFactory: clusterIconFactory,
tapHandler: $1)
}
super.init(map: map,
placemarkManagerCreator: placemarkManagerCreator,
clusterPlacemarkManagerCreator: clusterPlacemarkManagerCreator,
selectPlacemarkHandler: selectPlacemarkHandler)
}
public convenience init(map: MKMapView,
iconFactory: DefaultMarkerIconFactory<DataModel>? = nil,
clusterIconFactory: DefaultClusterMarkerIconFactory<DataModel>? = nil,
mapViewDelegate: MKMapViewDelegate? = nil,
selectPlacemarkHandler: @escaping SelectPlacemarkHandler)
where DataModel: MapLocatable, DataModel.Position == CLLocationCoordinate2D,
DataModel: Clusterable, DataModel.ClusterIdentifier == ClusteringIdentifier {
self.init(map: map,
positionGetter: { $0.position },
clusteringIdentifierGetter: { $0.clusterIdentifier },
iconFactory: iconFactory,
clusterIconFactory: clusterIconFactory,
mapViewDelegate: mapViewDelegate,
selectPlacemarkHandler: selectPlacemarkHandler)
}
open override func set(items: [DataModel]) {
super.set(items: items)
clusterPlacemarkManager?.addMarkers(to: map)
}
open override func remove(clusterPlacemarkManager: AppleClusterPlacemarkManager<DataModel>) {
super.remove(clusterPlacemarkManager: clusterPlacemarkManager)
clusterPlacemarkManager.removeMarkers(from: map)
}
}

View File

@ -1,58 +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 TIMapUtils
import MapKit
open class AppleMapUISettings: BaseMapUISettings<MKMapView> {
open class Defaults: BaseMapUISettings<MKMapView>.Defaults {
public static var showCompassButton: Bool {
false
}
}
public var showCompassButton = false
public init(showUserLocation: Bool = Defaults.showUserLocation,
isZoomEnabled: Bool = Defaults.isZoomEnabled,
isTiltEnabled: Bool = Defaults.isTiltEnabled,
isRotationEnabled: Bool = Defaults.isRotationEnabled,
showCompassButton: Bool = Defaults.showCompassButton) {
self.showCompassButton = showCompassButton
super.init(showUserLocation: showUserLocation,
isZoomEnabled: isZoomEnabled,
isTiltEnabled: isTiltEnabled,
isRotationEnabled: isRotationEnabled)
}
override open func apply(to mapView: MKMapView) {
super.apply(to: mapView)
mapView.showsUserLocation = showUserLocation
mapView.isZoomEnabled = isZoomEnabled
mapView.isPitchEnabled = isTiltEnabled
mapView.isRotateEnabled = isRotationEnabled
mapView.showsCompass = showCompassButton
}
}

View File

@ -1,91 +0,0 @@
//
// Copyright (c) 2022 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 TIMapUtils
import MapKit
open class ApplePlacemarkManager<Model>: BaseItemPlacemarkManager<MKAnnotationView, Model, CLLocationCoordinate2D>,
MKAnnotation {
// MARK: - MKAnnotation
/// A map where all placemarks are placed
public let map: MKMapView
/// Identifier required for correct cluster placement
public var clusteringIdentifier: String?
/// Point (coordinates) itself of the current placemark manager
public var coordinate: CLLocationCoordinate2D {
placemarkPosition
}
/// The current state of a manager's placemark
override public var state: MarkerState {
didSet {
guard let placemark = placemark else {
return
}
/*
Although the icon is being updated, it is necessary to manually deselect
the annotation of the current placemark if it is currently selected.
*/
if let annotation = placemark.annotation {
switch state {
case .default:
map.deselectAnnotation(annotation, animated: true)
case .selected:
map.selectAnnotation(annotation, animated: true)
}
}
placemark.image = iconFactory?.markerIcon(for: dataModel, state: state)
}
}
public init(map: MKMapView,
dataModel: Model,
position: CLLocationCoordinate2D,
clusteringIdentifier: String?,
iconFactory: AnyMarkerIconFactory<DataModel>?,
tapHandler: TapHandlerClosure?) {
self.map = map
self.clusteringIdentifier = clusteringIdentifier
super.init(placemarkPosition: position,
dataModel: dataModel,
iconFactory: iconFactory,
tapHandler: tapHandler)
}
// MARK: - PlacemarkManager
override open func configure(placemark: MKAnnotationView) {
super.configure(placemark: placemark)
// Setting required values of the current manager and placemark respectively
self.placemark?.clusteringIdentifier = clusteringIdentifier
self.state = .default
}
}

View File

@ -1,74 +0,0 @@
//
// Copyright (c) 2022 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 MapKit
import TIMapUtils
open class MKCameraUpdate: CameraUpdate, CameraUpdateFactory {
private let action: CameraUpdateAction<CLLocationCoordinate2D, MKMapRect>
public init(action: CameraUpdateAction<CLLocationCoordinate2D, MKMapRect>) {
self.action = action
}
// MARK: - CameraUpdateFactory
public static func update(for action: CameraUpdateAction<CLLocationCoordinate2D, MKMapRect>) -> MKCameraUpdate {
.init(action: action)
}
// MARK: - CameraUpdate
private func update(map: MKMapView, animated: Bool = true) {
switch action {
case let .focus(target, zoom):
map.set(zoomLevel: zoom,
at: target,
animated: animated)
case let .fit(bounds, insets):
map.setVisibleMapRect(bounds,
edgePadding: insets,
animated: animated)
case .zoomIn:
map.set(zoomLevel: map.zoomLevel + 1,
at: map.centerCoordinate,
animated: animated)
case .zoomOut:
map.set(zoomLevel: map.zoomLevel - 1,
at: map.centerCoordinate,
animated: animated)
}
}
public func update(map: MKMapView) {
update(map: map, animated: false)
}
public func update(map: MKMapView, animationDuration: TimeInterval) {
UIView.animate(withDuration: animationDuration) {
self.update(map: map, animated: true)
}
}
}

View File

@ -1,43 +0,0 @@
//
// Copyright (c) 2022 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 MapKit
import TIMapUtils
public extension MKMapRect {
static func from<C: Collection>(coordinates: C) -> MKMapRect where C.Element == CLLocationCoordinate2D {
guard let bbox = CLCoordinateBounds.from(coordinates: coordinates) else {
return MKMapRect.null
}
let southWest = MKMapPoint(bbox.southWest)
let northEast = MKMapPoint(bbox.northEast)
let origin = MKMapPoint(x: southWest.x,
y: northEast.y)
let size = MKMapSize(width: northEast.x - southWest.x,
height: southWest.y - northEast.y)
return MKMapRect(origin: origin, size: size)
}
}

View File

@ -1,42 +0,0 @@
//
// Copyright (c) 2022 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 MapKit
// https://stackoverflow.com/a/15020534
public extension MKMapView {
var zoomLevel: Float {
Float(log2(360 * ((frame.size.width / .expectedMapViewTileSize) / region.span.longitudeDelta))) + 1
}
func set(zoomLevel: Float, at centerCoordinate: CLLocationCoordinate2D, animated: Bool = true) {
let span = MKCoordinateSpan(latitudeDelta: 0,
longitudeDelta: 360 / pow(2, Double(zoomLevel)) * frame.size.width / .expectedMapViewTileSize)
setRegion(MKCoordinateRegion(center: centerCoordinate, span: span), animated: animated)
}
}
private extension CGFloat {
static var expectedMapViewTileSize: CGFloat {
256
}
}

View File

@ -1,42 +0,0 @@
//
// Copyright (c) 2022 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 ObjectiveC
public extension NSObject {
static func instanceMethodSelectors(of protocol: Protocol) -> [Selector] {
var methodsCount: UInt32 = 0
let methodsList = protocol_copyMethodDescriptionList(`protocol`, false, true, &methodsCount)
defer {
methodsList?.deallocate()
}
var runtimeSelectors: [Selector?] = []
for offset in 0..<Int(methodsCount) {
runtimeSelectors.append(methodsList?.advanced(by: offset).pointee.name)
}
return runtimeSelectors.compactMap { $0 }
}
}

View File

@ -1,23 +0,0 @@
Pod::Spec.new do |s|
s.name = 'TIAppleMapUtils'
s.version = '1.56.0'
s.summary = 'Set of helpers for map objects clustering and interacting using Apple MapKit.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
s.ios.deployment_target = '12.0'
s.swift_versions = ['5.7']
sources = 'Sources/**/*'
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
s.dependency 'TIMapUtils', s.version.to_s
end

View File

@ -1,54 +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 Foundation
public struct BundleIdentifier {
public let appPrefix: String
public let appIdentifier: String
public let fullIdentifier: String
public let defaultAppGroupIdenfier: String
static let bundleIdentifierSeparator = "."
public init(appPrefix: String, appIdentifier: String) {
self.appPrefix = appPrefix
self.appIdentifier = appIdentifier
self.fullIdentifier = appPrefix + Self.bundleIdentifierSeparator + appIdentifier
self.defaultAppGroupIdenfier = "group." + fullIdentifier
}
public init?(bundle: Bundle = .main) {
guard let fullIdenfifier = bundle.bundleIdentifier,
var components = bundle.bundleIdentifier?
.components(separatedBy: Self.bundleIdentifierSeparator),
let lastComponent = components.popLast() else {
return nil
}
self.fullIdentifier = fullIdenfifier
self.appIdentifier = lastComponent
self.appPrefix = components.joined(separator: Self.bundleIdentifierSeparator)
self.defaultAppGroupIdenfier = "group." + fullIdentifier
}
}

View File

@ -1,116 +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 Foundation
import TIFoundationUtils
import KeychainAccess
import TILogging
import UIKit
public struct CoreDependencies {
public var dateFormattersResusePool = DateFormattersReusePool()
public var iso8601DateFormattersReusePool = ISO8601DateFormattersReusePool()
public var jsonCodingConfigurator: JsonCodingConfigurator
public var jsonKeyValueDecoder: JSONKeyValueDecoder
public var jsonKeyValueEncoder: JSONKeyValueEncoder
public var device: UIDevice = .current
public var bundle: Bundle = .main
public var fileManager: FileManager = .default
public var logger: DefaultOSLogErrorLogger
public var keychain: Keychain
public var defaults: UserDefaults
public var appGroupDefaults: UserDefaults?
public var appGroupKeychain: Keychain?
public var appGroupCacheDirectory: URL?
public var networkCallbackQueue: DispatchQueue
public init(bundleIdentifier: BundleIdentifier,
customAppGroupIdentifier: String? = nil) {
jsonCodingConfigurator = JsonCodingConfigurator(dateFormattersReusePool: dateFormattersResusePool,
iso8601DateFormattersReusePool: iso8601DateFormattersReusePool)
jsonKeyValueDecoder = JSONKeyValueDecoder(jsonDecoder: jsonCodingConfigurator.jsonDecoder)
jsonKeyValueEncoder = JSONKeyValueEncoder(jsonEncoder: jsonCodingConfigurator.jsonEncoder)
logger = DefaultOSLogErrorLogger(subsystem: bundleIdentifier.fullIdentifier, category: "general")
keychain = Keychain(service: bundleIdentifier.fullIdentifier).accessibility(.whenUnlockedThisDeviceOnly)
defaults = .standard
let appGroupIdentifier = customAppGroupIdentifier ?? bundleIdentifier.defaultAppGroupIdenfier
if let containerURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) {
var appGroupCacheURL: URL
if #available(iOS 16.0, *) {
appGroupCacheURL = containerURL.appending(path: "Caches", directoryHint: .isDirectory)
} else {
appGroupCacheURL = containerURL.appendingPathComponent("Caches", isDirectory: true)
}
var resourceValues = URLResourceValues()
resourceValues.isExcludedFromBackup = true
do {
try fileManager.createDirectory(at: appGroupCacheURL,
withIntermediateDirectories: true)
try appGroupCacheURL.setResourceValues(resourceValues)
appGroupCacheDirectory = appGroupCacheURL
} catch {
logger.log(error: error, file: #file, line: #line)
}
appGroupDefaults = UserDefaults(suiteName: appGroupIdentifier)
appGroupKeychain = Keychain(service: bundleIdentifier.appPrefix,
accessGroup: appGroupIdentifier)
.accessibility(.whenUnlockedThisDeviceOnly)
} else {
appGroupCacheDirectory = nil
appGroupDefaults = nil
appGroupKeychain = nil
}
networkCallbackQueue = DispatchQueue(label: bundleIdentifier.fullIdentifier + ".network-callback-queue",
attributes: .concurrent)
}
public init?(bundle: Bundle = .main,
customAppGroupIdentifier: String? = nil) {
guard let bundleIdentifier = BundleIdentifier(bundle: bundle) else {
return nil
}
self.init(bundleIdentifier: bundleIdentifier,
customAppGroupIdentifier: customAppGroupIdentifier)
}
}

View File

@ -1,26 +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.
//
public protocol TargetDependencies {
static func assemble() -> Self
static func assembleForPreview() -> Self
}

View File

@ -1,25 +0,0 @@
Pod::Spec.new do |s|
s.name = 'TIApplication'
s.version = '1.56.0'
s.summary = 'Application architecture.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
s.ios.deployment_target = '12.0'
s.swift_versions = ['5.7']
sources = 'Sources/**/*'
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
s.dependency 'TIFoundationUtils', s.version.to_s
s.dependency 'TILogging', s.version.to_s
s.dependency 'KeychainAccess', "~> 4.2"
end

View File

@ -1,14 +0,0 @@
# TIAuth
Login, registration, confirmation and other related actions
## CodeConfirmPresenter
### Features
- Code confirm and code refresh actions
- Code refresh countdown in foreground and background
- Additional 2FA auth handling
- Remaining attempts handling
- Code autofill from custom sources (push, etc)
- UIKit and SwiftUI compatible

View File

@ -1,35 +0,0 @@
//
// Copyright (c) 2022 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
import LocalAuthentication
public protocol BiometryService {
var isBiometryAuthAvailable: Bool { get }
var biometryType: LABiometryType { get }
}
extension LAContext: BiometryService {
public var isBiometryAuthAvailable: Bool {
canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
}
}

View File

@ -1,27 +0,0 @@
//
// Copyright (c) 2022 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
public protocol BiometrySettingsStorage {
var isBiometryAuthEnabled: Bool { get set }
}

View File

@ -1,48 +0,0 @@
//
// Copyright (c) 2022 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 DefaultBiometrySettingsStorage: BiometrySettingsStorage {
public enum StorageKeys {
static var isBiometryAuthEnabledStorageKey: String {
"isBiometryAuthEnabled"
}
}
public var defaultsStorage: UserDefaults
// MARK: - BiometrySettingsService
public var isBiometryAuthEnabled: Bool {
get {
defaultsStorage.bool(forKey: StorageKeys.isBiometryAuthEnabledStorageKey)
}
set {
defaultsStorage.set(newValue, forKey: StorageKeys.isBiometryAuthEnabledStorageKey)
}
}
public init(defaultsStorage: UserDefaults = .standard) {
self.defaultsStorage = defaultsStorage
}
}

View File

@ -1,36 +0,0 @@
//
// Copyright (c) 2022 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
import TIUIKitCore
@MainActor
protocol CodeConfirmPresenter: LifecyclePresenter {
// MARK: - User actions handling
func inputChanged(newInput: String?)
func refreshCode()
// MARK: - Autofill
func autofill(code: String, with codeId: String?)
}

View File

@ -1,30 +0,0 @@
//
// Copyright (c) 2022 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.
//
@MainActor
public protocol CodeConfirmStateStorage: AnyObject {
var currentUserInput: String? { get set }
var canRefreshCodeAfter: Int? { get set }
var remainingAttempts: Int? { get set }
var isExecutingRequest: Bool { get set }
var canRequestNewCode: Bool { get set }
}

View File

@ -1,301 +0,0 @@
//
// Copyright (c) 2022 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
import TIFoundationUtils
@available(iOS 13.0, *)
open class DefaultCodeConfirmPresenter<ConfirmResponse: CodeConfirmResponse,
RefreshResponse: CodeRefreshResponse>: CodeConfirmPresenter {
open class Output {
public typealias OnConfirmSuccessClosure = (ConfirmResponse) -> Void
public var onConfirmSuccess: OnConfirmSuccessClosure
public init(onConfirmSuccess: @escaping OnConfirmSuccessClosure) {
self.onConfirmSuccess = onConfirmSuccess
}
}
open class Requests {
public typealias ConfirmRequestClosure = (String) async -> ConfirmResponse
public typealias RefreshRequestClosure = () async -> RefreshResponse
public var confirmRequest: ConfirmRequestClosure
public var refreshRequest: RefreshRequestClosure
public init(confirmRequest: @escaping ConfirmRequestClosure,
refreshRequest: @escaping RefreshRequestClosure) {
self.confirmRequest = confirmRequest
self.refreshRequest = refreshRequest
}
}
public struct Config {
public enum Defaults {
public static var codeLength: Int {
6
}
public static var autoRefresh: Bool {
true
}
}
public var codeLength: Int
public var autoRefresh: Bool
public init(codeLength: Int = Defaults.codeLength,
autoRefresh: Bool = Defaults.autoRefresh) {
self.codeLength = codeLength
self.autoRefresh = autoRefresh
}
}
private let codeRefreshTimer = TITimer(mode: .activeAndBackground)
private let codeLifetimeTimer = TITimer(mode: .activeAndBackground)
private var executingTask: Cancellable?
public var output: Output
public var requests: Requests
public weak var stateStorage: CodeConfirmStateStorage?
public var config = Config()
public var currentCodeResponse: CodeResponse
public init<Input: CodeResponse>(input: Input,
output: Output,
requests: Requests,
stateStorage: CodeConfirmStateStorage? = nil) {
self.currentCodeResponse = input
self.output = output
self.requests = requests
self.stateStorage = stateStorage
}
// MARK: - Requests
open func confirm(code: String) async {
stateStorage?.isExecutingRequest = true
let confirmResponse = await requests.confirmRequest(code)
isSuccessConfirm(response: confirmResponse)
? handle(successConfirmResponse: confirmResponse)
: handle(failureConfirmResponse: confirmResponse)
stateStorage?.isExecutingRequest = false
}
open func refreshCode() async {
stateStorage?.isExecutingRequest = true
let refreshResponse = await requests.refreshRequest()
if isSuccessRefresh(response: refreshResponse) {
handle(successRefreshResponse: refreshResponse)
} else {
handle(failureRefreshResponse: refreshResponse)
}
stateStorage?.isExecutingRequest = false
}
// MARK: - Response handling
open func handle(successConfirmResponse response: ConfirmResponse) {
if let additionalAuth = response.requiredAdditionalAuth {
handle(additionalAuth: additionalAuth)
} else {
output.onConfirmSuccess(response)
}
}
open func handle(failureConfirmResponse: ConfirmResponse) {
stateStorage?.currentUserInput = nil
if let remainingAttempts = failureConfirmResponse.remainingAttempts, remainingAttempts <= 0 {
onConfirmAttemptsExhausted()
}
// show error message, etc.
}
open func handle(additionalAuth auth: String) {
// custom subclass handling
}
open func onConfirmAttemptsExhausted() {
// custom subclass handling
}
open func handle(successRefreshResponse response: RefreshResponse) {
updateStateStorage(from: response)
start(codeLifetimeTimer: codeLifetimeTimer,
codeRefreshTimer: codeRefreshTimer,
for: response)
}
func handle(failureRefreshResponse response: RefreshResponse) {
updateStateStorage(from: response)
// show error message, etc.
}
// MARK: - Response processing
open func lifetimeDuration(of code: CodeResponse) -> Int? {
code.validUntil?.timeIntervalSinceNow.intValue
}
open func nonRefreshableInterval(of code: CodeResponse) -> Int? {
code.refreshableAfter?.timeIntervalSinceNow.intValue ?? lifetimeDuration(of: code)
}
open func isSuccessConfirm(response: ConfirmResponse) -> Bool {
true
}
open func isSuccessRefresh(response: RefreshResponse) -> Bool {
true
}
// MARK: - User actions handling
open func inputChanged(newInput: String?) {
stateStorage?.currentUserInput = newInput
if let code = newInput, code.count >= config.codeLength {
stateStorage?.isExecutingRequest = true
executingTask = Task {
await confirm(code: code)
}
}
}
open func refreshCode() {
stateStorage?.canRequestNewCode = false
stateStorage?.canRefreshCodeAfter = nil
executingTask = Task {
await refreshCode()
}
}
// MARK: - View lifecycle handling
open func viewDidPresented() {
start(codeLifetimeTimer: codeLifetimeTimer,
codeRefreshTimer: codeRefreshTimer,
for: currentCodeResponse)
}
open func viewWillDestroy() {
executingTask?.cancel()
}
// MARK: - Autofill
open func autofill(code: String, with codeId: String? = nil) {
guard currentCodeResponse.codeId == codeId else {
return
}
inputChanged(newInput: code)
}
// MARK: - Subclass customization
open func start(codeLifetimeTimer: TITimer,
codeRefreshTimer: TITimer,
for code: CodeResponse) {
start(codeRefreshTimer: codeRefreshTimer, for: code)
start(codeLifetimeTimer: codeLifetimeTimer, for: code)
}
open func start(codeRefreshTimer: TITimer, for code: CodeResponse) {
guard let nonRefreshableInterval = nonRefreshableInterval(of: code) else {
return
}
codeRefreshTimer.eventHandler = { [weak self] in
self?.updateRemaining(nonRefreshableInterval: nonRefreshableInterval,
elapsedInterval: $0)
}
codeRefreshTimer.start()
}
open func updateRemaining(nonRefreshableInterval: Int, elapsedInterval: TimeInterval) {
let secondsLeft = nonRefreshableInterval - elapsedInterval.intValue
stateStorage?.canRefreshCodeAfter = secondsLeft
stateStorage?.canRequestNewCode = secondsLeft <= 0
if secondsLeft < 0 {
codeRefreshTimer.pause()
}
}
open func start(codeLifetimeTimer: TITimer, for code: CodeResponse) {
guard let lifetimeInterval = lifetimeDuration(of: code) else {
return
}
codeLifetimeTimer.eventHandler = { [weak self] in
self?.updateRemaining(lifetimeInterval: lifetimeInterval,
elapsedInterval: $0)
}
codeLifetimeTimer.start()
}
open func updateRemaining(lifetimeInterval: Int, elapsedInterval: TimeInterval) {
let secondsLeft = lifetimeInterval - elapsedInterval.intValue
if secondsLeft < 0 {
codeLifetimeTimer.pause()
if config.autoRefresh {
refreshCode()
}
}
}
open func updateStateStorage(from response: CodeResponse) {
stateStorage?.remainingAttempts = response.remainingAttempts
stateStorage?.currentUserInput = nil
stateStorage?.canRefreshCodeAfter = nonRefreshableInterval(of: response)
stateStorage?.canRequestNewCode = (stateStorage?.canRefreshCodeAfter ?? 0) <= 0
}
}
private extension TimeInterval {
var intValue: Int {
Int(self)
}
}

View File

@ -1,93 +0,0 @@
//
// Copyright (c) 2022 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
import CommonCrypto
open class AESCipher: Cipher {
public struct CryptError: Error {
public let ccCryptorStatus: Int32
public let key: Data
public let iv: Data
}
public var iv: Data
public var key: Data
public init(iv: Data, key: Data) {
self.iv = iv
self.key = key
}
// MARK: - Cipher
public func encrypt(data: Data) -> Result<Data, CipherError> {
crypt(data: data, operation: CCOperation(kCCEncrypt))
}
public func decrypt(data: Data) -> Result<Data, CipherError> {
crypt(data: data, operation: CCOperation(kCCDecrypt))
}
private func crypt(data: Data, operation: CCOperation) -> Result<Data, CipherError> {
let cryptDataLength = data.count + kCCBlockSizeAES128
var cryptData = Data(count: cryptDataLength)
var bytesLength = Int.zero
let status = cryptData.withUnsafeMutableBytes { cryptBytes in
data.withUnsafeBytes { dataBytes in
iv.withUnsafeBytes { ivBytes in
key.withUnsafeBytes { keyBytes in
CCCrypt(operation,
CCAlgorithm(kCCAlgorithmAES),
CCOptions(kCCOptionPKCS7Padding),
keyBytes.baseAddress,
key.count,
ivBytes.baseAddress,
dataBytes.baseAddress,
data.count,
cryptBytes.baseAddress,
cryptDataLength,
&bytesLength)
}
}
}
}
guard status == kCCSuccess else {
let error = CryptError(ccCryptorStatus: status,
key: key,
iv: iv)
if operation == kCCEncrypt {
return .failure(.failedToEncrypt(data: data, error: error))
} else {
return .failure(.failedToDecrypt(encryptedData: data, error: error))
}
}
cryptData.removeSubrange(bytesLength..<cryptData.count)
return .success(cryptData)
}
}

View File

@ -1,28 +0,0 @@
//
// Copyright (c) 2022 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
public protocol Cipher {
func encrypt(data: Data) -> Result<Data, CipherError>
func decrypt(data: Data) -> Result<Data, CipherError>
}

View File

@ -1,28 +0,0 @@
//
// Copyright (c) 2022 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
public enum CipherError: Error {
case failedToEncrypt(data: Data, error: Error)
case failedToDecrypt(encryptedData: Data, error: Error)
}

View File

@ -1,78 +0,0 @@
//
// Copyright (c) 2022 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
import CommonCrypto
open class DefaultPBKDF2PasswordDerivator: PasswordDerivator {
public struct CryptError: Error {
public let derivationStatus: Int32
public let password: String
public let salt: Data
func asCipherError() -> CipherError {
var mutablePassword = password
return .failedToEncrypt(data: mutablePassword.withUTF8 { Data($0) },
error: self)
}
}
public func derive(password: String, salt: Data) -> Result<Data, CipherError> {
var failureResult: Result<Data, CipherError>?
let derivedKeyBytes = Array<UInt8>(unsafeUninitializedCapacity: CryptoConstants.keyLength) { derivedKeyBuffer, initializedCount in
guard let derivedKeyStartAddress = derivedKeyBuffer.baseAddress else {
failureResult = .failure(CryptError(derivationStatus: CCStatus(kCCMemoryFailure),
password: password,
salt: salt)
.asCipherError())
initializedCount = .zero
return
}
let deriviationStatus = salt.withContiguousStorageIfAvailable { saltBytes in
CCKeyDerivationPBKDF(
CCPBKDFAlgorithm(kCCPBKDF2),
password,
password.count,
saltBytes.baseAddress,
salt.count,
CCPBKDFAlgorithm(kCCPRFHmacAlgSHA512),
UInt32(CryptoConstants.pbkdf2NumberOfIterations),
derivedKeyStartAddress,
CryptoConstants.keyLength)
} ?? CCStatus(kCCParamError)
guard deriviationStatus == kCCSuccess else {
initializedCount = .zero
failureResult = .failure(CryptError(derivationStatus: deriviationStatus,
password: password,
salt: salt)
.asCipherError())
return
}
}
return failureResult ?? .success(Data(derivedKeyBytes))
}
}

View File

@ -1,41 +0,0 @@
//
// Copyright (c) 2022 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 DefaultSaltPreprocessor: SaltPreprocessor {
public typealias DeviceIdProviderClosure = () -> String?
private let deviceIdProvider: DeviceIdProviderClosure
public init(deviceIdProvider: @escaping DeviceIdProviderClosure) {
self.deviceIdProvider = deviceIdProvider
}
// MARK: - SaltPreprocessor
public func preprocess(salt: Data) -> Data {
deviceIdProvider().map {
salt + Data($0.utf8)
} ?? salt
}
}

View File

@ -1,98 +0,0 @@
//
// Copyright (c) 2022 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
import Security
open class DefaultTokenCipher: TokenCipher {
public var saltPreprocessor: SaltPreprocessor
public var passwordDerivator: PasswordDerivator
public init(saltPreprocessor: SaltPreprocessor, passwordDerivator: PasswordDerivator) {
self.saltPreprocessor = saltPreprocessor
self.passwordDerivator = passwordDerivator
}
open func createCipher(iv: Data, key: Data) -> Cipher {
AESCipher(iv: iv, key: key)
}
open func generateIV() -> Data {
generateRandomData(count: CryptoConstants.ivLength)
}
open func generateSalt() -> Data {
generateRandomData(count: CryptoConstants.saltLength)
}
open func generateRandomData(count: Int) -> Data {
let randomBytes = Array<UInt8>(unsafeUninitializedCapacity: count) { buffer, initializedCount in
guard let startAddress = buffer.baseAddress,
SecRandomCopyBytes(kSecRandomDefault,
count,
startAddress) == errSecSuccess else {
initializedCount = .zero
return
}
initializedCount = count
}
return Data(randomBytes)
}
// MARK: - TokenCipher
open func derive(password: String, using salt: Data) -> Result<Data, CipherError> {
passwordDerivator.derive(password: password,
salt: saltPreprocessor.preprocess(salt: salt))
}
open func encrypt(token: Data, using password: String) -> Result<StringEncryptionResult, CipherError> {
let iv = generateIV()
let salt = generateSalt()
return derive(password: password, using: salt)
.flatMap {
createCipher(iv: iv, key: $0)
.encrypt(data: token)
.map {
StringEncryptionResult(salt: salt, iv: iv, value: $0)
}
}
}
open func decrypt(token: StringEncryptionResult, using key: Data) -> Result<Data, CipherError> {
createCipher(iv: token.iv, key: key)
.decrypt(data: token.value)
}
open func decrypt(token: StringEncryptionResult, using password: String) -> Result<Data, CipherError> {
passwordDerivator.derive(password: password,
salt: saltPreprocessor.preprocess(salt: token.salt))
.flatMap {
createCipher(iv: token.iv,
key: $0)
.decrypt(data: token.value)
}
}
}

View File

@ -1,27 +0,0 @@
//
// Copyright (c) 2022 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
public protocol SaltPreprocessor {
func preprocess(salt: Data) -> Data
}

View File

@ -1,31 +0,0 @@
//
// Copyright (c) 2022 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
public protocol TokenCipher {
func derive(password: String, using salt: Data) -> Result<Data, CipherError>
func encrypt(token: Data, using password: String) -> Result<StringEncryptionResult, CipherError>
func decrypt(token: StringEncryptionResult, using key: Data) -> Result<Data, CipherError>
func decrypt(token: StringEncryptionResult, using password: String) -> Result<Data, CipherError>
}

View File

@ -1,35 +0,0 @@
//
// Copyright (c) 2022 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.
//
public struct EqualDigitsValidationRule: ValidationRule {
private let minEqualDigits: UInt
public init(minEqualDigits: UInt) {
self.minEqualDigits = minEqualDigits
}
// MARK: - ValidationRule
public func validate(input: String) -> Bool {
!input.containsSequenceOfEqualCharacters(minEqualCharacters: minEqualDigits)
}
}

View File

@ -1,39 +0,0 @@
//
// Copyright (c) 2022 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.
//
public struct OrderedDigitsValidationRule: ValidationRule {
private let ascendingSequence: Bool
private let minLength: UInt
public init(ascendingSequence: Bool, minLength: UInt) {
self.ascendingSequence = ascendingSequence
self.minLength = minLength
}
// MARK: - ValidationRule
public func validate(input: String) -> Bool {
ascendingSequence
? !input.containsAscendingSequence(minLength: minLength)
: !input.containsDescendingSequence(minLength: minLength)
}
}

View File

@ -1,91 +0,0 @@
//
// Copyright (c) 2022 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.
//
private extension Substring.SubSequence {
func recursivePairCheck(requiredMatches: UInt,
sequenceRequiredMatches: UInt,
checkClosure: (Character, Character) -> Bool) -> Bool {
guard sequenceRequiredMatches > 0 else {
return true
}
guard !isEmpty else {
return false
}
let tail = dropFirst()
guard let current = first, let next = tail.first else {
return false
}
let matched = checkClosure(current, next)
let reducedMatches = sequenceRequiredMatches - (matched ? 1 : 0)
let currentSequenceMatch = matched
&& tail.recursivePairCheck(requiredMatches: requiredMatches,
sequenceRequiredMatches: reducedMatches,
checkClosure: checkClosure)
return currentSequenceMatch || tail.recursivePairCheck(requiredMatches: requiredMatches,
sequenceRequiredMatches: requiredMatches,
checkClosure: checkClosure)
}
func recursivePairCheck(requiredMatches: UInt, checkClosure: (Character, Character) -> Bool) -> Bool {
recursivePairCheck(requiredMatches: requiredMatches,
sequenceRequiredMatches: requiredMatches,
checkClosure: checkClosure)
}
func containsOrderedSequence(minLength: UInt, orderingClosure: ((Int, Int) -> Bool)) -> Bool {
recursivePairCheck(requiredMatches: minLength - 1) {
guard let current = $0.intValue, let next = $1.intValue else {
return false
}
return orderingClosure(current, next)
}
}
}
private extension Character {
var intValue: Int? {
return Int(String(self))
}
}
extension String {
func containsSequenceOfEqualCharacters(minEqualCharacters: UInt) -> Bool {
Substring(self).recursivePairCheck(requiredMatches: minEqualCharacters - 1) { $0 == $1 }
}
func containsAscendingSequence(minLength: UInt) -> Bool {
Substring(self).containsOrderedSequence(minLength: minLength) { $0 + 1 == $1 }
}
func containsDescendingSequence(minLength: UInt) -> Bool {
Substring(self).containsOrderedSequence(minLength: minLength) { $0 - 1 == $1 }
}
}

View File

@ -1,25 +0,0 @@
//
// Copyright (c) 2022 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.
//
public protocol ValidationRule {
func validate(input: String) -> Bool
}

View File

@ -1,39 +0,0 @@
//
// Copyright (c) 2022 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 DefaultInputValidator<Violation: Hashable>: InputValidator {
public var rules: [Violation: ValidationRule]
public init(rules: [Violation: ValidationRule]) {
self.rules = rules
}
convenience init(violations: [Violation], rulesCreator: (Violation) -> ValidationRule) {
self.init(rules: .init(uniqueKeysWithValues: violations.map { ($0, rulesCreator($0)) }) )
}
// MARK: - InputValidator
open func validate(input: String) -> Set<Violation> {
Set(rules.filter { !$0.value.validate(input: input) }.keys)
}
}

View File

@ -1,35 +0,0 @@
//
// Copyright (c) 2022 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.
//
public enum DefaultViolation: Hashable {
case orderedDigits(ascending: Bool, minLength: UInt)
case equalDigits(minEqualDigits: UInt)
public var defaultValidationRule: ValidationRule {
switch self {
case let .orderedDigits(ascending, minLength):
return OrderedDigitsValidationRule(ascendingSequence: ascending, minLength: minLength)
case let .equalDigits(minEqualDigits):
return EqualDigitsValidationRule(minEqualDigits: minEqualDigits)
}
}
}

View File

@ -1,29 +0,0 @@
//
// Copyright (c) 2022 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.
//
public protocol InputValidator {
associatedtype Violation: Hashable
var rules: [Violation: ValidationRule] { get set }
func validate(input: String) -> Set<Violation>
}

View File

@ -1,25 +0,0 @@
//
// Copyright (c) 2022 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.
//
public protocol CodeConfirmResponse: CodeResponse {
var requiredAdditionalAuth: String? { get }
}

View File

@ -1,24 +0,0 @@
//
// Copyright (c) 2022 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.
//
public protocol CodeRefreshResponse: CodeResponse {
}

View File

@ -1,31 +0,0 @@
//
// Copyright (c) 2022 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
public protocol CodeResponse {
var validUntil: Date? { get }
var codeId: String? { get }
var refreshableAfter: Date? { get }
var confirmationId: String? { get }
var remainingAttempts: Int? { get }
}

View File

@ -1,68 +0,0 @@
//
// Copyright (c) 2022 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
public extension Result {
var success: Success? {
if case let .success(wrapped) = self {
return wrapped
}
return nil
}
}
extension Result: CodeResponse where Success: CodeResponse {
public var validUntil: Date? {
success?.validUntil
}
public var codeId: String? {
success?.codeId
}
public var refreshableAfter: Date? {
success?.refreshableAfter
}
public var confirmationId: String? {
success?.confirmationId
}
public var remainingAttempts: Int? {
success?.remainingAttempts
}
}
extension Result: CodeRefreshResponse where Success: CodeRefreshResponse {
}
extension Result: CodeConfirmResponse where Success: CodeConfirmResponse {
public var remainingAttempts: Int? {
success?.remainingAttempts
}
public var requiredAdditionalAuth: String? {
success?.requiredAdditionalAuth
}
}

View File

@ -1,41 +0,0 @@
//
// Copyright (c) 2022 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
enum CryptoConstants {
static var saltLength: Int {
32
}
static var ivLength: Int {
16
}
static var keyLength: Int {
32
}
static var pbkdf2NumberOfIterations: Int {
8192
}
}

View File

@ -1,72 +0,0 @@
//
// Copyright (c) 2022 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 KeychainAccess
import Foundation
import TIFoundationUtils
import LocalAuthentication
open class DefaultEncryptedTokenKeyStorage: SingleValueAuthKeychainStorage<Data> {
open class Defaults: SingleValueAuthKeychainStorage<StringEncryptionResult>.Defaults {
public static var encryptedTokenKeyStorageKey: StorageKey<Data> {
.init(rawValue: keychainServiceIdentifier + ".encryptedTokenKey")
}
public static var reusableLAContext: LAContext {
let context = LAContext()
context.touchIDAuthenticationAllowableReuseDuration = LATouchIDAuthenticationMaximumAllowableReuseDuration
return context
}
}
public init(keychain: Keychain = Keychain(service: Defaults.keychainServiceIdentifier),
localAuthContext: LAContext = Defaults.reusableLAContext,
encryptedTokenKeyStorageKey: StorageKey<Data> = Defaults.encryptedTokenKeyStorageKey,
appFirstRunCheckStorage: BoolValueDefaultsStorage = DefaultResetAuthSettingsStorage()) {
let getValueClosure: GetValueClosure = { keychain, storageKey in
do {
guard let value = try keychain.getData(storageKey.rawValue) else {
return .failure(.valueNotFound)
}
return .success(value)
} catch {
return .failure(.unableToExtractData(underlyingError: error))
}
}
let storeValueClosure: StoreValueClosure = { keychain, value, storageKey in
do {
return .success(try keychain.set(value, key: storageKey.rawValue))
} catch {
return .failure(.unableToWriteData(underlyingError: error))
}
}
super.init(keychain: keychain.authenticationContext(localAuthContext),
storageKey: encryptedTokenKeyStorageKey,
getValueClosure: getValueClosure,
storeValueClosure: storeValueClosure,
appFirstRunCheckStorage: appFirstRunCheckStorage)
}
}

View File

@ -1,68 +0,0 @@
//
// Copyright (c) 2022 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
import TIFoundationUtils
import KeychainAccess
open class DefaultEncryptedTokenStorage: SingleValueAuthKeychainStorage<StringEncryptionResult> {
open class Defaults: SingleValueAuthKeychainStorage<StringEncryptionResult>.Defaults {
public static var encryptedTokenStorageKey: StorageKey<StringEncryptionResult> {
.init(rawValue: keychainServiceIdentifier + ".encryptedToken")
}
}
public init(keychain: Keychain = Keychain(service: Defaults.keychainServiceIdentifier),
encryptedTokenStorageKey: StorageKey<StringEncryptionResult> = Defaults.encryptedTokenStorageKey,
appFirstRunCheckStorage: BoolValueDefaultsStorage = DefaultResetAuthSettingsStorage()) {
let getValueClosure: GetValueClosure = { keychain, storageKey in
do {
guard let value = try keychain.getData(storageKey.rawValue) else {
return .failure(.valueNotFound)
}
do {
return .success(try StringEncryptionResult(storableData: value))
} catch {
return .failure(.unableToDecode(underlyingError: error))
}
} catch {
return .failure(.unableToExtractData(underlyingError: error))
}
}
let storeValueClosure: StoreValueClosure = { keychain, value, storageKey in
do {
return .success(try keychain.set(value.asStorableData(), key: storageKey.rawValue))
} catch {
return .failure(.unableToWriteData(underlyingError: error))
}
}
super.init(keychain: keychain,
storageKey: encryptedTokenStorageKey,
getValueClosure: getValueClosure,
storeValueClosure: storeValueClosure,
appFirstRunCheckStorage: appFirstRunCheckStorage)
}
}

View File

@ -1,38 +0,0 @@
//
// Copyright (c) 2022 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 TIFoundationUtils
import Foundation
open class DefaultResetAuthSettingsStorage: DefaultAppFirstRunCheckStorage {
public enum Defaults {
public static var shouldResetAuthDataKey: StorageKey<Bool> {
.init(rawValue: "shouldResetAuthData")
}
}
public override init(defaults: UserDefaults = .standard,
storageKey: StorageKey<Bool> = Defaults.shouldResetAuthDataKey) {
super.init(defaults: defaults, storageKey: storageKey)
}
}

View File

@ -1,53 +0,0 @@
//
// Copyright (c) 2022 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 TIFoundationUtils
import KeychainAccess
import Foundation
import TIKeychainUtils
open class SingleValueAuthKeychainStorage<ValueType>:
AppInstallLifetimeSingleValueStorage<BaseSingleValueKeychainStorage<ValueType>, BoolValueDefaultsStorage> {
public typealias GetValueClosure = BaseSingleValueKeychainStorage<ValueType>.GetValueClosure
public typealias StoreValueClosure = BaseSingleValueKeychainStorage<ValueType>.StoreValueClosure
open class Defaults {
public static var keychainServiceIdentifier: String {
Bundle.main.bundleIdentifier ?? "ru.touchin.TIAuth"
}
}
public init(keychain: Keychain = Keychain(service: Defaults.keychainServiceIdentifier),
storageKey: StorageKey<ValueType>,
getValueClosure: @escaping GetValueClosure,
storeValueClosure: @escaping StoreValueClosure,
appFirstRunCheckStorage: BoolValueDefaultsStorage = DefaultResetAuthSettingsStorage()) {
let keychainStorage = BaseSingleValueKeychainStorage(keychain: keychain,
storageKey: storageKey,
getValueClosure: getValueClosure,
storeValueClosure: storeValueClosure)
super.init(storage: keychainStorage, appFirstRunCheckStorage: appFirstRunCheckStorage)
}
}

View File

@ -1,67 +0,0 @@
//
// Copyright (c) 2022 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
public struct StringEncryptionResult {
public struct DataRangeMismatch: Error {
public let dataLength: Int
public let valueRangeLowerBound: Int
}
public let salt: Data
public let iv: Data
public let value: Data
private static var saltRange: Range<Int> {
.zero..<CryptoConstants.saltLength
}
private static var ivRange: Range<Int> {
saltRange.endIndex..<CryptoConstants.saltLength + CryptoConstants.ivLength
}
private static var valueRange: PartialRangeFrom<Int> {
ivRange.endIndex...
}
public init(salt: Data, iv: Data, value: Data) {
self.salt = salt
self.iv = iv
self.value = value
}
public init(storableData: Data) throws {
guard Self.valueRange.contains(storableData.endIndex) else {
throw DataRangeMismatch(dataLength: storableData.count,
valueRangeLowerBound: Self.valueRange.lowerBound)
}
self.init(salt: storableData[Self.saltRange],
iv: storableData[Self.ivRange],
value: storableData[Self.valueRange])
}
public func asStorableData() -> Data {
salt + iv + value
}
}

View File

@ -1,25 +0,0 @@
Pod::Spec.new do |s|
s.name = 'TIAuth'
s.version = '1.56.0'
s.summary = 'Login, registration, confirmation and other related actions'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' }
s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
s.ios.deployment_target = '13.0'
s.swift_versions = ['5.7']
sources = 'Sources/**/*'
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
s.dependency 'TIFoundationUtils', s.version.to_s
s.dependency 'TIKeychainUtils', s.version.to_s
s.dependency 'TIUIKitCore', s.version.to_s
end

View File

@ -1,14 +0,0 @@
ENV["DEVELOPMENT_INSTALL"] = "true"
source 'https://git.svc.touchin.ru/TouchInstinct/Podspecs.git'
target 'TIBottomSheet' do
platform :ios, 11
use_frameworks!
pod 'TIUIElements', :path => '../../../../TIUIElements/TIUIElements.podspec'
pod 'TIUIKitCore', :path => '../../../../TIUIKitCore/TIUIKitCore.podspec'
pod 'TISwiftUtils', :path => '../../../../TISwiftUtils/TISwiftUtils.podspec'
pod 'TIBottomSheet', :path => '../../../../TIBottomSheet/TIBottomSheet.podspec'
pod 'TILogging', :path => '../../../../TILogging/TILogging.podspec'
end

View File

@ -1,62 +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 TIUIElements
import TIUIKitCore
import UIKit
extension BaseModalViewController {
open class BaseAppearance: UIView.BaseAppearance<UIView.DefaultWrappedLayout> {
public var presentationDetents: [ModalViewPresentationDetent]
public var dragViewState: DragView.State
public var headerViewState: ModalHeaderView.State
public var footerViewState: ModalFooterView<FooterContentView>.State
public init(layout: UIView.DefaultWrappedLayout = .defaultLayout,
background: UIViewBackground = UIViewColorBackground(color: .clear),
border: UIViewBorder = BaseUIViewBorder(),
shadow: UIViewShadow? = nil,
presentationDetents: [ModalViewPresentationDetent] = [.maxHeight],
dragViewState: DragView.State = .hidden,
headerViewState: ModalHeaderView.State = .hidden,
footerViewState: ModalFooterView<FooterContentView>.State = .hidden) {
self.presentationDetents = presentationDetents
self.dragViewState = dragViewState
self.headerViewState = headerViewState
self.footerViewState = footerViewState
super.init(layout: layout,
background: background,
border: border,
shadow: shadow)
}
}
public final class DefaultAppearance: BaseAppearance, ViewAppearance {
public static var defaultAppearance: Self {
Self()
}
}
}

View File

@ -1,473 +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 TISwiftUtils
import TIUIElements
import TIUIKitCore
import UIKit
import PanModal
open class BaseModalViewController<ContentView: UIView,
FooterContentView: UIView>: BaseInitializableViewController, PanModalPresentable {
// MARK: - Public Properties
public let dragView = DragView()
public let headerView = ModalHeaderView()
private(set) public lazy var contentView = createContentView()
private(set) public lazy var footerView = createFooterView()
public private(set) lazy var dragViewBottomToHeaderViewTopConstraint: NSLayoutConstraint = {
dragView.bottomAnchor.constraint(equalTo: headerView.topAnchor)
}()
public private(set) lazy var dragViewBottomToContentViewTopConstraint: NSLayoutConstraint = {
dragView.bottomAnchor.constraint(equalTo: contentView.topAnchor)
}()
public private(set) lazy var dragViewConstraints: SubviewConstraints = {
let trailingConstraint = dragView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
let edgeConstraints = EdgeConstraints(leadingConstraint: dragView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
trailingConstraint: trailingConstraint,
topConstraint: dragView.topAnchor.constraint(equalTo: view.topAnchor),
bottomConstraint: dragViewBottomToHeaderViewTopConstraint)
let centerXConstraint = dragView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
let centerYConstraint = dragView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
let centerConstraints = CenterConstraints(centerXConstraint: centerXConstraint,
centerYConstraint: centerYConstraint)
let sizeConstraints = SizeConstraints(widthConstraint: dragView.widthAnchor.constraint(equalToConstant: .zero),
heightConstraint: dragView.heightAnchor.constraint(equalToConstant: .zero))
return SubviewConstraints(edgeConstraints: edgeConstraints,
centerConstraints: centerConstraints,
sizeConstraints: sizeConstraints)
}()
public private(set) lazy var headerViewToSuperviewTopConstraint: NSLayoutConstraint = {
headerView.topAnchor.constraint(equalTo: view.topAnchor)
}()
public private(set) lazy var headerBottomToContentTopConstraint: NSLayoutConstraint = {
headerView.bottomAnchor.constraint(equalTo: contentView.topAnchor)
}()
public private(set) lazy var headerViewConstraints: SubviewConstraints = {
let leadingConstraint = headerView.leadingAnchor.constraint(equalTo: view.leadingAnchor)
let trailingConstraint = headerView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
let edgeConstraints = EdgeConstraints(leadingConstraint: leadingConstraint,
trailingConstraint: trailingConstraint,
topConstraint: dragViewBottomToHeaderViewTopConstraint,
bottomConstraint: headerBottomToContentTopConstraint)
let centerXConstraint = headerView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
let centerYConstraint = headerView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
let centerConstraints = CenterConstraints(centerXConstraint: centerXConstraint,
centerYConstraint: centerYConstraint)
let sizeConstraints = SizeConstraints(widthConstraint: headerView.widthAnchor.constraint(equalToConstant: .zero),
heightConstraint: headerView.heightAnchor.constraint(equalToConstant: .zero))
return SubviewConstraints(edgeConstraints: edgeConstraints,
centerConstraints: centerConstraints,
sizeConstraints: sizeConstraints)
}()
public private(set) lazy var contentViewTopToSuperviewConstraint: NSLayoutConstraint = {
contentView.topAnchor.constraint(equalTo: view.topAnchor)
}()
public private(set) lazy var contentViewBottomToSuperviewConstraint: NSLayoutConstraint = {
contentView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
}()
public private(set) lazy var contentViewConstraints: SubviewConstraints = {
let leadingConstraint = contentView.leadingAnchor.constraint(equalTo: view.leadingAnchor)
let trailingConstraint = contentView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
let edgeConstraints = EdgeConstraints(leadingConstraint: leadingConstraint,
trailingConstraint: trailingConstraint,
topConstraint: headerBottomToContentTopConstraint,
bottomConstraint: contentViewBottomToSuperviewConstraint)
let centerXConstraint = contentView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
let centerYConstraint = contentView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
let centerConstraints = CenterConstraints(centerXConstraint: centerXConstraint,
centerYConstraint: centerYConstraint)
let sizeConstraints = SizeConstraints(widthConstraint: contentView.widthAnchor.constraint(equalToConstant: .zero),
heightConstraint: contentView.heightAnchor.constraint(equalToConstant: .zero))
return SubviewConstraints(edgeConstraints: edgeConstraints,
centerConstraints: centerConstraints,
sizeConstraints: sizeConstraints)
}()
public private(set) lazy var footerViewConstraints: SubviewConstraints = {
let leadingConstraint = footerView.leadingAnchor.constraint(equalTo: view.leadingAnchor)
let trailingConstraint = footerView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
let edgeConstraints = EdgeConstraints(leadingConstraint: leadingConstraint,
trailingConstraint: trailingConstraint,
topConstraint: footerView.topAnchor.constraint(equalTo: contentView.bottomAnchor),
bottomConstraint: footerView.bottomAnchor.constraint(equalTo: view.bottomAnchor))
let centerXConstraint = footerView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
let centerYConstraint = footerView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
let centerConstraints = CenterConstraints(centerXConstraint: centerXConstraint,
centerYConstraint: centerYConstraint)
let sizeConstraints = SizeConstraints(widthConstraint: footerView.widthAnchor.constraint(equalToConstant: .zero),
heightConstraint: footerView.heightAnchor.constraint(equalToConstant: .zero))
return SubviewConstraints(edgeConstraints: edgeConstraints,
centerConstraints: centerConstraints,
sizeConstraints: sizeConstraints)
}()
// MARK: - Modal View Controller Configuration
public var viewControllerAppearance: BaseAppearance = .init(background: UIViewColorBackground(color: .white))
open var panScrollable: UIScrollView? {
contentView as? UIScrollView
}
public var panScrollableInsets: UIEdgeInsets = .zero {
didSet {
panScrollable?.contentInset = panScrollableInsets
}
}
public var dimmedView = DimmedView()
public var showDragIndicator = true
open var headerViewHeight: CGFloat {
let dragViewHeight = getHeight(of: dragView)
let headerViewHeight = getHeight(of: headerView)
let dragViewVerticalInsets = getDragViewVerticalInsets()
let headerViewVerticalInsets = getHeaderViewVerticalInsets()
return dragViewHeight + headerViewHeight + dragViewVerticalInsets + headerViewVerticalInsets
}
open var longFormHeight: PanModalHeight {
let detents = getSortedDetents()
return detents.max()?.panModalHeight(headerHeight: headerViewHeight) ?? .maxHeight
}
open var mediumFormHeight: PanModalHeight {
let detents = getSortedDetents()
if detents.count > 1 {
return detents[1].panModalHeight(headerHeight: headerViewHeight)
}
return detents.first?.panModalHeight(headerHeight: headerViewHeight) ?? .maxHeight
}
open var shortFormHeight: PanModalHeight {
let detents = getSortedDetents()
return detents.min()?.panModalHeight(headerHeight: headerViewHeight) ?? .maxHeight
}
private var keyboardDidShownObserver: NSObjectProtocol?
private var keyboardDidHiddenObserver: NSObjectProtocol?
// MARK: - Life Cycle
deinit {
let notificationCenter = NotificationCenter.default
if let keyboardDidShownObserver {
notificationCenter.removeObserver(keyboardDidShownObserver)
}
if let keyboardDidHiddenObserver {
notificationCenter.removeObserver(keyboardDidHiddenObserver)
}
}
// MARK: - BaseInitializableViewController
open override func addViews() {
super.addViews()
view.addSubviews(dragView, headerView, contentView, footerView)
}
open override func configureLayout() {
super.configureLayout()
for view in [dragView, headerView, contentView, footerView] {
view.translatesAutoresizingMaskIntoConstraints = false
}
configureDragViewLayout()
configureHeaderViewLayout()
configureContentViewLayout()
configureFooterViewLayout()
}
open override func bindViews() {
super.bindViews()
keyboardDidShownObserver = NotificationCenter.default
.addObserver(forName: UIResponder.keyboardDidShowNotification,
object: nil,
queue: .main) { [weak self] notification in
self?.configureLayoutForKeyboard(notification, isKeyboardHidden: false)
}
keyboardDidHiddenObserver = NotificationCenter.default
.addObserver(forName: UIResponder.keyboardDidHideNotification,
object: nil,
queue: .main) { [weak self] notification in
self?.configureLayoutForKeyboard(notification, isKeyboardHidden: true)
}
}
open override func configureAppearance() {
super.configureAppearance()
view.configureUIView(appearance: viewControllerAppearance)
configureDragViewAppearance()
configureHeaderViewAppearance()
configureFooterViewAppearance()
}
// MARK: - Open Methods
open func createContentView() -> ContentView {
ContentView()
}
open func createFooterView() -> ModalFooterView<FooterContentView> {
ModalFooterView<FooterContentView>()
}
open func configureLayoutForKeyboard(_ notification: Notification, isKeyboardHidden: Bool) {
guard let keyboardHeight = getKeyboardHeight(notification) else {
return
}
if case let .presented(footerViewAppearance) = viewControllerAppearance.footerViewState {
let bottomInset = footerViewAppearance.layout.insets.add(\.bottom,
to: \.bottom,
of: .vertical(bottom: keyboardHeight))
footerViewConstraints.edgeConstraints.bottomConstraint.constant = bottomInset
} else if let panScrollable {
var insets = panScrollableInsets
if isKeyboardHidden {
panScrollable.contentInset = insets
} else {
insets.bottom += keyboardHeight
panScrollable.contentInset = insets
}
}
}
// MARK: - Private Methods
private func configureDragViewLayout() {
guard case let .presented(dragViewAppearance) = viewControllerAppearance.dragViewState else {
return
}
let bottomConstraint: NSLayoutConstraint
let bottomConstant: CGFloat
if case let .presented(headerViewAppearance) = viewControllerAppearance.headerViewState {
dragViewBottomToContentViewTopConstraint.isActive = false
bottomConstraint = dragViewBottomToHeaderViewTopConstraint
bottomConstant = dragViewAppearance.layout.insets.add(\.bottom,
to: \.top,
of: headerViewAppearance.layout.insets)
} else {
dragViewBottomToHeaderViewTopConstraint.isActive = false
bottomConstraint = dragViewBottomToContentViewTopConstraint
bottomConstant = dragViewAppearance.layout.insets.bottom
}
dragViewConstraints.edgeConstraints.bottomConstraint = bottomConstraint
dragViewConstraints.update(from: dragViewAppearance.layout)
bottomConstraint.setActiveConstantOrDeactivate(constant: bottomConstant)
}
private func configureHeaderViewLayout() {
guard case let .presented(headerViewAppearance) = viewControllerAppearance.headerViewState else {
return
}
let topConstraint: NSLayoutConstraint
let topConstant: CGFloat
if case let .presented(dragViewAppearance) = viewControllerAppearance.dragViewState {
dragViewBottomToContentViewTopConstraint.isActive = false
topConstraint = dragViewBottomToHeaderViewTopConstraint
topConstant = dragViewAppearance.layout.insets.add(\.bottom,
to: \.top,
of: headerViewAppearance.layout.insets)
} else {
dragViewBottomToHeaderViewTopConstraint.isActive = false
topConstraint = headerViewToSuperviewTopConstraint
let topInset = headerViewAppearance.layout.insets.top
topConstant = topInset.isFinite ? topInset : .zero
}
headerViewConstraints.edgeConstraints.topConstraint = topConstraint
headerViewConstraints.update(from: headerViewAppearance.layout)
topConstraint.setActiveConstantOrDeactivate(constant: topConstant)
}
private func configureContentViewLayout() {
let topConstraint: NSLayoutConstraint
let topConstant: CGFloat
if case let .presented(headerViewAppearance) = viewControllerAppearance.headerViewState {
dragViewBottomToContentViewTopConstraint.isActive = false
contentViewTopToSuperviewConstraint.isActive = false
topConstraint = headerBottomToContentTopConstraint
topConstant = headerViewAppearance.layout.insets.bottom
} else if case let .presented(dragViewAppearance) = viewControllerAppearance.dragViewState {
contentViewTopToSuperviewConstraint.isActive = false
headerBottomToContentTopConstraint.isActive = false
topConstraint = dragViewBottomToContentViewTopConstraint
topConstant = dragViewAppearance.layout.insets.bottom
} else {
headerBottomToContentTopConstraint.isActive = false
dragViewBottomToContentViewTopConstraint.isActive = false
topConstraint = contentViewTopToSuperviewConstraint
topConstant = .zero
}
contentViewConstraints.edgeConstraints.topConstraint = topConstraint
let layout = UIView.DefaultWrappedLayout(insets: .horizontal(.zero)
.vertical(top: topConstant)
.replacingNan(with: .zero))
contentViewConstraints.update(from: layout)
}
private func configureFooterViewLayout() {
guard case let .presented(footerViewAppearance) = viewControllerAppearance.footerViewState else {
return
}
contentViewConstraints.edgeConstraints.bottomConstraint.isActive = false
contentViewConstraints.edgeConstraints.bottomConstraint = footerViewConstraints.edgeConstraints.topConstraint
footerViewConstraints.update(from: footerViewAppearance.layout)
}
private func configureDragViewAppearance() {
switch viewControllerAppearance.dragViewState {
case .hidden:
dragView.isHidden = true
case let .presented(appearance):
dragView.configure(appearance: appearance)
}
}
private func configureHeaderViewAppearance() {
switch viewControllerAppearance.headerViewState {
case .hidden:
headerView.isHidden = true
case let .presented(appearance):
headerView.configure(appearance: appearance)
}
}
private func configureFooterViewAppearance() {
switch viewControllerAppearance.footerViewState {
case .hidden:
footerView.isHidden = true
case let .presented(appearance):
footerView.configureBaseWrappedViewHolder(appearance: appearance)
}
}
private func getSortedDetents() -> [ModalViewPresentationDetent] {
viewControllerAppearance.presentationDetents.uniqued().sorted()
}
private func getHeight(of view: UIView) -> CGFloat {
guard !view.isHidden else {
return .zero
}
return getFittingSize(forView: view).height
}
private func getDragViewVerticalInsets() -> CGFloat {
guard case let .presented(appearance) = viewControllerAppearance.dragViewState else {
return .zero
}
return appearance.layout.insets.vertical(onNan: .zero)
}
private func getHeaderViewVerticalInsets() -> CGFloat {
guard case let .presented(appearance) = viewControllerAppearance.headerViewState else {
return .zero
}
return appearance.layout.insets.vertical(onNan: .zero)
}
private func getFittingSize(forView view: UIView) -> CGSize {
let targetSize = CGSize(width: UIScreen.main.bounds.width,
height: UIView.layoutFittingCompressedSize.height)
return view.systemLayoutSizeFitting(targetSize)
}
private func getKeyboardHeight(_ notification: Notification) -> CGFloat? {
guard let userInfo = notification.userInfo else {
return nil
}
return (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height
}
}

View File

@ -1,54 +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 UIKit
open class DefaultModalWrapperViewController<ContentController: UIViewController>: BaseModalViewController<UIView, UIView> {
private(set) public var contentViewController: ContentController
public init(contentViewController: ContentController) {
self.contentViewController = contentViewController
super.init(nibName: nil, bundle: nil)
addChild(contentViewController)
contentViewController.didMove(toParent: self)
}
@available(*, unavailable)
required public init?(coder: NSCoder) {
contentViewController = ContentController()
super.init(coder: coder)
}
open override func createContentView() -> UIView {
contentViewController.view
}
}
public extension UIViewController {
func wrappedInBottomSheetController() -> DefaultModalWrapperViewController<UIViewController> {
DefaultModalWrapperViewController(contentViewController: self)
}
}

View File

@ -1,34 +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 TIUIKitCore
extension UIViewShadow {
public static var defaultModalViewShadow: Self {
.init {
Color(.black)
Opacity(0.5)
Offset(0, -5)
Radius(10)
}
}
}

View File

@ -1,67 +0,0 @@
//
// Copyright (c) 2022 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
import PanModal
public struct ModalViewPresentationDetent: Hashable {
// MARK: - Default Values
public static var headerOnly: Self {
Self(height: -.greatestFiniteMagnitude)
}
public static func height(_ height: CGFloat) -> Self {
Self(height: height)
}
public static var maxHeight: Self {
Self(height: .greatestFiniteMagnitude)
}
// MARK: - Public Properties
public var height: CGFloat
// MARK: - Internal Methods
func panModalHeight(headerHeight: CGFloat = .zero) -> PanModalHeight {
if self == .headerOnly {
return .contentHeight(headerHeight)
}
if self == .maxHeight {
return .maxHeight
}
return .contentHeight(height)
}
}
// MARK: - Comparable
extension ModalViewPresentationDetent: Comparable {
public static func < (lhs: ModalViewPresentationDetent, rhs: ModalViewPresentationDetent) -> Bool {
lhs.height < rhs.height
}
}

View File

@ -1,70 +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 TIUIElements
import TIUIKitCore
import UIKit
public final class DragView: BaseInitializableView, AppearanceConfigurable {
// MARK: - Nested Types
private enum Constants {
static var dragViewTopInset: CGFloat {
8
}
static var dragViewBorder: UIViewBorder {
UIViewRoundedBorder(cornerRadius: dragViewSize.height / 2)
}
static var dragViewBackground: UIViewColorBackground {
UIViewColorBackground(color: .lightGray)
}
static var dragViewSize: CGSize {
CGSize(width: 52, height: 7)
}
}
public enum State {
case hidden
case presented(Appearance)
}
public final class Appearance: UIView.BaseWrappedAppearance<UIView.DefaultWrappedLayout>, WrappedViewAppearance {
public static var defaultAppearance: Self {
Self(layout: DefaultWrappedLayout(insets: .vertical(top: Constants.dragViewTopInset),
size: Constants.dragViewSize,
centerOffset: .centerHorizontal()),
background: Constants.dragViewBackground,
border: Constants.dragViewBorder)
}
}
// MARK: - AppearanceConfigurable
public func configure(appearance: Appearance) {
configureUIView(appearance: appearance)
}
}

View File

@ -1,45 +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 TIUIElements
import TIUIKitCore
import UIKit
open class ModalFooterView<ContentView: UIView>: ContainerView<ContentView> {
// MARK: - Nested Types
public enum State {
case hidden
case presented(Appearance)
}
}
public extension ModalFooterView {
final class Appearance: BaseWrappedViewHolderAppearance<DefaultWrappedAppearance, DefaultWrappedLayout>,
WrappedViewHolderAppearance {
public static var defaultAppearance: Self {
Self(layout: DefaultWrappedLayout(insets: .horizontal(.zero).vertical(bottom: .zero),
size: .fixedHeight(44)))
}
}
}

View File

@ -1,235 +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 TIUIElements
import TIUIKitCore
import UIKit
open class ModalHeaderView: BaseInitializableView, AppearanceConfigurable {
// MARK: - Nested Types
public enum State {
case hidden
case presented(Appearance)
}
public enum ContentViewState {
case leadingButton(BaseButtonStyle)
case trailingButton(BaseButtonStyle)
case buttons(leading: BaseButtonStyle, trailing: BaseButtonStyle)
case custom(view: UIView, appearance: UIView.BaseWrappedAppearance<UIView.DefaultWrappedLayout>)
}
// MARK: - Public properties
public let leadingButton = StatefulButton()
public let trailingButton = StatefulButton()
public private(set) lazy var leftTrailingToRightLeadingConstraint: NSLayoutConstraint = {
leadingButton.trailingAnchor.constraint(equalTo: trailingButton.leadingAnchor)
}()
public private(set) lazy var leftTrailingToSuperviewTrailing: NSLayoutConstraint = {
leadingButton.trailingAnchor.constraint(equalTo: trailingAnchor)
}()
public private(set) lazy var leadingButtonConstraints: SubviewConstraints = {
let edgeConstraints = EdgeConstraints(leadingConstraint: leadingButton.leadingAnchor.constraint(equalTo: leadingAnchor),
trailingConstraint: leftTrailingToRightLeadingConstraint,
topConstraint: leadingButton.topAnchor.constraint(equalTo: topAnchor),
bottomConstraint: leadingButton.bottomAnchor.constraint(equalTo: bottomAnchor))
let centerXConstraint = leadingButton.centerXAnchor.constraint(equalTo: centerXAnchor)
let centerYConstraint = leadingButton.centerYAnchor.constraint(equalTo: centerYAnchor)
let centerConstraints = CenterConstraints(centerXConstraint: centerXConstraint,
centerYConstraint: centerYConstraint)
let sizeConstraints = SizeConstraints(widthConstraint: leadingButton.widthAnchor.constraint(equalToConstant: .zero),
heightConstraint: leadingButton.heightAnchor.constraint(equalToConstant: .zero))
return SubviewConstraints(edgeConstraints: edgeConstraints,
centerConstraints: centerConstraints,
sizeConstraints: sizeConstraints)
}()
public private(set) lazy var rightLeadingToSuperviewLeadingConstraint: NSLayoutConstraint = {
trailingButton.leadingAnchor.constraint(equalTo: leadingAnchor)
}()
public private(set) lazy var trailingButtonConstraints: SubviewConstraints = {
let trailingConstraint = trailingButton.trailingAnchor.constraint(equalTo: trailingAnchor)
let edgeConstraints = EdgeConstraints(leadingConstraint: leftTrailingToRightLeadingConstraint,
trailingConstraint: trailingConstraint,
topConstraint: trailingButton.topAnchor.constraint(equalTo: topAnchor),
bottomConstraint: trailingButton.bottomAnchor.constraint(equalTo: bottomAnchor))
let centerXConstraint = trailingButton.centerXAnchor.constraint(equalTo: centerXAnchor)
let centerYConstraint = trailingButton.centerYAnchor.constraint(equalTo: centerYAnchor)
let centerConstraints = CenterConstraints(centerXConstraint: centerXConstraint,
centerYConstraint: centerYConstraint)
let sizeConstraints = SizeConstraints(widthConstraint: trailingButton.widthAnchor.constraint(equalToConstant: .zero),
heightConstraint: trailingButton.heightAnchor.constraint(equalToConstant: .zero))
return SubviewConstraints(edgeConstraints: edgeConstraints,
centerConstraints: centerConstraints,
sizeConstraints: sizeConstraints)
}()
public var customViewConstraints: SubviewConstraints?
// MARK: - BaseInitializableView
open override func addViews() {
super.addViews()
addSubviews(leadingButton, trailingButton)
}
open override func configureLayout() {
super.configureLayout()
for view in [leadingButton, trailingButton] {
view.translatesAutoresizingMaskIntoConstraints = false
}
}
// MARK: - AppearanceConfigurable
open func configure(appearance: Appearance) {
configureUIView(appearance: appearance)
configureContentView(state: appearance.contentViewState)
}
open func configureContentView(state: ContentViewState) {
var leadingButtonStyle: BaseButtonStyle?
var trailingButtonStyle: BaseButtonStyle?
switch state {
case let .leadingButton(style):
leadingButtonStyle = style
leadingButtonConstraints.edgeConstraints.trailingConstraint = leftTrailingToSuperviewTrailing
case let .trailingButton(style):
trailingButtonStyle = style
trailingButtonConstraints.edgeConstraints.leadingConstraint = rightLeadingToSuperviewLeadingConstraint
case let .buttons(leading, trailing):
leadingButtonStyle = leading
trailingButtonStyle = trailing
case let .custom(view, appearance):
configureCustomView(view, withLayout: appearance.layout)
view.configureUIView(appearance: appearance)
}
configure(buttonStyle: leadingButtonStyle, forButton: leadingButton, constraints: leadingButtonConstraints)
configure(buttonStyle: trailingButtonStyle, forButton: trailingButton, constraints: trailingButtonConstraints)
if let leadingButtonStyle, let trailingButtonStyle {
leadingButtonConstraints.edgeConstraints.trailingConstraint = leftTrailingToRightLeadingConstraint
trailingButtonConstraints.edgeConstraints.leadingConstraint = leftTrailingToRightLeadingConstraint
let spacing = leadingButtonStyle.appearance.layout.insets.add(\.right,
to: \.left,
of: trailingButtonStyle.appearance.layout.insets)
leftTrailingToRightLeadingConstraint.setActiveConstantOrDeactivate(constant: spacing)
}
}
// MARK: - Private methods
private func configureCustomView(_ view: UIView, withLayout layout: WrappedViewLayout) {
addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
let edgeConstraints = EdgeConstraints(leadingConstraint: view.leadingAnchor.constraint(equalTo: leadingAnchor),
trailingConstraint: view.trailingAnchor.constraint(equalTo: trailingAnchor),
topConstraint: view.topAnchor.constraint(equalTo: topAnchor),
bottomConstraint: view.bottomAnchor.constraint(equalTo: bottomAnchor))
let centerConstraints = CenterConstraints(centerXConstraint: view.centerXAnchor.constraint(equalTo: centerXAnchor),
centerYConstraint: view.centerYAnchor.constraint(equalTo: centerYAnchor))
let sizeConstraints = SizeConstraints(widthConstraint: view.widthAnchor.constraint(equalToConstant: .zero),
heightConstraint: view.heightAnchor.constraint(equalToConstant: .zero))
let customViewConstraints = SubviewConstraints(edgeConstraints: edgeConstraints,
centerConstraints: centerConstraints,
sizeConstraints: sizeConstraints)
self.customViewConstraints = customViewConstraints
customViewConstraints.update(from: layout)
}
private func configure(buttonStyle: BaseButtonStyle?,
forButton button: StatefulButton,
constraints: SubviewConstraints?) {
guard let buttonStyle else {
button.isHidden = true
return
}
button.isHidden = false
constraints?.update(from: buttonStyle.appearance.layout)
button.apply(style: buttonStyle)
}
}
// MARK: - Appearance
public extension ModalHeaderView {
final class Appearance: BaseWrappedAppearance<DefaultWrappedLayout>, WrappedViewAppearance {
public static var defaultAppearance: Self {
Self(layout: DefaultWrappedLayout(insets: .horizontal(.zero).vertical(top: .zero),
size: .fixedHeight(44)))
}
public var contentViewState: ContentViewState
public init(layout: DefaultWrappedLayout = .defaultLayout,
background: UIViewBackground = UIViewColorBackground(color: .clear),
border: UIViewBorder = BaseUIViewBorder(),
shadow: UIViewShadow? = nil,
contentViewState: ContentViewState = .leadingButton(.init())) {
self.contentViewState = contentViewState
super.init(layout: layout,
background: background,
border: border,
shadow: shadow)
}
}
}

View File

@ -1,50 +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 UIKit
import PanModal
open class PassthroughDimmedView: DimmedView {
public weak var hitTestHandlerView: UIView?
public var isTransparent = false
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let hitTestHandlerView else {
return super.hitTest(point, with: event)
}
return hitTestHandlerView.hitTest(point, with: event)
}
public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
hitTestHandlerView == nil || super.point(inside: point, with: event)
}
open override func onChange(dimState: DimmedView.DimState) {
super.onChange(dimState: dimState)
if isTransparent {
alpha = .zero
}
}
}

View File

@ -1,3 +0,0 @@
**/build/
**/nef/
LICENSE

View File

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>launcher</string>
<key>CFBundleIconFile</key>
<string>AppIcon</string>
<key>CFBundleIconName</key>
<string>AppIcon</string>
<key>CFBundleIdentifier</key>
<string>com.fortysevendeg.nef</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>LSApplicationCategoryType</key>
<string>public.app-category.developer-tools</string>
<key>LSMinimumSystemVersion</key>
<string>10.14</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2019 The nef Authors. All rights reserved.</string>
</dict>
</plist>

View File

@ -1,26 +0,0 @@
## gitignore nef files
**/build/
**/nef/
LICENSE
## User data
**/xcuserdata/
podfile.lock
**.DS_Store
## Obj-C/Swift specific
*.hmap
*.ipa
*.dSYM.zip
*.dSYM
## CocoaPods
**Pods**
## Carthage
**Carthage**
## SPM
.build
.swiftpm
swiftpm

View File

@ -1,14 +0,0 @@
ENV["DEVELOPMENT_INSTALL"] = "true"
source 'https://git.svc.touchin.ru/TouchInstinct/Podspecs.git'
target 'TIBottomSheet' do
platform :ios, 12
use_frameworks!
pod 'TIUIElements', :path => '../../../../TIUIElements/TIUIElements.podspec'
pod 'TIUIKitCore', :path => '../../../../TIUIKitCore/TIUIKitCore.podspec'
pod 'TISwiftUtils', :path => '../../../../TISwiftUtils/TISwiftUtils.podspec'
pod 'TIBottomSheet', :path => '../../../../TIBottomSheet/TIBottomSheet.podspec'
pod 'TILogging', :path => '../../../../TILogging/TILogging.podspec'
end

View File

@ -1,110 +0,0 @@
/*:
# TIBottomSheet
TIBottomSheet содержить базовую реализацию модального котроллера и немного видоизмененную библиотеку PanModal.
## Базовый контроллер
Для создания модального котроллера можно унаследоваться от `BaseModalViewController`. Данный клас принимает два generic типа: тип основного контента, тип контента футера.
*/
import TIBottomSheet
import UIKit
class EmptyViewController: BaseModalViewController<UIView, UIView> { }
/*:
## Обертка вокруг существующего контроллера
Может быть такое, что из уже существующего контроллера нужно сделать модальное окно. С этим может помочь обертка `DefaultModalWrapperViewController`. Данный контроллер является наследником BaseModalViewController, что позволяет его настраивать так же, как и базовый модальный котроллер
*/
import TIUIKitCore
final class OldMassiveViewController: BaseInitializableViewController {
// some implementation
}
typealias ModalOldMassiveViewController = DefaultModalWrapperViewController<OldMassiveViewController>
class PresentingViewController: BaseInitializableViewController {
// some implementation
@objc private func onButtonTapped() {
presentPanModal(ModalOldMassiveViewController(contentViewController: OldMassiveViewController()))
}
}
/*:
## Контент модального контроллера
Модальный котроллер может содержать следующие элементы: `DragView`, `HeaderView`, `FooterView`. Каждый из них является опциональным и без дополнительных настроек не будет показываться.
DragView - небольшая view, за которую пользователь "держит" модальный контроллер
HeaderView - контейнер, содержащий в себе кнопки назад/закрыть или какие-то другие элементы управления
FooterView - view, располагающаяся внизу контроллера, поверх всего контента (модальный контроллер уже настроен так, чтобы при скролле в самый низ, футер не перекрывал последнюю ячейку)
Для настройки каждого у котроллера есть свойство `viewControllerAppearance`. Через него будет настраиваться весь контроллер. Однако стоит заметить, что котроллер не будет настраивать передаваимую вью, содержащую основной контент. Стандартно котроллер будет пытаться расположить контент так, чтобы он заполнил все пространство.
Вот пример настройки внешнего вида так, чтобы был видет dragView и headerView с левой кнопкой:
*/
import TIUIElements
let customViewController = BaseModalViewController<UIView, UIView>()
customViewController.viewControllerAppearance = BaseModalViewController.DefaultAppearance.make {
$0.dragViewState = .presented(.defaultAppearance)
$0.headerViewState = .presented(.make {
$0.layout.size = .fixedHeight(52)
$0.backgroundColor = .white
$0.contentViewState = .leadingButton(.init(titles: [.normal: "Close"],
appearance: .init(stateAppearances: [
.normal: .init(background: UIViewColorBackground(color: .blue))
])))
})
}
/*:
## "Якори" контроллера
Раньше для настройки высоты контроллера необходимо было пользоваться свойствами `longFormHeight`, `shortFormHeight`. В базовом контроллере можно лишь передать список точек на которых контроллер должен будет задержаться:
*/
let detentsViewController = BaseModalViewController<UIView, UIView>()
detentsViewController.viewControllerAppearance.presentationDetents = [.headerOnly, .height(300), .maxHeight]
/*:
- headerOnly будет сам пытаться вычеслить высоту хедера и dragView, показывая только их
- height(_) будет показывать контроллер на переданной высоте
- maxHeight - вся высота экрана (до safeArea)
В данный массив не рекомендуется передавать больше 3 значений, т.к. модальное окно все равно сможет занять только 3 положения на экране.
## DimmedView и PassthroughDimmedView
Для контроля `DimmedView` (затемняющей view) есть отдельное свойство `dimmedView`. Эти классы позволяют настраивать поведение при тапе в затемнённую область и кастомизировать затемнение под ваши нужды.
*/
let shadowViewController = BaseModalViewController<UIView, UIView>()
let dimmedView = PassthroughDimmedView()
dimmedView.hitTestHandlerView = shadowViewController.view
dimmedView.configureUIView(appearance: UIView.DefaultAppearance(shadow: UIViewShadow(radius: 8,
color: .black,
opacity: 0.3)))
shadowViewController.dimmedView = dimmedView
/*:
## Контроль закрытия
`PanModalPresentable` не умеет в настройку закрытия контроллера, делая это самостоятельно через `dismiss(animated:completion:)`. Теперь можно настроить закрытие самостоятельно через свойства: `onTapToDismiss` и `onDragToDismiss`.
# Взаимодействие с PanModal
Если нет необходимости или возможности использовать `BaseModalViewController`, вы все так же можете пользоваться протоколом `PanModalRepresentable`. Вот список изменений протокола:
- Открытие/закрытие модального окна теперь можно настроить с помощью свойств `onTapToDismiss` и `onDragToDismiss`
- Можно настроить промежуточное состояние модального окна с `mediumFormHeight`
- `DimmedView` открыт для наследования и может создаваться в `dimmedView` у
> Для `BaseModalViewController` все свойства из `PanModalPresentable` все также работают, т.е. вы можете их переопределять, добавлять и изменять по необходимости.
*/

View File

@ -1,30 +0,0 @@
import UIKit
public protocol NefPlaygroundLiveViewable {}
extension UIView: NefPlaygroundLiveViewable {}
extension UIViewController: NefPlaygroundLiveViewable {}
#if NOT_IN_PLAYGROUND
public enum Nef {
public enum Playground {
public static func liveView(_ view: NefPlaygroundLiveViewable) {}
public static func needsIndefiniteExecution(_ state: Bool) {}
}
}
#else
import PlaygroundSupport
public enum Nef {
public enum Playground {
public static func liveView(_ view: NefPlaygroundLiveViewable) {
PlaygroundPage.current.liveView = (view as! PlaygroundLiveViewable)
}
public static func needsIndefiniteExecution(_ state: Bool) {
PlaygroundPage.current.needsIndefiniteExecution = state
}
}
}
#endif

View File

@ -1,47 +0,0 @@
import Foundation
import XCTest
public extension Nef {
static func run<T: XCTestCase>(testCase class: T.Type) {
startTestObserver()
T.defaultTestSuite.run()
}
static private func startTestObserver() {
_ = testObserverInstalled
}
static private var testObserverInstalled = { () -> NefTestFailObserver in
let testObserver = NefTestFailObserver()
XCTestObservationCenter.shared.addTestObserver(testObserver)
return testObserver
}()
}
// MARK: enrich the output for XCTest
fileprivate class NefTestFailObserver: NSObject, XCTestObservation {
private var numberOfFailedTests = 0
func testSuiteWillStart(_ testSuite: XCTestSuite) {
numberOfFailedTests = 0
}
func testSuiteDidFinish(_ testSuite: XCTestSuite) {
if numberOfFailedTests > 0 {
print("💢 Test Suite '\(testSuite.name)' finished with \(numberOfFailedTests) failed \(numberOfFailedTests > 1 ? "tests" : "test").")
} else {
print("🔅 Test Suite '\(testSuite.name)' finished successfully.")
}
}
func testCase(_ testCase: XCTestCase,
didFailWithDescription description: String,
inFile filePath: String?,
atLine lineNumber: Int) {
numberOfFailedTests += 1
print("Test Fail '\(testCase.name)':\(UInt(lineNumber)): \(description.description)")
}
}

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<playground version='6.0' target-platform='ios' display-mode='raw' buildActiveScheme='true'/>

View File

@ -1,396 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objects = {
/* Begin PBXBuildFile section */
83C08983988F66570478C40D /* Pods_TIBottomSheet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8EF3488B86B483233C2CC631 /* Pods_TIBottomSheet.framework */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
7B6955D74676A5427AC42234 /* Pods-TIBottomSheet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TIBottomSheet.debug.xcconfig"; path = "Target Support Files/Pods-TIBottomSheet/Pods-TIBottomSheet.debug.xcconfig"; sourceTree = "<group>"; };
8BACBE8322576CAD00266845 /* TIBottomSheet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TIBottomSheet.framework; sourceTree = BUILT_PRODUCTS_DIR; };
8BACBE8622576CAD00266845 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
8EF3488B86B483233C2CC631 /* Pods_TIBottomSheet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_TIBottomSheet.framework; sourceTree = BUILT_PRODUCTS_DIR; };
AA57D8210790AD14BCC54A7E /* Pods-TIBottomSheet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TIBottomSheet.release.xcconfig"; path = "Target Support Files/Pods-TIBottomSheet/Pods-TIBottomSheet.release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
8BACBE8022576CAD00266845 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
83C08983988F66570478C40D /* Pods_TIBottomSheet.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
11F06D2789C6CF40767861CF /* Frameworks */ = {
isa = PBXGroup;
children = (
8EF3488B86B483233C2CC631 /* Pods_TIBottomSheet.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
1F7782E3A7AD7291B7C09F56 /* Pods */ = {
isa = PBXGroup;
children = (
7B6955D74676A5427AC42234 /* Pods-TIBottomSheet.debug.xcconfig */,
AA57D8210790AD14BCC54A7E /* Pods-TIBottomSheet.release.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
8B39A26221D40F8700DE2643 = {
isa = PBXGroup;
children = (
8BACBE8422576CAD00266845 /* TIBottomSheet */,
8B39A26C21D40F8700DE2643 /* Products */,
1F7782E3A7AD7291B7C09F56 /* Pods */,
11F06D2789C6CF40767861CF /* Frameworks */,
);
sourceTree = "<group>";
};
8B39A26C21D40F8700DE2643 /* Products */ = {
isa = PBXGroup;
children = (
8BACBE8322576CAD00266845 /* TIBottomSheet.framework */,
);
name = Products;
sourceTree = "<group>";
};
8BACBE8422576CAD00266845 /* TIBottomSheet */ = {
isa = PBXGroup;
children = (
8BACBE8622576CAD00266845 /* Info.plist */,
);
path = TIBottomSheet;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
8BACBE7E22576CAD00266845 /* Headers */ = {
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXHeadersBuildPhase section */
/* Begin PBXNativeTarget section */
8BACBE8222576CAD00266845 /* TIBottomSheet */ = {
isa = PBXNativeTarget;
buildConfigurationList = 8BACBE8A22576CAD00266845 /* Build configuration list for PBXNativeTarget "TIBottomSheet" */;
buildPhases = (
4E98D4C60DCD00EB801E579E /* [CP] Check Pods Manifest.lock */,
8BACBE7E22576CAD00266845 /* Headers */,
8BACBE7F22576CAD00266845 /* Sources */,
8BACBE8022576CAD00266845 /* Frameworks */,
8BACBE8122576CAD00266845 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = TIBottomSheet;
productName = TIBottomSheet2;
productReference = 8BACBE8322576CAD00266845 /* TIBottomSheet.framework */;
productType = "com.apple.product-type.framework";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
8B39A26321D40F8700DE2643 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1010;
LastUpgradeCheck = 1200;
ORGANIZATIONNAME = "47 Degrees";
TargetAttributes = {
8BACBE8222576CAD00266845 = {
CreatedOnToolsVersion = 10.1;
};
};
};
buildConfigurationList = 8B39A26621D40F8700DE2643 /* Build configuration list for PBXProject "TIBottomSheet" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 8B39A26221D40F8700DE2643;
productRefGroup = 8B39A26C21D40F8700DE2643 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
8BACBE8222576CAD00266845 /* TIBottomSheet */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
8BACBE8122576CAD00266845 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
4E98D4C60DCD00EB801E579E /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-TIBottomSheet-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
8BACBE7F22576CAD00266845 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
8B39A27721D40F8800DE2643 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_TESTING_SEARCH_PATHS = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
8B39A27821D40F8800DE2643 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTING_SEARCH_PATHS = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Release;
};
8BACBE8822576CAD00266845 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7B6955D74676A5427AC42234 /* Pods-TIBottomSheet.debug.xcconfig */;
buildSettings = {
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Manual;
CURRENT_TIBottomSheet_VERSION = 1;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = "";
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = "$(SRCROOT)/TIBottomSheet/Info.plist";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 12.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.47deg.ios.TIBottomSheet;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
SWIFT_VERSION = 5;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Debug;
};
8BACBE8922576CAD00266845 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = AA57D8210790AD14BCC54A7E /* Pods-TIBottomSheet.release.xcconfig */;
buildSettings = {
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Manual;
CURRENT_TIBottomSheet_VERSION = 1;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = "";
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = "$(SRCROOT)/TIBottomSheet/Info.plist";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 12.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.47deg.ios.TIBottomSheet;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
SWIFT_VERSION = 5;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
8B39A26621D40F8700DE2643 /* Build configuration list for PBXProject "TIBottomSheet" */ = {
isa = XCConfigurationList;
buildConfigurations = (
8B39A27721D40F8800DE2643 /* Debug */,
8B39A27821D40F8800DE2643 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
8BACBE8A22576CAD00266845 /* Build configuration list for PBXNativeTarget "TIBottomSheet" */ = {
isa = XCConfigurationList;
buildConfigurations = (
8BACBE8822576CAD00266845 /* Debug */,
8BACBE8922576CAD00266845 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 8B39A26321D40F8700DE2643 /* Project object */;
}

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:TIBottomSheet.xcodeproj">
</FileRef>
</Workspace>

View File

@ -1,76 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1200"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "8BACBE8222576CAD00266845"
BuildableName = "TIBottomSheet.framework"
BlueprintName = "TIBottomSheet"
ReferencedContainer = "container:TIBottomSheet.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</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 = "8BACBE8222576CAD00266845"
BuildableName = "TIBottomSheet.framework"
BlueprintName = "TIBottomSheet"
ReferencedContainer = "container:TIBottomSheet.xcodeproj">
</BuildableReference>
</MacroExpansion>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "8BACBE8222576CAD00266845"
BuildableName = "TIBottomSheet.framework"
BlueprintName = "TIBottomSheet"
ReferencedContainer = "container:TIBottomSheet.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

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

View File

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2019. The nef authors.</string>
</dict>
</plist>

View File

@ -1,6 +0,0 @@
#!/bin/bash
workspace="TIBottomSheet.xcworkspace"
workspacePath=$(echo "$0" | rev | cut -f2- -d '/' | rev)
open "`pwd`/$workspacePath/$workspace"

View File

@ -1 +0,0 @@
TIBottomSheet.app/Contents/MacOS/TIBottomSheet.playground

View File

@ -1,29 +0,0 @@
Pod::Spec.new do |s|
s.name = 'TIBottomSheet'
s.version = '1.56.0'
s.summary = 'Base models for creating bottom sheet view controllers'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/src/tag/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'castlele' => 'nikita.semenov@touchin.ru',
'petropavel13' => 'ivan.smolin@touchin.ru'}
s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
s.ios.deployment_target = '12.0'
s.swift_versions = ['5.7']
sources = 'Sources/**/*'
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
s.dependency 'TIUIElements', s.version.to_s
s.dependency 'TIUIKitCore', s.version.to_s
s.dependency 'TISwiftUtils', s.version.to_s
s.dependency 'PanModal', '~> 1.3.0'
end

View File

@ -1,8 +0,0 @@
ENV["DEVELOPMENT_INSTALL"] = "true"
target 'TICoreGraphicsUtils' do
platform :ios, 11.0
use_frameworks!
pod 'TICoreGraphicsUtils', :path => '../../../../TICoreGraphicsUtils/TICoreGraphicsUtils.podspec'
end

View File

@ -1,41 +0,0 @@
//
// Copyright (c) 2022 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 CoreGraphics.CGGeometry
public extension CGSize {
var ceiledContextSize: CGSize {
CGSize(width: ceil(width), height: ceil(height))
}
}
public extension CGPoint {
func horizontallyFlipped() -> Self {
CGPoint(x: x, y: -y)
}
}
public extension CGRect {
func offset(by point: CGPoint) -> Self {
offsetBy(dx: point.x, dy: point.y)
}
}

View File

@ -1,68 +0,0 @@
//
// Copyright (c) 2022 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 CoreGraphics.CGGeometry
public extension CGSize {
func resizeRect(forNewSize newSize: CGSize, resizeMode: ResizeMode) -> CGRect {
let horizontalRatio = newSize.width / width
let verticalRatio = newSize.height / height
let ratio: CGFloat
switch resizeMode {
case .scaleToFill:
ratio = 1
case .scaleAspectFill:
ratio = max(horizontalRatio, verticalRatio)
case .scaleAspectFit:
ratio = min(horizontalRatio, verticalRatio)
}
let newWidth = resizeMode == .scaleToFill ? newSize.width : width * ratio
let newHeight = resizeMode == .scaleToFill ? newSize.height : height * ratio
let originX: CGFloat
let originY: CGFloat
if newWidth > newSize.width {
originX = (newSize.width - newWidth) / 2
} else if newWidth < newSize.width {
originX = newSize.width / 2 - newWidth / 2
} else {
originX = 0
}
if newHeight > newSize.height {
originY = (newSize.height - newHeight) / 2
} else if newHeight < newSize.height {
originY = newSize.height / 2 - newHeight / 2
} else {
originY = 0
}
return CGRect(origin: CGPoint(x: originX, y: originY),
size: CGSize(width: newWidth, height: newHeight))
}
}

View File

@ -1,25 +0,0 @@
//
// Copyright (c) 2022 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.
//
public enum ResizeMode {
case scaleToFill, scaleAspectFit, scaleAspectFill
}

View File

@ -1,84 +0,0 @@
//
// Copyright (c) 2022 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
public struct BorderDrawingOperation: DrawingOperation {
public var frameableContentRect: CGRect
public var border: CGFloat
public var color: CGColor
public var radius: CGFloat
public var exteriorBorder: Bool
public init(frameableContentRect: CGRect,
border: CGFloat,
color: CGColor,
radius: CGFloat,
exteriorBorder: Bool) {
self.frameableContentRect = frameableContentRect
self.border = border
self.color = color
self.radius = radius
self.exteriorBorder = exteriorBorder
}
// MARK: - DrawingOperation
public func affectedArea(in context: CGContext?) -> CGRect {
let margin = exteriorBorder ? border : 0
let width = frameableContentRect.width + margin * 2
let height = frameableContentRect.height + margin * 2
return CGRect(origin: frameableContentRect.origin,
size: CGSize(width: width, height: height))
}
public func apply(in context: CGContext) {
let drawArea = affectedArea(in: context)
let ctxSize = drawArea.size.ceiledContextSize
let ctxRect = CGRect(origin: frameableContentRect.origin,
size: CGSize(width: ctxSize.width, height: ctxSize.height))
let widthDiff = CGFloat(ctxSize.width) - drawArea.width // difference between context width and real width
let heightDiff = CGFloat(ctxSize.height) - drawArea.height // difference between context height and real height
let inset = ctxRect.insetBy(dx: border / 2 + widthDiff, dy: border / 2 + heightDiff)
context.setStrokeColor(color)
if radius != 0 {
context.setLineWidth(border)
let path = CGPath(roundedRect: inset,
cornerWidth: radius,
cornerHeight: radius,
transform: nil)
context.addPath(path)
context.strokePath()
} else {
context.stroke(inset, width: border)
}
}
}

View File

@ -1,50 +0,0 @@
//
// Copyright (c) 2022 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 CoreGraphics
import QuartzCore
public struct CALayerDrawingOperation: DrawingOperation {
public var layer: CALayer
public var offset: CGPoint
public init(layer: CALayer,
offset: CGPoint) {
self.layer = layer
self.offset = offset
}
// MARK: - DrawingOperation
public func affectedArea(in context: CGContext? = nil) -> CGRect {
CGRect(origin: offset, size: layer.bounds.size)
}
public func apply(in context: CGContext) {
let offsetTransform = CGAffineTransform(translationX: offset.x, y: offset.y)
context.concatenate(offsetTransform)
layer.render(in: context)
context.concatenate(offsetTransform.inverted())
}
}

View File

@ -1,28 +0,0 @@
//
// Copyright (c) 2022 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 CoreGraphics
public protocol DrawingOperation {
func affectedArea(in context: CGContext?) -> CGRect
func apply(in context: CGContext)
}

View File

@ -1,57 +0,0 @@
//
// Copyright (c) 2022 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 CoreGraphics
public protocol OrientationAwareDrawingOperation: DrawingOperation {
var flipHorizontallyDuringDrawing: Bool { get set }
}
extension OrientationAwareDrawingOperation {
func apply(in context: CGContext, operation: (CGContext) -> Void) {
if flipHorizontallyDuringDrawing {
let flipVertical = CGAffineTransform(a: 1,
b: 0,
c: 0,
d: -1,
tx: 0,
ty: affectedArea(in: context).height)
context.concatenate(flipVertical)
operation(context)
context.concatenate(flipVertical.inverted())
} else {
operation(context)
}
}
func offsetForDrawing(_ offset: CGPoint) -> CGPoint {
flipHorizontallyDuringDrawing ? offset.horizontallyFlipped() : offset
}
func affectedAreaForDrawing(in context: CGContext?) -> CGRect {
var area = affectedArea(in: context)
area.origin = offsetForDrawing(area.origin)
return area
}
}

View File

@ -1,82 +0,0 @@
//
// Copyright (c) 2022 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 CoreGraphics
public struct SolidFillDrawingOperation: DrawingOperation {
public enum Shape {
case rect(CGRect)
case ellipse(CGRect)
case path(CGPath)
}
public var color: CGColor
public var shape: Shape
public init(color: CGColor, shape: Shape) {
self.color = color
self.shape = shape
}
public init(color: CGColor, rect: CGRect) {
self.init(color: color, shape: .rect(rect))
}
public init(color: CGColor, ellipseRect: CGRect) {
self.init(color: color, shape: .ellipse(ellipseRect))
}
public init(color: CGColor, path: CGPath) {
self.init(color: color, shape: .path(path))
}
// MARK: - DrawingOperation
public func affectedArea(in context: CGContext? = nil) -> CGRect {
switch shape {
case let .rect(rect):
return rect
case let .ellipse(rect):
return rect
case let .path(path):
return path.boundingBox
}
}
public func apply(in context: CGContext) {
context.setFillColor(color)
switch shape {
case let .rect(rect):
context.fill(rect)
case let .ellipse(rect):
context.fillEllipse(in: rect)
case let .path(path):
context.addPath(path)
context.fillPath()
}
}
}

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